@kaelio/ktx 0.14.0 → 0.16.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.16.0-py3-none-any.whl +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/setup-commands.js +2 -0
- package/dist/connection-drivers.d.ts +1 -1
- package/dist/connection-drivers.js +2 -0
- package/dist/connection.js +1 -0
- package/dist/connectors/athena/connector.d.ts +160 -0
- package/dist/connectors/athena/connector.js +378 -0
- package/dist/connectors/athena/dialect.d.ts +31 -0
- package/dist/connectors/athena/dialect.js +146 -0
- package/dist/connectors/athena/live-database-introspection.d.ts +10 -0
- package/dist/connectors/athena/live-database-introspection.js +24 -0
- package/dist/connectors/duckdb/connector.d.ts +60 -0
- package/dist/connectors/duckdb/connector.js +282 -0
- package/dist/connectors/duckdb/dialect.d.ts +31 -0
- package/dist/connectors/duckdb/dialect.js +155 -0
- package/dist/connectors/duckdb/federated-attach.js +7 -0
- package/dist/connectors/duckdb/federated-executor.js +7 -13
- package/dist/connectors/duckdb/live-database-introspection.d.ts +8 -0
- package/dist/connectors/duckdb/live-database-introspection.js +24 -0
- package/dist/connectors/shared/duckdb-json-safe.d.ts +3 -0
- package/dist/connectors/shared/duckdb-json-safe.js +12 -0
- package/dist/context/connections/connection-type-dialect.d.ts +2 -0
- package/dist/context/connections/connection-type-dialect.js +20 -0
- package/dist/context/connections/connection-type.d.ts +6 -15
- package/dist/context/connections/connection-type.js +1 -10
- package/dist/context/connections/dialects.js +4 -0
- package/dist/context/connections/drivers.js +38 -0
- package/dist/context/connections/federation.d.ts +1 -1
- package/dist/context/connections/federation.js +6 -5
- package/dist/context/connections/local-warehouse-descriptor.d.ts +1 -0
- package/dist/context/connections/local-warehouse-descriptor.js +10 -0
- package/dist/context/connections/project-sql-executor.d.ts +24 -0
- package/dist/context/connections/project-sql-executor.js +56 -0
- package/dist/context/connections/query-policy.d.ts +12 -0
- package/dist/context/connections/query-policy.js +46 -0
- package/dist/context/core/config-reference.js +11 -1
- package/dist/context/daemon/semantic-layer-compute.d.ts +16 -0
- package/dist/context/daemon/semantic-layer-compute.js +44 -2
- package/dist/context/ingest/adapters/looker/mapping.d.ts +1 -0
- package/dist/context/ingest/adapters/looker/mapping.js +3 -10
- package/dist/context/ingest/adapters/looker/types.d.ts +6 -15
- package/dist/context/ingest/adapters/metabase/mapping.d.ts +1 -0
- package/dist/context/ingest/adapters/metabase/mapping.js +1 -0
- package/dist/context/ingest/adapters/sigma/chunk.d.ts +6 -0
- package/dist/context/ingest/adapters/sigma/chunk.js +119 -0
- package/dist/context/ingest/adapters/sigma/client-port.d.ts +45 -0
- package/dist/context/ingest/adapters/sigma/client-port.js +1 -0
- package/dist/context/ingest/adapters/sigma/client.d.ts +33 -0
- package/dist/context/ingest/adapters/sigma/client.js +176 -0
- package/dist/context/ingest/adapters/sigma/detect.d.ts +1 -0
- package/dist/context/ingest/adapters/sigma/detect.js +23 -0
- package/dist/context/ingest/adapters/sigma/fetch.d.ts +14 -0
- package/dist/context/ingest/adapters/sigma/fetch.js +191 -0
- package/dist/context/ingest/adapters/sigma/local-sigma.adapter.d.ts +17 -0
- package/dist/context/ingest/adapters/sigma/local-sigma.adapter.js +41 -0
- package/dist/context/ingest/adapters/sigma/project.d.ts +8 -0
- package/dist/context/ingest/adapters/sigma/project.js +186 -0
- package/dist/context/ingest/adapters/sigma/sigma.adapter.d.ts +18 -0
- package/dist/context/ingest/adapters/sigma/sigma.adapter.js +43 -0
- package/dist/context/ingest/adapters/sigma/types.d.ts +80 -0
- package/dist/context/ingest/adapters/sigma/types.js +82 -0
- package/dist/context/ingest/local-adapters.d.ts +2 -1
- package/dist/context/ingest/local-adapters.js +22 -0
- package/dist/context/mcp/context-tools.js +1 -0
- package/dist/context/mcp/local-project-ports.d.ts +1 -1
- package/dist/context/mcp/local-project-ports.js +53 -51
- package/dist/context/project/config.d.ts +122 -0
- package/dist/context/project/driver-schemas.d.ts +61 -0
- package/dist/context/project/driver-schemas.js +47 -0
- package/dist/context/scan/local-scan.js +3 -1
- package/dist/context/scan/types.d.ts +1 -1
- package/dist/context/sl/local-query.js +23 -6
- package/dist/context/sl/semantic-layer.service.d.ts +0 -4
- package/dist/context/sl/semantic-layer.service.js +3 -20
- package/dist/context/sl/tools/sl-warehouse-validation.d.ts +1 -1
- package/dist/context/sl/tools/sl-warehouse-validation.js +2 -2
- package/dist/context/sl/types.d.ts +1 -0
- package/dist/context/sql-analysis/dialect-notes.d.ts +1 -1
- package/dist/context/sql-analysis/dialect-notes.js +3 -2
- package/dist/context/sql-analysis/dialect.js +1 -0
- package/dist/context/sql-analysis/dialects/athena.md +12 -0
- package/dist/context/sql-analysis/dialects/duckdb.md +10 -0
- package/dist/local-adapters.js +17 -0
- package/dist/public-ingest.js +1 -0
- package/dist/setup-databases.d.ts +1 -1
- package/dist/setup-databases.js +52 -2
- package/dist/setup-sources.d.ts +2 -1
- package/dist/setup-sources.js +84 -0
- package/dist/skills/sigma_ingest/SKILL.md +189 -0
- package/dist/sql.js +9 -14
- package/dist/status-project.js +2 -1
- package/dist/telemetry/events.d.ts +1 -1
- package/package.json +3 -1
- package/assets/python/kaelio_ktx-0.14.0-py3-none-any.whl +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { columnDisplayPartCount, formatDialectDisplayRef, formatDialectTableName, limitOffsetClause, parseDialectDisplayRef, } from '../../context/connections/dialect-helpers.js';
|
|
2
|
+
/** @internal */
|
|
3
|
+
export class KtxDuckDbDialect {
|
|
4
|
+
type = 'duckdb';
|
|
5
|
+
typeMappings = {
|
|
6
|
+
TIMESTAMP: 'time',
|
|
7
|
+
'TIMESTAMP WITH TIME ZONE': 'time',
|
|
8
|
+
TIMESTAMPTZ: 'time',
|
|
9
|
+
DATE: 'time',
|
|
10
|
+
TIME: 'time',
|
|
11
|
+
BIGINT: 'number',
|
|
12
|
+
INTEGER: 'number',
|
|
13
|
+
SMALLINT: 'number',
|
|
14
|
+
TINYINT: 'number',
|
|
15
|
+
HUGEINT: 'number',
|
|
16
|
+
UBIGINT: 'number',
|
|
17
|
+
UINTEGER: 'number',
|
|
18
|
+
DECIMAL: 'number',
|
|
19
|
+
NUMERIC: 'number',
|
|
20
|
+
REAL: 'number',
|
|
21
|
+
FLOAT: 'number',
|
|
22
|
+
DOUBLE: 'number',
|
|
23
|
+
VARCHAR: 'string',
|
|
24
|
+
CHAR: 'string',
|
|
25
|
+
TEXT: 'string',
|
|
26
|
+
BLOB: 'string',
|
|
27
|
+
UUID: 'string',
|
|
28
|
+
BOOLEAN: 'boolean',
|
|
29
|
+
BOOL: 'boolean',
|
|
30
|
+
};
|
|
31
|
+
quoteIdentifier(identifier) {
|
|
32
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
33
|
+
}
|
|
34
|
+
// v1 introspects the `main` schema only and sets db=null on every table, so
|
|
35
|
+
// refs are single-namespace like SQLite — use the matching display shape.
|
|
36
|
+
formatTableName(table) {
|
|
37
|
+
return formatDialectTableName(table, this.quoteIdentifier.bind(this), 'sqlite');
|
|
38
|
+
}
|
|
39
|
+
formatDisplayRef(table) {
|
|
40
|
+
return formatDialectDisplayRef(table, 'sqlite');
|
|
41
|
+
}
|
|
42
|
+
parseDisplayRef(display) {
|
|
43
|
+
return parseDialectDisplayRef(display, 'sqlite');
|
|
44
|
+
}
|
|
45
|
+
columnDisplayTablePartCount() {
|
|
46
|
+
return columnDisplayPartCount('sqlite');
|
|
47
|
+
}
|
|
48
|
+
mapDataType(nativeType) {
|
|
49
|
+
return nativeType;
|
|
50
|
+
}
|
|
51
|
+
mapToDimensionType(nativeType) {
|
|
52
|
+
if (!nativeType) {
|
|
53
|
+
return 'string';
|
|
54
|
+
}
|
|
55
|
+
let normalized = nativeType.toUpperCase().trim();
|
|
56
|
+
if (normalized.includes('(')) {
|
|
57
|
+
normalized = normalized.split('(')[0].trim();
|
|
58
|
+
}
|
|
59
|
+
if (this.typeMappings[normalized]) {
|
|
60
|
+
return this.typeMappings[normalized];
|
|
61
|
+
}
|
|
62
|
+
if (normalized.includes('TIME') || normalized.includes('DATE')) {
|
|
63
|
+
return 'time';
|
|
64
|
+
}
|
|
65
|
+
if (normalized.includes('INT') ||
|
|
66
|
+
normalized.includes('DEC') ||
|
|
67
|
+
normalized.includes('NUM') ||
|
|
68
|
+
normalized.includes('REAL') ||
|
|
69
|
+
normalized.includes('FLOAT') ||
|
|
70
|
+
normalized.includes('DOUBLE')) {
|
|
71
|
+
return 'number';
|
|
72
|
+
}
|
|
73
|
+
if (normalized.includes('BOOL')) {
|
|
74
|
+
return 'boolean';
|
|
75
|
+
}
|
|
76
|
+
return 'string';
|
|
77
|
+
}
|
|
78
|
+
generateSampleQuery(tableName, limit, columns) {
|
|
79
|
+
const columnList = columns && columns.length > 0 ? columns.map((column) => this.quoteIdentifier(column)).join(', ') : '*';
|
|
80
|
+
return `SELECT ${columnList} FROM ${tableName} LIMIT ${limit}`;
|
|
81
|
+
}
|
|
82
|
+
generateColumnSampleQuery(tableName, columnName, limit) {
|
|
83
|
+
const quoted = this.quoteIdentifier(columnName);
|
|
84
|
+
return `SELECT ${quoted} FROM ${tableName} WHERE ${quoted} IS NOT NULL AND TRIM(CAST(${quoted} AS VARCHAR)) != '' LIMIT ${limit}`;
|
|
85
|
+
}
|
|
86
|
+
getRandomSampleFilter(samplePct) {
|
|
87
|
+
if (samplePct <= 0 || samplePct >= 1) {
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
return `RANDOM() < ${samplePct}`;
|
|
91
|
+
}
|
|
92
|
+
getTableSampleClause(samplePct) {
|
|
93
|
+
if (samplePct <= 0 || samplePct >= 1) {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
return `USING SAMPLE ${Math.round(samplePct * 100)} PERCENT (bernoulli)`;
|
|
97
|
+
}
|
|
98
|
+
getLimitOffsetClause(limit, offset) {
|
|
99
|
+
return limitOffsetClause(limit, offset);
|
|
100
|
+
}
|
|
101
|
+
getTopClause(_limit) {
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
getNullCountExpression(column) {
|
|
105
|
+
return `SUM(CASE WHEN ${column} IS NULL THEN 1 ELSE 0 END)`;
|
|
106
|
+
}
|
|
107
|
+
getDistinctCountExpression(column) {
|
|
108
|
+
return `COUNT(DISTINCT ${column})`;
|
|
109
|
+
}
|
|
110
|
+
textLengthExpression(columnSql) {
|
|
111
|
+
return `LENGTH(CAST(${columnSql} AS VARCHAR))`;
|
|
112
|
+
}
|
|
113
|
+
castToText(columnSql) {
|
|
114
|
+
return `CAST(${columnSql} AS VARCHAR)`;
|
|
115
|
+
}
|
|
116
|
+
getSampleValueAggregation(innerSql) {
|
|
117
|
+
return `(SELECT STRING_AGG(CAST(value AS VARCHAR), chr(31)) FROM (${innerSql}) AS relationship_profile_values)`;
|
|
118
|
+
}
|
|
119
|
+
generateCardinalitySampleQuery(tableName, columnName, sampleSize) {
|
|
120
|
+
return `
|
|
121
|
+
WITH sampled AS (
|
|
122
|
+
SELECT ${columnName} AS val
|
|
123
|
+
FROM ${tableName}
|
|
124
|
+
WHERE ${columnName} IS NOT NULL
|
|
125
|
+
LIMIT ${sampleSize}
|
|
126
|
+
)
|
|
127
|
+
SELECT COUNT(DISTINCT val) AS cardinality
|
|
128
|
+
FROM sampled
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
generateDistinctValuesQuery(tableName, columnName, limit) {
|
|
132
|
+
return `
|
|
133
|
+
SELECT DISTINCT CAST(${columnName} AS VARCHAR) AS val
|
|
134
|
+
FROM ${tableName}
|
|
135
|
+
WHERE ${columnName} IS NOT NULL
|
|
136
|
+
ORDER BY val
|
|
137
|
+
LIMIT ${limit}
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
generateColumnStatisticsQuery(_schemaName, _tableName) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
generateRandomizedCardinalitySampleQuery(tableName, columnName, sampleSize) {
|
|
144
|
+
return `
|
|
145
|
+
WITH sampled AS (
|
|
146
|
+
SELECT ${columnName} AS val
|
|
147
|
+
FROM ${tableName}
|
|
148
|
+
WHERE ${columnName} IS NOT NULL
|
|
149
|
+
USING SAMPLE ${sampleSize} ROWS
|
|
150
|
+
)
|
|
151
|
+
SELECT COUNT(DISTINCT val) AS cardinality
|
|
152
|
+
FROM sampled
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sqliteDatabasePathFromConfig } from '../sqlite/connector.js';
|
|
2
2
|
import { postgresPoolConfigFromConfig } from '../postgres/connector.js';
|
|
3
3
|
import { mysqlConnectionPoolConfigFromConfig, } from '../mysql/connector.js';
|
|
4
|
+
import { duckDbDatabasePathFromConfig } from '../../connectors/duckdb/connector.js';
|
|
4
5
|
function kvKeyword(value) {
|
|
5
6
|
// libpq/DuckDB key-value values quote with single quotes and backslash-escape.
|
|
6
7
|
return /[\s'\\]/.test(value) ? `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'` : value;
|
|
@@ -70,6 +71,12 @@ function mysqlAttachString(member, env) {
|
|
|
70
71
|
*/
|
|
71
72
|
export function federatedAttachTarget(member, env) {
|
|
72
73
|
switch (member.driver.toLowerCase()) {
|
|
74
|
+
case 'duckdb':
|
|
75
|
+
return duckDbDatabasePathFromConfig({
|
|
76
|
+
connectionId: member.connectionId,
|
|
77
|
+
projectDir: member.projectDir,
|
|
78
|
+
connection: member.connection,
|
|
79
|
+
});
|
|
73
80
|
case 'sqlite':
|
|
74
81
|
return sqliteDatabasePathFromConfig({
|
|
75
82
|
connectionId: member.connectionId,
|
|
@@ -3,20 +3,10 @@ import { federatedAttachTarget } from './federated-attach.js';
|
|
|
3
3
|
import { normalizeQueryRows } from '../../context/connections/query-executor.js';
|
|
4
4
|
import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js';
|
|
5
5
|
import { attachTypeForDriver } from '../../context/connections/federation.js';
|
|
6
|
+
import { toJsonSafeRows } from '../shared/duckdb-json-safe.js';
|
|
6
7
|
function quoteDuckdbIdentifier(id) {
|
|
7
8
|
return `"${id.replaceAll('"', '""')}"`;
|
|
8
9
|
}
|
|
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
10
|
/** @internal */
|
|
21
11
|
export function buildAttachStatements(members, env) {
|
|
22
12
|
const attachments = members.map((member) => ({
|
|
@@ -24,8 +14,12 @@ export function buildAttachStatements(members, env) {
|
|
|
24
14
|
url: federatedAttachTarget(member, env),
|
|
25
15
|
alias: member.connectionId,
|
|
26
16
|
}));
|
|
27
|
-
const
|
|
28
|
-
const
|
|
17
|
+
const loadTypes = [...new Set(attachments.map((a) => a.type).filter((type) => type !== null))];
|
|
18
|
+
const loadStatements = loadTypes.map((type) => `INSTALL ${type}; LOAD ${type};`);
|
|
19
|
+
const attachStatements = attachments.map(({ type, url, alias }) => {
|
|
20
|
+
const options = type === null ? 'READ_ONLY' : `TYPE ${type}, READ_ONLY`;
|
|
21
|
+
return `ATTACH '${url.replaceAll("'", "''")}' AS ${quoteDuckdbIdentifier(alias)} (${options});`;
|
|
22
|
+
});
|
|
29
23
|
return [...loadStatements, ...attachStatements];
|
|
30
24
|
}
|
|
31
25
|
export async function executeFederatedQuery(members, input, env = process.env) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LiveDatabaseIntrospectionPort } from '../../context/ingest/adapters/live-database/types.js';
|
|
2
|
+
import type { KtxProjectConnectionConfig } from '../../context/project/config.js';
|
|
3
|
+
export interface CreateDuckDbLiveDatabaseIntrospectionOptions {
|
|
4
|
+
projectDir?: string;
|
|
5
|
+
connections: Record<string, KtxProjectConnectionConfig>;
|
|
6
|
+
now?: () => Date;
|
|
7
|
+
}
|
|
8
|
+
export declare function createDuckDbLiveDatabaseIntrospection(options: CreateDuckDbLiveDatabaseIntrospectionOptions): LiveDatabaseIntrospectionPort;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { KtxDuckDbScanConnector } from './connector.js';
|
|
2
|
+
export function createDuckDbLiveDatabaseIntrospection(options) {
|
|
3
|
+
return {
|
|
4
|
+
async extractSchema(connectionId, introspectionOptions) {
|
|
5
|
+
const connection = options.connections[connectionId];
|
|
6
|
+
const connector = new KtxDuckDbScanConnector({
|
|
7
|
+
connectionId,
|
|
8
|
+
connection,
|
|
9
|
+
projectDir: options.projectDir,
|
|
10
|
+
now: options.now,
|
|
11
|
+
});
|
|
12
|
+
try {
|
|
13
|
+
return await connector.introspect({
|
|
14
|
+
connectionId,
|
|
15
|
+
driver: 'duckdb',
|
|
16
|
+
...(introspectionOptions?.tableScope ? { tableScope: introspectionOptions.tableScope } : {}),
|
|
17
|
+
}, { runId: `duckdb-${connectionId}` });
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
await connector.cleanup();
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const MIN_SAFE_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
|
|
2
|
+
const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
|
3
|
+
// DuckDB returns integer columns as JS bigint (unserializable by JSON). Values
|
|
4
|
+
// in Number's safe range become Number; larger magnitudes become strings so a
|
|
5
|
+
// BIGINT beyond 2^53 keeps its exact value instead of silently rounding.
|
|
6
|
+
/** @internal */
|
|
7
|
+
export function jsonSafeBigint(value) {
|
|
8
|
+
return value >= MIN_SAFE_BIGINT && value <= MAX_SAFE_BIGINT ? Number(value) : value.toString();
|
|
9
|
+
}
|
|
10
|
+
export function toJsonSafeRows(rows) {
|
|
11
|
+
return rows.map((row) => row.map((cell) => (typeof cell === 'bigint' ? jsonSafeBigint(cell) : cell)));
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const CONNECTION_TYPE_TO_SQLGLOT = {
|
|
2
|
+
POSTGRESQL: 'postgres',
|
|
3
|
+
SQLITE: 'sqlite',
|
|
4
|
+
DUCKDB: 'duckdb',
|
|
5
|
+
SQLSERVER: 'tsql',
|
|
6
|
+
BIGQUERY: 'bigquery',
|
|
7
|
+
SNOWFLAKE: 'snowflake',
|
|
8
|
+
MYSQL: 'mysql',
|
|
9
|
+
CLICKHOUSE: 'clickhouse',
|
|
10
|
+
ATHENA: 'athena',
|
|
11
|
+
METABASE: null,
|
|
12
|
+
LOOKER: null,
|
|
13
|
+
NOTION: null,
|
|
14
|
+
};
|
|
15
|
+
export function dialectForConnectionType(connectionType) {
|
|
16
|
+
return CONNECTION_TYPE_TO_SQLGLOT[connectionType.toUpperCase()] ?? 'postgres';
|
|
17
|
+
}
|
|
18
|
+
export function warehouseTargetDialect(connectionType) {
|
|
19
|
+
return CONNECTION_TYPE_TO_SQLGLOT[connectionType.toUpperCase()] ?? null;
|
|
20
|
+
}
|
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const connectionTypeSchema: z.ZodEnum<{
|
|
3
|
-
|
|
3
|
+
POSTGRESQL: "POSTGRESQL";
|
|
4
|
+
SQLITE: "SQLITE";
|
|
5
|
+
DUCKDB: "DUCKDB";
|
|
6
|
+
SQLSERVER: "SQLSERVER";
|
|
4
7
|
BIGQUERY: "BIGQUERY";
|
|
5
8
|
SNOWFLAKE: "SNOWFLAKE";
|
|
6
|
-
MYSQL: "MYSQL";
|
|
7
|
-
SQLSERVER: "SQLSERVER";
|
|
8
|
-
SQLITE: "SQLITE";
|
|
9
|
-
CLICKHOUSE: "CLICKHOUSE";
|
|
10
|
-
POSTGRESQL: "POSTGRESQL";
|
|
11
|
-
CENTRALREACH: "CENTRALREACH";
|
|
12
|
-
EPIC: "EPIC";
|
|
13
|
-
CERNER: "CERNER";
|
|
14
9
|
ATHENA: "ATHENA";
|
|
15
|
-
QUICKBOOKS: "QUICKBOOKS";
|
|
16
|
-
WORKDAY: "WORKDAY";
|
|
17
|
-
REST: "REST";
|
|
18
|
-
S3: "S3";
|
|
19
|
-
SLACK: "SLACK";
|
|
20
10
|
METABASE: "METABASE";
|
|
21
11
|
LOOKER: "LOOKER";
|
|
22
12
|
NOTION: "NOTION";
|
|
23
|
-
|
|
13
|
+
MYSQL: "MYSQL";
|
|
14
|
+
CLICKHOUSE: "CLICKHOUSE";
|
|
24
15
|
}>;
|
|
25
16
|
export type ConnectionType = z.infer<typeof connectionTypeSchema>;
|
|
@@ -2,23 +2,14 @@ import { z } from 'zod';
|
|
|
2
2
|
export const connectionTypeSchema = z.enum([
|
|
3
3
|
'POSTGRESQL',
|
|
4
4
|
'SQLITE',
|
|
5
|
+
'DUCKDB',
|
|
5
6
|
'SQLSERVER',
|
|
6
7
|
'BIGQUERY',
|
|
7
8
|
'SNOWFLAKE',
|
|
8
|
-
'CENTRALREACH',
|
|
9
|
-
'EPIC',
|
|
10
|
-
'CERNER',
|
|
11
9
|
'ATHENA',
|
|
12
|
-
'QUICKBOOKS',
|
|
13
|
-
'WORKDAY',
|
|
14
|
-
'REST',
|
|
15
|
-
'S3',
|
|
16
|
-
'SLACK',
|
|
17
10
|
'METABASE',
|
|
18
11
|
'LOOKER',
|
|
19
12
|
'NOTION',
|
|
20
13
|
'MYSQL',
|
|
21
14
|
'CLICKHOUSE',
|
|
22
|
-
'PLAIN',
|
|
23
|
-
'BETTERSTACK',
|
|
24
15
|
]);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { KtxAthenaDialect } from '../../connectors/athena/dialect.js';
|
|
1
2
|
import { KtxBigQueryDialect } from '../../connectors/bigquery/dialect.js';
|
|
2
3
|
import { KtxClickHouseDialect } from '../../connectors/clickhouse/dialect.js';
|
|
4
|
+
import { KtxDuckDbDialect } from '../../connectors/duckdb/dialect.js';
|
|
3
5
|
import { KtxMongoDbDialect } from '../../connectors/mongodb/dialect.js';
|
|
4
6
|
import { KtxMysqlDialect } from '../../connectors/mysql/dialect.js';
|
|
5
7
|
import { KtxPostgresDialect } from '../../connectors/postgres/dialect.js';
|
|
@@ -8,8 +10,10 @@ import { KtxSnowflakeDialect } from '../../connectors/snowflake/dialect.js';
|
|
|
8
10
|
import { KtxSqlServerDialect } from '../../connectors/sqlserver/dialect.js';
|
|
9
11
|
import { KtxExpectedError } from '../../errors.js';
|
|
10
12
|
const sqlDialectFactories = {
|
|
13
|
+
athena: () => new KtxAthenaDialect(),
|
|
11
14
|
bigquery: () => new KtxBigQueryDialect(),
|
|
12
15
|
clickhouse: () => new KtxClickHouseDialect(),
|
|
16
|
+
duckdb: () => new KtxDuckDbDialect(),
|
|
13
17
|
mysql: () => new KtxMysqlDialect(),
|
|
14
18
|
postgres: () => new KtxPostgresDialect(),
|
|
15
19
|
sqlite: () => new KtxSqliteDialect(),
|
|
@@ -3,6 +3,23 @@ function invalidConnectionConfig(driver) {
|
|
|
3
3
|
}
|
|
4
4
|
/** @internal */
|
|
5
5
|
export const driverRegistrations = {
|
|
6
|
+
athena: {
|
|
7
|
+
driver: 'athena',
|
|
8
|
+
scopeConfigKey: 'databases',
|
|
9
|
+
hasHistoricSqlReader: false,
|
|
10
|
+
load: async () => {
|
|
11
|
+
const m = await import('../../connectors/athena/connector.js');
|
|
12
|
+
return {
|
|
13
|
+
isConnectionConfig: (connection) => m.isKtxAthenaConnectionConfig(connection),
|
|
14
|
+
createScanConnector: ({ connectionId, connection }) => {
|
|
15
|
+
if (!m.isKtxAthenaConnectionConfig(connection)) {
|
|
16
|
+
throw invalidConnectionConfig('athena');
|
|
17
|
+
}
|
|
18
|
+
return new m.KtxAthenaScanConnector({ connectionId, connection });
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
},
|
|
6
23
|
bigquery: {
|
|
7
24
|
driver: 'bigquery',
|
|
8
25
|
scopeConfigKey: 'dataset_ids',
|
|
@@ -45,6 +62,27 @@ export const driverRegistrations = {
|
|
|
45
62
|
};
|
|
46
63
|
},
|
|
47
64
|
},
|
|
65
|
+
duckdb: {
|
|
66
|
+
driver: 'duckdb',
|
|
67
|
+
scopeConfigKey: null,
|
|
68
|
+
hasHistoricSqlReader: false,
|
|
69
|
+
load: async () => {
|
|
70
|
+
const m = await import('../../connectors/duckdb/connector.js');
|
|
71
|
+
return {
|
|
72
|
+
isConnectionConfig: (connection) => {
|
|
73
|
+
const typedConnection = connection;
|
|
74
|
+
return m.isKtxDuckDbConnectionConfig(typedConnection);
|
|
75
|
+
},
|
|
76
|
+
createScanConnector: ({ connectionId, connection, projectDir }) => {
|
|
77
|
+
const typedConnection = connection;
|
|
78
|
+
if (!m.isKtxDuckDbConnectionConfig(typedConnection)) {
|
|
79
|
+
throw invalidConnectionConfig('duckdb');
|
|
80
|
+
}
|
|
81
|
+
return new m.KtxDuckDbScanConnector({ connectionId, connection: typedConnection, projectDir });
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
},
|
|
48
86
|
mongodb: {
|
|
49
87
|
driver: 'mongodb',
|
|
50
88
|
scopeConfigKey: 'databases',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { KtxProjectConnectionConfig } from '../project/config.js';
|
|
2
2
|
/** Stable id for the runtime-derived federated connection. Never written to ktx.yaml. */
|
|
3
3
|
export declare const FEDERATED_CONNECTION_ID = "_ktx_federated";
|
|
4
|
-
export declare function attachTypeForDriver(driver: string): string;
|
|
4
|
+
export declare function attachTypeForDriver(driver: string): string | null;
|
|
5
5
|
export interface FederatedMember {
|
|
6
6
|
connectionId: string;
|
|
7
7
|
driver: string;
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/** Stable id for the runtime-derived federated connection. Never written to ktx.yaml. */
|
|
2
2
|
export const FEDERATED_CONNECTION_ID = '_ktx_federated';
|
|
3
3
|
/**
|
|
4
|
-
* Drivers DuckDB can ATTACH for federation.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Drivers DuckDB can ATTACH for federation. Membership is governed by this set;
|
|
5
|
+
* the attach TYPE is governed by attachTypeForDriver, which returns the driver
|
|
6
|
+
* name for extension-backed engines and null for a native DuckDB file (attached
|
|
7
|
+
* with no INSTALL/LOAD and no TYPE).
|
|
7
8
|
*/
|
|
8
|
-
const ATTACH_COMPATIBLE_DRIVERS = new Set(['postgres', 'mysql', 'sqlite']);
|
|
9
|
+
const ATTACH_COMPATIBLE_DRIVERS = new Set(['postgres', 'mysql', 'sqlite', 'duckdb']);
|
|
9
10
|
export function attachTypeForDriver(driver) {
|
|
10
11
|
const normalized = driver.toLowerCase();
|
|
11
12
|
if (!ATTACH_COMPATIBLE_DRIVERS.has(normalized)) {
|
|
12
13
|
throw new Error(`Driver "${driver}" cannot be attached by DuckDB federation.`);
|
|
13
14
|
}
|
|
14
|
-
return normalized;
|
|
15
|
+
return normalized === 'duckdb' ? null : normalized;
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
18
|
* Derives a virtual federated connection when a project declares 2+
|
|
@@ -16,6 +16,7 @@ export interface LocalConnectionInfo {
|
|
|
16
16
|
connectionType: string;
|
|
17
17
|
members?: string[];
|
|
18
18
|
hint?: string;
|
|
19
|
+
queryPolicy?: 'semantic-layer-only';
|
|
19
20
|
}
|
|
20
21
|
export declare function localConnectionToWarehouseDescriptor(id: string, connection: KtxProjectConnectionConfig | undefined): LocalWarehouseDescriptor | null;
|
|
21
22
|
export declare function localConnectionTypeForConfig(id: string, connection: KtxProjectConnectionConfig | undefined): string;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { connectionQueryPolicy } from './query-policy.js';
|
|
1
2
|
const DRIVER_TO_CONNECTION_TYPE = {
|
|
2
3
|
postgres: 'POSTGRESQL',
|
|
3
4
|
sqlite: 'SQLITE',
|
|
5
|
+
duckdb: 'DUCKDB',
|
|
4
6
|
sqlserver: 'SQLSERVER',
|
|
5
7
|
mysql: 'MYSQL',
|
|
6
8
|
clickhouse: 'CLICKHOUSE',
|
|
7
9
|
snowflake: 'SNOWFLAKE',
|
|
8
10
|
bigquery: 'BIGQUERY',
|
|
11
|
+
athena: 'ATHENA',
|
|
9
12
|
};
|
|
10
13
|
export function localConnectionToWarehouseDescriptor(id, connection) {
|
|
11
14
|
if (!connection) {
|
|
@@ -59,10 +62,17 @@ export function localConnectionInfoFromConfig(id, connection) {
|
|
|
59
62
|
if (!connection) {
|
|
60
63
|
return null;
|
|
61
64
|
}
|
|
65
|
+
const restricted = connectionQueryPolicy(connection) === 'semantic-layer-only';
|
|
62
66
|
return {
|
|
63
67
|
id,
|
|
64
68
|
name: id,
|
|
65
69
|
connectionType: localConnectionTypeForConfig(id, connection),
|
|
70
|
+
...(restricted
|
|
71
|
+
? {
|
|
72
|
+
queryPolicy: 'semantic-layer-only',
|
|
73
|
+
hint: 'Raw SQL is disabled on this connection; query it with sl_query using predefined measures.',
|
|
74
|
+
}
|
|
75
|
+
: {}),
|
|
66
76
|
};
|
|
67
77
|
}
|
|
68
78
|
function stringField(value) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { executeFederatedQuery } from '../../connectors/duckdb/federated-executor.js';
|
|
2
|
+
import type { SqlAnalysisPort } from '../sql-analysis/ports.js';
|
|
2
3
|
import type { KtxLocalProject } from '../project/project.js';
|
|
3
4
|
import type { KtxScanConnector } from '../scan/types.js';
|
|
4
5
|
import type { KtxSqlQueryExecutionInput, KtxSqlQueryExecutionResult } from './query-executor.js';
|
|
@@ -16,3 +17,26 @@ export interface ExecuteProjectReadOnlySqlDeps {
|
|
|
16
17
|
* scan connector.
|
|
17
18
|
*/
|
|
18
19
|
export declare function executeProjectReadOnlySql(deps: ExecuteProjectReadOnlySqlDeps): Promise<KtxSqlQueryExecutionResult>;
|
|
20
|
+
type RawSqlProgressCallback = (event: {
|
|
21
|
+
progress: number;
|
|
22
|
+
message: string;
|
|
23
|
+
}) => void | Promise<void>;
|
|
24
|
+
export interface ExecuteProjectRawSqlDeps {
|
|
25
|
+
project: KtxLocalProject;
|
|
26
|
+
connectionId: string;
|
|
27
|
+
sql: string;
|
|
28
|
+
maxRows: number;
|
|
29
|
+
sqlAnalysis: SqlAnalysisPort;
|
|
30
|
+
createConnector: (connectionId: string) => Promise<KtxScanConnector> | KtxScanConnector;
|
|
31
|
+
executeFederated?: typeof executeFederatedQuery;
|
|
32
|
+
runId: string;
|
|
33
|
+
onProgress?: RawSqlProgressCallback;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Single guarded path for user-authored (raw) SQL — `ktx sql` and the MCP
|
|
37
|
+
* sql_execution tool. Enforces the connection's query_policy and the parser
|
|
38
|
+
* read-only guard before executing; ktx-internal SQL (semantic-layer, ingest)
|
|
39
|
+
* calls executeProjectReadOnlySql directly and is not subject to query_policy.
|
|
40
|
+
*/
|
|
41
|
+
export declare function executeProjectRawSql(deps: ExecuteProjectRawSqlDeps): Promise<KtxSqlQueryExecutionResult>;
|
|
42
|
+
export {};
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { executeFederatedQuery } from '../../connectors/duckdb/federated-executor.js';
|
|
2
|
+
import { KtxExpectedError, KtxQueryError, isNativeProgrammingFault } from '../../errors.js';
|
|
3
|
+
import { sqlAnalysisDialectForDriver } from '../sql-analysis/dialect.js';
|
|
4
|
+
import { assertSafeConnectionId } from '../sl/source-files.js';
|
|
5
|
+
import { assertSqlQueryableConnection } from './dialects.js';
|
|
2
6
|
import { deriveFederatedConnection, FEDERATED_CONNECTION_ID } from './federation.js';
|
|
7
|
+
import { assertRawSqlAllowed } from './query-policy.js';
|
|
8
|
+
import { resolveConfiguredConnection } from './resolve-connection.js';
|
|
3
9
|
/**
|
|
4
10
|
* Single resolve-and-execute path for project read-only SQL. The federated
|
|
5
11
|
* connection is derived from declared state here so every executor entry point
|
|
@@ -37,3 +43,53 @@ export async function executeProjectReadOnlySql(deps) {
|
|
|
37
43
|
await connector?.cleanup?.();
|
|
38
44
|
}
|
|
39
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Single guarded path for user-authored (raw) SQL — `ktx sql` and the MCP
|
|
48
|
+
* sql_execution tool. Enforces the connection's query_policy and the parser
|
|
49
|
+
* read-only guard before executing; ktx-internal SQL (semantic-layer, ingest)
|
|
50
|
+
* calls executeProjectReadOnlySql directly and is not subject to query_policy.
|
|
51
|
+
*/
|
|
52
|
+
export async function executeProjectRawSql(deps) {
|
|
53
|
+
const { project } = deps;
|
|
54
|
+
await deps.onProgress?.({ progress: 0, message: 'Validating SQL' });
|
|
55
|
+
const isFederated = deps.connectionId === FEDERATED_CONNECTION_ID;
|
|
56
|
+
const connectionId = isFederated ? deps.connectionId : assertSafeConnectionId(deps.connectionId);
|
|
57
|
+
const connection = isFederated ? undefined : resolveConfiguredConnection(project.config, connectionId);
|
|
58
|
+
if (!isFederated) {
|
|
59
|
+
assertSqlQueryableConnection(connectionId, connection.driver);
|
|
60
|
+
}
|
|
61
|
+
assertRawSqlAllowed(project.config, project.projectDir, connectionId);
|
|
62
|
+
const dialect = sqlAnalysisDialectForDriver(isFederated ? 'duckdb' : connection.driver);
|
|
63
|
+
const validation = await deps.sqlAnalysis.validateReadOnly(deps.sql, dialect);
|
|
64
|
+
if (!validation.ok) {
|
|
65
|
+
// A read-only guard rejecting the caller's SQL is an expected outcome, not a
|
|
66
|
+
// ktx fault: classify it so reportException keeps it out of Error Tracking.
|
|
67
|
+
throw new KtxQueryError(validation.error ?? 'SQL is not read-only.');
|
|
68
|
+
}
|
|
69
|
+
await deps.onProgress?.({ progress: 0.3, message: 'Executing' });
|
|
70
|
+
const result = await executeProjectReadOnlySql({
|
|
71
|
+
project,
|
|
72
|
+
input: {
|
|
73
|
+
connectionId,
|
|
74
|
+
projectDir: project.projectDir,
|
|
75
|
+
connection,
|
|
76
|
+
sql: deps.sql,
|
|
77
|
+
maxRows: deps.maxRows,
|
|
78
|
+
},
|
|
79
|
+
createConnector: deps.createConnector,
|
|
80
|
+
executeFederated: deps.executeFederated,
|
|
81
|
+
runId: deps.runId,
|
|
82
|
+
}).catch((error) => {
|
|
83
|
+
// A warehouse/driver rejection (e.g. the caller's SQL failed to compile) is a
|
|
84
|
+
// surfaced operational outcome, not a ktx fault: mark it expected while
|
|
85
|
+
// preserving the warehouse's own diagnostics. A native JS error (TypeError,
|
|
86
|
+
// etc.) signals a bug in connector code — let it propagate unchanged so Error
|
|
87
|
+
// Tracking still sees it.
|
|
88
|
+
if (isNativeProgrammingFault(error) || error instanceof KtxExpectedError) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
throw new KtxQueryError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
92
|
+
});
|
|
93
|
+
await deps.onProgress?.({ progress: 1, message: `Fetched ${result.rowCount ?? result.rows.length} rows` });
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { KtxProjectConfig, KtxProjectConnectionConfig } from '../project/config.js';
|
|
2
|
+
export type KtxConnectionQueryPolicy = 'read-only-sql' | 'semantic-layer-only';
|
|
3
|
+
export declare function connectionQueryPolicy(connection: KtxProjectConnectionConfig | undefined): KtxConnectionQueryPolicy;
|
|
4
|
+
/** Member ids whose policy blocks raw SQL through the federated connection. */
|
|
5
|
+
export declare function restrictedFederatedMemberIds(config: KtxProjectConfig, projectDir: string): string[];
|
|
6
|
+
export declare function assertRawSqlAllowed(config: KtxProjectConfig, projectDir: string, connectionId: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* False only when the project has SQL-queryable connections and every one of
|
|
9
|
+
* them is restricted — then no raw-SQL surface can succeed and the
|
|
10
|
+
* sql_execution tool should not be offered at all.
|
|
11
|
+
*/
|
|
12
|
+
export declare function projectAllowsRawSql(config: KtxProjectConfig): boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { KtxQueryError } from '../../errors.js';
|
|
2
|
+
import { deriveFederatedConnection, FEDERATED_CONNECTION_ID } from './federation.js';
|
|
3
|
+
import { isSqlQueryableDriver } from './dialects.js';
|
|
4
|
+
export function connectionQueryPolicy(connection) {
|
|
5
|
+
return connection !== undefined && connection.query_policy === 'semantic-layer-only'
|
|
6
|
+
? 'semantic-layer-only'
|
|
7
|
+
: 'read-only-sql';
|
|
8
|
+
}
|
|
9
|
+
/** Member ids whose policy blocks raw SQL through the federated connection. */
|
|
10
|
+
export function restrictedFederatedMemberIds(config, projectDir) {
|
|
11
|
+
const descriptor = deriveFederatedConnection(config.connections, projectDir);
|
|
12
|
+
if (!descriptor) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return descriptor.members
|
|
16
|
+
.filter((member) => connectionQueryPolicy(member.connection) === 'semantic-layer-only')
|
|
17
|
+
.map((member) => member.connectionId);
|
|
18
|
+
}
|
|
19
|
+
export function assertRawSqlAllowed(config, projectDir, connectionId) {
|
|
20
|
+
if (connectionId === FEDERATED_CONNECTION_ID) {
|
|
21
|
+
const restricted = restrictedFederatedMemberIds(config, projectDir);
|
|
22
|
+
if (restricted.length > 0) {
|
|
23
|
+
throw new KtxQueryError(`Federated SQL execution is disabled: member connection(s) ${restricted
|
|
24
|
+
.map((id) => `"${id}"`)
|
|
25
|
+
.join(', ')} are restricted to semantic-layer queries (query_policy: semantic-layer-only in ktx.yaml).`);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (connectionQueryPolicy(config.connections[connectionId]) === 'semantic-layer-only') {
|
|
30
|
+
throw new KtxQueryError(`Connection "${connectionId}" is restricted to semantic-layer queries (query_policy: semantic-layer-only in ktx.yaml); ` +
|
|
31
|
+
'raw SQL execution is disabled. Query it through the semantic layer with predefined measures instead ' +
|
|
32
|
+
'(the sl_query tool or `ktx sl query`).');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* False only when the project has SQL-queryable connections and every one of
|
|
37
|
+
* them is restricted — then no raw-SQL surface can succeed and the
|
|
38
|
+
* sql_execution tool should not be offered at all.
|
|
39
|
+
*/
|
|
40
|
+
export function projectAllowsRawSql(config) {
|
|
41
|
+
const sqlConnections = Object.values(config.connections).filter((connection) => isSqlQueryableDriver(connection.driver));
|
|
42
|
+
if (sqlConnections.length === 0) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return sqlConnections.some((connection) => connectionQueryPolicy(connection) === 'read-only-sql');
|
|
46
|
+
}
|