fix: stop requiring readonly connection config

This commit is contained in:
Andrey Avtomonov 2026-05-13 19:21:41 +02:00
parent 754e4a9039
commit 7824b7f3b6
55 changed files with 103 additions and 292 deletions

View file

@ -63,8 +63,7 @@ agents.
"connections": [
{
"id": "my-warehouse",
"driver": "postgres",
"readonly": false
"driver": "postgres"
}
]
}

View file

@ -23,7 +23,6 @@ Agents should configure and ingest context sources in this order:
| Field | Required | Description |
|-------|----------|-------------|
| `driver` | Yes | Source adapter: `dbt`, `metricflow`, `lookml`, `metabase`, `looker`, or `notion` |
| `readonly` | Strongly recommended | Marks the source as read-only for KTX |
| `source_dir` | For local file sources | Absolute or project-relative source directory |
| `repo_url` | For Git-hosted sources | Git repository URL |
| `branch` | No | Git branch to read |
@ -49,7 +48,6 @@ connections:
my-dbt:
driver: dbt
source_dir: /path/to/dbt/project
readonly: true
```
For a Git-hosted project:
@ -62,7 +60,6 @@ connections:
branch: main
path: analytics/dbt # For monorepos
auth_token_ref: env:GITHUB_TOKEN
readonly: true
```
### Authentication
@ -110,7 +107,6 @@ connections:
branch: main
path: dbt_metrics # Subdirectory for monorepos
auth_token_ref: env:GITHUB_TOKEN
readonly: true
```
For a local path:
@ -157,7 +153,6 @@ connections:
branch: main
path: analytics # Subdirectory for monorepos
auth_token_ref: env:GITHUB_TOKEN
readonly: true
```
For a local path:
@ -219,7 +214,6 @@ connections:
syncEnabled:
"3": true
syncMode: ONLY # Only ingest mapped databases
readonly: true
```
### Authentication
@ -276,7 +270,6 @@ connections:
mappings:
connectionMappings:
postgres_connection: postgres-main # Looker conn → KTX conn
readonly: true
```
### Authentication
@ -329,7 +322,6 @@ connections:
crawl_mode: selected_roots
root_page_ids:
- "abc123def456..."
readonly: true
```
For crawling all accessible pages:
@ -340,7 +332,6 @@ connections:
driver: notion
auth_token_ref: env:NOTION_TOKEN
crawl_mode: all_accessible
readonly: true
```
### Authentication

View file

@ -21,7 +21,6 @@ Agents should prefer environment or file references over literal secrets.
| `url` | One of the connection methods | URL-style connectors | Database URL, `env:NAME`, or `file:/path/to/secret` |
| `host`, `port`, `database`, `username`, `password` | One of the connection methods | PostgreSQL, MySQL, ClickHouse, SQL Server | Field-by-field connection values |
| `schema` or `schemas` | No | schema-aware warehouses | Single schema or list of schemas to scan |
| `readonly` | Strongly recommended | all primary sources | Marks the connection as read-only in KTX config |
| `historicSql` | No | supported warehouses | Enables query-history ingestion when the warehouse supports it |
| `path` | Yes for path-style SQLite | SQLite | Local SQLite database path or `env:NAME` reference |
@ -37,7 +36,6 @@ connections:
driver: postgres
url: postgresql://user:password@host:5432/database
schema: public
readonly: true
```
Or with individual fields:
@ -55,7 +53,6 @@ connections:
- public
- analytics
ssl: true
readonly: true
```
### Authentication
@ -123,7 +120,6 @@ connections:
username: KTX_SERVICE
password: env:SNOWFLAKE_PASSWORD
role: ANALYST
readonly: true
```
For multiple schemas:
@ -196,7 +192,6 @@ connections:
credentials_json: file:~/.config/gcloud/bq-service-account.json
dataset_id: analytics
location: US
readonly: true
```
For multiple datasets:
@ -269,7 +264,6 @@ connections:
my-clickhouse:
driver: clickhouse
url: http://localhost:8123/analytics
readonly: true
```
Or with individual fields:
@ -284,7 +278,6 @@ connections:
username: default
password: env:CH_PASSWORD
ssl: false
readonly: true
```
### Authentication
@ -328,7 +321,6 @@ connections:
my-mysql:
driver: mysql
url: mysql://user:password@host:3306/database
readonly: true
```
Or with individual fields:
@ -343,7 +335,6 @@ connections:
username: ktx_reader
password: env:MYSQL_PASSWORD
ssl: true
readonly: true
```
### Authentication
@ -387,7 +378,6 @@ connections:
my-sqlserver:
driver: sqlserver
url: mssql://user:password@host:1433/database?trustServerCertificate=true
readonly: true
```
Or with individual fields:
@ -403,7 +393,6 @@ connections:
password: env:MSSQL_PASSWORD
schema: dbo
trustServerCertificate: true
readonly: true
```
For multiple schemas:
@ -455,7 +444,6 @@ connections:
my-sqlite:
driver: sqlite
path: ./data/warehouse.sqlite
readonly: true
```
Path supports multiple formats:

View file

@ -2,7 +2,6 @@ project: local-warehouse
connections:
warehouse:
driver: postgres
readonly: true
storage:
state: sqlite
search: sqlite-fts5

View file

@ -3,7 +3,6 @@ connections:
orbit:
driver: sqlite
path: ../../packages/context/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/data.sqlite
readonly: true
storage:
state: sqlite
search: sqlite-fts5

View file

@ -94,7 +94,7 @@ describe('runKtxConnection', () => {
const projectDir = join(tempDir, 'project');
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeConnections(projectDir, {
warehouse: { driver: 'postgres', url: 'env:DATABASE_URL', readonly: true },
warehouse: { driver: 'postgres', url: 'env:DATABASE_URL' },
docs: { driver: 'notion', auth_token_ref: 'env:NOTION_TOKEN', crawl_mode: 'all_accessible' },
});
const io = makeIo();
@ -123,7 +123,7 @@ describe('runKtxConnection', () => {
const projectDir = join(tempDir, 'project');
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeConnections(projectDir, {
warehouse: { driver: 'sqlite', readonly: true },
warehouse: { driver: 'sqlite' },
});
const { connector, introspect, cleanup } = nativeConnector('sqlite', ['customers', 'orders']);
const createScanConnector = vi.fn(async () => connector);
@ -202,7 +202,7 @@ describe('runKtxConnection', () => {
const projectDir = join(tempDir, 'project');
await initKtxProject({ projectDir, projectName: 'warehouse' });
await writeConnections(projectDir, {
warehouse: { driver: 'sqlite', readonly: true },
warehouse: { driver: 'sqlite' },
});
const cleanup = vi.fn(async () => undefined);
const connector: KtxScanConnector = {

View file

@ -57,7 +57,6 @@ function demoConfig(databasePath: string): string {
` ${DEMO_CONNECTION_ID}:`,
' driver: sqlite',
` path: ${JSON.stringify(databasePath)}`,
' readonly: true',
'storage:',
' state: sqlite',
' search: sqlite-fts5',

View file

@ -275,7 +275,6 @@ describe('runKtxDoctor', () => {
' warehouse:',
' driver: postgres',
' url: env:WAREHOUSE_DATABASE_URL',
' readonly: true',
' historicSql:',
' enabled: true',
' dialect: postgres',

View file

@ -25,7 +25,7 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
it('passes when no Postgres historic-SQL connections are enabled', async () => {
const checks = await runPostgresHistoricSqlDoctorChecks(
projectWithConnections({
warehouse: { driver: 'sqlite', path: './warehouse.db', readonly: true },
warehouse: { driver: 'sqlite', path: './warehouse.db' },
}),
{
postgresHistoricSqlProbe: vi.fn<PostgresHistoricSqlDoctorProbe>(),
@ -53,7 +53,6 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
warehouse: {
driver: 'postgres',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
},
}),
@ -66,7 +65,6 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
connection: {
driver: 'postgres',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
},
env: process.env,
@ -87,7 +85,6 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
warehouse: {
driver: 'postgres',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
},
}),
@ -119,7 +116,6 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
warehouse: {
driver: 'postgres',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
},
}),
@ -154,7 +150,6 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
warehouse: {
driver: 'mysql',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
},
}),
@ -180,7 +175,6 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
warehouse: {
driver: 'postgres',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
},
}),

View file

@ -86,8 +86,9 @@ async function defaultPostgresHistoricSqlProbe(
const [{ PostgresPgssReader }, { KtxPostgresHistoricSqlQueryClient, isKtxPostgresConnectionConfig }] =
await Promise.all([import('@ktx/context/ingest'), import('@ktx/connector-postgres')]);
const inputDriver = input.connection.driver ?? 'unknown';
if (!isKtxPostgresConnectionConfig(input.connection)) {
throw new Error(`Native PostgreSQL connector cannot run driver "${input.connection.driver ?? 'unknown'}"`);
throw new Error(`Native PostgreSQL connector cannot run driver "${inputDriver}"`);
}
const client = new KtxPostgresHistoricSqlQueryClient({

View file

@ -45,7 +45,6 @@ describe('CLI local ingest adapters', () => {
' warehouse:',
' driver: postgres',
' url: env:WAREHOUSE_DATABASE_URL',
' readonly: true',
' historicSql:',
' enabled: true',
' dialect: postgres',
@ -76,7 +75,6 @@ describe('CLI local ingest adapters', () => {
'connections:',
' bq:',
' driver: bigquery',
' readonly: true',
' dataset_id: analytics',
' location: us',
' credentials_json: \'{"project_id":"demo-project"}\'',
@ -110,7 +108,6 @@ describe('CLI local ingest adapters', () => {
'connections:',
' sf:',
' driver: snowflake',
' readonly: true',
' account: acct',
' warehouse: wh',
' database: ANALYTICS',

View file

@ -190,10 +190,9 @@ function enabledHistoricSqlDialect(connection: unknown): 'postgres' | 'bigquery'
function createEphemeralPostgresHistoricSqlClient(project: KtxLocalProject, connectionId: string) {
const connection = project.config.connections[connectionId] as KtxPostgresConnectionConfig | undefined;
const inputDriver = connection?.driver ?? 'unknown';
if (!isKtxPostgresConnectionConfig(connection)) {
throw new Error(
`Historic SQL local ingest requires a Postgres connection, got ${String(connection?.driver ?? 'unknown')}`,
);
throw new Error(`Historic SQL local ingest requires a Postgres connection, got ${String(inputDriver)}`);
}
return {
async executeQuery(sql: string, params?: unknown[]) {
@ -212,10 +211,9 @@ function createEphemeralPostgresHistoricSqlClient(project: KtxLocalProject, conn
function createEphemeralBigQueryHistoricSqlClient(project: KtxLocalProject, connectionId: string) {
const connection = project.config.connections[connectionId] as KtxBigQueryConnectionConfig | undefined;
const inputDriver = connection?.driver ?? 'unknown';
if (!isKtxBigQueryConnectionConfig(connection)) {
throw new Error(
`Historic SQL local ingest requires a BigQuery connection, got ${String(connection?.driver ?? 'unknown')}`,
);
throw new Error(`Historic SQL local ingest requires a BigQuery connection, got ${String(inputDriver)}`);
}
return {
async executeQuery(query: string) {
@ -243,10 +241,9 @@ async function createEphemeralSnowflakeHistoricSqlClient(
connectorModule: SnowflakeConnectorModule,
) {
const connection = project.config.connections[connectionId];
const inputDriver = connection?.driver ?? 'unknown';
if (!connectorModule.isKtxSnowflakeConnectionConfig(connection)) {
throw new Error(
`Historic SQL local ingest requires a Snowflake connection, got ${String(connection?.driver ?? 'unknown')}`,
);
throw new Error(`Historic SQL local ingest requires a Snowflake connection, got ${String(inputDriver)}`);
}
return {
async executeQuery(query: string) {
@ -308,10 +305,9 @@ function historicSqlOptionsForLocalRun(project: KtxLocalProject, options: KtxCli
}
if (dialect === 'bigquery') {
const inputDriver = connection?.driver ?? 'unknown';
if (!isKtxBigQueryConnectionConfig(connection)) {
throw new Error(
`Historic SQL local ingest requires a BigQuery connection, got ${String(connection?.driver ?? 'unknown')}`,
);
throw new Error(`Historic SQL local ingest requires a BigQuery connection, got ${String(inputDriver)}`);
}
return {
...base,

View file

@ -49,7 +49,6 @@ describe('createKtxCliScanConnector', () => {
' warehouse:',
' driver: sqlite',
' path: warehouse.db',
' readonly: true',
'',
].join('\n'),
'utf-8',
@ -72,7 +71,6 @@ describe('createKtxCliScanConnector', () => {
' warehouse:',
' driver: bigquery',
' dataset_id: analytics',
' readonly: true',
' max_bytes_billed: "987654321"',
'',
].join('\n'),
@ -123,7 +121,6 @@ describe('createKtxCliScanConnector', () => {
' warehouse:',
' type: postgres',
' url: postgresql://example/db',
' readonly: true',
'',
].join('\n'),
'utf-8',

View file

@ -861,7 +861,6 @@ describe('runKtxScan', () => {
' warehouse:',
' driver: mysql',
' url: env:MYSQL_URL',
' readonly: true',
'',
].join('\n'),
'utf-8',
@ -910,7 +909,6 @@ describe('runKtxScan', () => {
' warehouse:',
' driver: sqlite',
' path: warehouse.db',
' readonly: true',
'',
].join('\n'),
'utf-8',
@ -968,7 +966,6 @@ describe('runKtxScan', () => {
' database: analytics',
' username: reader',
' password: env:POSTGRES_PASSWORD',
' readonly: true',
'',
].join('\n'),
'utf-8',
@ -1035,7 +1032,6 @@ describe('runKtxScan', () => {
' database: analytics',
' username: reader',
' password: env:CLICKHOUSE_PASSWORD',
' readonly: true',
'',
].join('\n'),
'utf-8',
@ -1087,7 +1083,6 @@ describe('runKtxScan', () => {
' database: analytics',
' username: reader',
' schema: dbo',
' readonly: true',
'',
].join('\n'),
'utf-8',
@ -1153,7 +1148,6 @@ describe('runKtxScan', () => {
' dataset_id: analytics',
' credentials_json: env:BIGQUERY_CREDENTIALS_JSON',
' location: US',
' readonly: true',
'',
].join('\n'),
'utf-8',
@ -1222,7 +1216,6 @@ describe('runKtxScan', () => {
' database: ANALYTICS',
' schema_name: PUBLIC',
' username: reader',
' readonly: true',
'',
].join('\n'),
'utf-8',

View file

@ -218,7 +218,6 @@ describe('setup databases step', () => {
' warehouse:',
' driver: postgres',
' url: env:DATABASE_URL',
' readonly: true',
'',
].join('\n'),
'utf-8',
@ -281,7 +280,6 @@ describe('setup databases step', () => {
expect(config.connections['postgres-warehouse']).toEqual({
driver: 'postgres',
url: 'env:DATABASE_URL',
readonly: true,
});
});
@ -542,7 +540,6 @@ describe('setup databases step', () => {
' warehouse:',
' driver: postgres',
' url: env:DATABASE_URL',
' readonly: true',
'setup:',
' database_connection_ids:',
' - warehouse',
@ -583,7 +580,6 @@ describe('setup databases step', () => {
' warehouse:',
' driver: postgres',
' url: env:DATABASE_URL',
' readonly: true',
'setup:',
' database_connection_ids:',
' - warehouse',
@ -698,7 +694,6 @@ describe('setup databases step', () => {
' warehouse:',
' driver: postgres',
' url: env:DATABASE_URL',
' readonly: true',
'setup:',
' database_connection_ids:',
' - warehouse',
@ -843,7 +838,6 @@ describe('setup databases step', () => {
port: 5432,
database: 'analytics',
username: 'readonly',
readonly: true,
});
expect(connection.password).toMatch(/^file:/);
const secretPath = join(tempDir, '.ktx/secrets/postgres-warehouse-password');
@ -998,7 +992,6 @@ describe('setup databases step', () => {
expect(config.connections['postgres-warehouse']).toMatchObject({
driver: 'postgres',
url: 'env:DATABASE_URL',
readonly: true,
});
});
@ -1115,7 +1108,6 @@ describe('setup databases step', () => {
driver: 'postgres',
url: 'env:DATABASE_URL',
schemas: ['public'],
readonly: true,
});
expect(config.setup).toEqual({
database_connection_ids: ['warehouse'],
@ -1153,7 +1145,6 @@ describe('setup databases step', () => {
expect(config.connections.warehouse).toEqual({
driver: 'sqlite',
path: './warehouse.sqlite',
readonly: true,
});
expect(config.setup).toEqual({
database_connection_ids: ['warehouse'],
@ -1170,7 +1161,6 @@ describe('setup databases step', () => {
' warehouse:',
' driver: postgres',
' url: env:DATABASE_URL',
' readonly: true',
' analytics:',
' driver: snowflake',
' authMethod: password',
@ -1180,7 +1170,6 @@ describe('setup databases step', () => {
' schema_name: PUBLIC',
' username: reader',
' password: env:SNOWFLAKE_PASSWORD',
' readonly: true',
'',
].join('\n'),
'utf-8',
@ -1443,7 +1432,6 @@ describe('setup databases step', () => {
' driver: bigquery',
' dataset_id: analytics',
' credentials_json: env:BIGQUERY_CREDENTIALS_JSON',
' readonly: true',
'',
].join('\n'),
'utf-8',
@ -1492,7 +1480,6 @@ describe('setup databases step', () => {
' warehouse:',
' driver: postgres',
' url: env:DATABASE_URL',
' readonly: true',
'',
].join('\n'),
'utf-8',

View file

@ -593,7 +593,6 @@ async function buildFieldsConnectionConfig(input: {
username,
...(passwordRef ? { password: passwordRef } : {}),
...(input.args.databaseSchemas.length > 0 ? { schemas: input.args.databaseSchemas } : {}),
readonly: true,
};
}
@ -615,7 +614,6 @@ async function buildPastedUrlConnectionConfig(input: {
driver: input.driver,
url,
...(input.args.databaseSchemas.length > 0 ? { schemas: input.args.databaseSchemas } : {}),
readonly: true,
};
}
@ -629,7 +627,6 @@ async function buildPastedUrlConnectionConfig(input: {
driver: input.driver,
url: ref,
...(input.args.databaseSchemas.length > 0 ? { schemas: input.args.databaseSchemas } : {}),
readonly: true,
};
}
@ -637,7 +634,6 @@ async function buildPastedUrlConnectionConfig(input: {
driver: input.driver,
url,
...(input.args.databaseSchemas.length > 0 ? { schemas: input.args.databaseSchemas } : {}),
readonly: true,
};
}
@ -661,14 +657,12 @@ async function buildUrlConnectionConfig(input: {
driver: input.driver,
url: ref,
...(input.args.databaseSchemas.length > 0 ? { schemas: input.args.databaseSchemas } : {}),
readonly: true,
};
}
return {
driver: input.driver,
url,
...(input.args.databaseSchemas.length > 0 ? { schemas: input.args.databaseSchemas } : {}),
readonly: true,
};
}
@ -706,7 +700,7 @@ async function buildConnectionConfig(input: {
'SQLite database file\nEnter a relative or absolute path, for example ./warehouse.sqlite.',
));
if (path === undefined) return 'back';
return path ? { driver: 'sqlite', path, readonly: true } : null;
return path ? { driver: 'sqlite', path } : null;
}
if (driver === 'postgres' || driver === 'mysql' || driver === 'clickhouse' || driver === 'sqlserver') {
return await buildUrlConnectionConfig({ driver, connectionId: input.connectionId, args, prompts });
@ -728,7 +722,6 @@ async function buildConnectionConfig(input: {
dataset_id: datasetId,
credentials_json: normalizeFileReference(credentialsPath),
...(location ? { location } : {}),
readonly: true,
};
}
if (driver === 'snowflake') {
@ -767,7 +760,6 @@ async function buildConnectionConfig(input: {
username,
password: passwordRef,
...(role ? { role } : {}),
readonly: true,
};
}
throw new Error(`Unsupported database driver: ${driver}`);

View file

@ -98,7 +98,7 @@ describe('setup sources step', () => {
...config,
connections: {
...config.connections,
warehouse: { driver: 'postgres', url: 'env:DATABASE_URL', readonly: true },
warehouse: { driver: 'postgres', url: 'env:DATABASE_URL' },
},
setup: {
...config.setup,
@ -455,7 +455,6 @@ describe('setup sources step', () => {
driver: 'snowflake',
account: 'acme',
database: 'analytics',
readonly: true,
});
const cases: Array<{

View file

@ -170,7 +170,6 @@ describe('setup status', () => {
' warehouse:',
' driver: postgres',
' url: env:DATABASE_URL',
' readonly: true',
'',
].join('\n'),
'utf-8',
@ -192,7 +191,6 @@ describe('setup status', () => {
' warehouse:',
' driver: postgres',
' url: env:DATABASE_URL',
' readonly: true',
'',
].join('\n'),
'utf-8',
@ -1373,7 +1371,6 @@ describe('setup status', () => {
' warehouse:',
' driver: postgres',
' url: env:DEMO_DATABASE_URL',
' readonly: true',
'',
].join('\n'),
'utf-8',

View file

@ -190,7 +190,7 @@ joins: []
it('runs sl query and prints SQL output', async () => {
const projectDir = join(tempDir, 'project');
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
project.config.connections.warehouse = { driver: 'postgres', readonly: true };
project.config.connections.warehouse = { driver: 'postgres' };
await project.fileStore.writeFile(
'semantic-layer/warehouse/orders.yaml',
`name: orders
@ -247,7 +247,7 @@ joins: []
it('runs sl query from a JSON query file', async () => {
const projectDir = join(tempDir, 'project');
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
project.config.connections.warehouse = { driver: 'postgres', readonly: true };
project.config.connections.warehouse = { driver: 'postgres' };
await project.fileStore.writeFile(
'semantic-layer/warehouse/orders.yaml',
`name: orders
@ -314,7 +314,7 @@ joins: []
it('creates default sl query compute through the managed runtime helper', async () => {
const projectDir = join(tempDir, 'project');
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
project.config.connections.warehouse = { driver: 'postgres', readonly: true };
project.config.connections.warehouse = { driver: 'postgres' };
await project.fileStore.writeFile(
'semantic-layer/warehouse/orders.yaml',
`name: orders
@ -375,7 +375,7 @@ joins: []
it('executes sl query through the injected query executor', async () => {
const projectDir = join(tempDir, 'project');
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
project.config.connections.warehouse = { driver: 'postgres', url: 'postgres://example/db', readonly: true };
project.config.connections.warehouse = { driver: 'postgres', url: 'postgres://example/db' };
await project.fileStore.writeFile(
'semantic-layer/warehouse/orders.yaml',
`name: orders
@ -471,7 +471,7 @@ joins: []
`);
db.close();
project.config.connections.warehouse = { driver: 'sqlite', path: 'warehouse.db', readonly: true };
project.config.connections.warehouse = { driver: 'sqlite', path: 'warehouse.db' };
await writeFile(
join(projectDir, 'ktx.yaml'),
[
@ -480,7 +480,6 @@ joins: []
' warehouse:',
' driver: sqlite',
' path: warehouse.db',
' readonly: true',
'',
].join('\n'),
'utf-8',

View file

@ -106,7 +106,6 @@ async function writeSqliteScanConfig(projectDir: string, dbPath: string, enrich
' warehouse:',
' driver: sqlite',
` path: ${JSON.stringify(dbPath)}`,
' readonly: true',
'ingest:',
' adapters:',
' - live-database',

View file

@ -100,7 +100,6 @@ const connection = {
dataset_id: 'analytics',
credentials_json: JSON.stringify({ project_id: 'project-1', client_email: 'reader@example.test' }),
location: 'US',
readonly: true,
};
describe('KtxBigQueryScanConnector', () => {
@ -112,12 +111,6 @@ describe('KtxBigQueryScanConnector', () => {
datasetIds: ['analytics'],
location: 'US',
});
expect(() =>
bigQueryConnectionConfigFromConfig({
connectionId: 'warehouse',
connection: { ...connection, readonly: false },
}),
).toThrow('Native BigQuery connector requires connections.warehouse.readonly: true');
});
it('introspects datasets, table metadata, primary keys, and normalized types', async () => {

View file

@ -30,7 +30,6 @@ export interface KtxBigQueryConnectionConfig {
dataset_ids?: string[];
credentials_json?: string;
location?: string;
readonly?: boolean;
[key: string]: unknown;
}
@ -194,7 +193,9 @@ function normalizeValue(value: unknown): unknown {
return value;
}
export function isKtxBigQueryConnectionConfig(connection: KtxBigQueryConnectionConfig | undefined): boolean {
export function isKtxBigQueryConnectionConfig(
connection: KtxBigQueryConnectionConfig | undefined,
): connection is KtxBigQueryConnectionConfig {
return String(connection?.driver ?? '').toLowerCase() === 'bigquery';
}
@ -203,11 +204,9 @@ export function bigQueryConnectionConfigFromConfig(input: {
connection: KtxBigQueryConnectionConfig | undefined;
env?: NodeJS.ProcessEnv;
}): KtxBigQueryResolvedConnectionConfig {
const inputDriver = input.connection?.driver ?? 'unknown';
if (!isKtxBigQueryConnectionConfig(input.connection)) {
throw new Error(`Native BigQuery connector cannot run driver "${input.connection?.driver ?? 'unknown'}"`);
}
if (input.connection?.readonly !== true) {
throw new Error(`Native BigQuery connector requires connections.${input.connectionId}.readonly: true`);
throw new Error(`Native BigQuery connector cannot run driver "${inputDriver}"`);
}
const env = input.env ?? process.env;

View file

@ -112,7 +112,6 @@ describe('KtxClickHouseScanConnector', () => {
username: 'reader',
password: 'test-pass', // pragma: allowlist secret
ssl: true,
readonly: true,
},
}),
).toMatchObject({
@ -123,12 +122,6 @@ describe('KtxClickHouseScanConnector', () => {
password: 'test-pass', // pragma: allowlist secret
ssl: true,
});
expect(() =>
clickHouseClientConfigFromConfig({
connectionId: 'warehouse',
connection: { driver: 'clickhouse', host: 'ch.example.test', database: 'analytics', readonly: false },
}),
).toThrow('Native ClickHouse connector requires connections.warehouse.readonly: true');
});
it('introspects schema, primary keys, comments, row counts, and views', async () => {
@ -140,7 +133,6 @@ describe('KtxClickHouseScanConnector', () => {
database: 'analytics',
username: 'reader',
password: 'test-pass', // pragma: allowlist secret
readonly: true,
},
clientFactory: fakeClientFactory(),
now: () => new Date('2026-04-29T14:00:00.000Z'),
@ -189,7 +181,6 @@ describe('KtxClickHouseScanConnector', () => {
database: 'analytics',
username: 'reader',
password: 'test-pass', // pragma: allowlist secret
readonly: true,
},
clientFactory,
});
@ -253,7 +244,6 @@ describe('KtxClickHouseScanConnector', () => {
database: 'analytics',
username: 'reader',
password: 'test-pass', // pragma: allowlist secret
readonly: true,
},
},
clientFactory: fakeClientFactory(),

View file

@ -35,7 +35,6 @@ export interface KtxClickHouseConnectionConfig {
password?: string;
url?: string;
ssl?: boolean;
readonly?: boolean;
[key: string]: unknown;
}
@ -193,7 +192,9 @@ function isNullableClickHouseType(type: string): boolean {
return type.startsWith('Nullable(') || type.startsWith('LowCardinality(Nullable(');
}
export function isKtxClickHouseConnectionConfig(connection: KtxClickHouseConnectionConfig | undefined): boolean {
export function isKtxClickHouseConnectionConfig(
connection: KtxClickHouseConnectionConfig | undefined,
): connection is KtxClickHouseConnectionConfig {
return String(connection?.driver ?? '').toLowerCase() === 'clickhouse';
}
@ -202,11 +203,9 @@ export function clickHouseClientConfigFromConfig(input: {
connection: KtxClickHouseConnectionConfig | undefined;
env?: NodeJS.ProcessEnv;
}): KtxClickHouseResolvedClientConfig {
const inputDriver = input.connection?.driver ?? 'unknown';
if (!isKtxClickHouseConnectionConfig(input.connection)) {
throw new Error(`Native ClickHouse connector cannot run driver "${input.connection?.driver ?? 'unknown'}"`);
}
if (input.connection?.readonly !== true) {
throw new Error(`Native ClickHouse connector requires connections.${input.connectionId}.readonly: true`);
throw new Error(`Native ClickHouse connector cannot run driver "${inputDriver}"`);
}
const env = input.env ?? process.env;

View file

@ -92,7 +92,7 @@ function fakePoolFactory(): KtxMysqlPoolFactory {
describe('KtxMysqlScanConnector', () => {
it('resolves MySQL connection configuration safely', () => {
expect(isKtxMysqlConnectionConfig({ driver: 'mysql', host: 'localhost', database: 'analytics', readonly: true })).toBe(true);
expect(isKtxMysqlConnectionConfig({ driver: 'mysql', host: 'localhost', database: 'analytics' })).toBe(true);
expect(isKtxMysqlConnectionConfig({ driver: 'postgres', host: 'localhost', database: 'analytics' })).toBe(false);
expect(
mysqlConnectionPoolConfigFromConfig({
@ -105,7 +105,6 @@ describe('KtxMysqlScanConnector', () => {
username: 'reader',
password: 'secret', // pragma: allowlist secret
ssl: true,
readonly: true,
},
}),
).toMatchObject({
@ -116,12 +115,6 @@ describe('KtxMysqlScanConnector', () => {
password: 'secret', // pragma: allowlist secret
ssl: { rejectUnauthorized: false },
});
expect(() =>
mysqlConnectionPoolConfigFromConfig({
connectionId: 'warehouse',
connection: { driver: 'mysql', host: 'db.example.test', database: 'analytics', readonly: false },
}),
).toThrow('Native MySQL connector requires connections.warehouse.readonly: true');
});
it('introspects schema, primary keys, comments, row counts, views, and foreign keys', async () => {
@ -133,7 +126,6 @@ describe('KtxMysqlScanConnector', () => {
database: 'analytics',
username: 'reader',
password: 'secret', // pragma: allowlist secret
readonly: true,
},
poolFactory: fakePoolFactory(),
now: () => new Date('2026-04-29T12:00:00.000Z'),
@ -192,7 +184,6 @@ describe('KtxMysqlScanConnector', () => {
database: 'analytics',
username: 'reader',
password: 'secret', // pragma: allowlist secret
readonly: true,
},
poolFactory,
});
@ -249,7 +240,6 @@ describe('KtxMysqlScanConnector', () => {
database: 'analytics',
username: 'reader',
password: 'secret', // pragma: allowlist secret
readonly: true,
},
},
poolFactory: fakePoolFactory(),

View file

@ -35,7 +35,6 @@ export interface KtxMysqlConnectionConfig {
password?: string;
url?: string;
ssl?: boolean | { rejectUnauthorized?: boolean };
readonly?: boolean;
[key: string]: unknown;
}
@ -232,7 +231,9 @@ function queryParams(params: Record<string, unknown> | unknown[] | undefined): u
return Array.isArray(params) ? params : Object.values(params);
}
export function isKtxMysqlConnectionConfig(connection: KtxMysqlConnectionConfig | undefined): boolean {
export function isKtxMysqlConnectionConfig(
connection: KtxMysqlConnectionConfig | undefined,
): connection is KtxMysqlConnectionConfig {
return String(connection?.driver ?? '').toLowerCase() === 'mysql';
}
@ -241,11 +242,9 @@ export function mysqlConnectionPoolConfigFromConfig(input: {
connection: KtxMysqlConnectionConfig | undefined;
env?: NodeJS.ProcessEnv;
}): KtxMysqlPoolConfig {
const inputDriver = input.connection?.driver ?? 'unknown';
if (!isKtxMysqlConnectionConfig(input.connection)) {
throw new Error(`Native MySQL connector cannot run driver "${input.connection?.driver ?? 'unknown'}"`);
}
if (input.connection?.readonly !== true) {
throw new Error(`Native MySQL connector requires connections.${input.connectionId}.readonly: true`);
throw new Error(`Native MySQL connector cannot run driver "${inputDriver}"`);
}
const env = input.env ?? process.env;

View file

@ -102,7 +102,7 @@ function metadataResults(): Map<string, FakeQueryResult> {
describe('KtxPostgresScanConnector', () => {
it('resolves configuration safely', () => {
expect(isKtxPostgresConnectionConfig({ driver: 'postgres', url: 'env:DATABASE_URL', readonly: true })).toBe(true);
expect(isKtxPostgresConnectionConfig({ driver: 'postgres', url: 'env:DATABASE_URL' })).toBe(true);
expect(isKtxPostgresConnectionConfig({ driver: 'postgresql', host: 'db', database: 'analytics' })).toBe(true);
expect(isKtxPostgresConnectionConfig({ driver: 'mysql', host: 'db' })).toBe(false);
expect(
@ -115,7 +115,6 @@ describe('KtxPostgresScanConnector', () => {
username: 'reader',
password: 'test-password', // pragma: allowlist secret
schemas: ['analytics', 'public'],
readonly: true,
ssl: true,
rejectUnauthorized: false,
},
@ -134,7 +133,6 @@ describe('KtxPostgresScanConnector', () => {
connection: {
driver: 'postgres',
url: 'env:DEMO_DATABASE_URL',
readonly: true,
},
env: {
DEMO_DATABASE_URL: 'postgresql://reader@demo.example.test:5432/demo?sslmode=prefer',
@ -148,12 +146,16 @@ describe('KtxPostgresScanConnector', () => {
});
expect(libpqPreferConfig).not.toHaveProperty('connectionString');
expect(libpqPreferConfig).not.toHaveProperty('ssl');
expect(() =>
expect(
postgresPoolConfigFromConfig({
connectionId: 'warehouse',
connection: { driver: 'postgres', host: 'db.example.test', database: 'analytics', username: 'reader' },
}),
).toThrow('Native PostgreSQL connector requires connections.warehouse.readonly: true');
).toMatchObject({
host: 'db.example.test',
database: 'analytics',
user: 'reader',
});
});
it('introspects schemas, tables, views, primary keys, comments, row counts, and foreign keys', async () => {
@ -166,7 +168,6 @@ describe('KtxPostgresScanConnector', () => {
username: 'reader',
password: 'test-password', // pragma: allowlist secret
schema: 'public',
readonly: true,
},
poolFactory: fakePoolFactory(metadataResults()),
now: () => new Date('2026-04-29T10:00:00.000Z'),
@ -225,7 +226,6 @@ describe('KtxPostgresScanConnector', () => {
username: 'reader',
password: 'test-password', // pragma: allowlist secret
schema: 'public',
readonly: true,
},
poolFactory: fakePoolFactory(metadataResults()),
});
@ -274,7 +274,6 @@ describe('KtxPostgresScanConnector', () => {
username: 'reader',
password: 'test-password', // pragma: allowlist secret
schema: 'public',
readonly: true,
},
},
poolFactory: fakePoolFactory(metadataResults()),
@ -347,7 +346,6 @@ describe('KtxPostgresScanConnector', () => {
username: 'reader',
password: 'test-password', // pragma: allowlist secret
schema: 'public',
readonly: true,
},
},
poolFactory: endAwarePoolFactory,
@ -383,7 +381,6 @@ describe('KtxPostgresScanConnector', () => {
database: 'analytics',
username: 'reader',
password: 'test-password', // pragma: allowlist secret
readonly: true,
},
poolFactory,
});

View file

@ -61,7 +61,6 @@ export interface KtxPostgresConnectionConfig {
sslmode?: string;
sslMode?: string;
rejectUnauthorized?: boolean;
readonly?: boolean;
[key: string]: unknown;
}
@ -291,7 +290,9 @@ function searchPathSchemasFromConnection(connection: KtxPostgresConnectionConfig
return schemas.includes('public') ? schemas : [...schemas, 'public'];
}
export function isKtxPostgresConnectionConfig(connection: KtxPostgresConnectionConfig | undefined): boolean {
export function isKtxPostgresConnectionConfig(
connection: KtxPostgresConnectionConfig | undefined,
): connection is KtxPostgresConnectionConfig {
const driver = String(connection?.driver ?? '').toLowerCase();
return driver === 'postgres' || driver === 'postgresql';
}
@ -301,11 +302,9 @@ export function postgresPoolConfigFromConfig(input: {
connection: KtxPostgresConnectionConfig | undefined;
env?: NodeJS.ProcessEnv;
}): KtxPostgresPoolConfig {
const inputDriver = input.connection?.driver ?? 'unknown';
if (!isKtxPostgresConnectionConfig(input.connection)) {
throw new Error(`Native PostgreSQL connector cannot run driver "${input.connection?.driver ?? 'unknown'}"`);
}
if (input.connection?.readonly !== true) {
throw new Error(`Native PostgreSQL connector requires connections.${input.connectionId}.readonly: true`);
throw new Error(`Native PostgreSQL connector cannot run driver "${inputDriver}"`);
}
const env = input.env ?? process.env;

View file

@ -30,7 +30,6 @@ describe('KtxPostgresHistoricSqlQueryClient', () => {
connectionId: 'warehouse',
connection: {
driver: 'postgres',
readonly: true,
url: 'postgresql://readonly:secret@pg.example.test/warehouse', // pragma: allowlist secret
},
poolFactory,

View file

@ -78,7 +78,6 @@ describe('KtxSnowflakeScanConnector', () => {
warehouse: 'WH',
database: 'ANALYTICS',
username: 'reader',
readonly: true,
}),
).toBe(true);
expect(isKtxSnowflakeConnectionConfig({ driver: 'bigquery' })).toBe(false);
@ -94,7 +93,6 @@ describe('KtxSnowflakeScanConnector', () => {
schema_name: 'PUBLIC',
username: 'reader',
password: 'fixture-pass', // pragma: allowlist secret
readonly: true,
},
}),
).toMatchObject({
@ -105,12 +103,6 @@ describe('KtxSnowflakeScanConnector', () => {
username: 'reader',
authMethod: 'password',
});
expect(() =>
snowflakeConnectionConfigFromConfig({
connectionId: 'warehouse',
connection: { driver: 'snowflake', account: 'acct', readonly: false },
}),
).toThrow('Native Snowflake connector requires connections.warehouse.readonly: true');
});
it('introspects schema, primary keys, comments, row counts, and dimensions', async () => {
@ -125,7 +117,6 @@ describe('KtxSnowflakeScanConnector', () => {
schema_name: 'PUBLIC',
username: 'reader',
password: 'fixture-pass', // pragma: allowlist secret
readonly: true,
},
driverFactory: fakeDriverFactory(),
now: () => new Date('2026-04-29T18:00:00.000Z'),
@ -185,7 +176,6 @@ describe('KtxSnowflakeScanConnector', () => {
schema_name: 'PUBLIC',
username: 'reader',
password: 'fixture-pass', // pragma: allowlist secret
readonly: true,
},
driverFactory,
});
@ -243,7 +233,6 @@ describe('KtxSnowflakeScanConnector', () => {
schema_name: 'PUBLIC',
username: 'reader',
password: 'fixture-pass', // pragma: allowlist secret
readonly: true,
},
},
driverFactory: fakeDriverFactory(),

View file

@ -38,7 +38,6 @@ export interface KtxSnowflakeConnectionConfig {
privateKey?: string;
passphrase?: string;
role?: string;
readonly?: boolean;
[key: string]: unknown;
}
@ -191,7 +190,9 @@ function toSnowflakeBinds(params: unknown[] | undefined): snowflake.Binds | unde
return params?.map((value) => toSnowflakeBind(value));
}
export function isKtxSnowflakeConnectionConfig(connection: KtxSnowflakeConnectionConfig | undefined): boolean {
export function isKtxSnowflakeConnectionConfig(
connection: KtxSnowflakeConnectionConfig | undefined,
): connection is KtxSnowflakeConnectionConfig {
return String(connection?.driver ?? '').toLowerCase() === 'snowflake';
}
@ -200,11 +201,9 @@ export function snowflakeConnectionConfigFromConfig(input: {
connection: KtxSnowflakeConnectionConfig | undefined;
env?: NodeJS.ProcessEnv;
}): KtxSnowflakeResolvedConnectionConfig {
const inputDriver = input.connection?.driver ?? 'unknown';
if (!isKtxSnowflakeConnectionConfig(input.connection)) {
throw new Error(`Native Snowflake connector cannot run driver "${input.connection?.driver ?? 'unknown'}"`);
}
if (input.connection?.readonly !== true) {
throw new Error(`Native Snowflake connector requires connections.${input.connectionId}.readonly: true`);
throw new Error(`Native Snowflake connector cannot run driver "${inputDriver}"`);
}
const env = input.env ?? process.env;
const authMethod = input.connection?.authMethod ?? 'password';
@ -395,7 +394,7 @@ class SnowflakeSdkDriver implements KtxSnowflakeDriver {
private async createConnection(): Promise<snowflake.Connection> {
const patch = await this.sdkOptionsProvider?.resolve({
account: this.resolved.account,
connection: { ...this.resolved, driver: 'snowflake', readonly: true },
connection: { ...this.resolved, driver: 'snowflake' },
});
if (patch?.close) {
this.closeSdkOptions.push(patch.close);

View file

@ -53,45 +53,43 @@ describe('KtxSqliteScanConnector', () => {
writeFileSync(pointerPath, dbPath, 'utf-8');
try {
expect(isKtxSqliteConnectionConfig({ driver: 'sqlite', path: 'warehouse.db', readonly: true })).toBe(true);
expect(isKtxSqliteConnectionConfig({ driver: 'postgres', url: 'env:DATABASE_URL', readonly: true })).toBe(
false,
);
expect(isKtxSqliteConnectionConfig({ driver: 'sqlite', path: 'warehouse.db' })).toBe(true);
expect(isKtxSqliteConnectionConfig({ driver: 'postgres', url: 'env:DATABASE_URL' })).toBe(false);
expect(
sqliteDatabasePathFromConfig({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'sqlite', path: 'warehouse.db', readonly: true },
connection: { driver: 'sqlite', path: 'warehouse.db' },
}),
).toBe(dbPath);
expect(
sqliteDatabasePathFromConfig({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'sqlite', url: 'env:KTX_SQLITE_TEST_URL', readonly: true },
connection: { driver: 'sqlite', url: 'env:KTX_SQLITE_TEST_URL' },
}),
).toBe(dbPath);
expect(
sqliteDatabasePathFromConfig({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'sqlite', url: `file://${dbPath}`, readonly: true },
connection: { driver: 'sqlite', url: `file://${dbPath}` },
}),
).toBe(dbPath);
expect(
sqliteDatabasePathFromConfig({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'sqlite', path: `file:${pointerPath}`, readonly: true },
connection: { driver: 'sqlite', path: `file:${pointerPath}` },
}),
).toBe(dbPath);
expect(() =>
expect(
sqliteDatabasePathFromConfig({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'sqlite', path: 'warehouse.db', readonly: false },
connection: { driver: 'sqlite', path: 'warehouse.db' },
}),
).toThrow('Native SQLite connector requires connections.warehouse.readonly: true');
).toBe(dbPath);
} finally {
if (originalDatabaseUrl === undefined) {
delete process.env.KTX_SQLITE_TEST_URL;
@ -104,7 +102,7 @@ describe('KtxSqliteScanConnector', () => {
it('introspects schema, primary keys, row counts, views, and foreign keys', async () => {
const connector = new KtxSqliteScanConnector({
connectionId: 'warehouse',
connection: { driver: 'sqlite', path: dbPath, readonly: true },
connection: { driver: 'sqlite', path: dbPath },
now: () => new Date('2026-04-29T10:00:00.000Z'),
});
@ -151,7 +149,7 @@ describe('KtxSqliteScanConnector', () => {
it('runs samples, distinct values, statistics, and read-only SQL', async () => {
const connector = new KtxSqliteScanConnector({
connectionId: 'warehouse',
connection: { driver: 'sqlite', path: dbPath, readonly: true },
connection: { driver: 'sqlite', path: dbPath },
});
await expect(
@ -199,7 +197,7 @@ describe('KtxSqliteScanConnector', () => {
const introspection = createSqliteLiveDatabaseIntrospection({
projectDir: tempDir,
connections: {
warehouse: { driver: 'sqlite', path: 'warehouse.db', readonly: true },
warehouse: { driver: 'sqlite', path: 'warehouse.db' },
},
now: () => new Date('2026-04-29T10:00:00.000Z'),
});

View file

@ -29,7 +29,6 @@ export interface KtxSqliteConnectionConfig {
path?: string;
url?: string;
file_path?: string;
readonly?: boolean;
[key: string]: unknown;
}
@ -135,17 +134,17 @@ function stripLeadingSqlComments(sql: string): string {
return sql.slice(index);
}
export function isKtxSqliteConnectionConfig(connection: KtxSqliteConnectionConfig | undefined): boolean {
export function isKtxSqliteConnectionConfig(
connection: KtxSqliteConnectionConfig | undefined,
): connection is KtxSqliteConnectionConfig {
const driver = String(connection?.driver ?? '').toLowerCase();
return driver === 'sqlite' || driver === 'sqlite3';
}
export function sqliteDatabasePathFromConfig(input: SqliteDatabasePathInput): string {
const inputDriver = input.connection?.driver ?? 'unknown';
if (!isKtxSqliteConnectionConfig(input.connection)) {
throw new Error(`Native SQLite connector cannot run driver "${input.connection?.driver ?? 'unknown'}"`);
}
if (input.connection?.readonly !== true) {
throw new Error(`Native SQLite connector requires connections.${input.connectionId}.readonly: true`);
throw new Error(`Native SQLite connector cannot run driver "${inputDriver}"`);
}
const configuredPath =
stringConfigValue(input.connection, 'path') ??

View file

@ -145,7 +145,6 @@ describe('KtxSqlServerScanConnector', () => {
driver: 'sqlserver',
host: 'localhost',
database: 'analytics',
readonly: true,
}),
).toBe(true);
expect(isKtxSqlServerConnectionConfig({ driver: 'mysql', host: 'localhost', database: 'analytics' })).toBe(false);
@ -159,7 +158,6 @@ describe('KtxSqlServerScanConnector', () => {
database: 'analytics',
username: 'reader',
trustServerCertificate: false,
readonly: true,
},
}),
).toMatchObject({
@ -169,12 +167,6 @@ describe('KtxSqlServerScanConnector', () => {
user: 'reader',
options: { encrypt: true, trustServerCertificate: false },
});
expect(() =>
sqlServerConnectionPoolConfigFromConfig({
connectionId: 'warehouse',
connection: { driver: 'sqlserver', host: 'db.example.test', database: 'analytics', readonly: false },
}),
).toThrow('Native SQL Server connector requires connections.warehouse.readonly: true');
});
it('introspects schema, primary keys, comments, row counts, views, and foreign keys', async () => {
@ -186,7 +178,6 @@ describe('KtxSqlServerScanConnector', () => {
database: 'analytics',
username: 'reader',
schema: 'dbo',
readonly: true,
},
poolFactory: fakePoolFactory(),
now: () => new Date('2026-04-29T16:00:00.000Z'),
@ -246,7 +237,6 @@ describe('KtxSqlServerScanConnector', () => {
database: 'analytics',
username: 'reader',
schema: 'dbo',
readonly: true,
},
poolFactory,
});
@ -315,7 +305,6 @@ describe('KtxSqlServerScanConnector', () => {
database: 'analytics',
username: 'reader',
schema: 'dbo',
readonly: true,
},
},
poolFactory: fakePoolFactory(),

View file

@ -37,7 +37,6 @@ export interface KtxSqlServerConnectionConfig {
schema?: string;
schemas?: string[];
trustServerCertificate?: boolean;
readonly?: boolean;
[key: string]: unknown;
}
@ -234,7 +233,9 @@ function limitSqlForSqlServerExecution(sqlText: string, maxRows: number | undefi
return `SELECT TOP ${maxRows} * FROM (${trimmed}) AS ktx_query_result`;
}
export function isKtxSqlServerConnectionConfig(connection: KtxSqlServerConnectionConfig | undefined): boolean {
export function isKtxSqlServerConnectionConfig(
connection: KtxSqlServerConnectionConfig | undefined,
): connection is KtxSqlServerConnectionConfig {
return String(connection?.driver ?? '').toLowerCase() === 'sqlserver';
}
@ -243,11 +244,9 @@ export function sqlServerConnectionPoolConfigFromConfig(input: {
connection: KtxSqlServerConnectionConfig | undefined;
env?: NodeJS.ProcessEnv;
}): KtxSqlServerPoolConfig {
const inputDriver = input.connection?.driver ?? 'unknown';
if (!isKtxSqlServerConnectionConfig(input.connection)) {
throw new Error(`Native SQL Server connector cannot run driver "${input.connection?.driver ?? 'unknown'}"`);
}
if (input.connection?.readonly !== true) {
throw new Error(`Native SQL Server connector requires connections.${input.connectionId}.readonly: true`);
throw new Error(`Native SQL Server connector cannot run driver "${inputDriver}"`);
}
const env = input.env ?? process.env;

View file

@ -26,14 +26,14 @@ describe('createDefaultLocalQueryExecutor', () => {
await expect(
executor.execute({
connectionId: 'pg',
connection: { driver: 'postgres', readonly: true },
connection: { driver: 'postgres' },
sql: 'select 1',
}),
).resolves.toMatchObject({ headers: ['pg'] });
await expect(
executor.execute({
connectionId: 'local',
connection: { driver: 'sqlite', readonly: true },
connection: { driver: 'sqlite' },
sql: 'select 1',
}),
).resolves.toMatchObject({ headers: ['sqlite'] });
@ -51,7 +51,7 @@ describe('createDefaultLocalQueryExecutor', () => {
await expect(
executor.execute({
connectionId: 'warehouse',
connection: { driver: 'snowflake', readonly: true },
connection: { driver: 'snowflake' },
sql: 'select 1',
}),
).rejects.toThrow('No local query executor is configured for driver "snowflake".');

View file

@ -37,7 +37,7 @@ describe('createPostgresQueryExecutor', () => {
const result = await executor.execute({
connectionId: 'warehouse',
connection: { driver: 'postgres', url: 'postgres://example/db', readonly: true },
connection: { driver: 'postgres', url: 'postgres://example/db' },
sql: 'select status, count(*) as order_count from public.orders group by status',
maxRows: 50,
});
@ -80,7 +80,7 @@ describe('createPostgresQueryExecutor', () => {
await expect(
executor.execute({
connectionId: 'warehouse',
connection: { driver: 'postgres', url: 'postgres://example/db', readonly: true },
connection: { driver: 'postgres', url: 'postgres://example/db' },
sql: 'select * from broken',
maxRows: 10,
}),
@ -89,23 +89,15 @@ describe('createPostgresQueryExecutor', () => {
expect(client.end).toHaveBeenCalledTimes(1);
});
it('requires a Postgres url and read-only connection config', async () => {
it('requires a Postgres url', async () => {
const executor = createPostgresQueryExecutor({ clientFactory: vi.fn() });
await expect(
executor.execute({
connectionId: 'warehouse',
connection: { driver: 'postgres', readonly: true },
connection: { driver: 'postgres' },
sql: 'select 1',
}),
).rejects.toThrow('Local Postgres execution requires connections.warehouse.url');
await expect(
executor.execute({
connectionId: 'warehouse',
connection: { driver: 'postgres', url: 'postgres://example/db', readonly: false },
sql: 'select 1',
}),
).rejects.toThrow('Local query execution requires connections.warehouse.readonly: true');
});
});

View file

@ -37,18 +37,16 @@ export function createPostgresQueryExecutor(options: PostgresQueryExecutorOption
return {
async execute(input: KtxSqlQueryExecutionInput): Promise<KtxSqlQueryExecutionResult> {
const driver = connectionDriver(input);
const connection = input.connection;
if (driver !== 'postgres' && driver !== 'postgresql') {
throw new Error(`Local Postgres execution cannot run driver "${input.connection?.driver ?? 'unknown'}".`);
throw new Error(`Local Postgres execution cannot run driver "${connection?.driver ?? 'unknown'}".`);
}
if (input.connection?.readonly !== true) {
throw new Error(`Local query execution requires connections.${input.connectionId}.readonly: true.`);
}
if (typeof input.connection.url !== 'string' || input.connection.url.trim().length === 0) {
if (typeof connection?.url !== 'string' || connection.url.trim().length === 0) {
throw new Error(`Local Postgres execution requires connections.${input.connectionId}.url.`);
}
const client = clientFactory({
connectionString: input.connection.url,
connectionString: connection.url,
statement_timeout: options.statementTimeoutMs ?? 30_000,
query_timeout: options.queryTimeoutMs ?? 35_000,
connectionTimeoutMillis: options.connectionTimeoutMs ?? 5_000,

View file

@ -38,7 +38,7 @@ describe('createSqliteQueryExecutor', () => {
const result = await executor.execute({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'sqlite', path: 'warehouse.db', readonly: true },
connection: { driver: 'sqlite', path: 'warehouse.db' },
sql: 'select status, count(*) as order_count from orders group by status order by status',
maxRows: 10,
});
@ -60,7 +60,7 @@ describe('createSqliteQueryExecutor', () => {
sqliteDatabasePathFromConnection({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'sqlite', url: `file://${dbPath}`, readonly: true },
connection: { driver: 'sqlite', url: `file://${dbPath}` },
sql: 'select 1',
}),
).toBe(dbPath);
@ -74,7 +74,7 @@ describe('createSqliteQueryExecutor', () => {
sqliteDatabasePathFromConnection({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'sqlite', path: `file:${pointerPath}`, readonly: true },
connection: { driver: 'sqlite', path: `file:${pointerPath}` },
sql: 'select 1',
}),
).toBe(dbPath);
@ -89,7 +89,7 @@ describe('createSqliteQueryExecutor', () => {
sqliteDatabasePathFromConnection({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'sqlite', url: 'env:KTX_SQLITE_TEST_URL', readonly: true },
connection: { driver: 'sqlite', url: 'env:KTX_SQLITE_TEST_URL' },
sql: 'select 1',
}),
).toBe(dbPath);
@ -109,20 +109,20 @@ describe('createSqliteQueryExecutor', () => {
executor.execute({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'sqlite', path: 'warehouse.db', readonly: true },
connection: { driver: 'sqlite', path: 'warehouse.db' },
sql: 'delete from orders',
}),
).rejects.toThrow('Only read-only SELECT/WITH queries can be executed locally');
});
it('requires a SQLite driver, read-only config, and a database path', async () => {
it('requires a SQLite driver and a database path', async () => {
const executor = createSqliteQueryExecutor();
await expect(
executor.execute({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'postgres', path: 'warehouse.db', readonly: true },
connection: { driver: 'postgres', path: 'warehouse.db' },
sql: 'select 1',
}),
).rejects.toThrow('Local SQLite execution cannot run driver "postgres"');
@ -131,16 +131,7 @@ describe('createSqliteQueryExecutor', () => {
executor.execute({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'sqlite', path: 'warehouse.db', readonly: false },
sql: 'select 1',
}),
).rejects.toThrow('Local query execution requires connections.warehouse.readonly: true');
await expect(
executor.execute({
connectionId: 'warehouse',
projectDir: tempDir,
connection: { driver: 'sqlite', readonly: true },
connection: { driver: 'sqlite' },
sql: 'select 1',
}),
).rejects.toThrow('Local SQLite execution requires connections.warehouse.path or connections.warehouse.url');

View file

@ -54,9 +54,6 @@ export function sqliteDatabasePathFromConnection(input: KtxSqlQueryExecutionInpu
if (driver !== 'sqlite' && driver !== 'sqlite3') {
throw new Error(`Local SQLite execution cannot run driver "${input.connection?.driver ?? 'unknown'}".`);
}
if (input.connection?.readonly !== true) {
throw new Error(`Local query execution requires connections.${input.connectionId}.readonly: true.`);
}
const pathValue = stringConfigValue(input.connection, 'path');
const urlValue = stringConfigValue(input.connection, 'url');

View file

@ -45,7 +45,6 @@ describe('createDaemonLiveDatabaseIntrospection', () => {
warehouse: {
driver: 'postgres',
url: 'postgres://localhost:5432/warehouse',
readonly: true,
},
},
schemas: ['public'],
@ -157,7 +156,6 @@ describe('createDaemonLiveDatabaseIntrospection', () => {
warehouse: {
driver: 'postgresql',
url: 'postgres://localhost:5432/warehouse',
readonly: true,
},
},
baseUrl: `http://127.0.0.1:${address.port}`,
@ -186,20 +184,18 @@ describe('createDaemonLiveDatabaseIntrospection', () => {
}
});
it('requires a configured read-only postgres connection with a url', async () => {
it('requires a configured postgres connection with a url', async () => {
const introspection = createDaemonLiveDatabaseIntrospection({
connections: {
warehouse: {
driver: 'postgres',
url: 'postgres://localhost:5432/warehouse',
readonly: false,
},
},
runJson: vi.fn(async () => daemonResponse),
});
await expect(introspection.extractSchema('warehouse')).rejects.toThrow(
'Local live-database ingest requires connections.warehouse.readonly: true.',
'Local live-database ingest requires connections.warehouse.url.',
);
});
@ -210,7 +206,6 @@ describe('createDaemonLiveDatabaseIntrospection', () => {
warehouse: {
driver: 'snowflake',
url: 'snowflake://example',
readonly: true,
},
},
runJson,

View file

@ -162,9 +162,6 @@ function requirePostgresConnection(
if (driver !== 'postgres') {
throw new Error(`Local live-database ingest cannot run driver "${connection?.driver ?? 'unknown'}".`);
}
if (connection?.readonly !== true) {
throw new Error(`Local live-database ingest requires connections.${connectionId}.readonly: true.`);
}
if (typeof connection.url !== 'string' || connection.url.trim().length === 0) {
throw new Error(`Local live-database ingest requires connections.${connectionId}.url.`);
}

View file

@ -39,7 +39,6 @@ async function writeLiveDatabaseConfig(projectDir: string): Promise<void> {
' warehouse:',
' driver: postgres',
' url: postgres://localhost:5432/warehouse',
' readonly: true',
'ingest:',
' adapters:',
' - live-database',

View file

@ -75,7 +75,6 @@ describe('createLocalProjectMcpContextPorts', () => {
project.config.connections.warehouse = {
driver: 'postgres',
url: 'env:DATABASE_URL',
readonly: true,
};
const ports = createLocalProjectMcpContextPorts(project);
@ -89,7 +88,6 @@ describe('createLocalProjectMcpContextPorts', () => {
project.config.connections.warehouse = {
driver: 'postgres',
url: 'env:DATABASE_URL',
readonly: true,
};
const connector = testConnector();
const createConnector = vi.fn(async () => connector);
@ -125,7 +123,6 @@ describe('createLocalProjectMcpContextPorts', () => {
const project = await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
project.config.connections.warehouse = {
driver: 'postgres',
readonly: true,
};
project.config.ingest.adapters = ['fake'];
project.config.ingest.embeddings = {
@ -633,7 +630,6 @@ describe('createLocalProjectMcpContextPorts', () => {
project.config.connections.warehouse = {
driver: 'postgres',
url: 'env:DATABASE_URL',
readonly: true,
};
const shapeOnlyPorts = createLocalProjectMcpContextPorts(project);
await shapeOnlyPorts.semanticLayer?.writeSource({
@ -720,7 +716,6 @@ describe('createLocalProjectMcpContextPorts', () => {
project.config.connections.warehouse = {
driver: 'postgres',
url: 'env:DATABASE_URL',
readonly: true,
};
const shapeOnlyPorts = createLocalProjectMcpContextPorts(project);
await shapeOnlyPorts.semanticLayer?.writeSource({
@ -958,7 +953,6 @@ describe('createLocalProjectMcpContextPorts', () => {
project.config.connections.warehouse = {
driver: 'postgres',
url: 'postgres://localhost:5432/warehouse',
readonly: true,
};
project.config.ingest.adapters = ['live-database'];
project.config.llm = {
@ -1034,7 +1028,6 @@ describe('createLocalProjectMcpContextPorts', () => {
project.config.connections.warehouse = {
driver: 'postgres',
url: 'env:DATABASE_URL',
readonly: true,
};
project.config.ingest.adapters = ['live-database'];
const ports = createLocalProjectMcpContextPorts(project, {

View file

@ -145,7 +145,7 @@ describe('createLocalProjectMemoryCapture', () => {
it('captures a semantic-layer source for a named local connection id', async () => {
const project = await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
project.config.connections.warehouse = { driver: 'postgres', readonly: true };
project.config.connections.warehouse = { driver: 'postgres' };
const agentRunner = {
runLoop: async ({
toolSet,

View file

@ -69,7 +69,6 @@ export interface KtxProjectScanConfig {
export interface KtxProjectConnectionConfig {
driver: string;
url?: string;
readonly?: boolean;
[key: string]: unknown;
}

View file

@ -110,7 +110,6 @@ async function writeLiveDatabaseConfig(projectDir: string): Promise<void> {
' warehouse:',
' driver: postgres',
' url: env:DATABASE_URL',
' readonly: true',
'ingest:',
' adapters:',
' - live-database',
@ -1006,7 +1005,6 @@ describe('local scan', () => {
' warehouse:',
' driver: postgres',
' url: env:DATABASE_URL',
' readonly: true',
'ingest:',
' adapters:',
' - live-database',
@ -1363,7 +1361,6 @@ describe('local scan', () => {
' warehouse:',
' driver: sqlite',
' path: warehouse.db',
' readonly: true',
'ingest:',
' adapters:',
' - live-database',
@ -1396,7 +1393,6 @@ describe('local scan', () => {
' warehouse:',
' driver: mysql',
' url: env:MYSQL_URL',
' readonly: true',
'ingest:',
' adapters:',
' - live-database',
@ -1432,7 +1428,6 @@ describe('local scan', () => {
' database: analytics',
' username: reader',
' password: env:CLICKHOUSE_PASSWORD',
' readonly: true',
'ingest:',
' adapters:',
' - live-database',
@ -1468,7 +1463,6 @@ describe('local scan', () => {
' database: analytics',
' username: reader',
' schema: dbo',
' readonly: true',
'ingest:',
' adapters:',
' - live-database',

View file

@ -24,7 +24,6 @@ async function writeWarehouseConfig(projectDir: string): Promise<void> {
' warehouse:',
' driver: sqlite',
' path: warehouse.db',
' readonly: true',
'ingest:',
' adapters:',
' - live-database',

View file

@ -28,7 +28,6 @@ async function createProject(projectDir: string): Promise<void> {
' warehouse:',
' driver: sqlite',
' path: warehouse.db',
' readonly: true',
'ingest:',
' adapters:',
' - live-database',

View file

@ -14,7 +14,7 @@ describe('compileLocalSlQuery', () => {
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'ktx-local-query-'));
project = await initKtxProject({ projectDir: join(tempDir, 'project'), projectName: 'warehouse' });
project.config.connections.warehouse = { driver: 'postgres', readonly: true };
project.config.connections.warehouse = { driver: 'postgres' };
await project.fileStore.writeFile(
'semantic-layer/warehouse/orders.yaml',
`name: orders
@ -222,7 +222,7 @@ grain: []
expect(queryExecutor.execute).toHaveBeenCalledWith({
connectionId: 'warehouse',
projectDir: project.projectDir,
connection: { driver: 'postgres', readonly: true },
connection: { driver: 'postgres' },
sql: 'select status, count(*) as order_count from public.orders group by status',
maxRows: 10,
});
@ -248,7 +248,7 @@ grain: []
});
it('requires connectionId when multiple connections are configured', async () => {
project.config.connections.analytics = { driver: 'bigquery', readonly: true };
project.config.connections.analytics = { driver: 'bigquery' };
await expect(
compileLocalSlQuery(project, {

View file

@ -231,7 +231,6 @@ async function main() {
driver: 'sqlserver',
url,
schemas: ['dbo', 'HumanResources', 'Person', 'Production', 'Purchasing', 'Sales'],
readonly: true,
trustServerCertificate: true,
},
now: () => new Date('2026-05-07T00:00:00.000Z'),

View file

@ -50,7 +50,6 @@ describe('standalone example docs', () => {
config,
/path: \.\.\/\.\.\/packages\/context\/test\/fixtures\/relationship-benchmarks\/orbit_style_product_no_declared_constraints\/data\.sqlite/,
);
assert.match(config, /readonly: true/);
assert.match(config, /llm_proposals: false/);
assert.match(config, /validation_required_for_manifest: true/);
});

View file

@ -92,7 +92,6 @@ export function buildKtxYaml(postgresUrl) {
' warehouse:',
' driver: postgres',
` url: "${postgresUrl}"`,
' readonly: true',
'storage:',
' state: sqlite',
' search: sqlite-fts5',

View file

@ -59,7 +59,6 @@ describe('installed live-database artifact smoke helpers', () => {
' warehouse:',
' driver: postgres',
' url: "postgresql://ktx:postgres@127.0.0.1:15432/warehouse"', // pragma: allowlist secret
' readonly: true',
'storage:',
' state: sqlite',
' search: sqlite-fts5',

View file

@ -646,7 +646,6 @@ try {
' warehouse:',
' driver: sqlite',
' path: warehouse.db',
' readonly: true',
'storage:',
' state: sqlite',
' search: sqlite-fts5',