mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-13 08:15:14 +02:00
fix(setup): require explicit no-input database scope (#286)
* test(setup): supply explicit --no-input scope to disabled-mode database tests * fix(setup): require explicit database scope in --no-input instead of auto-scanning the warehouse * docs(setup): document --no-input database scope requirement
This commit is contained in:
parent
036a745fc1
commit
853f39a7c3
4 changed files with 138 additions and 47 deletions
|
|
@ -125,6 +125,14 @@ incomplete.
|
|||
MySQL, and SQL Server; `schema_names` for Snowflake; `dataset_ids` for
|
||||
BigQuery; and `databases` for ClickHouse.
|
||||
|
||||
With `--no-input`, scope for a scope-bearing driver (PostgreSQL, MySQL,
|
||||
ClickHouse, SQL Server, BigQuery, Snowflake) must come from `--database-schema`
|
||||
or from existing connection config in `ktx.yaml` (for example
|
||||
`connections.<id>.dataset_ids`). When neither is set, the database step fails
|
||||
fast and prints the missing scope flag and config key — non-interactive setup
|
||||
never auto-discovers and scans every schema. SQLite has no scope and is
|
||||
unaffected.
|
||||
|
||||
### Query History
|
||||
|
||||
| Flag | Description |
|
||||
|
|
|
|||
|
|
@ -1382,35 +1382,32 @@ async function maybeConfigureDatabaseScope(input: {
|
|||
const cliSchemas = input.args.databaseSchemas;
|
||||
|
||||
if (input.args.inputMode === 'disabled') {
|
||||
if (spec) {
|
||||
let scopeToWrite: string[] = cliSchemas;
|
||||
if (scopeToWrite.length === 0) {
|
||||
try {
|
||||
scopeToWrite = unique(
|
||||
await (input.deps.listSchemas ?? defaultListSchemas)(input.projectDir, input.connectionId),
|
||||
);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
input.io.stderr.write(
|
||||
`Could not discover ${spec.promptLabel.toLowerCase()} for ${input.connectionId}; ${detail}\n`,
|
||||
);
|
||||
return okValidateResult();
|
||||
}
|
||||
}
|
||||
if (scopeToWrite.length > 0) {
|
||||
await writeScopeConfig({
|
||||
projectDir: input.projectDir,
|
||||
connectionId: input.connectionId,
|
||||
values: scopeToWrite,
|
||||
spec,
|
||||
});
|
||||
const capitalNounPlural = spec.nounPlural[0]!.toUpperCase() + spec.nounPlural.slice(1);
|
||||
writeSetupSection(input.io, `${capitalNounPlural} saved for ${input.connectionId}`, [
|
||||
`✓ ${scopeToWrite.join(', ')}`,
|
||||
]);
|
||||
}
|
||||
if (!spec) {
|
||||
return okValidateResult();
|
||||
}
|
||||
return okValidateResult();
|
||||
if (cliSchemas.length > 0) {
|
||||
await writeScopeConfig({
|
||||
projectDir: input.projectDir,
|
||||
connectionId: input.connectionId,
|
||||
values: cliSchemas,
|
||||
spec,
|
||||
});
|
||||
const capitalNounPlural = spec.nounPlural[0]!.toUpperCase() + spec.nounPlural.slice(1);
|
||||
writeSetupSection(input.io, `${capitalNounPlural} saved for ${input.connectionId}`, [
|
||||
`✓ ${cliSchemas.join(', ')}`,
|
||||
]);
|
||||
return okValidateResult();
|
||||
}
|
||||
if (existingScope.length > 0) {
|
||||
return okValidateResult();
|
||||
}
|
||||
writePrefixedLines(
|
||||
(chunk) => input.io.stderr.write(chunk),
|
||||
`No ${spec.nounPlural} configured for ${input.connectionId}. ` +
|
||||
`Pass --database-schema <${spec.noun}> (repeatable) or set ` +
|
||||
`connections.${input.connectionId}.${spec.configArrayField} in ktx.yaml.`,
|
||||
);
|
||||
return failedValidateResult();
|
||||
}
|
||||
|
||||
if (spec && cliSchemas.length > 0) {
|
||||
|
|
|
|||
|
|
@ -2145,17 +2145,11 @@ describe('setup databases step', () => {
|
|||
expect(listTables).toHaveBeenCalledWith(tempDir, 'postgres-warehouse', ['analytics']);
|
||||
});
|
||||
|
||||
it('auto-selects all discovered Postgres schemas in non-interactive setup', async () => {
|
||||
it('fails non-interactive setup when a scope-bearing connection has no schema configured', async () => {
|
||||
const io = makeIo();
|
||||
const prompts = makePromptAdapter({});
|
||||
const testConnection = vi.fn(async () => 0);
|
||||
const scanConnection = vi.fn(async asyncScanProjectDir => {
|
||||
const config = parseKtxProjectConfig(await readFile(join(asyncScanProjectDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.connections.warehouse).toMatchObject({
|
||||
schemas: ['orbit_analytics', 'orbit_raw', 'public'],
|
||||
});
|
||||
return 0;
|
||||
});
|
||||
const scanConnection = vi.fn(async () => 0);
|
||||
const listSchemas = vi.fn(async () => ['orbit_analytics', 'orbit_raw', 'public']);
|
||||
|
||||
const result = await runKtxSetupDatabasesStep(
|
||||
|
|
@ -2172,13 +2166,93 @@ describe('setup databases step', () => {
|
|||
{ prompts, testConnection, scanConnection, listSchemas },
|
||||
);
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(listSchemas).not.toHaveBeenCalled();
|
||||
expect(scanConnection).not.toHaveBeenCalled();
|
||||
expect(io.stderr()).toContain('--database-schema');
|
||||
expect(io.stderr()).toContain('connections.warehouse.schemas');
|
||||
});
|
||||
|
||||
it('preserves existing BigQuery dataset_ids in non-interactive setup without rediscovering', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'connections:',
|
||||
' bigquery-warehouse:',
|
||||
' driver: bigquery',
|
||||
' dataset_ids:',
|
||||
" - 'sales'",
|
||||
' credentials_json: env:BIGQUERY_CREDENTIALS_JSON',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const io = makeIo();
|
||||
const testConnection = vi.fn(async () => 0);
|
||||
const scanConnection = vi.fn(async () => 0);
|
||||
const listSchemas = vi.fn(async () => ['sales', 'stripe', 'posthog', 'linear']);
|
||||
|
||||
const result = await runKtxSetupDatabasesStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
databaseConnectionIds: ['bigquery-warehouse'],
|
||||
databaseSchemas: [],
|
||||
skipDatabases: false,
|
||||
},
|
||||
io.io,
|
||||
{ testConnection, scanConnection, listSchemas },
|
||||
);
|
||||
|
||||
expect(result.status).toBe('ready');
|
||||
expect(prompts.multiselect).not.toHaveBeenCalled();
|
||||
expect(listSchemas).not.toHaveBeenCalled();
|
||||
expect(scanConnection).toHaveBeenCalledWith(tempDir, 'bigquery-warehouse', expect.anything());
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.connections['bigquery-warehouse']).toMatchObject({
|
||||
driver: 'bigquery',
|
||||
dataset_ids: ['sales'],
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves existing Postgres schemas in non-interactive setup without rediscovering', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
' schemas:',
|
||||
" - 'analytics'",
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const io = makeIo();
|
||||
const testConnection = vi.fn(async () => 0);
|
||||
const scanConnection = vi.fn(async () => 0);
|
||||
const listSchemas = vi.fn(async () => ['analytics', 'raw', 'public']);
|
||||
|
||||
const result = await runKtxSetupDatabasesStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
databaseConnectionIds: ['warehouse'],
|
||||
databaseSchemas: [],
|
||||
skipDatabases: false,
|
||||
},
|
||||
io.io,
|
||||
{ testConnection, scanConnection, listSchemas },
|
||||
);
|
||||
|
||||
expect(result.status).toBe('ready');
|
||||
expect(listSchemas).not.toHaveBeenCalled();
|
||||
expect(scanConnection).toHaveBeenCalledWith(tempDir, 'warehouse', expect.anything());
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.connections.warehouse).toMatchObject({
|
||||
schemas: ['orbit_analytics', 'orbit_raw', 'public'],
|
||||
driver: 'postgres',
|
||||
schemas: ['analytics'],
|
||||
});
|
||||
expect(io.stdout()).toContain('✓ orbit_analytics, orbit_raw, public');
|
||||
});
|
||||
|
||||
it('adds one non-interactive Postgres URL connection, tests it, scans it, and marks databases complete', async () => {
|
||||
|
|
@ -2265,6 +2339,8 @@ describe('setup databases step', () => {
|
|||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
' schemas:',
|
||||
" - 'public'",
|
||||
' analytics:',
|
||||
' driver: snowflake',
|
||||
' authMethod: password',
|
||||
|
|
@ -2312,7 +2388,7 @@ describe('setup databases step', () => {
|
|||
databaseDrivers: ['postgres'],
|
||||
databaseConnectionId: 'warehouse',
|
||||
databaseUrl: 'env:DATABASE_URL',
|
||||
databaseSchemas: [],
|
||||
databaseSchemas: ['public'],
|
||||
skipDatabases: false,
|
||||
},
|
||||
io.io,
|
||||
|
|
@ -2342,7 +2418,7 @@ describe('setup databases step', () => {
|
|||
databaseDrivers: ['postgres'],
|
||||
databaseConnectionId: 'warehouse',
|
||||
databaseUrl: 'env:DATABASE_URL',
|
||||
databaseSchemas: [],
|
||||
databaseSchemas: ['public'],
|
||||
skipDatabases: false,
|
||||
},
|
||||
io.io,
|
||||
|
|
@ -2409,7 +2485,7 @@ describe('setup databases step', () => {
|
|||
databaseDrivers: ['postgres'],
|
||||
databaseConnectionId: 'warehouse',
|
||||
databaseUrl: 'env:DATABASE_URL',
|
||||
databaseSchemas: [],
|
||||
databaseSchemas: ['public'],
|
||||
skipDatabases: false,
|
||||
},
|
||||
io.io,
|
||||
|
|
@ -2470,7 +2546,7 @@ describe('setup databases step', () => {
|
|||
inputMode: 'disabled',
|
||||
databaseDrivers: ['snowflake'],
|
||||
databaseConnectionId: 'snowflake',
|
||||
databaseSchemas: [],
|
||||
databaseSchemas: ['PUBLIC'],
|
||||
enableQueryHistory: true,
|
||||
queryHistoryWindowDays: 30,
|
||||
queryHistoryServiceAccountPatterns: ['^svc_'],
|
||||
|
|
@ -2532,7 +2608,7 @@ describe('setup databases step', () => {
|
|||
inputMode: 'disabled',
|
||||
databaseDrivers: ['snowflake'],
|
||||
databaseConnectionId: 'snowflake',
|
||||
databaseSchemas: [],
|
||||
databaseSchemas: ['PUBLIC'],
|
||||
skipDatabases: false,
|
||||
},
|
||||
io.io,
|
||||
|
|
@ -2818,6 +2894,8 @@ describe('setup databases step', () => {
|
|||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
' schemas:',
|
||||
" - 'public'",
|
||||
' context:',
|
||||
' queryHistory:',
|
||||
' enabled: true',
|
||||
|
|
@ -3172,6 +3250,8 @@ describe('setup databases step', () => {
|
|||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
' schemas:',
|
||||
" - 'public'",
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
|
|
@ -3228,6 +3308,8 @@ describe('setup databases step', () => {
|
|||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' readonly: true',
|
||||
' schemas:',
|
||||
" - 'public'",
|
||||
' historicSql:',
|
||||
' enabled: true',
|
||||
' dialect: postgres',
|
||||
|
|
@ -3324,7 +3406,7 @@ describe('setup databases step', () => {
|
|||
databaseDrivers: ['postgres'],
|
||||
databaseConnectionId: 'warehouse',
|
||||
databaseUrl: 'env:DATABASE_URL',
|
||||
databaseSchemas: [],
|
||||
databaseSchemas: ['public'],
|
||||
enableQueryHistory: true,
|
||||
skipDatabases: false,
|
||||
},
|
||||
|
|
@ -3373,7 +3455,7 @@ describe('setup databases step', () => {
|
|||
inputMode: 'disabled',
|
||||
databaseDrivers: ['snowflake'],
|
||||
databaseConnectionId: 'warehouse',
|
||||
databaseSchemas: [],
|
||||
databaseSchemas: ['PUBLIC'],
|
||||
enableQueryHistory: true,
|
||||
skipDatabases: false,
|
||||
},
|
||||
|
|
@ -3485,7 +3567,7 @@ describe('setup databases step', () => {
|
|||
databaseDrivers: ['postgres'],
|
||||
databaseConnectionId: 'replay',
|
||||
databaseUrl: 'env:DATABASE_URL',
|
||||
databaseSchemas: [],
|
||||
databaseSchemas: ['public'],
|
||||
skipDatabases: false,
|
||||
},
|
||||
io.io,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,10 @@ Do not discover these inputs across multiple setup runs.
|
|||
--skip-agents
|
||||
```
|
||||
|
||||
- `--database-schema` is required for scope-bearing drivers (Postgres,
|
||||
MySQL, ClickHouse, SQL Server, BigQuery, Snowflake) in `--no-input`:
|
||||
setup fails fast without it unless the connection already has scope in
|
||||
`ktx.yaml`. SQLite needs no scope.
|
||||
- Configure one new database connection per setup invocation. For multiple
|
||||
connections, rerun setup once per connection.
|
||||
- Pasting a literal `--database-url` is safe: the CLI relocates the URL
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue