@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.
Files changed (96) hide show
  1. package/assets/python/kaelio_ktx-0.16.0-py3-none-any.whl +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/commands/setup-commands.js +2 -0
  5. package/dist/connection-drivers.d.ts +1 -1
  6. package/dist/connection-drivers.js +2 -0
  7. package/dist/connection.js +1 -0
  8. package/dist/connectors/athena/connector.d.ts +160 -0
  9. package/dist/connectors/athena/connector.js +378 -0
  10. package/dist/connectors/athena/dialect.d.ts +31 -0
  11. package/dist/connectors/athena/dialect.js +146 -0
  12. package/dist/connectors/athena/live-database-introspection.d.ts +10 -0
  13. package/dist/connectors/athena/live-database-introspection.js +24 -0
  14. package/dist/connectors/duckdb/connector.d.ts +60 -0
  15. package/dist/connectors/duckdb/connector.js +282 -0
  16. package/dist/connectors/duckdb/dialect.d.ts +31 -0
  17. package/dist/connectors/duckdb/dialect.js +155 -0
  18. package/dist/connectors/duckdb/federated-attach.js +7 -0
  19. package/dist/connectors/duckdb/federated-executor.js +7 -13
  20. package/dist/connectors/duckdb/live-database-introspection.d.ts +8 -0
  21. package/dist/connectors/duckdb/live-database-introspection.js +24 -0
  22. package/dist/connectors/shared/duckdb-json-safe.d.ts +3 -0
  23. package/dist/connectors/shared/duckdb-json-safe.js +12 -0
  24. package/dist/context/connections/connection-type-dialect.d.ts +2 -0
  25. package/dist/context/connections/connection-type-dialect.js +20 -0
  26. package/dist/context/connections/connection-type.d.ts +6 -15
  27. package/dist/context/connections/connection-type.js +1 -10
  28. package/dist/context/connections/dialects.js +4 -0
  29. package/dist/context/connections/drivers.js +38 -0
  30. package/dist/context/connections/federation.d.ts +1 -1
  31. package/dist/context/connections/federation.js +6 -5
  32. package/dist/context/connections/local-warehouse-descriptor.d.ts +1 -0
  33. package/dist/context/connections/local-warehouse-descriptor.js +10 -0
  34. package/dist/context/connections/project-sql-executor.d.ts +24 -0
  35. package/dist/context/connections/project-sql-executor.js +56 -0
  36. package/dist/context/connections/query-policy.d.ts +12 -0
  37. package/dist/context/connections/query-policy.js +46 -0
  38. package/dist/context/core/config-reference.js +11 -1
  39. package/dist/context/daemon/semantic-layer-compute.d.ts +16 -0
  40. package/dist/context/daemon/semantic-layer-compute.js +44 -2
  41. package/dist/context/ingest/adapters/looker/mapping.d.ts +1 -0
  42. package/dist/context/ingest/adapters/looker/mapping.js +3 -10
  43. package/dist/context/ingest/adapters/looker/types.d.ts +6 -15
  44. package/dist/context/ingest/adapters/metabase/mapping.d.ts +1 -0
  45. package/dist/context/ingest/adapters/metabase/mapping.js +1 -0
  46. package/dist/context/ingest/adapters/sigma/chunk.d.ts +6 -0
  47. package/dist/context/ingest/adapters/sigma/chunk.js +119 -0
  48. package/dist/context/ingest/adapters/sigma/client-port.d.ts +45 -0
  49. package/dist/context/ingest/adapters/sigma/client-port.js +1 -0
  50. package/dist/context/ingest/adapters/sigma/client.d.ts +33 -0
  51. package/dist/context/ingest/adapters/sigma/client.js +176 -0
  52. package/dist/context/ingest/adapters/sigma/detect.d.ts +1 -0
  53. package/dist/context/ingest/adapters/sigma/detect.js +23 -0
  54. package/dist/context/ingest/adapters/sigma/fetch.d.ts +14 -0
  55. package/dist/context/ingest/adapters/sigma/fetch.js +191 -0
  56. package/dist/context/ingest/adapters/sigma/local-sigma.adapter.d.ts +17 -0
  57. package/dist/context/ingest/adapters/sigma/local-sigma.adapter.js +41 -0
  58. package/dist/context/ingest/adapters/sigma/project.d.ts +8 -0
  59. package/dist/context/ingest/adapters/sigma/project.js +186 -0
  60. package/dist/context/ingest/adapters/sigma/sigma.adapter.d.ts +18 -0
  61. package/dist/context/ingest/adapters/sigma/sigma.adapter.js +43 -0
  62. package/dist/context/ingest/adapters/sigma/types.d.ts +80 -0
  63. package/dist/context/ingest/adapters/sigma/types.js +82 -0
  64. package/dist/context/ingest/local-adapters.d.ts +2 -1
  65. package/dist/context/ingest/local-adapters.js +22 -0
  66. package/dist/context/mcp/context-tools.js +1 -0
  67. package/dist/context/mcp/local-project-ports.d.ts +1 -1
  68. package/dist/context/mcp/local-project-ports.js +53 -51
  69. package/dist/context/project/config.d.ts +122 -0
  70. package/dist/context/project/driver-schemas.d.ts +61 -0
  71. package/dist/context/project/driver-schemas.js +47 -0
  72. package/dist/context/scan/local-scan.js +3 -1
  73. package/dist/context/scan/types.d.ts +1 -1
  74. package/dist/context/sl/local-query.js +23 -6
  75. package/dist/context/sl/semantic-layer.service.d.ts +0 -4
  76. package/dist/context/sl/semantic-layer.service.js +3 -20
  77. package/dist/context/sl/tools/sl-warehouse-validation.d.ts +1 -1
  78. package/dist/context/sl/tools/sl-warehouse-validation.js +2 -2
  79. package/dist/context/sl/types.d.ts +1 -0
  80. package/dist/context/sql-analysis/dialect-notes.d.ts +1 -1
  81. package/dist/context/sql-analysis/dialect-notes.js +3 -2
  82. package/dist/context/sql-analysis/dialect.js +1 -0
  83. package/dist/context/sql-analysis/dialects/athena.md +12 -0
  84. package/dist/context/sql-analysis/dialects/duckdb.md +10 -0
  85. package/dist/local-adapters.js +17 -0
  86. package/dist/public-ingest.js +1 -0
  87. package/dist/setup-databases.d.ts +1 -1
  88. package/dist/setup-databases.js +52 -2
  89. package/dist/setup-sources.d.ts +2 -1
  90. package/dist/setup-sources.js +84 -0
  91. package/dist/skills/sigma_ingest/SKILL.md +189 -0
  92. package/dist/sql.js +9 -14
  93. package/dist/status-project.js +2 -1
  94. package/dist/telemetry/events.d.ts +1 -1
  95. package/package.json +3 -1
  96. package/assets/python/kaelio_ktx-0.14.0-py3-none-any.whl +0 -0
@@ -1,5 +1,5 @@
1
1
  import type { KtxTableRefKey } from './table-ref.js';
2
- export type KtxConnectionDriver = 'sqlite' | 'postgres' | 'sqlserver' | 'bigquery' | 'snowflake' | 'mysql' | 'clickhouse' | 'mongodb';
2
+ export type KtxConnectionDriver = 'sqlite' | 'duckdb' | 'postgres' | 'sqlserver' | 'bigquery' | 'snowflake' | 'mysql' | 'clickhouse' | 'athena' | 'mongodb';
3
3
  /** Canonical scan-mode registry. Runtime validation derives its allowlist here. */
4
4
  export declare const KTX_SCAN_MODES: readonly ["structural", "relationships", "enriched"];
5
5
  export type KtxScanMode = (typeof KTX_SCAN_MODES)[number];
@@ -1,15 +1,31 @@
1
1
  import { isSqlQueryableDriver } from '../connections/dialects.js';
2
2
  import { FEDERATED_CONNECTION_ID } from '../connections/federation.js';
3
+ import { connectionQueryPolicy, restrictedFederatedMemberIds } from '../connections/query-policy.js';
3
4
  import { resolveRequiredConnectionId } from '../connections/resolve-connection.js';
4
5
  import { sqlAnalysisDialectForDriver } from '../sql-analysis/dialect.js';
5
6
  import { loadLocalSlSourceRecords } from './local-sl.js';
6
7
  import { toResolvedWire } from './semantic-layer.service.js';
7
8
  import { assertSafeConnectionId } from './source-files.js';
8
9
  const COMPILE_ONLY_REASON = 'Local semantic-layer query compiled SQL but no data-source execution adapter is configured.';
9
- const FEDERATED_SL_QUERY_UNSUPPORTED = `Semantic-layer queries are per-connection and cannot target the federated connection '${FEDERATED_CONNECTION_ID}'. ` +
10
- `Run a cross-database query as read-only SQL instead ktx sql -c ${FEDERATED_CONNECTION_ID} "SELECT ..." or the sql_execution tool — ` +
11
- 'using catalog-qualified table names (connectionId.schema.table, or connectionId.table for sqlite; ' +
12
- 'double-quote ids that are not bare identifiers, e.g. "books-db".public.books).';
10
+ const FEDERATED_SL_QUERY_PREFIX = `Semantic-layer queries are per-connection and cannot target the federated connection '${FEDERATED_CONNECTION_ID}'. `;
11
+ // The raw-SQL fallback is only valid when federated raw SQL is allowed; when a
12
+ // member is restricted (query_policy: semantic-layer-only), assertRawSqlAllowed
13
+ // rejects the same path, so directing the agent there would burn a guaranteed
14
+ // failure. Derive the message from the restricted-member set instead.
15
+ function federatedSlQueryUnsupportedMessage(project) {
16
+ const restricted = restrictedFederatedMemberIds(project.config, project.projectDir);
17
+ if (restricted.length > 0) {
18
+ return (FEDERATED_SL_QUERY_PREFIX +
19
+ `Cross-database SQL through '${FEDERATED_CONNECTION_ID}' is also disabled because member connection(s) ` +
20
+ `${restricted.map((id) => `'${id}'`).join(', ')} are restricted to semantic-layer queries ` +
21
+ '(query_policy: semantic-layer-only). Query each connection on its own through the semantic layer ' +
22
+ '(the sl_query tool or `ktx sl query` with its connection id).');
23
+ }
24
+ return (FEDERATED_SL_QUERY_PREFIX +
25
+ `Run a cross-database query as read-only SQL instead — ktx sql -c ${FEDERATED_CONNECTION_ID} "SELECT ..." or the sql_execution tool — ` +
26
+ 'using catalog-qualified table names (connectionId.schema.table, or connectionId.table for sqlite; ' +
27
+ 'double-quote ids that are not bare identifiers, e.g. "books-db".public.books).');
28
+ }
13
29
  function resolveLocalConnectionId(project, requested) {
14
30
  return assertSafeConnectionId(resolveRequiredConnectionId(project.config, requested));
15
31
  }
@@ -39,7 +55,7 @@ function headersFromColumns(columns) {
39
55
  }
40
56
  export async function compileLocalSlQuery(project, options) {
41
57
  if (options.connectionId === FEDERATED_CONNECTION_ID) {
42
- throw new Error(FEDERATED_SL_QUERY_UNSUPPORTED);
58
+ throw new Error(federatedSlQueryUnsupportedMessage(project));
43
59
  }
44
60
  await options.onProgress?.({ progress: 0, message: 'Compiling query' });
45
61
  const connectionId = resolveLocalConnectionId(project, options.connectionId);
@@ -50,11 +66,12 @@ export async function compileLocalSlQuery(project, options) {
50
66
  }
51
67
  const dialect = sqlAnalysisDialectForDriver(driver);
52
68
  const sources = await loadComputableSources(project, connectionId);
69
+ const predefinedMeasuresOnly = connectionQueryPolicy(project.config.connections[connectionId]) === 'semantic-layer-only';
53
70
  await options.onProgress?.({ progress: 0.3, message: 'Generating SQL' });
54
71
  const response = await options.compute.query({
55
72
  sources,
56
73
  dialect,
57
- query: options.query,
74
+ query: { ...options.query, predefined_measures_only: predefinedMeasuresOnly },
58
75
  });
59
76
  if (!options.execute) {
60
77
  await options.onProgress?.({ progress: 1, message: 'Fetched 0 rows' });
@@ -147,10 +147,6 @@ export declare class SemanticLayerService {
147
147
  descriptions: Record<string, string>;
148
148
  }>;
149
149
  }>;
150
- /**
151
- * All callers should use this instead of maintaining their own dialect maps.
152
- */
153
- static mapDialect(connectionType: string): string;
154
150
  /**
155
151
  * Execute a semantic layer query: load composed sources, generate SQL via
156
152
  * the python SL engine, and execute the generated SQL against the data source.
@@ -1,5 +1,6 @@
1
1
  import YAML from 'yaml';
2
2
  import { noopLogger } from '../../context/core/config.js';
3
+ import { dialectForConnectionType } from '../connections/connection-type-dialect.js';
3
4
  import { normalizeSemanticLayerDescriptions } from './description-normalization.js';
4
5
  import { isOverlaySource, resolvedSourceSchema, sourceDefinitionSchema, sourceOverlaySchema } from './schemas.js';
5
6
  import { isSlYamlPath, resolveSlSourceFile, slDeclaredSourceName, slSourceFilePath } from './source-files.js';
@@ -572,7 +573,7 @@ export class SemanticLayerService {
572
573
  if (!connection) {
573
574
  throw new Error(`Data source not found: ${connectionId}`);
574
575
  }
575
- return SemanticLayerService.mapDialect(connection.connectionType);
576
+ return dialectForConnectionType(connection.connectionType);
576
577
  }
577
578
  async listFilesForConnection(connectionId) {
578
579
  const dir = `${SL_DIR_PREFIX}/${connectionId}`;
@@ -937,24 +938,6 @@ export class SemanticLayerService {
937
938
  }
938
939
  return { columns, tables };
939
940
  }
940
- /**
941
- * All callers should use this instead of maintaining their own dialect maps.
942
- */
943
- static mapDialect(connectionType) {
944
- const normalized = connectionType.toUpperCase();
945
- const map = {
946
- POSTGRES: 'postgres',
947
- BIGQUERY: 'bigquery',
948
- SNOWFLAKE: 'snowflake',
949
- MYSQL: 'mysql',
950
- SQLSERVER: 'tsql',
951
- SQLITE: 'sqlite',
952
- DUCKDB: 'duckdb',
953
- CLICKHOUSE: 'clickhouse',
954
- DATABRICKS: 'databricks',
955
- };
956
- return map[normalized] ?? 'postgres';
957
- }
958
941
  /**
959
942
  * Execute a semantic layer query: load composed sources, generate SQL via
960
943
  * the python SL engine, and execute the generated SQL against the data source.
@@ -980,7 +963,7 @@ export class SemanticLayerService {
980
963
  if (!connection) {
981
964
  throw new Error(`Data source not found: ${connectionId}`);
982
965
  }
983
- const dialect = SemanticLayerService.mapDialect(connection.connectionType);
966
+ const dialect = dialectForConnectionType(connection.connectionType);
984
967
  // 3. Generate SQL via python SL engine
985
968
  const { data: slResult, error: slError } = await this.python.query({
986
969
  sources: sources.map(toResolvedWire),
@@ -1,7 +1,7 @@
1
1
  import type { GitService } from '../../../context/core/git.service.js';
2
2
  import type { KtxFileStorePort } from '../../../context/core/file-store.js';
3
3
  import type { SlConnectionCatalogPort, SlSourcesIndexPort } from '../ports.js';
4
- import { SemanticLayerService } from '../semantic-layer.service.js';
4
+ import type { SemanticLayerService } from '../semantic-layer.service.js';
5
5
  export interface SlValidationDeps {
6
6
  semanticLayerService: SemanticLayerService;
7
7
  connections: SlConnectionCatalogPort;
@@ -1,14 +1,14 @@
1
1
  import YAML from 'yaml';
2
2
  import { SYSTEM_GIT_AUTHOR } from '../../../context/tools/authors.js';
3
+ import { dialectForConnectionType } from '../../connections/connection-type-dialect.js';
3
4
  import { sourceOverlaySchema } from '../schemas.js';
4
- import { SemanticLayerService } from '../semantic-layer.service.js';
5
5
  import { resolveSlSourceFile, slSourceFilePath } from '../source-files.js';
6
6
  import { sourceDefinitionSchema } from './base-semantic-layer.tool.js';
7
7
  function resolveDialect(warehouse) {
8
8
  if (!warehouse) {
9
9
  return null;
10
10
  }
11
- return SemanticLayerService.mapDialect(warehouse);
11
+ return dialectForConnectionType(warehouse);
12
12
  }
13
13
  function wrapWithZeroRowQuery(sql, dialect) {
14
14
  if (dialect === 'tsql') {
@@ -115,6 +115,7 @@ export interface SemanticLayerQueryInput {
115
115
  }>;
116
116
  limit?: number;
117
117
  include_empty?: boolean;
118
+ predefined_measures_only?: boolean;
118
119
  }
119
120
  export interface SemanticLayerQueryExecutionResult {
120
121
  sql: string;
@@ -1,6 +1,6 @@
1
1
  import type { SqlAnalysisDialect } from './ports.js';
2
2
  /** @internal Dialects with an authored ./dialects/<dialect>.md file. */
3
- export declare const DIALECTS_WITH_NOTES: readonly ["postgres", "mysql", "snowflake", "bigquery", "sqlite", "clickhouse", "tsql"];
3
+ export declare const DIALECTS_WITH_NOTES: readonly ["postgres", "mysql", "snowflake", "bigquery", "sqlite", "duckdb", "clickhouse", "tsql", "athena"];
4
4
  /**
5
5
  * SQL syntax notes for a resolved dialect. Falls back to `postgres` — the
6
6
  * resolver's own default for unrecognized drivers — so any SQL connection yields
@@ -4,8 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  // dialect), served by the sql_dialect_notes MCP tool. They are package-internal:
5
5
  // copy-runtime-assets.mjs ships them to dist, and they are never installed onto an
6
6
  // agent target. The set covers every dialect reachable from a configured warehouse
7
- // driver; duckdb/databricks are intentionally absent because no connector produces
8
- // them.
7
+ // driver; databricks is intentionally absent because no connector produces it.
9
8
  /** @internal Dialects with an authored ./dialects/<dialect>.md file. */
10
9
  export const DIALECTS_WITH_NOTES = [
11
10
  'postgres',
@@ -13,8 +12,10 @@ export const DIALECTS_WITH_NOTES = [
13
12
  'snowflake',
14
13
  'bigquery',
15
14
  'sqlite',
15
+ 'duckdb',
16
16
  'clickhouse',
17
17
  'tsql',
18
+ 'athena',
18
19
  ];
19
20
  const notesCache = new Map();
20
21
  function readDialectNotes(dialect) {
@@ -14,6 +14,7 @@ const SQLGLOT_DIALECTS = {
14
14
  duckdb: 'duckdb',
15
15
  clickhouse: 'clickhouse',
16
16
  databricks: 'databricks',
17
+ athena: 'athena',
17
18
  };
18
19
  export function sqlAnalysisDialectForDriver(driver) {
19
20
  return SQLGLOT_DIALECTS[(driver ?? '').toLowerCase()] ?? 'postgres';
@@ -0,0 +1,12 @@
1
+ **athena** SQL conventions (Trino engine over the Glue Data Catalog):
2
+ - **FQTN:** `database.table` (e.g. `analytics.orders`); a bare `table` resolves against the query's default database. Cross-catalog is `catalog.database.table` (e.g. `awsdatacatalog.analytics.orders`).
3
+ - **Identifiers:** case-insensitive and folded to lowercase; double-quote (`"Name"`) to keep case, spaces, or a reserved word. String literals use single quotes only.
4
+ - **Date/time:** native `DATE`/`TIMESTAMP`. Bucket with `date_trunc('month', ts)`, pull parts with `EXTRACT(YEAR FROM ts)`, shift with `date_add('day', -30, current_date)`, difference with `date_diff('day', a, b)`, and format with `date_format(ts, '%Y-%m')`. Parse text with `date_parse(str, '%Y-%m-%d')` or `from_iso8601_timestamp(str)`; `current_date` / `now()` are available.
5
+ - **Top-N / windows:** Athena has no `QUALIFY` — wrap the window in a subquery and filter it: `SELECT * FROM (SELECT ..., ROW_NUMBER() OVER (PARTITION BY key ORDER BY x DESC) AS rn FROM t) WHERE rn = 1` returns one row per key. Use `ORDER BY ... LIMIT n` for a global top-N, and paginate with `OFFSET m LIMIT n` (offset first — `LIMIT n OFFSET m` is a syntax error).
6
+ - **Series:** `CROSS JOIN UNNEST(sequence(DATE '2023-01-01', DATE '2023-12-01', INTERVAL '1' MONTH)) AS s(d)` expands a generated array into a date spine (use `sequence(1, 12)` for integers), then `LEFT JOIN` the aggregated facts onto it so empty periods still appear.
7
+ - **Rolling window over time:** a native `RANGE` frame spans real dates and tolerates gaps — `AVG(amount) OVER (ORDER BY day RANGE BETWEEN INTERVAL '29' DAY PRECEDING AND CURRENT ROW)` is a trailing 30-day average without a spine; guard minimum periods with `COUNT(*) OVER (<same frame>)`.
8
+ - **Approximate aggregates:** `approx_distinct(x)` for cardinality and `approx_percentile(x, 0.5)` for quantiles are far cheaper than exact `COUNT(DISTINCT ...)` on large scans.
9
+ - **Arrays & maps:** explode with `CROSS JOIN UNNEST(arr) AS t(x)` (add `WITH ORDINALITY` for an index); build with `array_agg(x)`, join with `array_join(arr, ',')`, index 1-based (`arr[1]`), and read a map with `element_at(m, key)`.
10
+ - **Safe cast:** `TRY_CAST(x AS DOUBLE)` yields `NULL` for a value that does not parse instead of raising, so counting residual `NULL`s catches an encoding the sample missed; `TRY(expr)` swallows other runtime errors.
11
+ - **Integer division:** `/` between integers truncates (`5 / 2` → `2`); cast an operand (`x / CAST(y AS DOUBLE)`) to keep the fraction, and round only in the final projection.
12
+ - **JSON:** `json_extract_scalar(col, '$.a.b')` returns varchar, `json_extract(col, '$.a')` returns json; cast a JSON string with `CAST(json_parse(col) AS ...)`.
@@ -0,0 +1,10 @@
1
+ **duckdb** SQL conventions:
2
+ - **FQTN:** `schema.table` within one database (e.g. `main.orders`); a bare `table` resolves against `main`. A second, attached database is `db.schema.table` (e.g. `sales.main.orders`).
3
+ - **Identifiers:** case-insensitive; double-quote (`"Name"`) to keep a name with spaces or a reserved word.
4
+ - **Date/time:** native `DATE`/`TIMESTAMP`. Bucket with `date_trunc('month', ts)`, pull parts with `EXTRACT(YEAR FROM ts)`, format with `strftime(ts, '%Y-%m')`, and use `CURRENT_DATE`; cast text with `col::DATE` (or `TRY_CAST(col AS DATE)` to null bad values).
5
+ - **Series:** `FROM generate_series(DATE '2023-01-01', DATE '2023-12-01', INTERVAL 1 MONTH) AS s(d)` builds a date spine (use `range(...)` for integers), then `LEFT JOIN` the aggregated facts onto it so empty periods still appear.
6
+ - **Rolling window over time:** a native calendar-range frame spans real dates and tolerates gaps — `AVG(amount) OVER (ORDER BY day RANGE BETWEEN INTERVAL 29 DAYS PRECEDING AND CURRENT ROW)` is a trailing 30-day average without a spine; guard minimum periods with `COUNT(*) OVER (<same frame>)`.
7
+ - **Integer division:** unlike postgres, `/` is true division (`5 / 2` → `2.5`), so a ratio keeps its fraction; use `//` for floor division (`5 // 2` → `2`) when you want the integer quotient, and round only in the final projection.
8
+ - **Safe cast:** duckdb has `TRY_CAST` — `TRY_CAST(x AS DOUBLE)` yields `NULL` for a value that does not parse instead of raising, so counting residual `NULL`s among non-sentinel rows catches an encoding the sample missed without a regex guard.
9
+ - **Top-N / windows:** filter a window inline with `QUALIFY` — `SELECT ... QUALIFY ROW_NUMBER() OVER (PARTITION BY key ORDER BY x DESC) = 1` returns one row per key without a wrapping CTE; use `ORDER BY ... LIMIT n` for a global top-N.
10
+ - **JSON / semi-structured:** `col->'k'` returns JSON, `col->>'k'` returns text, deep path `json_extract(col, '$.a.b')`; duckdb also has native `STRUCT`, `LIST`, and `MAP` — read a struct field with `col.field` and a list element with `col[1]` (1-indexed).
@@ -1,3 +1,5 @@
1
+ import { createAthenaLiveDatabaseIntrospection } from './connectors/athena/live-database-introspection.js';
2
+ import { isKtxAthenaConnectionConfig } from './connectors/athena/connector.js';
1
3
  import { createBigQueryLiveDatabaseIntrospection } from './connectors/bigquery/live-database-introspection.js';
2
4
  import { isKtxBigQueryConnectionConfig, KtxBigQueryScanConnector } from './connectors/bigquery/connector.js';
3
5
  import { createClickHouseLiveDatabaseIntrospection } from './connectors/clickhouse/live-database-introspection.js';
@@ -9,6 +11,8 @@ import { isKtxPostgresConnectionConfig } from './connectors/postgres/connector.j
9
11
  import { KtxPostgresHistoricSqlQueryClient } from './connectors/postgres/historic-sql-query-client.js';
10
12
  import { createSqliteLiveDatabaseIntrospection } from './connectors/sqlite/live-database-introspection.js';
11
13
  import { isKtxSqliteConnectionConfig } from './connectors/sqlite/connector.js';
14
+ import { createDuckDbLiveDatabaseIntrospection } from './connectors/duckdb/live-database-introspection.js';
15
+ import { isKtxDuckDbConnectionConfig } from './connectors/duckdb/connector.js';
12
16
  import { createSqlServerLiveDatabaseIntrospection } from './connectors/sqlserver/live-database-introspection.js';
13
17
  import { isKtxSqlServerConnectionConfig } from './connectors/sqlserver/connector.js';
14
18
  import { BigQueryHistoricSqlQueryHistoryReader } from './context/ingest/adapters/historic-sql/bigquery-query-history-reader.js';
@@ -74,6 +78,10 @@ function createKtxCliLiveDatabaseIntrospection(project, options = {}) {
74
78
  projectDir: project.projectDir,
75
79
  connections: project.config.connections,
76
80
  });
81
+ const duckdb = createDuckDbLiveDatabaseIntrospection({
82
+ projectDir: project.projectDir,
83
+ connections: project.config.connections,
84
+ });
77
85
  const mysql = createMysqlLiveDatabaseIntrospection({
78
86
  connections: project.config.connections,
79
87
  });
@@ -89,6 +97,9 @@ function createKtxCliLiveDatabaseIntrospection(project, options = {}) {
89
97
  const bigquery = createBigQueryLiveDatabaseIntrospection({
90
98
  connections: project.config.connections,
91
99
  });
100
+ const athena = createAthenaLiveDatabaseIntrospection({
101
+ connections: project.config.connections,
102
+ });
92
103
  return {
93
104
  async extractSchema(connectionId, options) {
94
105
  const connection = project.config.connections[connectionId];
@@ -109,6 +120,9 @@ function createKtxCliLiveDatabaseIntrospection(project, options = {}) {
109
120
  if (isKtxSqliteConnectionConfig(connection)) {
110
121
  return sqlite.extractSchema(connectionId, options);
111
122
  }
123
+ if (isKtxDuckDbConnectionConfig(connection)) {
124
+ return duckdb.extractSchema(connectionId, options);
125
+ }
112
126
  if (isKtxMysqlConnectionConfig(connection)) {
113
127
  return mysql.extractSchema(connectionId, options);
114
128
  }
@@ -121,6 +135,9 @@ function createKtxCliLiveDatabaseIntrospection(project, options = {}) {
121
135
  if (isKtxBigQueryConnectionConfig(connection)) {
122
136
  return bigquery.extractSchema(connectionId, options);
123
137
  }
138
+ if (isKtxAthenaConnectionConfig(connection)) {
139
+ return athena.extractSchema(connectionId, options);
140
+ }
124
141
  if (hasSnowflakeDriver(connection)) {
125
142
  const { createSnowflakeLiveDatabaseIntrospection } = await import('./connectors/snowflake/live-database-introspection.js');
126
143
  const { isKtxSnowflakeConnectionConfig } = await import('./connectors/snowflake/connector.js');
@@ -21,6 +21,7 @@ const sourceAdapterByDriver = new Map([
21
21
  ['metricflow', 'metricflow'],
22
22
  ['dbt', 'dbt'],
23
23
  ['lookml', 'lookml'],
24
+ ['sigma', 'sigma'],
24
25
  ]);
25
26
  export function publicProgressMessage(message, target) {
26
27
  let current = message;
@@ -9,7 +9,7 @@ import { type DatabaseScopePickResult, type PickDatabaseScopeArgs } from './data
9
9
  import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
10
10
  import type { ManagedPythonCoreDaemonOptions } from './managed-python-http.js';
11
11
  import { type KtxSetupPromptOption } from './setup-prompts.js';
12
- export type KtxSetupDatabaseDriver = 'sqlite' | 'postgres' | 'mysql' | 'clickhouse' | 'sqlserver' | 'bigquery' | 'snowflake' | 'mongodb';
12
+ export type KtxSetupDatabaseDriver = 'sqlite' | 'duckdb' | 'postgres' | 'mysql' | 'clickhouse' | 'sqlserver' | 'bigquery' | 'snowflake' | 'athena' | 'mongodb';
13
13
  export interface KtxSetupDatabasesArgs {
14
14
  projectDir: string;
15
15
  inputMode: 'auto' | 'disabled';
@@ -37,8 +37,10 @@ const DRIVER_OPTIONS = [
37
37
  { value: 'mysql', label: 'MySQL' },
38
38
  { value: 'clickhouse', label: 'ClickHouse' },
39
39
  { value: 'sqlserver', label: 'SQL Server' },
40
+ { value: 'athena', label: 'Amazon Athena' },
40
41
  { value: 'mongodb', label: 'MongoDB' },
41
42
  { value: 'sqlite', label: 'SQLite' },
43
+ { value: 'duckdb', label: 'DuckDB' },
42
44
  ];
43
45
  const DRIVER_LABELS = Object.fromEntries(DRIVER_OPTIONS.map((option) => [option.value, option.label]));
44
46
  const HISTORIC_SQL_DIALECT_BY_DRIVER = {
@@ -48,12 +50,14 @@ const HISTORIC_SQL_DIALECT_BY_DRIVER = {
48
50
  };
49
51
  const DEFAULT_CONNECTION_IDS = {
50
52
  sqlite: 'sqlite-local',
53
+ duckdb: 'duckdb-local',
51
54
  postgres: 'postgres-warehouse',
52
55
  mysql: 'mysql-warehouse',
53
56
  clickhouse: 'clickhouse-warehouse',
54
57
  sqlserver: 'sqlserver-warehouse',
55
58
  bigquery: 'bigquery-warehouse',
56
59
  snowflake: 'snowflake-warehouse',
60
+ athena: 'athena-warehouse',
57
61
  mongodb: 'mongodb-source',
58
62
  };
59
63
  const SUGGESTED_SCOPE_PATTERN = /^(mart|prod|analytics|core|dim|fact|gold)(_|$)/i;
@@ -118,6 +122,13 @@ const SCOPE_DISCOVERY_SPECS = {
118
122
  configSingleField: 'schema_name',
119
123
  suggest: defaultSuggest,
120
124
  },
125
+ athena: {
126
+ noun: 'database',
127
+ nounPlural: 'databases',
128
+ promptLabel: 'Glue databases',
129
+ configArrayField: 'databases',
130
+ suggest: defaultSuggest,
131
+ },
121
132
  };
122
133
  const DRIVER_CONNECTION_DEFAULTS = {
123
134
  postgres: { port: '5432' },
@@ -541,6 +552,15 @@ async function buildConnectionConfig(input) {
541
552
  return 'back';
542
553
  return path ? { driver: 'sqlite', path } : null;
543
554
  }
555
+ if (driver === 'duckdb') {
556
+ if (args.inputMode === 'disabled' && !args.databaseUrl)
557
+ return null;
558
+ const path = args.databaseUrl ??
559
+ (await promptText(prompts, 'DuckDB database file\nEnter a relative or absolute path, for example ./warehouse.duckdb.', stringConfigField(input.existingConnection, 'path')));
560
+ if (path === undefined)
561
+ return 'back';
562
+ return path ? { driver: 'duckdb', path } : null;
563
+ }
544
564
  if (driver === 'postgres' || driver === 'mysql' || driver === 'clickhouse' || driver === 'sqlserver') {
545
565
  return await buildUrlConnectionConfig({
546
566
  driver,
@@ -664,6 +684,34 @@ async function buildConnectionConfig(input) {
664
684
  ...(role ? { role } : {}),
665
685
  };
666
686
  }
687
+ if (driver === 'athena') {
688
+ if (args.inputMode === 'disabled' && !args.databaseUrl)
689
+ return null;
690
+ const region = await promptText(prompts, 'AWS region\nFor example us-east-1.', stringConfigField(input.existingConnection, 'region'));
691
+ if (region === undefined)
692
+ return 'back';
693
+ if (!region)
694
+ return null;
695
+ const s3StagingDir = await promptText(prompts, 'S3 staging directory\nAthena writes query results here. For example s3://my-bucket/athena-results/.', stringConfigField(input.existingConnection, 's3_staging_dir'));
696
+ if (s3StagingDir === undefined)
697
+ return 'back';
698
+ if (!s3StagingDir)
699
+ return null;
700
+ const workgroup = await promptText(prompts, 'Athena workgroup (optional)\nPress Enter to use the default workgroup "primary".', stringConfigField(input.existingConnection, 'workgroup'));
701
+ if (workgroup === undefined)
702
+ return 'back';
703
+ const catalog = await promptText(prompts, 'Glue Data Catalog name (optional)\nPress Enter to use the default "AwsDataCatalog".', stringConfigField(input.existingConnection, 'catalog'));
704
+ if (catalog === undefined)
705
+ return 'back';
706
+ return {
707
+ driver: 'athena',
708
+ region,
709
+ s3_staging_dir: s3StagingDir,
710
+ ...(workgroup ? { workgroup } : {}),
711
+ ...(catalog ? { catalog } : {}),
712
+ ...scriptedScopeConfigForDriver('athena', args.databaseSchemas),
713
+ };
714
+ }
667
715
  throw new Error(`Unsupported database driver: ${driver}`);
668
716
  }
669
717
  async function maybeApplyHistoricSqlConfig(input) {
@@ -1031,9 +1079,11 @@ async function maybeConfigureDatabaseScope(input) {
1031
1079
  const project = await loadKtxProject({ projectDir: input.projectDir });
1032
1080
  const connection = project.config.connections[input.connectionId];
1033
1081
  const driver = normalizeDriver(connection?.driver);
1034
- if (!driver || driver === 'sqlite')
1082
+ const spec = driver ? SCOPE_DISCOVERY_SPECS[driver] : undefined;
1083
+ // Drivers with no scope spec are single-namespace (sqlite, duckdb): there is no
1084
+ // schema to choose, so skip the scope picker and ingest every table.
1085
+ if (!driver || !spec)
1035
1086
  return okValidateResult();
1036
- const spec = SCOPE_DISCOVERY_SPECS[driver];
1037
1087
  const existingTables = connection?.enabled_tables;
1038
1088
  const hasExistingTables = Array.isArray(existingTables) && existingTables.length > 0;
1039
1089
  const existingScope = spec ? configuredScopeValues(connection, spec) : [];
@@ -3,7 +3,7 @@ import { type KtxProjectConnectionConfig } from './context/project/config.js';
3
3
  import type { KtxCliIo } from './cli-runtime.js';
4
4
  import { pickNotionRootPages } from './notion-page-picker.js';
5
5
  import { type KtxSetupPromptOption } from './setup-prompts.js';
6
- export type KtxSetupSourceType = 'dbt' | 'metricflow' | 'metabase' | 'looker' | 'lookml' | 'notion' | 'gdrive';
6
+ export type KtxSetupSourceType = 'dbt' | 'metricflow' | 'metabase' | 'looker' | 'lookml' | 'notion' | 'sigma' | 'gdrive';
7
7
  export interface KtxSetupSourcesArgs {
8
8
  projectDir: string;
9
9
  inputMode: 'auto' | 'disabled';
@@ -99,6 +99,7 @@ export interface KtxSetupSourcesDeps {
99
99
  validateLooker?: (projectDir: string, connectionId: string) => Promise<SourceValidationResult>;
100
100
  validateLookml?: (connection: KtxProjectConnectionConfig) => Promise<SourceValidationResult>;
101
101
  validateNotion?: (connection: KtxProjectConnectionConfig) => Promise<SourceValidationResult>;
102
+ validateSigma?: (connection: KtxProjectConnectionConfig) => Promise<SourceValidationResult>;
102
103
  validateGdrive?: (connection: KtxProjectConnectionConfig) => Promise<SourceValidationResult>;
103
104
  pickNotionRootPages?: typeof pickNotionRootPages;
104
105
  discoverMetabaseDatabases?: (args: {
@@ -10,6 +10,8 @@ import { createGoogleDocsClients, verifyGdriveFolderAndCountDocs, } from './cont
10
10
  import { gdriveServiceAccountKeySchema } from './context/ingest/adapters/gdrive/types.js';
11
11
  import { cloneOrPull, testRepoConnection } from './context/ingest/repo-fetch.js';
12
12
  import { DEFAULT_METABASE_CLIENT_CONFIG, MetabaseClient } from './context/ingest/adapters/metabase/client.js';
13
+ import { DEFAULT_SIGMA_CLIENT_CONFIG, DefaultSigmaClient } from './context/ingest/adapters/sigma/client.js';
14
+ import { sigmaRuntimeConfigFromLocalConnection } from './context/ingest/adapters/sigma/local-sigma.adapter.js';
13
15
  import { discoverMetabaseDatabases } from './context/ingest/adapters/metabase/mapping.js';
14
16
  import { loadDbtSchemaFiles } from './context/ingest/dbt-shared/schema-files.js';
15
17
  import { loadProjectInfo } from './context/ingest/dbt-shared/project-vars.js';
@@ -38,6 +40,7 @@ const SOURCE_OPTIONS = [
38
40
  { value: 'metricflow', label: 'MetricFlow' },
39
41
  { value: 'looker', label: 'Looker' },
40
42
  { value: 'lookml', label: 'LookML' },
43
+ { value: 'sigma', label: 'Sigma Computing' },
41
44
  { value: 'gdrive', label: 'Google Drive' },
42
45
  ];
43
46
  const SOURCE_LABELS = Object.fromEntries(SOURCE_OPTIONS.map((option) => [option.value, option.label]));
@@ -124,6 +127,7 @@ const SOURCE_CREDENTIAL_FLAG = {
124
127
  notion: { field: 'sourceAuthTokenRef', flag: '--source-auth-token-ref' },
125
128
  metabase: { field: 'sourceApiKeyRef', flag: '--source-api-key-ref' },
126
129
  looker: { field: 'sourceClientSecretRef', flag: '--source-client-secret-ref' },
130
+ sigma: { field: 'sourceClientSecretRef', flag: '--source-client-secret-ref' },
127
131
  gdrive: { field: null, flag: '--gdrive-service-account-key-ref' },
128
132
  };
129
133
  const ALL_SOURCE_CREDENTIAL_FLAGS = [
@@ -422,6 +426,17 @@ function buildNotionConnection(args) {
422
426
  max_knowledge_updates_per_run: 20,
423
427
  };
424
428
  }
429
+ function buildSigmaConnection(args) {
430
+ if (!args.sourceClientId) {
431
+ throw new Error('Missing Sigma client id: pass --source-client-id.');
432
+ }
433
+ return {
434
+ driver: 'sigma',
435
+ api_url: args.sourceUrl ?? 'https://api.sigmacomputing.com',
436
+ client_id: args.sourceClientId,
437
+ client_secret_ref: credentialRef(args.sourceClientSecretRef, 'Sigma client secret ref'),
438
+ };
439
+ }
425
440
  function buildGdriveConnection(args) {
426
441
  const folderId = args.gdriveFolderId?.trim();
427
442
  if (!folderId) {
@@ -544,6 +559,24 @@ async function defaultValidateNotion(connection) {
544
559
  }
545
560
  return { ok: true, detail: `roots=${roots.length}` };
546
561
  }
562
+ async function defaultValidateSigma(connection) {
563
+ try {
564
+ const runtimeConfig = sigmaRuntimeConfigFromLocalConnection('sigma-main', connection);
565
+ const client = new DefaultSigmaClient(runtimeConfig, DEFAULT_SIGMA_CLIENT_CONFIG);
566
+ try {
567
+ const result = await client.testConnection();
568
+ return result.success
569
+ ? { ok: true, detail: 'Sigma API connection verified' }
570
+ : { ok: false, message: result.error ?? 'Sigma connection test failed' };
571
+ }
572
+ finally {
573
+ await client.cleanup();
574
+ }
575
+ }
576
+ catch (err) {
577
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
578
+ }
579
+ }
547
580
  async function defaultValidateGdrive(connection) {
548
581
  const config = parseGdriveConnectionConfig(connection);
549
582
  const keyText = await resolveGdriveServiceAccountKey(config.service_account_key_ref);
@@ -1110,6 +1143,45 @@ async function promptForInteractiveSource(args, source, prompts, io, deps, defau
1110
1143
  },
1111
1144
  ]);
1112
1145
  }
1146
+ if (source === 'sigma') {
1147
+ return await runSourcePromptSteps(initialState, () => [
1148
+ ...connectionSteps,
1149
+ async (state) => {
1150
+ const sourceUrl = await promptText(prompts, {
1151
+ message: 'Sigma API URL',
1152
+ initialValue: state.sourceUrl ?? 'https://api.sigmacomputing.com',
1153
+ });
1154
+ if (sourceUrl === undefined)
1155
+ return 'back';
1156
+ state.sourceUrl = sourceUrl;
1157
+ return 'next';
1158
+ },
1159
+ async (state) => {
1160
+ const sourceClientId = await promptText(prompts, {
1161
+ message: 'Sigma client ID',
1162
+ ...(state.sourceClientId ? { initialValue: state.sourceClientId } : {}),
1163
+ });
1164
+ if (sourceClientId === undefined)
1165
+ return 'back';
1166
+ state.sourceClientId = sourceClientId;
1167
+ return 'next';
1168
+ },
1169
+ async (state) => {
1170
+ const ref = await chooseSourceCredentialRef({
1171
+ prompts,
1172
+ projectDir: args.projectDir,
1173
+ label: 'Sigma client secret',
1174
+ envName: 'SIGMA_CLIENT_SECRET',
1175
+ secretFileName: `${state.sourceConnectionId ?? 'sigma-main'}-client-secret`,
1176
+ existingRef: state.sourceClientSecretRef,
1177
+ });
1178
+ if (ref === 'back')
1179
+ return 'back';
1180
+ state.sourceClientSecretRef = ref;
1181
+ return 'next';
1182
+ },
1183
+ ]);
1184
+ }
1113
1185
  if (source === 'notion') {
1114
1186
  return await runSourcePromptSteps(initialState, (state) => [
1115
1187
  ...connectionSteps,
@@ -1355,6 +1427,12 @@ function sourceArgsFromExistingConnection(input) {
1355
1427
  }
1356
1428
  return sourceArgs;
1357
1429
  }
1430
+ if (input.source === 'sigma') {
1431
+ sourceArgs.sourceUrl = stringField(input.connection.api_url) ?? undefined;
1432
+ sourceArgs.sourceClientId = stringField(input.connection.client_id) ?? undefined;
1433
+ sourceArgs.sourceClientSecretRef = stringField(input.connection.client_secret_ref) ?? undefined;
1434
+ return sourceArgs;
1435
+ }
1358
1436
  if (input.source === 'gdrive') {
1359
1437
  sourceArgs.gdriveServiceAccountKeyRef = stringField(input.connection.service_account_key_ref);
1360
1438
  sourceArgs.gdriveFolderId = stringField(input.connection.folder_id);
@@ -1488,6 +1566,9 @@ function buildConnection(source, args) {
1488
1566
  if (source === 'lookml') {
1489
1567
  return buildLookmlConnection(args);
1490
1568
  }
1569
+ if (source === 'sigma') {
1570
+ return buildSigmaConnection(args);
1571
+ }
1491
1572
  if (source === 'notion') {
1492
1573
  return buildNotionConnection(args);
1493
1574
  }
@@ -1511,6 +1592,9 @@ async function validateSource(source, args, deps) {
1511
1592
  if (source === 'lookml') {
1512
1593
  return await (deps.validateLookml ?? defaultValidateLookml)(args.connection);
1513
1594
  }
1595
+ if (source === 'sigma') {
1596
+ return await (deps.validateSigma ?? defaultValidateSigma)(args.connection);
1597
+ }
1514
1598
  if (source === 'notion') {
1515
1599
  return await (deps.validateNotion ?? defaultValidateNotion)(args.connection);
1516
1600
  }