mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
fix: stop requiring readonly connection config
This commit is contained in:
parent
754e4a9039
commit
7824b7f3b6
55 changed files with 103 additions and 292 deletions
|
|
@ -63,8 +63,7 @@ agents.
|
|||
"connections": [
|
||||
{
|
||||
"id": "my-warehouse",
|
||||
"driver": "postgres",
|
||||
"readonly": false
|
||||
"driver": "postgres"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ project: local-warehouse
|
|||
connections:
|
||||
warehouse:
|
||||
driver: postgres
|
||||
readonly: true
|
||||
storage:
|
||||
state: sqlite
|
||||
search: sqlite-fts5
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -275,7 +275,6 @@ describe('runKtxDoctor', () => {
|
|||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:WAREHOUSE_DATABASE_URL',
|
||||
' readonly: true',
|
||||
' historicSql:',
|
||||
' enabled: true',
|
||||
' dialect: postgres',
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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<{
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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') ??
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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".');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ export interface KtxProjectScanConfig {
|
|||
export interface KtxProjectConnectionConfig {
|
||||
driver: string;
|
||||
url?: string;
|
||||
readonly?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ async function writeWarehouseConfig(projectDir: string): Promise<void> {
|
|||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
' path: warehouse.db',
|
||||
' readonly: true',
|
||||
'ingest:',
|
||||
' adapters:',
|
||||
' - live-database',
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ async function createProject(projectDir: string): Promise<void> {
|
|||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
' path: warehouse.db',
|
||||
' readonly: true',
|
||||
'ingest:',
|
||||
' adapters:',
|
||||
' - live-database',
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ export function buildKtxYaml(postgresUrl) {
|
|||
' warehouse:',
|
||||
' driver: postgres',
|
||||
` url: "${postgresUrl}"`,
|
||||
' readonly: true',
|
||||
'storage:',
|
||||
' state: sqlite',
|
||||
' search: sqlite-fts5',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -646,7 +646,6 @@ try {
|
|||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
' path: warehouse.db',
|
||||
' readonly: true',
|
||||
'storage:',
|
||||
' state: sqlite',
|
||||
' search: sqlite-fts5',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue