@kaelio/ktx 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/python/kaelio_ktx-0.13.0-py3-none-any.whl +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/admin.js +1 -1
- package/dist/clack.d.ts +16 -0
- package/dist/clack.js +37 -6
- package/dist/claude-code-prompt-caching.js +1 -1
- package/dist/cli-program.js +3 -3
- package/dist/cli-runtime.js +2 -2
- package/dist/commands/connection-commands.js +1 -1
- package/dist/commands/ingest-commands.js +4 -4
- package/dist/commands/mcp-commands.js +12 -12
- package/dist/commands/runtime-commands.js +4 -4
- package/dist/commands/setup-commands.js +19 -5
- package/dist/commands/sl-commands.js +1 -1
- package/dist/commands/sql-commands.js +1 -1
- package/dist/commands/status-commands.js +1 -1
- package/dist/connection.js +15 -3
- package/dist/connectors/bigquery/connector.js +1 -14
- package/dist/connectors/clickhouse/connector.js +2 -16
- package/dist/connectors/duckdb/federated-attach.d.ts +7 -0
- package/dist/connectors/duckdb/federated-attach.js +86 -0
- package/dist/connectors/duckdb/federated-executor.d.ts +5 -0
- package/dist/connectors/duckdb/federated-executor.js +59 -0
- package/dist/connectors/mysql/connector.js +2 -16
- package/dist/connectors/postgres/connector.js +1 -14
- package/dist/connectors/shared/string-reference.d.ts +6 -0
- package/dist/connectors/shared/string-reference.js +19 -0
- package/dist/connectors/snowflake/connector.d.ts +1 -1
- package/dist/connectors/snowflake/connector.js +1 -14
- package/dist/connectors/sqlite/connector.js +2 -25
- package/dist/connectors/sqlserver/connector.js +4 -17
- package/dist/context/connections/connection-type.d.ts +1 -1
- package/dist/context/connections/federation.d.ts +33 -0
- package/dist/context/connections/federation.js +51 -0
- package/dist/context/connections/local-warehouse-descriptor.d.ts +2 -0
- package/dist/context/connections/project-sql-executor.d.ts +18 -0
- package/dist/context/connections/project-sql-executor.js +39 -0
- package/dist/context/connections/query-executor.d.ts +2 -2
- package/dist/context/connections/read-only-sql.d.ts +1 -0
- package/dist/context/connections/read-only-sql.js +119 -4
- package/dist/context/connections/resolve-connection.d.ts +12 -0
- package/dist/context/connections/resolve-connection.js +37 -0
- package/dist/context/core/git-env.d.ts +4 -0
- package/dist/context/core/git-env.js +5 -1
- package/dist/context/core/git.service.d.ts +23 -0
- package/dist/context/core/git.service.js +71 -8
- package/dist/context/ingest/adapters/historic-sql/projection.js +2 -1
- package/dist/context/ingest/adapters/live-database/manifest.d.ts +3 -0
- package/dist/context/ingest/adapters/live-database/manifest.js +19 -11
- package/dist/context/ingest/adapters/looker/client.js +7 -2
- package/dist/context/ingest/adapters/looker/factory.d.ts +8 -1
- package/dist/context/ingest/adapters/looker/factory.js +9 -0
- package/dist/context/ingest/adapters/looker/mapping.js +1 -1
- package/dist/context/ingest/adapters/looker/types.d.ts +1 -1
- package/dist/context/ingest/adapters/metabase/client.d.ts +1 -1
- package/dist/context/ingest/adapters/metabase/client.js +1 -1
- package/dist/context/ingest/adapters/metabase/local-metabase.adapter.js +1 -1
- package/dist/context/ingest/adapters/metabase/mapping.js +6 -6
- package/dist/context/ingest/artifact-gates.d.ts +2 -6
- package/dist/context/ingest/artifact-gates.js +5 -47
- package/dist/context/ingest/constrained-repair.d.ts +55 -0
- package/dist/context/ingest/constrained-repair.js +167 -0
- package/dist/context/ingest/final-gate-repair.d.ts +9 -11
- package/dist/context/ingest/final-gate-repair.js +40 -128
- package/dist/context/ingest/finalization-scope.d.ts +1 -1
- package/dist/context/ingest/finalization-scope.js +15 -15
- package/dist/context/ingest/ingest-bundle.runner.d.ts +1 -0
- package/dist/context/ingest/ingest-bundle.runner.js +101 -67
- package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +6 -13
- package/dist/context/ingest/isolated-diff/patch-integrator.js +32 -109
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +8 -9
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +63 -141
- package/dist/context/ingest/local-bundle-runtime.d.ts +2 -0
- package/dist/context/ingest/local-bundle-runtime.js +9 -10
- package/dist/context/ingest/local-ingest.d.ts +2 -0
- package/dist/context/ingest/local-ingest.js +2 -0
- package/dist/context/ingest/memory-flow/view-model.js +1 -1
- package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -6
- package/dist/context/ingest/stages/stage-3-work-units.js +2 -1
- package/dist/context/ingest/stages/validate-wu-sources.d.ts +7 -1
- package/dist/context/ingest/stages/validate-wu-sources.js +109 -4
- package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.d.ts +2 -0
- package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.js +1 -1
- package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +3 -3
- package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.d.ts +3 -1
- package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +15 -1
- package/dist/context/llm/ai-sdk-runtime.js +2 -2
- package/dist/context/llm/claude-code-runtime.js +19 -3
- package/dist/context/llm/local-config.js +1 -1
- package/dist/context/llm/runtime-tools.js +2 -2
- package/dist/context/mcp/context-tools.js +33 -8
- package/dist/context/mcp/local-project-ports.js +63 -89
- package/dist/context/mcp/types.d.ts +2 -0
- package/dist/context/memory/local-memory.js +4 -1
- package/dist/context/memory/memory-agent.service.js +1 -1
- package/dist/context/project/config.d.ts +11 -4
- package/dist/context/project/config.js +85 -30
- package/dist/context/project/driver-schemas.js +1 -1
- package/dist/context/project/mappings-yaml-schema.js +2 -2
- package/dist/context/project/project.js +12 -4
- package/dist/context/scan/description-generation.js +4 -4
- package/dist/context/scan/local-enrichment-artifacts.js +33 -4
- package/dist/context/scan/local-scan.js +2 -2
- package/dist/context/scan/local-structural-artifacts.js +5 -5
- package/dist/context/scan/relationship-benchmark-report.js +1 -1
- package/dist/context/scan/relationship-discovery.js +3 -3
- package/dist/context/scan/relationship-llm-proposal.js +3 -3
- package/dist/context/sl/local-query.js +31 -44
- package/dist/context/sl/local-sl.d.ts +0 -8
- package/dist/context/sl/local-sl.js +71 -70
- package/dist/context/sl/semantic-layer.service.d.ts +25 -8
- package/dist/context/sl/semantic-layer.service.js +109 -56
- package/dist/context/sl/source-files.d.ts +48 -0
- package/dist/context/sl/source-files.js +138 -0
- package/dist/context/sl/tools/base-semantic-layer.tool.d.ts +2 -2
- package/dist/context/sl/tools/base-semantic-layer.tool.js +2 -7
- package/dist/context/sl/tools/sl-edit-source.tool.js +10 -8
- package/dist/context/sl/tools/sl-warehouse-validation.js +55 -27
- package/dist/context/sl/tools/sl-write-source.tool.js +12 -9
- package/dist/context/sql-analysis/dialect.d.ts +2 -0
- package/dist/context/sql-analysis/dialect.js +20 -0
- package/dist/context/tools/base-tool.d.ts +6 -19
- package/dist/context/tools/base-tool.js +0 -14
- package/dist/context-build-view.js +5 -5
- package/dist/database-tree-picker.js +18 -3
- package/dist/demo-assets.js +0 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -23
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +44 -0
- package/dist/ingest-query-executor.d.ts +2 -0
- package/dist/ingest-query-executor.js +8 -22
- package/dist/ingest.d.ts +1 -1
- package/dist/ingest.js +8 -2
- package/dist/io/symbols.d.ts +2 -0
- package/dist/io/symbols.js +2 -0
- package/dist/io/tty.d.ts +8 -0
- package/dist/io/tty.js +16 -0
- package/dist/llm/embedding-health.js +1 -1
- package/dist/llm/embedding-provider.js +3 -3
- package/dist/llm/model-provider.js +1 -1
- package/dist/local-adapters.d.ts +1 -0
- package/dist/local-adapters.js +2 -2
- package/dist/local-scan-connectors.js +1 -1
- package/dist/managed-local-embeddings.js +17 -8
- package/dist/managed-mcp-daemon.js +3 -3
- package/dist/managed-python-command.d.ts +7 -0
- package/dist/managed-python-command.js +34 -8
- package/dist/managed-python-daemon.js +2 -2
- package/dist/managed-python-http.js +3 -3
- package/dist/managed-python-runtime.d.ts +30 -1
- package/dist/managed-python-runtime.js +134 -18
- package/dist/managed-uv-release.d.ts +7 -0
- package/dist/managed-uv-release.js +11 -0
- package/dist/mcp-http-server.js +4 -4
- package/dist/mcp-server-factory.js +3 -3
- package/dist/mcp-stdio-server.js +1 -1
- package/dist/memory-flow-hud.js +2 -2
- package/dist/next-steps.js +2 -2
- package/dist/prompt-navigation.d.ts +17 -0
- package/dist/prompt-navigation.js +49 -3
- package/dist/prompts/memory_agent_bundle_ingest_work_unit.md +2 -2
- package/dist/prompts/memory_agent_external_ingest.md +2 -2
- package/dist/public-ingest-copy.js +1 -1
- package/dist/public-ingest.js +3 -3
- package/dist/release-version.js +1 -1
- package/dist/runtime-requirements.js +1 -1
- package/dist/runtime.js +9 -9
- package/dist/scan.js +1 -1
- package/dist/setup-agents.d.ts +21 -15
- package/dist/setup-agents.js +143 -66
- package/dist/setup-banner.d.ts +20 -0
- package/dist/setup-banner.js +39 -0
- package/dist/setup-context.js +24 -15
- package/dist/setup-databases.d.ts +3 -0
- package/dist/setup-databases.js +47 -59
- package/dist/setup-demo-tour.js +12 -8
- package/dist/setup-embeddings.js +9 -9
- package/dist/setup-interrupt.js +1 -1
- package/dist/setup-models.d.ts +4 -1
- package/dist/setup-models.js +54 -28
- package/dist/setup-project.js +29 -5
- package/dist/setup-prompts.js +16 -1
- package/dist/setup-ready-menu.js +1 -1
- package/dist/setup-sources.js +28 -12
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +14 -13
- package/dist/skills/analytics/SKILL.md +3 -3
- package/dist/skills/dbt_ingest/SKILL.md +3 -3
- package/dist/skills/looker_ingest/SKILL.md +3 -3
- package/dist/skills/lookml_ingest/SKILL.md +7 -7
- package/dist/skills/metabase_ingest/SKILL.md +4 -4
- package/dist/skills/metricflow_ingest/SKILL.md +15 -15
- package/dist/skills/notion_synthesize/SKILL.md +1 -1
- package/dist/skills/sl/SKILL.md +3 -3
- package/dist/skills/sl_capture/SKILL.md +1 -1
- package/dist/skills/wiki_capture/SKILL.md +1 -1
- package/dist/source-mapping.js +1 -1
- package/dist/sql.d.ts +2 -0
- package/dist/sql.js +35 -53
- package/dist/startup-profile.js +1 -1
- package/dist/status-project.d.ts +0 -2
- package/dist/status-project.js +4 -6
- package/dist/telemetry/events.d.ts +3 -2
- package/dist/telemetry/events.js +11 -1
- package/dist/telemetry/exception.js +14 -0
- package/dist/text-ingest.js +1 -1
- package/dist/tree-picker-tui.d.ts +0 -1
- package/dist/tree-picker-tui.js +2 -3
- package/package.json +2 -1
- package/assets/python/kaelio_ktx-0.11.0-py3-none-any.whl +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DuckDBInstance } from '@duckdb/node-api';
|
|
2
|
+
import { federatedAttachTarget } from './federated-attach.js';
|
|
3
|
+
import { normalizeQueryRows } from '../../context/connections/query-executor.js';
|
|
4
|
+
import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js';
|
|
5
|
+
import { attachTypeForDriver } from '../../context/connections/federation.js';
|
|
6
|
+
function quoteDuckdbIdentifier(id) {
|
|
7
|
+
return `"${id.replaceAll('"', '""')}"`;
|
|
8
|
+
}
|
|
9
|
+
const MIN_SAFE_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
|
|
10
|
+
const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
|
11
|
+
// DuckDB returns integer columns as JS bigint (unserializable by JSON). Values
|
|
12
|
+
// in Number's safe range become Number; larger magnitudes become strings so a
|
|
13
|
+
// BIGINT beyond 2^53 keeps its exact value instead of silently rounding.
|
|
14
|
+
function jsonSafeBigint(value) {
|
|
15
|
+
return value >= MIN_SAFE_BIGINT && value <= MAX_SAFE_BIGINT ? Number(value) : value.toString();
|
|
16
|
+
}
|
|
17
|
+
function toJsonSafeRows(rows) {
|
|
18
|
+
return rows.map((row) => row.map((cell) => (typeof cell === 'bigint' ? jsonSafeBigint(cell) : cell)));
|
|
19
|
+
}
|
|
20
|
+
/** @internal */
|
|
21
|
+
export function buildAttachStatements(members, env) {
|
|
22
|
+
const attachments = members.map((member) => ({
|
|
23
|
+
type: attachTypeForDriver(member.driver),
|
|
24
|
+
url: federatedAttachTarget(member, env),
|
|
25
|
+
alias: member.connectionId,
|
|
26
|
+
}));
|
|
27
|
+
const loadStatements = [...new Set(attachments.map((a) => a.type))].map((type) => `INSTALL ${type}; LOAD ${type};`);
|
|
28
|
+
const attachStatements = attachments.map(({ type, url, alias }) => `ATTACH '${url.replaceAll("'", "''")}' AS ${quoteDuckdbIdentifier(alias)} (TYPE ${type}, READ_ONLY);`);
|
|
29
|
+
return [...loadStatements, ...attachStatements];
|
|
30
|
+
}
|
|
31
|
+
export async function executeFederatedQuery(members, input, env = process.env) {
|
|
32
|
+
const sql = limitSqlForExecution(assertReadOnlySql(input.sql), input.maxRows);
|
|
33
|
+
const attachStatements = buildAttachStatements(members, env);
|
|
34
|
+
const instance = await DuckDBInstance.create(':memory:');
|
|
35
|
+
try {
|
|
36
|
+
const connection = await instance.connect();
|
|
37
|
+
try {
|
|
38
|
+
for (const statement of attachStatements) {
|
|
39
|
+
await connection.run(statement);
|
|
40
|
+
}
|
|
41
|
+
const reader = await connection.runAndReadAll(sql);
|
|
42
|
+
const rows = toJsonSafeRows(normalizeQueryRows(reader.getRows()));
|
|
43
|
+
const headers = reader.columnNames();
|
|
44
|
+
return {
|
|
45
|
+
headers,
|
|
46
|
+
rows,
|
|
47
|
+
totalRows: rows.length,
|
|
48
|
+
command: 'SELECT',
|
|
49
|
+
rowCount: rows.length,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
connection.closeSync();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
instance.closeSync();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import mysql from 'mysql2/promise';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
5
2
|
import { getDialectForDriver } from '../../context/connections/dialects.js';
|
|
3
|
+
import { resolveStringReference } from '../shared/string-reference.js';
|
|
6
4
|
import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js';
|
|
7
5
|
import { constraintDiscoveryWarning, tryConstraintQuery, } from '../../context/scan/constraint-discovery.js';
|
|
8
6
|
import { scopedTableNames } from '../../context/scan/table-ref.js';
|
|
@@ -16,18 +14,6 @@ function stringConfigValue(connection, key, env) {
|
|
|
16
14
|
const value = connection?.[key];
|
|
17
15
|
return typeof value === 'string' && value.trim().length > 0 ? resolveStringReference(value.trim(), env) : undefined;
|
|
18
16
|
}
|
|
19
|
-
function resolveStringReference(value, env) {
|
|
20
|
-
if (value.startsWith('env:')) {
|
|
21
|
-
const envName = value.slice('env:'.length);
|
|
22
|
-
return env[envName] ?? '';
|
|
23
|
-
}
|
|
24
|
-
if (value.startsWith('file:')) {
|
|
25
|
-
const rawPath = value.slice('file:'.length);
|
|
26
|
-
const path = rawPath.startsWith('~') ? resolve(homedir(), rawPath.slice(1)) : rawPath;
|
|
27
|
-
return readFileSync(path, 'utf-8').trim();
|
|
28
|
-
}
|
|
29
|
-
return value;
|
|
30
|
-
}
|
|
31
17
|
function maybeNumber(value) {
|
|
32
18
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
33
19
|
}
|
|
@@ -508,7 +494,7 @@ export class KtxMysqlScanConnector {
|
|
|
508
494
|
}
|
|
509
495
|
assertConnection(connectionId) {
|
|
510
496
|
if (connectionId !== this.connectionId) {
|
|
511
|
-
throw new Error(`
|
|
497
|
+
throw new Error(`ktx MySQL connector ${this.id} cannot serve connection ${connectionId}`);
|
|
512
498
|
}
|
|
513
499
|
}
|
|
514
500
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { resolveStringReference } from '../shared/string-reference.js';
|
|
4
2
|
import { getDialectForDriver } from '../../context/connections/dialects.js';
|
|
5
3
|
import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js';
|
|
6
4
|
import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js';
|
|
@@ -88,17 +86,6 @@ function stringConfigValue(connection, key, env) {
|
|
|
88
86
|
const value = connection?.[key];
|
|
89
87
|
return typeof value === 'string' && value.trim().length > 0 ? resolveStringReference(value.trim(), env) : undefined;
|
|
90
88
|
}
|
|
91
|
-
function resolveStringReference(value, env) {
|
|
92
|
-
if (value.startsWith('env:')) {
|
|
93
|
-
return env[value.slice('env:'.length)] ?? '';
|
|
94
|
-
}
|
|
95
|
-
if (value.startsWith('file:')) {
|
|
96
|
-
const rawPath = value.slice('file:'.length);
|
|
97
|
-
const path = rawPath.startsWith('~') ? resolve(homedir(), rawPath.slice(1)) : rawPath;
|
|
98
|
-
return readFileSync(path, 'utf-8').trim();
|
|
99
|
-
}
|
|
100
|
-
return value;
|
|
101
|
-
}
|
|
102
89
|
function numberValue(value) {
|
|
103
90
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
104
91
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a config string that may reference an environment variable
|
|
3
|
+
* (`env:NAME`) or a file (`file:/path`, `~` expands to the home dir).
|
|
4
|
+
* Plain values pass through unchanged.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveStringReference(value: string, env: NodeJS.ProcessEnv): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a config string that may reference an environment variable
|
|
6
|
+
* (`env:NAME`) or a file (`file:/path`, `~` expands to the home dir).
|
|
7
|
+
* Plain values pass through unchanged.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveStringReference(value, env) {
|
|
10
|
+
if (value.startsWith('env:')) {
|
|
11
|
+
return env[value.slice('env:'.length)] ?? '';
|
|
12
|
+
}
|
|
13
|
+
if (value.startsWith('file:')) {
|
|
14
|
+
const rawPath = value.slice('file:'.length);
|
|
15
|
+
const path = rawPath.startsWith('~') ? resolve(homedir(), rawPath.slice(rawPath[1] === '/' ? 2 : 1)) : rawPath;
|
|
16
|
+
return readFileSync(path, 'utf-8').trim();
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
@@ -72,7 +72,7 @@ export interface KtxSnowflakeScanConnectorOptions {
|
|
|
72
72
|
connectionId: string;
|
|
73
73
|
connection: KtxSnowflakeConnectionConfig | undefined;
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
75
|
+
* ktx project directory. When provided, snowflake-sdk's logger is redirected to
|
|
76
76
|
* `<projectDir>/.ktx/logs/snowflake.log` so its JSON output does not bleed into
|
|
77
77
|
* the CLI's TTY. Tests that use a fake driverFactory can leave this undefined.
|
|
78
78
|
*/
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { createPrivateKey } from 'node:crypto';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
5
2
|
import { getDialectForDriver } from '../../context/connections/dialects.js';
|
|
3
|
+
import { resolveStringReference } from '../shared/string-reference.js';
|
|
6
4
|
import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js';
|
|
7
5
|
import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js';
|
|
8
6
|
import { scopedTableNames } from '../../context/scan/table-ref.js';
|
|
@@ -11,17 +9,6 @@ import snowflake from 'snowflake-sdk';
|
|
|
11
9
|
import { assertSafeSnowflakeIdentifier, quoteSnowflakeIdentifier } from './identifiers.js';
|
|
12
10
|
import { configureSnowflakeSdkLogger } from './sdk-logger.js';
|
|
13
11
|
const DATE_TYPES = ['DATE', 'TIMESTAMP', 'TIMESTAMP_LTZ', 'TIMESTAMP_NTZ', 'TIMESTAMP_TZ', 'TIME'];
|
|
14
|
-
function resolveStringReference(value, env) {
|
|
15
|
-
if (value.startsWith('env:')) {
|
|
16
|
-
return env[value.slice('env:'.length)] ?? '';
|
|
17
|
-
}
|
|
18
|
-
if (value.startsWith('file:')) {
|
|
19
|
-
const rawPath = value.slice('file:'.length);
|
|
20
|
-
const path = rawPath.startsWith('~') ? resolve(homedir(), rawPath.slice(1)) : rawPath;
|
|
21
|
-
return readFileSync(path, 'utf-8').trim();
|
|
22
|
-
}
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
12
|
function stringConfigValue(connection, key, env) {
|
|
26
13
|
const value = connection?.[key];
|
|
27
14
|
return typeof value === 'string' && value.trim().length > 0 ? resolveStringReference(value.trim(), env) : undefined;
|
|
@@ -35,29 +35,6 @@ function sqlitePathFromUrl(url) {
|
|
|
35
35
|
}
|
|
36
36
|
return url;
|
|
37
37
|
}
|
|
38
|
-
function stripLeadingSqlComments(sql) {
|
|
39
|
-
let index = 0;
|
|
40
|
-
while (index < sql.length) {
|
|
41
|
-
while (/\s/.test(sql[index] ?? '')) {
|
|
42
|
-
index += 1;
|
|
43
|
-
}
|
|
44
|
-
if (sql.startsWith('--', index)) {
|
|
45
|
-
const end = sql.indexOf('\n', index + 2);
|
|
46
|
-
index = end === -1 ? sql.length : end + 1;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (sql.startsWith('/*', index)) {
|
|
50
|
-
const end = sql.indexOf('*/', index + 2);
|
|
51
|
-
if (end === -1) {
|
|
52
|
-
return sql.slice(index);
|
|
53
|
-
}
|
|
54
|
-
index = end + 2;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
return sql.slice(index);
|
|
60
|
-
}
|
|
61
38
|
export function isKtxSqliteConnectionConfig(connection) {
|
|
62
39
|
const driver = String(connection?.driver ?? '').toLowerCase();
|
|
63
40
|
return driver === 'sqlite';
|
|
@@ -171,7 +148,7 @@ export class KtxSqliteScanConnector {
|
|
|
171
148
|
}
|
|
172
149
|
async executeReadOnly(input, _ctx) {
|
|
173
150
|
this.assertConnection(input.connectionId);
|
|
174
|
-
const result = this.query(limitSqlForExecution(
|
|
151
|
+
const result = this.query(limitSqlForExecution(input.sql, input.maxRows), input.params);
|
|
175
152
|
return { ...result, rowCount: result.rows.length };
|
|
176
153
|
}
|
|
177
154
|
async getColumnDistinctValues(table, columnName, options) {
|
|
@@ -274,7 +251,7 @@ export class KtxSqliteScanConnector {
|
|
|
274
251
|
}
|
|
275
252
|
assertConnection(connectionId) {
|
|
276
253
|
if (connectionId !== this.connectionId) {
|
|
277
|
-
throw new Error(`
|
|
254
|
+
throw new Error(`ktx SQLite connector ${this.id} cannot serve connection ${connectionId}`);
|
|
278
255
|
}
|
|
279
256
|
}
|
|
280
257
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { assertReadOnlySql } from '../../context/connections/read-only-sql.js';
|
|
1
|
+
import { assertReadOnlySql, stripTrailingSqlNoise } from '../../context/connections/read-only-sql.js';
|
|
2
2
|
import { getDialectForDriver } from '../../context/connections/dialects.js';
|
|
3
3
|
import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js';
|
|
4
4
|
import { scopedTableNames } from '../../context/scan/table-ref.js';
|
|
5
5
|
import { connectorTestFailure, createKtxConnectorCapabilities, } from '../../context/scan/types.js';
|
|
6
|
-
import { readFileSync } from 'node:fs';
|
|
7
|
-
import { homedir } from 'node:os';
|
|
8
|
-
import { resolve } from 'node:path';
|
|
9
6
|
import sql from 'mssql';
|
|
7
|
+
import { resolveStringReference } from '../shared/string-reference.js';
|
|
10
8
|
function sqlTypeDeclaration(type) {
|
|
11
9
|
if (typeof type === 'function') {
|
|
12
10
|
try {
|
|
@@ -79,17 +77,6 @@ function stringConfigValue(connection, key, env) {
|
|
|
79
77
|
const value = connection?.[key];
|
|
80
78
|
return typeof value === 'string' && value.trim().length > 0 ? resolveStringReference(value.trim(), env) : undefined;
|
|
81
79
|
}
|
|
82
|
-
function resolveStringReference(value, env) {
|
|
83
|
-
if (value.startsWith('env:')) {
|
|
84
|
-
return env[value.slice('env:'.length)] ?? '';
|
|
85
|
-
}
|
|
86
|
-
if (value.startsWith('file:')) {
|
|
87
|
-
const rawPath = value.slice('file:'.length);
|
|
88
|
-
const path = rawPath.startsWith('~') ? resolve(homedir(), rawPath.slice(1)) : rawPath;
|
|
89
|
-
return readFileSync(path, 'utf-8').trim();
|
|
90
|
-
}
|
|
91
|
-
return value;
|
|
92
|
-
}
|
|
93
80
|
function parseSqlServerUrl(url) {
|
|
94
81
|
const parsed = new URL(url);
|
|
95
82
|
return {
|
|
@@ -142,7 +129,7 @@ function isDeniedError(error) {
|
|
|
142
129
|
return number === 229 || number === 230 || number === 297;
|
|
143
130
|
}
|
|
144
131
|
function limitSqlForSqlServerExecution(sqlText, maxRows) {
|
|
145
|
-
const trimmed = assertReadOnlySql(sqlText)
|
|
132
|
+
const trimmed = stripTrailingSqlNoise(assertReadOnlySql(sqlText));
|
|
146
133
|
if (!maxRows) {
|
|
147
134
|
return trimmed;
|
|
148
135
|
}
|
|
@@ -575,7 +562,7 @@ export class KtxSqlServerScanConnector {
|
|
|
575
562
|
}
|
|
576
563
|
assertConnection(connectionId) {
|
|
577
564
|
if (connectionId !== this.connectionId) {
|
|
578
|
-
throw new Error(`
|
|
565
|
+
throw new Error(`ktx SQL Server connector ${this.id} cannot serve connection ${connectionId}`);
|
|
579
566
|
}
|
|
580
567
|
}
|
|
581
568
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const connectionTypeSchema: z.ZodEnum<{
|
|
3
|
+
PLAIN: "PLAIN";
|
|
3
4
|
BIGQUERY: "BIGQUERY";
|
|
4
5
|
SNOWFLAKE: "SNOWFLAKE";
|
|
5
6
|
MYSQL: "MYSQL";
|
|
@@ -19,7 +20,6 @@ export declare const connectionTypeSchema: z.ZodEnum<{
|
|
|
19
20
|
METABASE: "METABASE";
|
|
20
21
|
LOOKER: "LOOKER";
|
|
21
22
|
NOTION: "NOTION";
|
|
22
|
-
PLAIN: "PLAIN";
|
|
23
23
|
BETTERSTACK: "BETTERSTACK";
|
|
24
24
|
}>;
|
|
25
25
|
export type ConnectionType = z.infer<typeof connectionTypeSchema>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { KtxProjectConnectionConfig } from '../project/config.js';
|
|
2
|
+
/** Stable id for the runtime-derived federated connection. Never written to ktx.yaml. */
|
|
3
|
+
export declare const FEDERATED_CONNECTION_ID = "_ktx_federated";
|
|
4
|
+
export declare function attachTypeForDriver(driver: string): string;
|
|
5
|
+
export interface FederatedMember {
|
|
6
|
+
connectionId: string;
|
|
7
|
+
driver: string;
|
|
8
|
+
projectDir: string;
|
|
9
|
+
connection: KtxProjectConnectionConfig;
|
|
10
|
+
}
|
|
11
|
+
export interface FederatedConnectionDescriptor {
|
|
12
|
+
id: typeof FEDERATED_CONNECTION_ID;
|
|
13
|
+
driver: 'duckdb';
|
|
14
|
+
members: FederatedMember[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Derives a virtual federated connection when a project declares 2+
|
|
18
|
+
* attach-compatible databases. Returns null otherwise — single-DB and
|
|
19
|
+
* incompatible projects are unaffected.
|
|
20
|
+
*/
|
|
21
|
+
export declare function deriveFederatedConnection(connections: Record<string, KtxProjectConnectionConfig>, projectDir: string): FederatedConnectionDescriptor | null;
|
|
22
|
+
export interface FederatedConnectionListing {
|
|
23
|
+
id: typeof FEDERATED_CONNECTION_ID;
|
|
24
|
+
driver: 'duckdb';
|
|
25
|
+
members: string[];
|
|
26
|
+
hint: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Listing-facing view of the virtual federated connection for `ktx connection`
|
|
30
|
+
* and MCP `connection_list`. Derived from the same declared state as
|
|
31
|
+
* deriveFederatedConnection, so both surfaces describe one connection.
|
|
32
|
+
*/
|
|
33
|
+
export declare function federatedConnectionListing(connections: Record<string, KtxProjectConnectionConfig>, projectDir: string): FederatedConnectionListing | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Stable id for the runtime-derived federated connection. Never written to ktx.yaml. */
|
|
2
|
+
export const FEDERATED_CONNECTION_ID = '_ktx_federated';
|
|
3
|
+
/**
|
|
4
|
+
* Drivers DuckDB can ATTACH for federation. The driver name doubles as the
|
|
5
|
+
* DuckDB extension/TYPE name, so this set is the single source of truth for
|
|
6
|
+
* both membership (a driver participates iff it appears here) and attach type.
|
|
7
|
+
*/
|
|
8
|
+
const ATTACH_COMPATIBLE_DRIVERS = new Set(['postgres', 'mysql', 'sqlite']);
|
|
9
|
+
export function attachTypeForDriver(driver) {
|
|
10
|
+
const normalized = driver.toLowerCase();
|
|
11
|
+
if (!ATTACH_COMPATIBLE_DRIVERS.has(normalized)) {
|
|
12
|
+
throw new Error(`Driver "${driver}" cannot be attached by DuckDB federation.`);
|
|
13
|
+
}
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Derives a virtual federated connection when a project declares 2+
|
|
18
|
+
* attach-compatible databases. Returns null otherwise — single-DB and
|
|
19
|
+
* incompatible projects are unaffected.
|
|
20
|
+
*/
|
|
21
|
+
export function deriveFederatedConnection(connections, projectDir) {
|
|
22
|
+
const members = Object.entries(connections)
|
|
23
|
+
.filter(([, config]) => ATTACH_COMPATIBLE_DRIVERS.has(config.driver.toLowerCase()))
|
|
24
|
+
.map(([connectionId, config]) => ({
|
|
25
|
+
connectionId,
|
|
26
|
+
driver: config.driver.toLowerCase(),
|
|
27
|
+
projectDir,
|
|
28
|
+
connection: config,
|
|
29
|
+
}));
|
|
30
|
+
if (members.length < 2) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return { id: FEDERATED_CONNECTION_ID, driver: 'duckdb', members };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Listing-facing view of the virtual federated connection for `ktx connection`
|
|
37
|
+
* and MCP `connection_list`. Derived from the same declared state as
|
|
38
|
+
* deriveFederatedConnection, so both surfaces describe one connection.
|
|
39
|
+
*/
|
|
40
|
+
export function federatedConnectionListing(connections, projectDir) {
|
|
41
|
+
const descriptor = deriveFederatedConnection(connections, projectDir);
|
|
42
|
+
if (!descriptor) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
id: FEDERATED_CONNECTION_ID,
|
|
47
|
+
driver: 'duckdb',
|
|
48
|
+
members: descriptor.members.map((member) => member.connectionId),
|
|
49
|
+
hint: 'Cross-database queries run here. Name tables connectionId.schema.table (or connectionId.table for sqlite); double-quote any id that is not a bare SQL identifier, e.g. "books-db".public.books.',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -14,6 +14,8 @@ export interface LocalConnectionInfo {
|
|
|
14
14
|
id: string;
|
|
15
15
|
name: string;
|
|
16
16
|
connectionType: string;
|
|
17
|
+
members?: string[];
|
|
18
|
+
hint?: string;
|
|
17
19
|
}
|
|
18
20
|
export declare function localConnectionToWarehouseDescriptor(id: string, connection: KtxProjectConnectionConfig | undefined): LocalWarehouseDescriptor | null;
|
|
19
21
|
export declare function localConnectionTypeForConfig(id: string, connection: KtxProjectConnectionConfig | undefined): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { executeFederatedQuery } from '../../connectors/duckdb/federated-executor.js';
|
|
2
|
+
import type { KtxLocalProject } from '../project/project.js';
|
|
3
|
+
import type { KtxScanConnector } from '../scan/types.js';
|
|
4
|
+
import type { KtxSqlQueryExecutionInput, KtxSqlQueryExecutionResult } from './query-executor.js';
|
|
5
|
+
export interface ExecuteProjectReadOnlySqlDeps {
|
|
6
|
+
project: KtxLocalProject;
|
|
7
|
+
input: KtxSqlQueryExecutionInput;
|
|
8
|
+
createConnector: (connectionId: string) => Promise<KtxScanConnector> | KtxScanConnector;
|
|
9
|
+
executeFederated?: typeof executeFederatedQuery;
|
|
10
|
+
runId?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Single resolve-and-execute path for project read-only SQL. The federated
|
|
14
|
+
* connection is derived from declared state here so every executor entry point
|
|
15
|
+
* routes `_ktx_federated` identically; standard connections go through the
|
|
16
|
+
* scan connector.
|
|
17
|
+
*/
|
|
18
|
+
export declare function executeProjectReadOnlySql(deps: ExecuteProjectReadOnlySqlDeps): Promise<KtxSqlQueryExecutionResult>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { executeFederatedQuery } from '../../connectors/duckdb/federated-executor.js';
|
|
2
|
+
import { deriveFederatedConnection, FEDERATED_CONNECTION_ID } from './federation.js';
|
|
3
|
+
/**
|
|
4
|
+
* Single resolve-and-execute path for project read-only SQL. The federated
|
|
5
|
+
* connection is derived from declared state here so every executor entry point
|
|
6
|
+
* routes `_ktx_federated` identically; standard connections go through the
|
|
7
|
+
* scan connector.
|
|
8
|
+
*/
|
|
9
|
+
export async function executeProjectReadOnlySql(deps) {
|
|
10
|
+
const { project, input } = deps;
|
|
11
|
+
if (input.connectionId === FEDERATED_CONNECTION_ID) {
|
|
12
|
+
const descriptor = deriveFederatedConnection(project.config.connections, project.projectDir);
|
|
13
|
+
if (!descriptor) {
|
|
14
|
+
throw new Error('Federated execution requested but fewer than 2 attach-compatible connections exist.');
|
|
15
|
+
}
|
|
16
|
+
const runFederated = deps.executeFederated ?? executeFederatedQuery;
|
|
17
|
+
return runFederated(descriptor.members, input);
|
|
18
|
+
}
|
|
19
|
+
let connector = null;
|
|
20
|
+
try {
|
|
21
|
+
connector = await deps.createConnector(input.connectionId);
|
|
22
|
+
if (!connector.capabilities.readOnlySql || !connector.executeReadOnly) {
|
|
23
|
+
throw new Error(`Connection "${input.connectionId}" driver "${connector.driver}" does not support read-only SQL execution.`);
|
|
24
|
+
}
|
|
25
|
+
const ctx = { runId: deps.runId ?? 'sql-execution' };
|
|
26
|
+
const result = await connector.executeReadOnly({ connectionId: input.connectionId, sql: input.sql, maxRows: input.maxRows }, ctx);
|
|
27
|
+
return {
|
|
28
|
+
headers: result.headers,
|
|
29
|
+
...(result.headerTypes ? { headerTypes: result.headerTypes } : {}),
|
|
30
|
+
rows: result.rows,
|
|
31
|
+
totalRows: result.totalRows,
|
|
32
|
+
command: 'SELECT',
|
|
33
|
+
rowCount: result.rowCount,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await connector?.cleanup?.();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -6,8 +6,9 @@ export interface KtxSqlQueryExecutionInput {
|
|
|
6
6
|
sql: string;
|
|
7
7
|
maxRows?: number;
|
|
8
8
|
}
|
|
9
|
-
interface KtxSqlQueryExecutionResult {
|
|
9
|
+
export interface KtxSqlQueryExecutionResult {
|
|
10
10
|
headers: string[];
|
|
11
|
+
headerTypes?: string[];
|
|
11
12
|
rows: unknown[][];
|
|
12
13
|
totalRows: number;
|
|
13
14
|
command: string;
|
|
@@ -17,4 +18,3 @@ export interface KtxSqlQueryExecutorPort {
|
|
|
17
18
|
execute(input: KtxSqlQueryExecutionInput): Promise<KtxSqlQueryExecutionResult>;
|
|
18
19
|
}
|
|
19
20
|
export declare function normalizeQueryRows(rows: unknown[]): unknown[][];
|
|
20
|
-
export {};
|
|
@@ -1,19 +1,134 @@
|
|
|
1
|
+
import { KtxQueryError } from '../../errors.js';
|
|
1
2
|
const MUTATING_SQL = /^\s*(insert|update|delete|merge|alter|drop|create|truncate|grant|revoke|copy|call|do|vacuum|analyze|refresh)\b/i;
|
|
2
3
|
const READ_SQL = /^\s*(select|with)\b/i;
|
|
4
|
+
// Agents (and the daemon's sqlglot validator, which ignores comments) routinely
|
|
5
|
+
// emit read-only queries prefixed with `-- ...` or `/* ... */`. Strip leading
|
|
6
|
+
// comments so the prefix check sees the real statement; otherwise valid SELECT/WITH
|
|
7
|
+
// SQL is rejected here while the parser-backed validator accepts it.
|
|
8
|
+
function stripLeadingSqlComments(sql) {
|
|
9
|
+
let index = 0;
|
|
10
|
+
while (index < sql.length) {
|
|
11
|
+
while (/\s/.test(sql[index] ?? '')) {
|
|
12
|
+
index += 1;
|
|
13
|
+
}
|
|
14
|
+
if (sql.startsWith('--', index)) {
|
|
15
|
+
const end = sql.indexOf('\n', index + 2);
|
|
16
|
+
index = end === -1 ? sql.length : end + 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (sql.startsWith('/*', index)) {
|
|
20
|
+
const end = sql.indexOf('*/', index + 2);
|
|
21
|
+
if (end === -1) {
|
|
22
|
+
return sql.slice(index);
|
|
23
|
+
}
|
|
24
|
+
index = end + 2;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
return sql.slice(index);
|
|
30
|
+
}
|
|
31
|
+
// Lexes past one string literal, quoted identifier, or comment starting at
|
|
32
|
+
// `index`, using standard-SQL rules ('' and "" escapes; no dialect extensions
|
|
33
|
+
// such as backslash escapes or dollar quoting). Returns the index after the
|
|
34
|
+
// token, or `index` unchanged when no quoted/comment token starts there.
|
|
35
|
+
function skipQuotedOrComment(sql, index) {
|
|
36
|
+
const quote = sql[index];
|
|
37
|
+
if (quote === "'" || quote === '"') {
|
|
38
|
+
let i = index + 1;
|
|
39
|
+
while (i < sql.length) {
|
|
40
|
+
if (sql[i] === quote) {
|
|
41
|
+
if (sql[i + 1] === quote) {
|
|
42
|
+
i += 2;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
return i + 1;
|
|
46
|
+
}
|
|
47
|
+
i += 1;
|
|
48
|
+
}
|
|
49
|
+
return sql.length;
|
|
50
|
+
}
|
|
51
|
+
if (sql.startsWith('--', index)) {
|
|
52
|
+
const end = sql.indexOf('\n', index + 2);
|
|
53
|
+
return end === -1 ? sql.length : end + 1;
|
|
54
|
+
}
|
|
55
|
+
if (sql.startsWith('/*', index)) {
|
|
56
|
+
const end = sql.indexOf('*/', index + 2);
|
|
57
|
+
return end === -1 ? sql.length : end + 2;
|
|
58
|
+
}
|
|
59
|
+
return index;
|
|
60
|
+
}
|
|
61
|
+
// Backstop against statement smuggling (`select 1; drop table x`): reject any
|
|
62
|
+
// semicolon that is followed by real content. Semicolons inside string
|
|
63
|
+
// literals, quoted identifiers, and comments are fine, as are trailing
|
|
64
|
+
// semicolons (optionally followed by whitespace and comments). This deliberately
|
|
65
|
+
// lexes standard SQL only, so dialect-specific escapes can cause a false
|
|
66
|
+
// reject — never a false accept; the canonical gate is the daemon's
|
|
67
|
+
// sqlglot-backed validateReadOnly.
|
|
68
|
+
function assertSingleSqlStatement(sql) {
|
|
69
|
+
let index = 0;
|
|
70
|
+
let sawSemicolon = false;
|
|
71
|
+
while (index < sql.length) {
|
|
72
|
+
const skipped = skipQuotedOrComment(sql, index);
|
|
73
|
+
if (skipped > index) {
|
|
74
|
+
index = skipped;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (sql[index] === ';') {
|
|
78
|
+
sawSemicolon = true;
|
|
79
|
+
}
|
|
80
|
+
else if (sawSemicolon && !/\s/.test(sql[index])) {
|
|
81
|
+
throw new KtxQueryError('Only one SQL statement can be executed.');
|
|
82
|
+
}
|
|
83
|
+
index += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
3
86
|
export function assertReadOnlySql(sql) {
|
|
4
|
-
const trimmed = sql.trim();
|
|
87
|
+
const trimmed = stripLeadingSqlComments(sql).trim();
|
|
5
88
|
if (!READ_SQL.test(trimmed) || MUTATING_SQL.test(trimmed)) {
|
|
6
|
-
throw new
|
|
89
|
+
throw new KtxQueryError('Only read-only SELECT/WITH queries can be executed locally.');
|
|
7
90
|
}
|
|
91
|
+
assertSingleSqlStatement(trimmed);
|
|
8
92
|
return trimmed;
|
|
9
93
|
}
|
|
94
|
+
// `assertReadOnlySql` deliberately keeps trailing semicolons, comments, and
|
|
95
|
+
// whitespace (e.g. `select 1; -- done`) — harmless for direct single-statement
|
|
96
|
+
// execution. A row-limit subquery wrapper needs a bare expression instead: a
|
|
97
|
+
// trailing `;` would sit illegally inside the subquery, and a trailing line
|
|
98
|
+
// comment would comment out the closing paren and limit clause. Lex forward with
|
|
99
|
+
// the same standard-SQL rules as the single-statement gate and truncate at the
|
|
100
|
+
// end of the last meaningful token, dropping trailing semicolons, comments, and
|
|
101
|
+
// whitespace. Characters inside string literals and quoted identifiers stay
|
|
102
|
+
// meaningful, so a `;` or `--` within a literal is never mistaken for a
|
|
103
|
+
// terminator (a plain regex cannot make that distinction).
|
|
104
|
+
export function stripTrailingSqlNoise(sql) {
|
|
105
|
+
let index = 0;
|
|
106
|
+
let meaningfulEnd = 0;
|
|
107
|
+
while (index < sql.length) {
|
|
108
|
+
if (sql.startsWith('--', index) || sql.startsWith('/*', index)) {
|
|
109
|
+
index = skipQuotedOrComment(sql, index);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const afterQuoted = skipQuotedOrComment(sql, index);
|
|
113
|
+
if (afterQuoted > index) {
|
|
114
|
+
meaningfulEnd = afterQuoted;
|
|
115
|
+
index = afterQuoted;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (sql[index] !== ';' && !/\s/.test(sql[index] ?? '')) {
|
|
119
|
+
meaningfulEnd = index + 1;
|
|
120
|
+
}
|
|
121
|
+
index += 1;
|
|
122
|
+
}
|
|
123
|
+
return sql.slice(0, meaningfulEnd);
|
|
124
|
+
}
|
|
10
125
|
export function limitSqlForExecution(sql, maxRows) {
|
|
11
|
-
const trimmed = assertReadOnlySql(sql)
|
|
126
|
+
const trimmed = stripTrailingSqlNoise(assertReadOnlySql(sql));
|
|
12
127
|
if (!maxRows) {
|
|
13
128
|
return trimmed;
|
|
14
129
|
}
|
|
15
130
|
if (!Number.isInteger(maxRows) || maxRows <= 0) {
|
|
16
|
-
throw new
|
|
131
|
+
throw new KtxQueryError('maxRows must be a positive integer.');
|
|
17
132
|
}
|
|
18
133
|
return `select * from (${trimmed}) as ktx_query_result limit ${maxRows}`;
|
|
19
134
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { KtxProjectConfig, KtxProjectConnectionConfig } from '../project/config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Look up a connection by id, throwing an expected (caller-driven) error that
|
|
4
|
+
* names the configured connections so an agent or CLI user can self-correct.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveConfiguredConnection(config: KtxProjectConfig, connectionId: string): KtxProjectConnectionConfig;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the connection id to run against: validate a requested id against the
|
|
9
|
+
* configured connections, or default to the sole connection when none is given.
|
|
10
|
+
* Throws an expected error that lists the configured connections otherwise.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveRequiredConnectionId(config: KtxProjectConfig, requested: string | undefined): string;
|