@kaelio/ktx 0.13.1 → 0.14.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.1-py3-none-any.whl → kaelio_ktx-0.14.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli-program.js +1 -1
- package/dist/commands/ingest-commands.d.ts +9 -0
- package/dist/commands/ingest-commands.js +37 -1
- package/dist/commands/knowledge-commands.js +3 -0
- package/dist/commands/setup-commands.js +14 -1
- package/dist/connection-drivers.d.ts +2 -0
- package/dist/connection-drivers.js +7 -3
- package/dist/connection.d.ts +3 -0
- package/dist/connection.js +27 -0
- package/dist/connectors/bigquery/connector.d.ts +17 -6
- package/dist/connectors/bigquery/connector.js +152 -76
- package/dist/connectors/bigquery/dialect.d.ts +2 -2
- package/dist/connectors/clickhouse/connector.d.ts +1 -0
- package/dist/connectors/clickhouse/connector.js +45 -16
- package/dist/connectors/clickhouse/dialect.d.ts +2 -2
- package/dist/connectors/mongodb/connector.d.ts +69 -0
- package/dist/connectors/mongodb/connector.js +307 -0
- package/dist/connectors/mongodb/dialect.d.ts +17 -0
- package/dist/connectors/mongodb/dialect.js +50 -0
- package/dist/connectors/mongodb/live-database-introspection.d.ts +10 -0
- package/dist/connectors/mongodb/live-database-introspection.js +24 -0
- package/dist/connectors/mongodb/schema-inference.d.ts +20 -0
- package/dist/connectors/mongodb/schema-inference.js +110 -0
- package/dist/connectors/mysql/connector.d.ts +1 -0
- package/dist/connectors/mysql/connector.js +18 -2
- package/dist/connectors/mysql/dialect.d.ts +2 -2
- package/dist/connectors/postgres/connector.d.ts +1 -0
- package/dist/connectors/postgres/connector.js +20 -3
- package/dist/connectors/postgres/dialect.d.ts +2 -2
- package/dist/connectors/snowflake/connector.d.ts +1 -0
- package/dist/connectors/snowflake/connector.js +27 -3
- package/dist/connectors/snowflake/dialect.d.ts +2 -2
- package/dist/connectors/sqlite/connector.d.ts +11 -1
- package/dist/connectors/sqlite/connector.js +120 -17
- package/dist/connectors/sqlite/dialect.d.ts +2 -2
- package/dist/connectors/sqlite/read-query-child.d.ts +1 -0
- package/dist/connectors/sqlite/read-query-child.js +23 -0
- package/dist/connectors/sqlserver/connector.d.ts +2 -0
- package/dist/connectors/sqlserver/connector.js +20 -3
- package/dist/connectors/sqlserver/dialect.d.ts +2 -2
- package/dist/context/cache/content-result-cache.d.ts +36 -0
- package/dist/context/cache/content-result-cache.js +14 -0
- package/dist/context/cache/sqlite-content-result-cache.d.ts +18 -0
- package/dist/context/cache/sqlite-content-result-cache.js +223 -0
- package/dist/context/connections/bigquery-identifiers.d.ts +1 -0
- package/dist/context/connections/bigquery-identifiers.js +7 -0
- package/dist/context/connections/configured-connections.d.ts +9 -0
- package/dist/context/connections/configured-connections.js +18 -0
- package/dist/context/connections/dialects.d.ts +30 -4
- package/dist/context/connections/dialects.js +38 -11
- package/dist/context/connections/drivers.js +21 -0
- package/dist/context/connections/gdrive-config.d.ts +19 -0
- package/dist/context/connections/gdrive-config.js +57 -0
- package/dist/context/connections/query-deadline.d.ts +31 -0
- package/dist/context/connections/query-deadline.js +37 -0
- package/dist/context/ingest/adapters/gdrive/chunk.d.ts +3 -0
- package/dist/context/ingest/adapters/gdrive/chunk.js +74 -0
- package/dist/context/ingest/adapters/gdrive/detect.d.ts +1 -0
- package/dist/context/ingest/adapters/gdrive/detect.js +20 -0
- package/dist/context/ingest/adapters/gdrive/fetch.d.ts +6 -0
- package/dist/context/ingest/adapters/gdrive/fetch.js +95 -0
- package/dist/context/ingest/adapters/gdrive/gdrive-client.d.ts +30 -0
- package/dist/context/ingest/adapters/gdrive/gdrive-client.js +132 -0
- package/dist/context/ingest/adapters/gdrive/gdrive.adapter.d.ts +11 -0
- package/dist/context/ingest/adapters/gdrive/gdrive.adapter.js +27 -0
- package/dist/context/ingest/adapters/gdrive/normalize.d.ts +2 -0
- package/dist/context/ingest/adapters/gdrive/normalize.js +256 -0
- package/dist/context/ingest/adapters/gdrive/types.d.ts +153 -0
- package/dist/context/ingest/adapters/gdrive/types.js +36 -0
- package/dist/context/ingest/adapters/live-database/daemon-introspection.js +24 -0
- package/dist/context/ingest/adapters/live-database/fetch-report.d.ts +8 -0
- package/dist/context/ingest/adapters/live-database/fetch-report.js +37 -0
- package/dist/context/ingest/adapters/live-database/live-database.adapter.d.ts +2 -1
- package/dist/context/ingest/adapters/live-database/live-database.adapter.js +9 -2
- package/dist/context/ingest/adapters/live-database/manifest.d.ts +2 -0
- package/dist/context/ingest/adapters/live-database/manifest.js +8 -4
- package/dist/context/ingest/adapters/live-database/scan-outcome.d.ts +17 -0
- package/dist/context/ingest/adapters/live-database/scan-outcome.js +40 -0
- package/dist/context/ingest/adapters/live-database/stage.js +5 -5
- package/dist/context/ingest/adapters/metabase/types.d.ts +1 -1
- package/dist/context/ingest/adapters/metabase/types.js +1 -1
- package/dist/context/ingest/artifact-gates.d.ts +32 -1
- package/dist/context/ingest/artifact-gates.js +85 -18
- package/dist/context/ingest/final-gate-prune.d.ts +35 -0
- package/dist/context/ingest/final-gate-prune.js +229 -0
- package/dist/context/ingest/ingest-bundle.runner.d.ts +3 -0
- package/dist/context/ingest/ingest-bundle.runner.js +459 -240
- package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +0 -11
- package/dist/context/ingest/isolated-diff/patch-integrator.js +0 -44
- package/dist/context/ingest/isolated-diff/work-unit-executor.d.ts +1 -0
- package/dist/context/ingest/isolated-diff/work-unit-executor.js +13 -4
- package/dist/context/ingest/local-adapters.js +6 -0
- package/dist/context/ingest/local-bundle-runtime.js +7 -2
- package/dist/context/ingest/local-ingest.js +1 -1
- package/dist/context/ingest/local-stage-ingest.d.ts +3 -1
- package/dist/context/ingest/local-stage-ingest.js +3 -1
- package/dist/context/ingest/ports.d.ts +3 -0
- package/dist/context/ingest/report-snapshot.js +13 -3
- package/dist/context/ingest/reports.d.ts +3 -3
- package/dist/context/ingest/reports.js +3 -1
- package/dist/context/ingest/stages/build-wu-context.d.ts +1 -0
- package/dist/context/ingest/stages/build-wu-context.js +2 -1
- package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -1
- package/dist/context/ingest/stages/stage-3-work-units.js +4 -10
- package/dist/context/ingest/stages/validate-wu-sources.d.ts +12 -0
- package/dist/context/ingest/stages/validate-wu-sources.js +23 -8
- package/dist/context/ingest/tools/read-raw-file.tool.js +10 -5
- package/dist/context/ingest/tools/read-raw-span.tool.js +10 -5
- package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +1 -1
- package/dist/context/ingest/tools/warehouse-verification/entity-details.tool.js +1 -1
- package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +1 -1
- package/dist/context/ingest/types.d.ts +3 -0
- package/dist/context/ingest/wiki-body-refs.d.ts +28 -0
- package/dist/context/ingest/wiki-body-refs.js +37 -8
- package/dist/context/ingest/wiki-sl-ref-repair.d.ts +1 -0
- package/dist/context/ingest/wiki-sl-ref-repair.js +4 -0
- package/dist/context/ingest/work-unit-cache.d.ts +67 -0
- package/dist/context/ingest/work-unit-cache.js +230 -0
- package/dist/context/llm/ai-sdk-runtime.d.ts +1 -0
- package/dist/context/llm/ai-sdk-runtime.js +5 -0
- package/dist/context/llm/claude-code-runtime.d.ts +4 -1
- package/dist/context/llm/claude-code-runtime.js +38 -8
- package/dist/context/llm/codex-runtime.d.ts +4 -1
- package/dist/context/llm/codex-runtime.js +48 -35
- package/dist/context/llm/runtime-port.d.ts +26 -0
- package/dist/context/llm/subprocess-generate-object-child.d.ts +1 -0
- package/dist/context/llm/subprocess-generate-object-child.js +34 -0
- package/dist/context/llm/subprocess-generate-object.d.ts +42 -0
- package/dist/context/llm/subprocess-generate-object.js +122 -0
- package/dist/context/mcp/context-tools.d.ts +2 -0
- package/dist/context/mcp/context-tools.js +111 -17
- package/dist/context/mcp/local-project-ports.d.ts +7 -0
- package/dist/context/mcp/local-project-ports.js +29 -0
- package/dist/context/mcp/logger.d.ts +24 -0
- package/dist/context/mcp/logger.js +49 -0
- package/dist/context/mcp/server.js +2 -0
- package/dist/context/mcp/types.d.ts +17 -0
- package/dist/context/memory/memory-agent.service.js +1 -1
- package/dist/context/project/config.d.ts +42 -0
- package/dist/context/project/config.js +5 -0
- package/dist/context/project/driver-schemas.d.ts +20 -0
- package/dist/context/project/driver-schemas.js +50 -1
- package/dist/context/project/project.js +1 -1
- package/dist/context/project/setup-config.js +1 -0
- package/dist/context/scan/description-generation.d.ts +4 -0
- package/dist/context/scan/description-generation.js +100 -13
- package/dist/context/scan/enabled-tables.d.ts +5 -0
- package/dist/context/scan/enabled-tables.js +13 -1
- package/dist/context/scan/enrichment-state.d.ts +50 -7
- package/dist/context/scan/enrichment-state.js +30 -13
- package/dist/context/scan/local-enrichment-artifacts.d.ts +41 -0
- package/dist/context/scan/local-enrichment-artifacts.js +155 -32
- package/dist/context/scan/local-enrichment.d.ts +39 -4
- package/dist/context/scan/local-enrichment.js +345 -92
- package/dist/context/scan/local-scan.d.ts +4 -1
- package/dist/context/scan/local-scan.js +54 -13
- package/dist/context/scan/local-structural-artifacts.d.ts +3 -1
- package/dist/context/scan/local-structural-artifacts.js +5 -0
- package/dist/context/scan/object-introspection.d.ts +21 -0
- package/dist/context/scan/object-introspection.js +35 -0
- package/dist/context/scan/relationship-benchmarks.js +5 -3
- package/dist/context/scan/relationship-composite-candidates.d.ts +6 -3
- package/dist/context/scan/relationship-composite-candidates.js +13 -1
- package/dist/context/scan/relationship-detection-budget.d.ts +35 -0
- package/dist/context/scan/relationship-detection-budget.js +53 -0
- package/dist/context/scan/relationship-diagnostics.d.ts +5 -0
- package/dist/context/scan/relationship-diagnostics.js +2 -0
- package/dist/context/scan/relationship-discovery.d.ts +9 -3
- package/dist/context/scan/relationship-discovery.js +27 -4
- package/dist/context/scan/relationship-llm-proposal.js +36 -27
- package/dist/context/scan/relationship-profiling.d.ts +7 -3
- package/dist/context/scan/relationship-profiling.js +53 -41
- package/dist/context/scan/relationship-validation.d.ts +6 -4
- package/dist/context/scan/relationship-validation.js +46 -32
- package/dist/context/scan/sqlite-local-enrichment-state-store.d.ts +8 -1
- package/dist/context/scan/sqlite-local-enrichment-state-store.js +71 -159
- package/dist/context/scan/types.d.ts +5 -3
- package/dist/context/scan/types.js +2 -0
- package/dist/context/sl/local-query.js +5 -0
- package/dist/context/sl/pglite-sl-search-prototype.js +1 -1
- package/dist/context/sl/semantic-layer.service.js +1 -1
- package/dist/context/sl/source-files.d.ts +5 -0
- package/dist/context/sl/source-files.js +17 -11
- package/dist/context/sl/tools/connection-id-schema.js +1 -1
- package/dist/context/sql-analysis/dialect-notes.d.ts +9 -0
- package/dist/context/sql-analysis/dialect-notes.js +40 -0
- package/dist/context/sql-analysis/dialects/bigquery.md +13 -0
- package/dist/context/sql-analysis/dialects/clickhouse.md +9 -0
- package/dist/context/sql-analysis/dialects/mysql.md +9 -0
- package/dist/context/sql-analysis/dialects/postgres.md +10 -0
- package/dist/context/sql-analysis/dialects/snowflake.md +10 -0
- package/dist/context/sql-analysis/dialects/sqlite.md +11 -0
- package/dist/context/sql-analysis/dialects/tsql.md +10 -0
- package/dist/context/wiki/keys.js +1 -1
- package/dist/context/wiki/knowledge-wiki.service.d.ts +4 -4
- package/dist/context/wiki/knowledge-wiki.service.js +2 -1
- package/dist/context/wiki/local-knowledge.d.ts +13 -0
- package/dist/context/wiki/local-knowledge.js +50 -6
- package/dist/context/wiki/sqlite-knowledge-index.d.ts +2 -0
- package/dist/context/wiki/sqlite-knowledge-index.js +21 -5
- package/dist/context/wiki/tools/wiki-write.tool.d.ts +2 -0
- package/dist/context/wiki/tools/wiki-write.tool.js +27 -0
- package/dist/context/wiki/types.d.ts +6 -0
- package/dist/context-build-view.d.ts +3 -0
- package/dist/context-build-view.js +1 -0
- package/dist/knowledge.d.ts +2 -0
- package/dist/knowledge.js +12 -1
- package/dist/local-adapters.js +11 -0
- package/dist/mcp-http-server.js +15 -1
- package/dist/mcp-server-factory.d.ts +2 -0
- package/dist/mcp-server-factory.js +15 -1
- package/dist/mcp-stdio-server.js +10 -2
- package/dist/notion-page-picker.js +1 -1
- package/dist/prompts/memory_agent_external_ingest.md +2 -0
- package/dist/public-ingest.d.ts +3 -1
- package/dist/public-ingest.js +3 -0
- package/dist/scan.d.ts +3 -1
- package/dist/scan.js +7 -0
- package/dist/setup-databases.d.ts +1 -1
- package/dist/setup-databases.js +43 -1
- package/dist/setup-sources.d.ts +5 -1
- package/dist/setup-sources.js +124 -48
- package/dist/setup.d.ts +3 -0
- package/dist/setup.js +6 -1
- package/dist/skills/analytics/SKILL.md +200 -2
- package/dist/skills/gdrive_synthesize/SKILL.md +97 -0
- package/dist/skills/wiki_capture/SKILL.md +24 -0
- package/dist/sql.js +4 -0
- package/dist/status-project.d.ts +4 -0
- package/dist/status-project.js +54 -2
- package/dist/telemetry/events.d.ts +2 -2
- package/dist/text-ingest.d.ts +4 -0
- package/dist/text-ingest.js +58 -29
- package/dist/verbatim-ingest.d.ts +46 -0
- package/dist/verbatim-ingest.js +193 -0
- package/package.json +5 -1
- package/dist/context/ingest/final-gate-repair.d.ts +0 -28
- package/dist/context/ingest/final-gate-repair.js +0 -91
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveStringReference } from '../shared/string-reference.js';
|
|
2
|
-
import {
|
|
2
|
+
import { getSqlDialectForDriver } from '../../context/connections/dialects.js';
|
|
3
|
+
import { resolveQueryDeadlineMs, queryDeadlineExceededError } from '../../context/connections/query-deadline.js';
|
|
3
4
|
import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js';
|
|
4
5
|
import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js';
|
|
5
6
|
import { scopedTableNames } from '../../context/scan/table-ref.js';
|
|
@@ -74,6 +75,10 @@ function isDeniedError(error) {
|
|
|
74
75
|
const code = error.code;
|
|
75
76
|
return code === '42501' || code === '42P01';
|
|
76
77
|
}
|
|
78
|
+
// 57014 = query_canceled, which is how statement_timeout surfaces.
|
|
79
|
+
function isPostgresTimeoutError(error) {
|
|
80
|
+
return Boolean(error) && typeof error === 'object' && error.code === '57014';
|
|
81
|
+
}
|
|
77
82
|
function queryRows(result) {
|
|
78
83
|
const headers = (result.fields ?? []).map((field) => field.name);
|
|
79
84
|
return result.rows.map((row) => headers.map((header) => row[header]));
|
|
@@ -169,9 +174,13 @@ export function postgresPoolConfigFromConfig(input) {
|
|
|
169
174
|
: { host, port: numberValue(merged.port) ?? 5432, database, user, password }),
|
|
170
175
|
};
|
|
171
176
|
const searchPathSchemas = searchPathSchemasFromConnection(merged);
|
|
177
|
+
// statement_timeout (ms) bounds every query on connections from this pool, so
|
|
178
|
+
// the server itself aborts a runaway query and frees the connection cleanly.
|
|
179
|
+
const serverOptions = [`-c statement_timeout=${resolveQueryDeadlineMs(merged)}`];
|
|
172
180
|
if (searchPathSchemas.length > 0) {
|
|
173
|
-
|
|
181
|
+
serverOptions.unshift(`-c search_path=${searchPathSchemas.join(',')}`);
|
|
174
182
|
}
|
|
183
|
+
config.options = serverOptions.join(' ');
|
|
175
184
|
if (merged.ssl && sslmode !== 'prefer' && sslmode !== 'disable') {
|
|
176
185
|
config.ssl = { rejectUnauthorized: merged.rejectUnauthorized ?? true };
|
|
177
186
|
}
|
|
@@ -195,7 +204,8 @@ export class KtxPostgresScanConnector {
|
|
|
195
204
|
poolFactory;
|
|
196
205
|
endpointResolver;
|
|
197
206
|
now;
|
|
198
|
-
|
|
207
|
+
deadlineMs;
|
|
208
|
+
dialect = getSqlDialectForDriver('postgres');
|
|
199
209
|
pool = null;
|
|
200
210
|
lastIdlePoolError = null;
|
|
201
211
|
resolvedEndpoint = null;
|
|
@@ -210,6 +220,7 @@ export class KtxPostgresScanConnector {
|
|
|
210
220
|
this.poolFactory = options.poolFactory ?? new DefaultPostgresPoolFactory();
|
|
211
221
|
this.endpointResolver = options.endpointResolver;
|
|
212
222
|
this.now = options.now ?? (() => new Date());
|
|
223
|
+
this.deadlineMs = resolveQueryDeadlineMs(this.connection);
|
|
213
224
|
this.id = `postgres:${options.connectionId}`;
|
|
214
225
|
}
|
|
215
226
|
async testConnection() {
|
|
@@ -533,6 +544,12 @@ export class KtxPostgresScanConnector {
|
|
|
533
544
|
rowCount: result.rows.length,
|
|
534
545
|
};
|
|
535
546
|
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
if (isPostgresTimeoutError(error)) {
|
|
549
|
+
throw queryDeadlineExceededError(this.deadlineMs, { cause: error });
|
|
550
|
+
}
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
536
553
|
finally {
|
|
537
554
|
client.release();
|
|
538
555
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { KtxSqlDialect } from '../../context/connections/dialects.js';
|
|
2
2
|
import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/types.js';
|
|
3
3
|
type PostgresTableNameRef = Pick<KtxTableRef, 'name'> & Partial<Pick<KtxTableRef, 'catalog' | 'db'>>;
|
|
4
4
|
/** @internal */
|
|
5
|
-
export declare class KtxPostgresDialect implements
|
|
5
|
+
export declare class KtxPostgresDialect implements KtxSqlDialect {
|
|
6
6
|
readonly type: "postgres";
|
|
7
7
|
private readonly typeMappings;
|
|
8
8
|
quoteIdentifier(identifier: string): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createPrivateKey } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { getSqlDialectForDriver } from '../../context/connections/dialects.js';
|
|
3
|
+
import { resolveQueryDeadlineMs, queryDeadlineExceededError } from '../../context/connections/query-deadline.js';
|
|
3
4
|
import { resolveStringReference } from '../shared/string-reference.js';
|
|
4
5
|
import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js';
|
|
5
6
|
import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js';
|
|
@@ -43,6 +44,19 @@ function isDeniedError(error) {
|
|
|
43
44
|
}
|
|
44
45
|
return false;
|
|
45
46
|
}
|
|
47
|
+
// Snowflake cancels with code 604 and a "reached its statement ... timeout"
|
|
48
|
+
// message once STATEMENT_TIMEOUT_IN_SECONDS elapses.
|
|
49
|
+
function isSnowflakeTimeoutError(error) {
|
|
50
|
+
if (!error || typeof error !== 'object') {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const code = error.code;
|
|
54
|
+
const message = error.message;
|
|
55
|
+
return (code === 604 ||
|
|
56
|
+
code === '604' ||
|
|
57
|
+
code === '000604' ||
|
|
58
|
+
(typeof message === 'string' && /reached its (statement|warehouse) .*timeout/i.test(message)));
|
|
59
|
+
}
|
|
46
60
|
function normalizeSnowflakeValue(value, columnType) {
|
|
47
61
|
if (columnType && DATE_TYPES.some((type) => columnType.toUpperCase().includes(type))) {
|
|
48
62
|
if (typeof value === 'number') {
|
|
@@ -131,6 +145,7 @@ export function snowflakeConnectionConfigFromConfig(input) {
|
|
|
131
145
|
connectionId: input.connectionId,
|
|
132
146
|
defaultValue: 4,
|
|
133
147
|
}),
|
|
148
|
+
deadlineMs: resolveQueryDeadlineMs(input.connection),
|
|
134
149
|
};
|
|
135
150
|
const role = stringConfigValue(input.connection, 'role', env);
|
|
136
151
|
if (role) {
|
|
@@ -178,12 +193,21 @@ class SnowflakeSdkDriver {
|
|
|
178
193
|
}
|
|
179
194
|
async query(sql, params) {
|
|
180
195
|
const binds = Array.isArray(params) ? toSnowflakeBinds(params) : undefined;
|
|
196
|
+
const statementTimeoutSeconds = Math.ceil(this.resolved.deadlineMs / 1000);
|
|
181
197
|
try {
|
|
182
198
|
const pool = await this.getPool();
|
|
183
|
-
const result = await pool.use(async (connection) =>
|
|
199
|
+
const result = await pool.use(async (connection) => {
|
|
200
|
+
// Bound the statement server-side; Snowflake cancels and frees the
|
|
201
|
+
// warehouse slot when STATEMENT_TIMEOUT_IN_SECONDS is reached.
|
|
202
|
+
await this.executeSnowflakeQuery(connection, `ALTER SESSION SET STATEMENT_TIMEOUT_IN_SECONDS = ${statementTimeoutSeconds}`);
|
|
203
|
+
return this.executeSnowflakeQuery(connection, sql, binds);
|
|
204
|
+
});
|
|
184
205
|
return { ...result, totalRows: result.rows.length, rowCount: result.rows.length };
|
|
185
206
|
}
|
|
186
207
|
catch (error) {
|
|
208
|
+
if (isSnowflakeTimeoutError(error)) {
|
|
209
|
+
throw queryDeadlineExceededError(this.resolved.deadlineMs, { cause: error });
|
|
210
|
+
}
|
|
187
211
|
const message = error instanceof Error ? error.message : String(error);
|
|
188
212
|
if (/timeout/i.test(message) && /pool|acquire/i.test(message)) {
|
|
189
213
|
throw new Error("Snowflake session pool exhausted after 60s - consider lowering maxConnections or increasing your account's concurrent-statement limit.");
|
|
@@ -355,7 +379,7 @@ export class KtxSnowflakeScanConnector {
|
|
|
355
379
|
});
|
|
356
380
|
resolved;
|
|
357
381
|
driverFactory;
|
|
358
|
-
dialect =
|
|
382
|
+
dialect = getSqlDialectForDriver('snowflake');
|
|
359
383
|
now;
|
|
360
384
|
driverInstance = null;
|
|
361
385
|
constructor(options) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { KtxSqlDialect } from '../../context/connections/dialects.js';
|
|
2
2
|
import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/types.js';
|
|
3
3
|
type SnowflakeTableNameRef = Pick<KtxTableRef, 'name'> & Partial<Pick<KtxTableRef, 'catalog' | 'db'>>;
|
|
4
4
|
/** @internal */
|
|
5
|
-
export declare class KtxSnowflakeDialect implements
|
|
5
|
+
export declare class KtxSnowflakeDialect implements KtxSqlDialect {
|
|
6
6
|
readonly type: "snowflake";
|
|
7
7
|
private readonly typeMappings;
|
|
8
8
|
quoteIdentifier(identifier: string): string;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
1
2
|
import { type KtxConnectorTestResult, type KtxColumnSampleInput, type KtxColumnSampleResult, type KtxColumnStatsInput, type KtxColumnStatsResult, type KtxQueryResult, type KtxReadOnlyQueryInput, type KtxScanConnector, type KtxScanContext, type KtxScanInput, type KtxSchemaSnapshot, type KtxTableListEntry, type KtxTableRef, type KtxTableSampleInput, type KtxTableSampleResult } from '../../context/scan/types.js';
|
|
2
3
|
export interface KtxSqliteConnectionConfig {
|
|
3
4
|
driver?: string;
|
|
4
5
|
path?: string;
|
|
5
6
|
url?: string;
|
|
7
|
+
query_timeout_ms?: number;
|
|
6
8
|
[key: string]: unknown;
|
|
7
9
|
}
|
|
8
10
|
/** @internal */
|
|
11
|
+
export declare function forkReadQueryChild(): ChildProcess;
|
|
12
|
+
/** @internal */
|
|
9
13
|
export interface SqliteDatabasePathInput {
|
|
10
14
|
connectionId: string;
|
|
11
15
|
projectDir?: string;
|
|
@@ -13,6 +17,8 @@ export interface SqliteDatabasePathInput {
|
|
|
13
17
|
}
|
|
14
18
|
export interface KtxSqliteScanConnectorOptions extends SqliteDatabasePathInput {
|
|
15
19
|
now?: () => Date;
|
|
20
|
+
/** @internal Test seam: spawn the read-query child so tests can observe its lifecycle. */
|
|
21
|
+
spawnReadQueryChild?: () => ChildProcess;
|
|
16
22
|
}
|
|
17
23
|
export interface KtxSqliteReadOnlyQueryInput extends KtxReadOnlyQueryInput {
|
|
18
24
|
params?: Record<string, unknown> | unknown[];
|
|
@@ -36,6 +42,8 @@ export declare class KtxSqliteScanConnector implements KtxScanConnector {
|
|
|
36
42
|
private readonly connectionId;
|
|
37
43
|
private readonly dbPath;
|
|
38
44
|
private readonly now;
|
|
45
|
+
private readonly deadlineMs;
|
|
46
|
+
private readonly spawnReadQueryChild;
|
|
39
47
|
private readonly dialect;
|
|
40
48
|
private db;
|
|
41
49
|
constructor(options: KtxSqliteScanConnectorOptions);
|
|
@@ -46,7 +54,8 @@ export declare class KtxSqliteScanConnector implements KtxScanConnector {
|
|
|
46
54
|
sampleTable(input: KtxTableSampleInput, _ctx: KtxScanContext): Promise<KtxTableSampleResult>;
|
|
47
55
|
sampleColumn(input: KtxColumnSampleInput, _ctx: KtxScanContext): Promise<KtxColumnSampleResult>;
|
|
48
56
|
columnStats(_input: KtxColumnStatsInput, _ctx: KtxScanContext): Promise<KtxColumnStatsResult | null>;
|
|
49
|
-
executeReadOnly(input: KtxSqliteReadOnlyQueryInput,
|
|
57
|
+
executeReadOnly(input: KtxSqliteReadOnlyQueryInput, ctx: KtxScanContext): Promise<KtxQueryResult>;
|
|
58
|
+
private runReadQueryOffProcess;
|
|
50
59
|
getColumnDistinctValues(table: KtxTableRef, columnName: string, options: KtxSqliteColumnDistinctValuesOptions): Promise<KtxSqliteColumnDistinctValuesResult | null>;
|
|
51
60
|
getTableRowCount(tableName: string): Promise<number>;
|
|
52
61
|
qTableName(table: Pick<KtxTableRef, 'name'>): string;
|
|
@@ -55,6 +64,7 @@ export declare class KtxSqliteScanConnector implements KtxScanConnector {
|
|
|
55
64
|
private database;
|
|
56
65
|
private query;
|
|
57
66
|
private readTable;
|
|
67
|
+
private readRowCount;
|
|
58
68
|
private mapForeignKeys;
|
|
59
69
|
private assertConnection;
|
|
60
70
|
}
|
|
@@ -3,11 +3,29 @@ import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { isAbsolute, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import {
|
|
6
|
+
import { fork } from 'node:child_process';
|
|
7
|
+
import { getSqlDialectForDriver } from '../../context/connections/dialects.js';
|
|
8
|
+
import { resolveQueryDeadlineMs, queryDeadlineExceededError } from '../../context/connections/query-deadline.js';
|
|
7
9
|
import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js';
|
|
8
10
|
import { normalizeQueryRows } from '../../context/connections/query-executor.js';
|
|
9
11
|
import { connectorTestFailure, createKtxConnectorCapabilities } from '../../context/scan/types.js';
|
|
10
12
|
import { scopedTableNames } from '../../context/scan/table-ref.js';
|
|
13
|
+
import { tryIntrospectObject } from '../../context/scan/object-introspection.js';
|
|
14
|
+
// In dist, connector.js and read-query-child.js are siblings; under vitest the
|
|
15
|
+
// compiled .js is absent and Node strips types from the .ts when forking it.
|
|
16
|
+
const readQueryChildUrl = existsSync(fileURLToPath(new URL('./read-query-child.js', import.meta.url)))
|
|
17
|
+
? new URL('./read-query-child.js', import.meta.url)
|
|
18
|
+
: new URL('./read-query-child.ts', import.meta.url);
|
|
19
|
+
/** @internal */
|
|
20
|
+
export function forkReadQueryChild() {
|
|
21
|
+
// Empty execArgv so the child is a clean Node process (no inherited vitest /
|
|
22
|
+
// inspector flags); advanced serialization preserves BigInt/Buffer in rows.
|
|
23
|
+
return fork(readQueryChildUrl, {
|
|
24
|
+
execArgv: [],
|
|
25
|
+
serialization: 'advanced',
|
|
26
|
+
stdio: ['ignore', 'ignore', 'inherit', 'ipc'],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
11
29
|
function stringConfigValue(connection, key) {
|
|
12
30
|
const value = connection?.[key];
|
|
13
31
|
return typeof value === 'string' && value.trim().length > 0 ? resolveStringReference(key, value.trim()) : undefined;
|
|
@@ -66,12 +84,16 @@ export class KtxSqliteScanConnector {
|
|
|
66
84
|
connectionId;
|
|
67
85
|
dbPath;
|
|
68
86
|
now;
|
|
69
|
-
|
|
87
|
+
deadlineMs;
|
|
88
|
+
spawnReadQueryChild;
|
|
89
|
+
dialect = getSqlDialectForDriver('sqlite');
|
|
70
90
|
db = null;
|
|
71
91
|
constructor(options) {
|
|
72
92
|
this.connectionId = options.connectionId;
|
|
73
93
|
this.dbPath = sqliteDatabasePathFromConfig(options);
|
|
74
94
|
this.now = options.now ?? (() => new Date());
|
|
95
|
+
this.deadlineMs = resolveQueryDeadlineMs(options.connection);
|
|
96
|
+
this.spawnReadQueryChild = options.spawnReadQueryChild ?? forkReadQueryChild;
|
|
75
97
|
this.id = `sqlite:${options.connectionId}`;
|
|
76
98
|
}
|
|
77
99
|
async testConnection() {
|
|
@@ -89,14 +111,24 @@ export class KtxSqliteScanConnector {
|
|
|
89
111
|
async introspect(input, _ctx) {
|
|
90
112
|
this.assertConnection(input.connectionId);
|
|
91
113
|
const database = this.database();
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const tables =
|
|
114
|
+
const allObjects = database
|
|
115
|
+
.prepare(`SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name`)
|
|
116
|
+
.all();
|
|
117
|
+
const scopedNames = input.tableScope
|
|
118
|
+
? new Set(scopedTableNames(input.tableScope, { catalog: null, db: null }))
|
|
119
|
+
: null;
|
|
120
|
+
const selectedObjects = scopedNames ? allObjects.filter((object) => scopedNames.has(object.name)) : allObjects;
|
|
121
|
+
const tables = [];
|
|
122
|
+
const warnings = [];
|
|
123
|
+
for (const object of selectedObjects) {
|
|
124
|
+
const outcome = await tryIntrospectObject({ object: object.name }, () => this.readTable(database, object));
|
|
125
|
+
if (outcome.ok) {
|
|
126
|
+
tables.push(outcome.table);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
warnings.push(outcome.warning);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
100
132
|
const fileStats = existsSync(this.dbPath) ? statSync(this.dbPath) : null;
|
|
101
133
|
return {
|
|
102
134
|
connectionId: this.connectionId,
|
|
@@ -108,8 +140,12 @@ export class KtxSqliteScanConnector {
|
|
|
108
140
|
file_size: fileStats ? fileStats.size : 0,
|
|
109
141
|
table_count: tables.length,
|
|
110
142
|
total_columns: tables.reduce((sum, table) => sum + table.columns.length, 0),
|
|
143
|
+
// Carries the full object inventory so a zero-match enabled_tables scope
|
|
144
|
+
// can report which objects were actually available.
|
|
145
|
+
...(scopedNames ? { discovered_object_names: allObjects.map((object) => object.name) } : {}),
|
|
111
146
|
},
|
|
112
147
|
tables,
|
|
148
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
113
149
|
};
|
|
114
150
|
}
|
|
115
151
|
async listSchemas() {
|
|
@@ -146,11 +182,71 @@ export class KtxSqliteScanConnector {
|
|
|
146
182
|
async columnStats(_input, _ctx) {
|
|
147
183
|
return null;
|
|
148
184
|
}
|
|
149
|
-
async executeReadOnly(input,
|
|
185
|
+
async executeReadOnly(input, ctx) {
|
|
150
186
|
this.assertConnection(input.connectionId);
|
|
151
|
-
|
|
187
|
+
// Validate and row-limit on the main thread so invalid SQL fails instantly
|
|
188
|
+
// without spawning a process and read-only enforcement stays at the boundary.
|
|
189
|
+
const sql = limitSqlForExecution(input.sql, input.maxRows);
|
|
190
|
+
const result = await this.runReadQueryOffProcess(sql, input.params, ctx.signal);
|
|
152
191
|
return { ...result, rowCount: result.rows.length };
|
|
153
192
|
}
|
|
193
|
+
// The LLM-SQL path runs off the event loop in a short-lived child process so a
|
|
194
|
+
// pathological scan cannot freeze the MCP server, and the deadline is enforced
|
|
195
|
+
// by SIGKILL-ing that process. A synchronous better-sqlite3 scan never yields,
|
|
196
|
+
// so a worker-thread terminate cannot interrupt it — only the OS reclaiming the
|
|
197
|
+
// whole process frees the CPU. One short-lived process per call; killed on
|
|
198
|
+
// completion, deadline, or external abort.
|
|
199
|
+
runReadQueryOffProcess(sql, params, signal) {
|
|
200
|
+
const deadlineMs = this.deadlineMs;
|
|
201
|
+
const dbPath = this.dbPath;
|
|
202
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
203
|
+
const child = this.spawnReadQueryChild();
|
|
204
|
+
let settled = false;
|
|
205
|
+
const onDeadline = () => settle(() => rejectPromise(queryDeadlineExceededError(deadlineMs)));
|
|
206
|
+
const timer = setTimeout(onDeadline, deadlineMs);
|
|
207
|
+
function settle(finish) {
|
|
208
|
+
if (settled) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
settled = true;
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
signal?.removeEventListener('abort', onDeadline);
|
|
214
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
215
|
+
child.kill('SIGKILL');
|
|
216
|
+
}
|
|
217
|
+
finish();
|
|
218
|
+
}
|
|
219
|
+
child.on('message', (message) => {
|
|
220
|
+
if (message.ok) {
|
|
221
|
+
settle(() => resolvePromise({
|
|
222
|
+
headers: message.headers,
|
|
223
|
+
rows: normalizeQueryRows(message.rows),
|
|
224
|
+
totalRows: message.totalRows,
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
settle(() => rejectPromise(new Error(message.message)));
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
child.on('error', (error) => settle(() => rejectPromise(error)));
|
|
232
|
+
child.on('exit', (code, processSignal) => {
|
|
233
|
+
if (!settled) {
|
|
234
|
+
settle(() => rejectPromise(new Error(`SQLite read process exited before returning a result (code ${code}, signal ${processSignal}).`)));
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
if (signal?.aborted) {
|
|
238
|
+
onDeadline();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
signal?.addEventListener('abort', onDeadline, { once: true });
|
|
242
|
+
try {
|
|
243
|
+
child.send({ dbPath, sql, params });
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
settle(() => rejectPromise(error instanceof Error ? error : new Error(String(error))));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
154
250
|
async getColumnDistinctValues(table, columnName, options) {
|
|
155
251
|
const sampleSize = options.sampleSize ?? 10000;
|
|
156
252
|
const tableName = this.qTableName(table);
|
|
@@ -213,11 +309,7 @@ export class KtxSqliteScanConnector {
|
|
|
213
309
|
const foreignKeys = database
|
|
214
310
|
.prepare(`PRAGMA foreign_key_list(${this.dialect.quoteIdentifier(table.name)})`)
|
|
215
311
|
.all();
|
|
216
|
-
const estimatedRows = table.type === 'table'
|
|
217
|
-
? Number(database
|
|
218
|
-
.prepare(`SELECT COUNT(*) AS count FROM ${this.dialect.quoteIdentifier(table.name)}`)
|
|
219
|
-
.get().count)
|
|
220
|
-
: null;
|
|
312
|
+
const estimatedRows = table.type === 'table' ? this.readRowCount(database, table.name) : null;
|
|
221
313
|
return {
|
|
222
314
|
catalog: null,
|
|
223
315
|
db: null,
|
|
@@ -237,6 +329,17 @@ export class KtxSqliteScanConnector {
|
|
|
237
329
|
foreignKeys: this.mapForeignKeys(foreignKeys),
|
|
238
330
|
};
|
|
239
331
|
}
|
|
332
|
+
// A row-count read is profiling, not structure: a failure here leaves the
|
|
333
|
+
// object's structure intact rather than skipping the whole object.
|
|
334
|
+
readRowCount(database, name) {
|
|
335
|
+
try {
|
|
336
|
+
const row = database.prepare(`SELECT COUNT(*) AS count FROM ${this.dialect.quoteIdentifier(name)}`).get();
|
|
337
|
+
return Number(row.count);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
240
343
|
mapForeignKeys(rows) {
|
|
241
344
|
return rows
|
|
242
345
|
.sort((a, b) => a.id - b.id || a.seq - b.seq)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { KtxSqlDialect } from '../../context/connections/dialects.js';
|
|
2
2
|
import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/types.js';
|
|
3
3
|
type SqliteTableNameRef = Pick<KtxTableRef, 'name'> & Partial<Pick<KtxTableRef, 'catalog' | 'db'>>;
|
|
4
4
|
/** @internal */
|
|
5
|
-
export declare class KtxSqliteDialect implements
|
|
5
|
+
export declare class KtxSqliteDialect implements KtxSqlDialect {
|
|
6
6
|
readonly type: "sqlite";
|
|
7
7
|
private readonly typeMappings;
|
|
8
8
|
quoteIdentifier(identifier: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
process.once('message', (request) => {
|
|
3
|
+
let db;
|
|
4
|
+
let response;
|
|
5
|
+
try {
|
|
6
|
+
db = new Database(request.dbPath, { readonly: true, fileMustExist: true });
|
|
7
|
+
const statement = db.prepare(request.sql);
|
|
8
|
+
const rows = (request.params ? statement.all(request.params) : statement.all());
|
|
9
|
+
response = {
|
|
10
|
+
ok: true,
|
|
11
|
+
headers: statement.columns().map((column) => column.name),
|
|
12
|
+
rows,
|
|
13
|
+
totalRows: rows.length,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
response = { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
db?.close();
|
|
21
|
+
}
|
|
22
|
+
process.send?.(response, () => process.exit(0));
|
|
23
|
+
});
|
|
@@ -20,6 +20,7 @@ export interface KtxSqlServerPoolConfig {
|
|
|
20
20
|
database: string;
|
|
21
21
|
user: string;
|
|
22
22
|
password?: string;
|
|
23
|
+
requestTimeout: number;
|
|
23
24
|
options: {
|
|
24
25
|
encrypt: true;
|
|
25
26
|
trustServerCertificate: boolean;
|
|
@@ -109,6 +110,7 @@ export declare class KtxSqlServerScanConnector implements KtxScanConnector {
|
|
|
109
110
|
private readonly poolFactory;
|
|
110
111
|
private readonly endpointResolver?;
|
|
111
112
|
private readonly now;
|
|
113
|
+
private readonly deadlineMs;
|
|
112
114
|
private readonly dialect;
|
|
113
115
|
private pool;
|
|
114
116
|
private resolvedEndpoint;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assertReadOnlySql, hoistLeadingCte, stripTrailingSqlNoise } from '../../context/connections/read-only-sql.js';
|
|
2
|
-
import {
|
|
2
|
+
import { getSqlDialectForDriver } from '../../context/connections/dialects.js';
|
|
3
|
+
import { resolveQueryDeadlineMs, queryDeadlineExceededError } from '../../context/connections/query-deadline.js';
|
|
3
4
|
import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js';
|
|
4
5
|
import { scopedTableNames } from '../../context/scan/table-ref.js';
|
|
5
6
|
import { connectorTestFailure, createKtxConnectorCapabilities, } from '../../context/scan/types.js';
|
|
@@ -128,6 +129,10 @@ function isDeniedError(error) {
|
|
|
128
129
|
const number = error.number;
|
|
129
130
|
return number === 229 || number === 230 || number === 297;
|
|
130
131
|
}
|
|
132
|
+
// mssql raises a RequestError with code 'ETIMEOUT' once requestTimeout elapses.
|
|
133
|
+
function isSqlServerTimeoutError(error) {
|
|
134
|
+
return Boolean(error) && typeof error === 'object' && error.code === 'ETIMEOUT';
|
|
135
|
+
}
|
|
131
136
|
function limitSqlForSqlServerExecution(sqlText, maxRows) {
|
|
132
137
|
const trimmed = stripTrailingSqlNoise(assertReadOnlySql(sqlText));
|
|
133
138
|
if (!maxRows) {
|
|
@@ -176,6 +181,7 @@ export function sqlServerConnectionPoolConfigFromConfig(input) {
|
|
|
176
181
|
database,
|
|
177
182
|
user,
|
|
178
183
|
password: stringConfigValue(merged, 'password', env),
|
|
184
|
+
requestTimeout: resolveQueryDeadlineMs(merged),
|
|
179
185
|
options: { encrypt: true, trustServerCertificate: merged.trustServerCertificate ?? true },
|
|
180
186
|
pool: { max: maxConnections, min: 0, idleTimeoutMillis: 30000 },
|
|
181
187
|
};
|
|
@@ -199,7 +205,8 @@ export class KtxSqlServerScanConnector {
|
|
|
199
205
|
poolFactory;
|
|
200
206
|
endpointResolver;
|
|
201
207
|
now;
|
|
202
|
-
|
|
208
|
+
deadlineMs;
|
|
209
|
+
dialect = getSqlDialectForDriver('sqlserver');
|
|
203
210
|
pool = null;
|
|
204
211
|
resolvedEndpoint = null;
|
|
205
212
|
constructor(options) {
|
|
@@ -215,6 +222,7 @@ export class KtxSqlServerScanConnector {
|
|
|
215
222
|
this.poolFactory = options.poolFactory ?? new DefaultSqlServerPoolFactory();
|
|
216
223
|
this.endpointResolver = options.endpointResolver;
|
|
217
224
|
this.now = options.now ?? (() => new Date());
|
|
225
|
+
this.deadlineMs = resolveQueryDeadlineMs(this.connection);
|
|
218
226
|
this.id = `sqlserver:${options.connectionId}`;
|
|
219
227
|
}
|
|
220
228
|
async testConnection() {
|
|
@@ -548,7 +556,16 @@ export class KtxSqlServerScanConnector {
|
|
|
548
556
|
request.input(key, value);
|
|
549
557
|
}
|
|
550
558
|
}
|
|
551
|
-
|
|
559
|
+
let result;
|
|
560
|
+
try {
|
|
561
|
+
result = await request.query(assertReadOnlySql(query));
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
if (isSqlServerTimeoutError(error)) {
|
|
565
|
+
throw queryDeadlineExceededError(this.deadlineMs, { cause: error });
|
|
566
|
+
}
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
552
569
|
const recordset = result.recordset ?? [];
|
|
553
570
|
const columnMetadata = recordset.columns ?? {};
|
|
554
571
|
const metadataHeaders = Object.keys(columnMetadata);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { KtxSqlDialect } from '../../context/connections/dialects.js';
|
|
2
2
|
import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/types.js';
|
|
3
3
|
type SqlServerTableNameRef = Pick<KtxTableRef, 'name'> & Partial<Pick<KtxTableRef, 'catalog' | 'db'>>;
|
|
4
4
|
/** @internal */
|
|
5
|
-
export declare class KtxSqlServerDialect implements
|
|
5
|
+
export declare class KtxSqlServerDialect implements KtxSqlDialect {
|
|
6
6
|
readonly type: "sqlserver";
|
|
7
7
|
private readonly typeMappings;
|
|
8
8
|
quoteIdentifier(identifier: string): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type ContentCacheMetadata = Record<string, unknown>;
|
|
2
|
+
export interface ContentResultCacheLookup {
|
|
3
|
+
namespace: string;
|
|
4
|
+
scopeKey: string;
|
|
5
|
+
inputHash: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ContentResultCacheCompleted<TOutput = unknown> extends ContentResultCacheLookup {
|
|
8
|
+
runId: string;
|
|
9
|
+
status: 'completed';
|
|
10
|
+
output: TOutput;
|
|
11
|
+
errorMessage: null;
|
|
12
|
+
metadata: ContentCacheMetadata;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ContentResultCacheFailed extends ContentResultCacheLookup {
|
|
16
|
+
runId: string;
|
|
17
|
+
status: 'failed';
|
|
18
|
+
output: null;
|
|
19
|
+
errorMessage: string;
|
|
20
|
+
metadata: ContentCacheMetadata;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
}
|
|
23
|
+
export type ContentResultCacheRecord<TOutput = unknown> = ContentResultCacheCompleted<TOutput> | ContentResultCacheFailed;
|
|
24
|
+
export interface ContentResultCache {
|
|
25
|
+
findCompletedResult<TOutput = unknown>(input: ContentResultCacheLookup): Promise<ContentResultCacheCompleted<TOutput> | null>;
|
|
26
|
+
findLatestCompletedResult(input: {
|
|
27
|
+
namespace: string;
|
|
28
|
+
scopeKey: string;
|
|
29
|
+
}): Promise<ContentResultCacheCompleted | null>;
|
|
30
|
+
saveCompletedResult<TOutput = unknown>(input: Omit<ContentResultCacheCompleted<TOutput>, 'status' | 'errorMessage'>): Promise<void>;
|
|
31
|
+
saveFailedResult(input: Omit<ContentResultCacheFailed, 'status' | 'output'>): Promise<void>;
|
|
32
|
+
deleteResult(input: ContentResultCacheLookup): Promise<void>;
|
|
33
|
+
listRunResults(runId: string): Promise<ContentResultCacheRecord[]>;
|
|
34
|
+
}
|
|
35
|
+
export declare function stableContentHash(value: unknown): string;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
function stableJson(value) {
|
|
3
|
+
if (Array.isArray(value)) {
|
|
4
|
+
return `[${value.map(stableJson).join(',')}]`;
|
|
5
|
+
}
|
|
6
|
+
if (value && typeof value === 'object') {
|
|
7
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
8
|
+
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(',')}}`;
|
|
9
|
+
}
|
|
10
|
+
return JSON.stringify(value) ?? 'undefined';
|
|
11
|
+
}
|
|
12
|
+
export function stableContentHash(value) {
|
|
13
|
+
return createHash('sha256').update(stableJson(value)).digest('hex');
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ContentResultCache, ContentResultCacheCompleted, ContentResultCacheFailed, ContentResultCacheLookup, ContentResultCacheRecord } from './content-result-cache.js';
|
|
2
|
+
export interface SqliteContentResultCacheOptions {
|
|
3
|
+
dbPath: string;
|
|
4
|
+
}
|
|
5
|
+
export declare class SqliteContentResultCache implements ContentResultCache {
|
|
6
|
+
private readonly db;
|
|
7
|
+
constructor(options: SqliteContentResultCacheOptions);
|
|
8
|
+
private dropResultsTableIfPrimaryKeyDiffers;
|
|
9
|
+
findCompletedResult<TOutput = unknown>(input: ContentResultCacheLookup): Promise<ContentResultCacheCompleted<TOutput> | null>;
|
|
10
|
+
findLatestCompletedResult(input: {
|
|
11
|
+
namespace: string;
|
|
12
|
+
scopeKey: string;
|
|
13
|
+
}): Promise<ContentResultCacheCompleted | null>;
|
|
14
|
+
saveCompletedResult<TOutput = unknown>(input: Omit<ContentResultCacheCompleted<TOutput>, 'status' | 'errorMessage'>): Promise<void>;
|
|
15
|
+
saveFailedResult(input: Omit<ContentResultCacheFailed, 'status' | 'output'>): Promise<void>;
|
|
16
|
+
deleteResult(input: ContentResultCacheLookup): Promise<void>;
|
|
17
|
+
listRunResults(runId: string): Promise<ContentResultCacheRecord[]>;
|
|
18
|
+
}
|