@hypequery/clickhouse 2.0.1 → 2.1.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hypequery/clickhouse
2
2
 
3
- Typed ClickHouse query builder for hypequery.
3
+ Typed query builder for ClickHouse.
4
4
 
5
5
  Use it when you want schema-aware queries, typed results, and a fluent API that stays close to how ClickHouse actually works.
6
6
 
@@ -1,5 +1,39 @@
1
+ /**
2
+ * Minimal ClickHouse client surface needed by the type generator.
3
+ */
4
+ export interface TypeGenerationClickHouseClient {
5
+ query(options: { query: string; format: 'JSONEachRow' }): Promise<{
6
+ json(): Promise<Array<Record<string, string>>>;
7
+ }>;
8
+ }
9
+
10
+ /**
11
+ * Options for generating TypeScript type definitions from ClickHouse.
12
+ */
13
+ export interface GenerateTypesOptions {
14
+ includeTables?: string[];
15
+ excludeTables?: string[];
16
+ client?: TypeGenerationClickHouseClient;
17
+ generatedBy?: string;
18
+ includeUsageExample?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Converts a ClickHouse type string to a TypeScript type string.
23
+ */
24
+ export declare function clickhouseToTsType(type: string): string;
25
+
1
26
  /**
2
27
  * Generates TypeScript type definitions from the ClickHouse database schema
3
28
  * @param outputPath - The file path where the type definitions will be written
29
+ * @param options - Options for type generation
30
+ */
31
+ export declare function generateTypes(outputPath: string, options?: GenerateTypesOptions): Promise<void>;
32
+
33
+ /**
34
+ * Generates TypeScript type definition contents from a ClickHouse client.
4
35
  */
5
- export declare function generateTypes(outputPath: string): Promise<void>;
36
+ export declare function generateTypeDefinitions(
37
+ client: TypeGenerationClickHouseClient,
38
+ options?: GenerateTypesOptions,
39
+ ): Promise<string>;
@@ -19,6 +19,9 @@ dotenv.config();
19
19
  * @typedef {Object} GenerateTypesOptions
20
20
  * @property {string[]} [includeTables] - List of tables to include
21
21
  * @property {string[]} [excludeTables] - List of tables to exclude
22
+ * @property {object} [client] - ClickHouse client to use instead of ClickHouseConnection
23
+ * @property {string} [generatedBy] - Package name to include in the generated file header
24
+ * @property {boolean} [includeUsageExample] - Whether to append the historical usage example
22
25
  */
23
26
 
24
27
  /**
@@ -28,8 +31,30 @@ dotenv.config();
28
31
  * @returns {Promise<void>}
29
32
  */
30
33
  export async function generateTypes(outputPath, options = {}) {
31
- const client = ClickHouseConnection.getClient();
32
- const { includeTables = [], excludeTables = [] } = options;
34
+ const client = options.client || ClickHouseConnection.getClient();
35
+ const typeDefinitions = await generateTypeDefinitions(client, options);
36
+
37
+ // Ensure the output directory exists
38
+ const outputDir = path.dirname(path.resolve(outputPath));
39
+ await fs.mkdir(outputDir, { recursive: true });
40
+
41
+ // Write the file
42
+ await fs.writeFile(path.resolve(outputPath), typeDefinitions);
43
+ }
44
+
45
+ /**
46
+ * Generates TypeScript type definition contents from a ClickHouse client.
47
+ * @param {object} client - ClickHouse client with a query method
48
+ * @param {GenerateTypesOptions} [options] - Options for type generation
49
+ * @returns {Promise<string>}
50
+ */
51
+ export async function generateTypeDefinitions(client, options = {}) {
52
+ const {
53
+ includeTables = [],
54
+ excludeTables = [],
55
+ generatedBy = '@hypequery/clickhouse',
56
+ includeUsageExample = true,
57
+ } = options;
33
58
 
34
59
  // Get all tables
35
60
  const tablesQuery = await client.query({
@@ -52,7 +77,7 @@ export async function generateTypes(outputPath, options = {}) {
52
77
  console.warn('Warning: No tables match the filter criteria. Check your include/exclude options.');
53
78
  }
54
79
 
55
- let typeDefinitions = `// Generated by @hypequery/clickhouse
80
+ let typeDefinitions = `// Generated by ${generatedBy}
56
81
  // This file defines TypeScript types based on your ClickHouse database schema
57
82
 
58
83
  /**
@@ -95,8 +120,8 @@ export interface IntrospectedSchema {`;
95
120
  typeDefinitions += '\n}\n\n';
96
121
  }
97
122
 
98
- // Add a usage example
99
- typeDefinitions += `
123
+ if (includeUsageExample) {
124
+ typeDefinitions += `
100
125
  /**
101
126
  * Usage example:
102
127
  *
@@ -114,13 +139,9 @@ export interface IntrospectedSchema {`;
114
139
  * .execute();
115
140
  */
116
141
  `;
142
+ }
117
143
 
118
- // Ensure the output directory exists
119
- const outputDir = path.dirname(path.resolve(outputPath));
120
- await fs.mkdir(outputDir, { recursive: true });
121
-
122
- // Write the file
123
- await fs.writeFile(path.resolve(outputPath), typeDefinitions);
144
+ return typeDefinitions;
124
145
  }
125
146
 
126
147
  /**
@@ -1,2 +1,8 @@
1
1
  // CLI module type declarations
2
- export { generateTypes } from './generate-types';
2
+ export {
3
+ clickhouseToTsType,
4
+ generateTypeDefinitions,
5
+ generateTypes,
6
+ type GenerateTypesOptions,
7
+ type TypeGenerationClickHouseClient,
8
+ } from './generate-types';
package/dist/cli/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // CLI module exports
2
- export { generateTypes } from './generate-types.js';
2
+ export { clickhouseToTsType, generateTypeDefinitions, generateTypes } from './generate-types.js';
@@ -33,10 +33,20 @@ function splitTopLevelArgs(value) {
33
33
  }
34
34
 
35
35
  function unwrapType(type, wrapperName) {
36
- const prefix = `${wrapperName}(`;
37
- return type.startsWith(prefix) && type.endsWith(')')
38
- ? type.slice(prefix.length, -1)
39
- : null;
36
+ // Trim whitespace and handle case-insensitive matching
37
+ const trimmedType = type.trim();
38
+ const lowerType = trimmedType.toLowerCase();
39
+ const lowerWrapper = wrapperName.toLowerCase();
40
+ const prefix = `${lowerWrapper}(`;
41
+
42
+ if (lowerType.startsWith(prefix) && trimmedType.endsWith(')')) {
43
+ // Extract inner type, preserving original case
44
+ const innerStart = trimmedType.indexOf('(') + 1;
45
+ const innerEnd = trimmedType.lastIndexOf(')');
46
+ return trimmedType.slice(innerStart, innerEnd).trim();
47
+ }
48
+
49
+ return null;
40
50
  }
41
51
 
42
52
  function getPrimitiveTsType(type) {
@@ -45,6 +55,8 @@ function getPrimitiveTsType(type) {
45
55
  switch (lowerType) {
46
56
  case 'string':
47
57
  case 'uuid':
58
+ case 'ipv4':
59
+ case 'ipv6':
48
60
  return 'string';
49
61
  case 'int8':
50
62
  case 'int16':
@@ -72,9 +84,15 @@ function getPrimitiveTsType(type) {
72
84
  case 'bool':
73
85
  case 'boolean':
74
86
  return 'boolean';
87
+ case 'json':
88
+ return 'unknown';
75
89
  default:
76
90
  if (type.startsWith('FixedString(')) return 'string';
77
91
  if (type.startsWith('Decimal(')) return 'number';
92
+ if (type.startsWith('Decimal32(')) return 'number';
93
+ if (type.startsWith('Decimal64(')) return 'number';
94
+ if (type.startsWith('Decimal128(')) return 'number';
95
+ if (type.startsWith('Decimal256(')) return 'number';
78
96
  if (type.startsWith('DateTime64(')) return 'string';
79
97
  if (type.startsWith('DateTime(')) return 'string';
80
98
  if (type.startsWith('Enum8(')) return 'string';
@@ -1 +1 @@
1
- {"version":3,"file":"clickhouse-dialect.d.ts","sourceRoot":"","sources":["../../../src/core/dialects/clickhouse-dialect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAExE,qBAAa,iBAAkB,YAAW,UAAU;IAClD,QAAQ,CAAC,IAAI,gBAAgB;IAC7B,OAAO,CAAC,SAAS,CAAsB;IAEvC,YAAY,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,mBAAmB,GAAG,aAAa;IA6D3F,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAQ5E,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;CAK1D"}
1
+ {"version":3,"file":"clickhouse-dialect.d.ts","sourceRoot":"","sources":["../../../src/core/dialects/clickhouse-dialect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAExE,qBAAa,iBAAkB,YAAW,UAAU;IAClD,QAAQ,CAAC,IAAI,gBAAgB;IAC7B,OAAO,CAAC,SAAS,CAAsB;IAEvC,YAAY,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,mBAAmB,GAAG,aAAa;IA6D3F,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAQ5E,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;CAK1D"}
@@ -1,4 +1,5 @@
1
1
  import { SQLFormatter } from '../formatters/sql-formatter.js';
2
+ import { formatIntervalLiteral } from '../utils/sql-literals.js';
2
3
  export class ClickHouseDialect {
3
4
  name = 'clickhouse';
4
5
  formatter = new SQLFormatter();
@@ -52,7 +53,7 @@ export class ClickHouseDialect {
52
53
  }
53
54
  formatTimeInterval(column, interval, method) {
54
55
  if (method === 'toStartOfInterval') {
55
- return `${method}(${column}, INTERVAL ${interval})`;
56
+ return `${method}(${column}, INTERVAL ${formatIntervalLiteral(interval)})`;
56
57
  }
57
58
  return `${method}(${column})`;
58
59
  }
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../../src/core/tests/integration/setup.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,yBAAyB,EACzB,sBAAsB,EACtB,SAAS,EASV,MAAM,kDAAkD,CAAC;AA4B1D,eAAO,MAAM,wBAAwB;;;;;;EAyBpC,CAAC;AAGF,eAAO,MAAM,2BAA2B,yGAevC,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,OAAO,CAE/E,CAAC;AAGF,eAAO,MAAM,wBAAwB,QAAa,OAAO,CAAC,IAAI,CAO7D,CAAC;AAGF,eAAO,MAAM,iBAAiB,GAC5B,oBAAgB,EAChB,sBAAoB,KACnB,OAAO,CAAC,IAAI,CAOd,CAAC;AAGF,eAAO,MAAM,uBAAuB,QAAa,OAAO,CAAC,IAAI,CAU5D,CAAC;AAGF,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,KAAK,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,OAAO,CAAC;QACnB,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,CAAC,CAAC;IACH,KAAK,EAAE,KAAK,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;IACH,MAAM,EAAE,KAAK,CAAC;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;CACJ;AAED,OAAO,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,SAAS,EAAE,CAAC;AAKxE,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,IAAI,CAuBtD,CAAC"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../../src/core/tests/integration/setup.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,yBAAyB,EACzB,sBAAsB,EACtB,SAAS,EAQV,MAAM,kDAAkD,CAAC;AA4B1D,eAAO,MAAM,wBAAwB;;;;;;EAyBpC,CAAC;AAGF,eAAO,MAAM,2BAA2B,yGAevC,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,OAAO,CAE/E,CAAC;AAGF,eAAO,MAAM,wBAAwB,QAAa,OAAO,CAAC,IAAI,CAO7D,CAAC;AAGF,eAAO,MAAM,iBAAiB,GAC5B,oBAAgB,EAChB,sBAAoB,KACnB,OAAO,CAAC,IAAI,CAOd,CAAC;AAGF,eAAO,MAAM,uBAAuB,QAAa,OAAO,CAAC,IAAI,CAU5D,CAAC;AAGF,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,KAAK,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,OAAO,CAAC;QACnB,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,CAAC,CAAC;IACH,KAAK,EAAE,KAAK,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;IACH,MAAM,EAAE,KAAK,CAAC;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;CACJ;AAED,OAAO,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,SAAS,EAAE,CAAC;AAKxE,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,IAAI,CAuBtD,CAAC"}
@@ -1,8 +1,6 @@
1
1
  import { ClickHouseConnection } from '../../connection.js';
2
2
  import { logger as hypeQueryLogger } from '../../utils/logger.js';
3
- import { CLICKHOUSE_CONTAINER_NAME, TEST_CONNECTION_CONFIG, TEST_DATA, detectComposeCommand, ensureDockerDaemon, isContainerRunning as sharedIsContainerRunning, seedClickHouseDatabase, startClickHouseContainer as sharedStartClickHouseContainer, stopClickHouseContainer as sharedStopClickHouseContainer, waitForClickHouse as sharedWaitForClickHouse,
4
- // @ts-expect-error: shared test harness is plain JS
5
- } from '../../../../../../testing/clickhouse/harness.mjs';
3
+ import { CLICKHOUSE_CONTAINER_NAME, TEST_CONNECTION_CONFIG, TEST_DATA, detectComposeCommand, ensureDockerDaemon, isContainerRunning as sharedIsContainerRunning, seedClickHouseDatabase, startClickHouseContainer as sharedStartClickHouseContainer, stopClickHouseContainer as sharedStopClickHouseContainer, waitForClickHouse as sharedWaitForClickHouse, } from '../../../../../../testing/clickhouse/harness.mjs';
6
4
  // Disable the hypequery logger to prevent "logs after tests" errors
7
5
  // This must be done early in the setup, before any queries run
8
6
  hypeQueryLogger.configure({ enabled: false });
@@ -1 +1 @@
1
- {"version":3,"file":"sql-expressions.d.ts","sourceRoot":"","sources":["../../../src/core/utils/sql-expressions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sql-expressions.d.ts","sourceRoot":"","sources":["../../../src/core/utils/sql-expressions.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { formatIntervalLiteral, quoteStringLiteral } from './sql-literals.js';
1
2
  /**
2
3
  * Creates a raw SQL expression
3
4
  * @param sql The SQL expression string
@@ -34,17 +35,18 @@ export function toDateTime(field, alias) {
34
35
  }
35
36
  export function formatDateTime(field, format, options = {}) {
36
37
  const { timezone, alias } = options;
37
- let sql = `formatDateTime(${field}, '${format}'`;
38
+ let sql = `formatDateTime(${field}, ${quoteStringLiteral(format)}`;
38
39
  if (timezone) {
39
- sql += `, '${timezone}'`;
40
+ sql += `, ${quoteStringLiteral(timezone)}`;
40
41
  }
41
42
  sql += ')';
42
43
  return alias ? rawAs(sql, alias) : raw(sql);
43
44
  }
44
45
  export function toStartOfInterval(field, interval, alias) {
46
+ const sql = `toStartOfInterval(${field}, INTERVAL ${formatIntervalLiteral(interval)})`;
45
47
  return alias
46
- ? rawAs(`toStartOfInterval(${field}, INTERVAL ${interval})`, alias)
47
- : raw(`toStartOfInterval(${field}, INTERVAL ${interval})`);
48
+ ? rawAs(sql, alias)
49
+ : raw(sql);
48
50
  }
49
51
  function toStartOfUnit(functionName, field, alias) {
50
52
  return alias
@@ -0,0 +1,3 @@
1
+ export declare function formatIntervalLiteral(interval: string): string;
2
+ export declare function quoteStringLiteral(value: string): string;
3
+ //# sourceMappingURL=sql-literals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql-literals.d.ts","sourceRoot":"","sources":["../../../src/core/utils/sql-literals.ts"],"names":[],"mappings":"AA2BA,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAc9D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAExD"}
@@ -0,0 +1,39 @@
1
+ const INTERVAL_UNITS = {
2
+ second: 'SECOND',
3
+ seconds: 'SECOND',
4
+ minute: 'MINUTE',
5
+ minutes: 'MINUTE',
6
+ hour: 'HOUR',
7
+ hours: 'HOUR',
8
+ day: 'DAY',
9
+ days: 'DAY',
10
+ week: 'WEEK',
11
+ weeks: 'WEEK',
12
+ month: 'MONTH',
13
+ months: 'MONTH',
14
+ quarter: 'QUARTER',
15
+ quarters: 'QUARTER',
16
+ year: 'YEAR',
17
+ years: 'YEAR',
18
+ millisecond: 'MILLISECOND',
19
+ milliseconds: 'MILLISECOND',
20
+ microsecond: 'MICROSECOND',
21
+ microseconds: 'MICROSECOND',
22
+ nanosecond: 'NANOSECOND',
23
+ nanoseconds: 'NANOSECOND',
24
+ };
25
+ const INTERVAL_PATTERN = /^(\d+)\s+([a-z]+)$/i;
26
+ export function formatIntervalLiteral(interval) {
27
+ const normalized = interval.trim();
28
+ const match = normalized.match(INTERVAL_PATTERN);
29
+ const unit = match ? INTERVAL_UNITS[match[2].toLowerCase()] : undefined;
30
+ if (!match || !unit) {
31
+ throw new Error(`Invalid time interval: "${interval}". Expected "<number> <unit>" where unit is ` +
32
+ 'one of second, minute, hour, day, week, month, quarter, year, millisecond, microsecond, nanosecond ' +
33
+ '(e.g. "1 day", "15 minute").');
34
+ }
35
+ return `${match[1]} ${unit}`;
36
+ }
37
+ export function quoteStringLiteral(value) {
38
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
39
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,CAa9C;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAgBvE"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,CAc9C;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAgBvE"}
@@ -6,7 +6,8 @@ export function escapeValue(value) {
6
6
  return value.toString();
7
7
  }
8
8
  else if (typeof value === 'string') {
9
- return `'${value.replace(/'/g, "''")}'`;
9
+ const escaped = value.replace(/\\/g, '\\\\').replace(/'/g, "''");
10
+ return `'${escaped}'`;
10
11
  }
11
12
  else if (value instanceof Date) {
12
13
  return `'${value.toISOString()}'`;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * ClickHouse Semantic Backend
3
+ *
4
+ * This module implements the SemanticBackend interface from @hypequery/datasets
5
+ * for ClickHouse databases. It translates database-agnostic semantic plans
6
+ * (PlanNode) into ClickHouse-specific SQL and executes them.
7
+ *
8
+ * Architecture:
9
+ * - @hypequery/datasets: Owns semantic planning, validation, and execution protocol
10
+ * - @hypequery/clickhouse: Implements SQL translation and execution for ClickHouse
11
+ *
12
+ * Usage (recommended): pass the query builder you already use for
13
+ * hand-written queries, so semantic and ad hoc queries share one connection.
14
+ * ```ts
15
+ * import { createDatasetClient } from '@hypequery/datasets';
16
+ * import { createQueryBuilder } from '@hypequery/clickhouse';
17
+ *
18
+ * const db = createQueryBuilder({
19
+ * url: process.env.CLICKHOUSE_URL,
20
+ * username: process.env.CLICKHOUSE_USER,
21
+ * password: process.env.CLICKHOUSE_PASSWORD,
22
+ * database: process.env.CLICKHOUSE_DATABASE,
23
+ * });
24
+ *
25
+ * const analytics = createDatasetClient({ queryBuilder: db });
26
+ * ```
27
+ *
28
+ * Advanced: `createBackend` exposes the database-agnostic SemanticBackend
29
+ * protocol directly. Reach for it when you want a standalone backend instance
30
+ * rather than sharing a query builder.
31
+ * ```ts
32
+ * import { createBackend } from '@hypequery/clickhouse/datasets';
33
+ *
34
+ * const analytics = createDatasetClient({
35
+ * backend: createBackend({ url, username, password, database }),
36
+ * });
37
+ * ```
38
+ */
39
+ import { type SemanticBackend } from '@hypequery/datasets';
40
+ import type { CreateQueryBuilderConfig } from './core/query-builder.js';
41
+ import type { SchemaDefinition } from './core/types/builder-state.js';
42
+ export type CreateBackendConfig = CreateQueryBuilderConfig;
43
+ /**
44
+ * Create ClickHouse Semantic Backend
45
+ *
46
+ * Creates a SemanticBackend implementation that translates database-agnostic
47
+ * semantic plans into ClickHouse SQL and executes them.
48
+ *
49
+ * @param config - ClickHouse connection configuration
50
+ * @returns SemanticBackend interface for executing semantic queries
51
+ */
52
+ export declare function createBackend<Schema extends SchemaDefinition<Schema>>(config: CreateBackendConfig): SemanticBackend;
53
+ //# sourceMappingURL=datasets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datasets.d.ts","sourceRoot":"","sources":["../src/datasets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAGL,KAAK,eAAe,EAGrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEtE,MAAM,MAAM,mBAAmB,GAAG,wBAAwB,CAAC;AAuW3D;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,MAAM,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACnE,MAAM,EAAE,mBAAmB,GAC1B,eAAe,CAqDjB"}
@@ -0,0 +1,401 @@
1
+ /**
2
+ * ClickHouse Semantic Backend
3
+ *
4
+ * This module implements the SemanticBackend interface from @hypequery/datasets
5
+ * for ClickHouse databases. It translates database-agnostic semantic plans
6
+ * (PlanNode) into ClickHouse-specific SQL and executes them.
7
+ *
8
+ * Architecture:
9
+ * - @hypequery/datasets: Owns semantic planning, validation, and execution protocol
10
+ * - @hypequery/clickhouse: Implements SQL translation and execution for ClickHouse
11
+ *
12
+ * Usage (recommended): pass the query builder you already use for
13
+ * hand-written queries, so semantic and ad hoc queries share one connection.
14
+ * ```ts
15
+ * import { createDatasetClient } from '@hypequery/datasets';
16
+ * import { createQueryBuilder } from '@hypequery/clickhouse';
17
+ *
18
+ * const db = createQueryBuilder({
19
+ * url: process.env.CLICKHOUSE_URL,
20
+ * username: process.env.CLICKHOUSE_USER,
21
+ * password: process.env.CLICKHOUSE_PASSWORD,
22
+ * database: process.env.CLICKHOUSE_DATABASE,
23
+ * });
24
+ *
25
+ * const analytics = createDatasetClient({ queryBuilder: db });
26
+ * ```
27
+ *
28
+ * Advanced: `createBackend` exposes the database-agnostic SemanticBackend
29
+ * protocol directly. Reach for it when you want a standalone backend instance
30
+ * rather than sharing a query builder.
31
+ * ```ts
32
+ * import { createBackend } from '@hypequery/clickhouse/datasets';
33
+ *
34
+ * const analytics = createDatasetClient({
35
+ * backend: createBackend({ url, username, password, database }),
36
+ * });
37
+ * ```
38
+ */
39
+ import { createQueryBuilder } from './core/query-builder.js';
40
+ // =============================================================================
41
+ // ClickHouse SQL Generation Utilities
42
+ // =============================================================================
43
+ const GRAIN_FUNCTIONS = {
44
+ day: 'toStartOfDay',
45
+ week: 'toStartOfWeek',
46
+ month: 'toStartOfMonth',
47
+ quarter: 'toStartOfQuarter',
48
+ year: 'toStartOfYear',
49
+ };
50
+ /**
51
+ * SQL Literal Rendering
52
+ * Safely escapes values for direct SQL inclusion
53
+ */
54
+ function renderLiteral(value) {
55
+ if (value === null)
56
+ return 'NULL';
57
+ if (typeof value === 'number')
58
+ return String(value);
59
+ if (typeof value === 'boolean')
60
+ return value ? '1' : '0';
61
+ // SQL string escaping: single quotes are escaped by doubling them
62
+ return `'${String(value).replace(/'/g, "''")}'`;
63
+ }
64
+ /**
65
+ * Time Grain Rendering
66
+ * Converts semantic grain to ClickHouse function
67
+ */
68
+ function renderGrain(field, unit) {
69
+ return `${GRAIN_FUNCTIONS[unit]}(${field})`;
70
+ }
71
+ /**
72
+ * Filter Rendering - Applies filters using query builder WHERE clauses
73
+ * This is the preferred method when working with the query builder
74
+ */
75
+ function applyFilters(builder, filters) {
76
+ let qb = builder;
77
+ for (const filter of filters) {
78
+ qb = qb.where(filter.field, filter.operator, filter.value);
79
+ }
80
+ return qb;
81
+ }
82
+ /**
83
+ * Type guard to check if value is a valid literal for SQL rendering
84
+ */
85
+ function isLiteralValue(value) {
86
+ return (typeof value === 'string' ||
87
+ typeof value === 'number' ||
88
+ typeof value === 'boolean' ||
89
+ value === null);
90
+ }
91
+ /**
92
+ * Safely render a filter value as a SQL literal
93
+ * Throws if value is not a valid literal type
94
+ */
95
+ function renderFilterValue(value) {
96
+ if (!isLiteralValue(value)) {
97
+ throw new Error(`Invalid filter value type: ${typeof value}`);
98
+ }
99
+ return renderLiteral(value);
100
+ }
101
+ /**
102
+ * Filter Condition Rendering - Converts filter to SQL WHERE clause string
103
+ * Used for filtered aggregations (IF conditions) where query builder can't be used
104
+ */
105
+ function renderFilterCondition(filter) {
106
+ const { field, operator, value } = filter;
107
+ switch (operator) {
108
+ case 'eq':
109
+ return `${field} = ${renderFilterValue(value)}`;
110
+ case 'neq':
111
+ return `${field} != ${renderFilterValue(value)}`;
112
+ case 'gt':
113
+ return `${field} > ${renderFilterValue(value)}`;
114
+ case 'gte':
115
+ return `${field} >= ${renderFilterValue(value)}`;
116
+ case 'lt':
117
+ return `${field} < ${renderFilterValue(value)}`;
118
+ case 'lte':
119
+ return `${field} <= ${renderFilterValue(value)}`;
120
+ case 'like':
121
+ return `${field} LIKE ${renderFilterValue(value)}`;
122
+ case 'in':
123
+ case 'notIn': {
124
+ if (!Array.isArray(value) || value.length === 0) {
125
+ throw new Error(`"${operator}" filters require a non-empty array.`);
126
+ }
127
+ const values = value.map(renderFilterValue).join(', ');
128
+ const op = operator === 'in' ? 'IN' : 'NOT IN';
129
+ return `${field} ${op} (${values})`;
130
+ }
131
+ case 'between': {
132
+ if (!Array.isArray(value) || value.length !== 2) {
133
+ throw new Error('"between" filters require a two-item array.');
134
+ }
135
+ return `${field} BETWEEN ${renderFilterValue(value[0])} AND ${renderFilterValue(value[1])}`;
136
+ }
137
+ default:
138
+ throw new Error(`Unsupported filter operator "${operator}".`);
139
+ }
140
+ }
141
+ /**
142
+ * Filtered Aggregation Field Rendering
143
+ * Generates ClickHouse IF() expressions for conditional aggregations
144
+ * Example: SUM(if(status = 'completed', amount, 0))
145
+ */
146
+ function renderFilteredAggregationField(aggregation) {
147
+ if (!aggregation.filters?.length) {
148
+ return aggregation.field;
149
+ }
150
+ // Combine multiple filters with AND
151
+ const condition = aggregation.filters
152
+ .map(renderFilterCondition)
153
+ .map((part) => `(${part})`)
154
+ .join(' AND ');
155
+ // Use appropriate fallback: 0 for SUM, NULL for others
156
+ const fallback = aggregation.aggregation === 'sum' ? '0' : 'NULL';
157
+ return `if(${condition}, ${aggregation.field}, ${fallback})`;
158
+ }
159
+ /**
160
+ * Expression Rendering
161
+ * Converts semantic expressions (formulas) to SQL
162
+ * Used for derived metrics
163
+ */
164
+ function renderExpression(expression) {
165
+ switch (expression.kind) {
166
+ case 'ref':
167
+ return expression.name;
168
+ case 'literal':
169
+ return renderLiteral(expression.value);
170
+ case 'binary': {
171
+ const operators = {
172
+ add: '+',
173
+ subtract: '-',
174
+ multiply: '*',
175
+ divide: '/',
176
+ };
177
+ const op = operators[expression.operator];
178
+ const left = renderExpression(expression.left);
179
+ const right = renderExpression(expression.right);
180
+ return `(${left}) ${op} (${right})`;
181
+ }
182
+ case 'function': {
183
+ const args = expression.args.map(renderExpression);
184
+ // Special case functions with custom SQL
185
+ if (expression.name === 'nullIfZero') {
186
+ return `NULLIF(${args[0]}, 0)`;
187
+ }
188
+ if (expression.name === 'coalesce') {
189
+ return `COALESCE(${args.join(', ')})`;
190
+ }
191
+ // Standard functions
192
+ const functionMap = {
193
+ round: 'ROUND',
194
+ floor: 'FLOOR',
195
+ ceil: 'CEIL',
196
+ };
197
+ const fn = functionMap[expression.name];
198
+ if (!fn) {
199
+ throw new Error(`Unsupported function: ${expression.name}`);
200
+ }
201
+ return `${fn}(${args.join(', ')})`;
202
+ }
203
+ default:
204
+ throw new Error('Unsupported semantic expression.');
205
+ }
206
+ }
207
+ // =============================================================================
208
+ // Query Builder Integration
209
+ // =============================================================================
210
+ /**
211
+ * Apply Aggregations
212
+ * Translates semantic aggregations to query builder method calls
213
+ */
214
+ function applyAggregations(builder, plan) {
215
+ let qb = builder;
216
+ for (const aggregation of plan.aggregations) {
217
+ const field = renderFilteredAggregationField(aggregation);
218
+ const { name, aggregation: aggType } = aggregation;
219
+ switch (aggType) {
220
+ case 'sum':
221
+ qb = qb.sum(field, name);
222
+ break;
223
+ case 'count':
224
+ qb = qb.count(field, name);
225
+ break;
226
+ case 'countDistinct':
227
+ qb = qb.countDistinct(field, name);
228
+ break;
229
+ case 'avg':
230
+ qb = qb.avg(field, name);
231
+ break;
232
+ case 'min':
233
+ qb = qb.min(field, name);
234
+ break;
235
+ case 'max':
236
+ qb = qb.max(field, name);
237
+ break;
238
+ }
239
+ }
240
+ return qb;
241
+ }
242
+ /**
243
+ * Append Order/Limit/Offset
244
+ * Applies result modifiers to query builder
245
+ */
246
+ function appendOrderLimitOffset(builder, plan) {
247
+ let qb = builder;
248
+ // Order by
249
+ for (const order of plan.orderBy ?? []) {
250
+ qb = qb.orderBy(order.field, order.direction.toUpperCase());
251
+ }
252
+ // Pagination
253
+ if (plan.limit != null)
254
+ qb = qb.limit(plan.limit);
255
+ if (plan.offset != null)
256
+ qb = qb.offset(plan.offset);
257
+ return qb;
258
+ }
259
+ // =============================================================================
260
+ // Plan Translation to SQL
261
+ // =============================================================================
262
+ /**
263
+ * Build Aggregate Query
264
+ * Translates semantic aggregate plan to ClickHouse query builder
265
+ */
266
+ function buildAggregateQuery(queryBuilder, plan) {
267
+ let qb = queryBuilder.table(plan.source);
268
+ // Build SELECT and GROUP BY for dimensions
269
+ const selectParts = [];
270
+ const groupByParts = [];
271
+ // Time grain (period column)
272
+ if (plan.grain) {
273
+ const grainSql = renderGrain(plan.grain.field, plan.grain.unit);
274
+ selectParts.push(`${grainSql} AS ${plan.grain.output}`);
275
+ groupByParts.push(plan.grain.output);
276
+ }
277
+ // Dimensions
278
+ for (const dimension of plan.dimensions) {
279
+ const columnSql = dimension.field === dimension.name
280
+ ? dimension.name
281
+ : `${dimension.field} AS ${dimension.name}`;
282
+ selectParts.push(columnSql);
283
+ groupByParts.push(dimension.name);
284
+ }
285
+ // Apply SELECT clause
286
+ if (selectParts.length > 0) {
287
+ qb = qb.select(selectParts);
288
+ }
289
+ // Apply aggregations (measures)
290
+ qb = applyAggregations(qb, plan);
291
+ // Apply GROUP BY
292
+ if (groupByParts.length > 0) {
293
+ qb = qb.groupBy(groupByParts);
294
+ }
295
+ // Apply tenant filter (auto-injected)
296
+ if (plan.tenant) {
297
+ qb = qb.where(plan.tenant.field, plan.tenant.operator, plan.tenant.value);
298
+ }
299
+ // Apply user filters
300
+ qb = applyFilters(qb, plan.filters);
301
+ // Apply order/limit/offset
302
+ return appendOrderLimitOffset(qb, plan);
303
+ }
304
+ /**
305
+ * Build Derived Metric SQL
306
+ * Generates CTE-based query for derived metrics (formulas over base metrics)
307
+ *
308
+ * Note: Uses string concatenation for outer query since query builder
309
+ * doesn't support CTEs natively. Inner query uses query builder for safety.
310
+ */
311
+ function buildDerivedSQL(queryBuilder, plan) {
312
+ if (plan.input.kind !== 'aggregate') {
313
+ throw new Error('ClickHouse datasets currently supports derived metrics over aggregate input plans only.');
314
+ }
315
+ // Build inner aggregate query using query builder
316
+ const inputQuery = buildAggregateQuery(queryBuilder, plan.input);
317
+ const { sql, parameters } = inputQuery.toSQLWithParams();
318
+ // Passthrough columns (grain + dimensions)
319
+ const passthrough = [
320
+ ...(plan.input.grain ? [plan.input.grain.output] : []),
321
+ ...plan.input.dimensions.map((dim) => dim.name),
322
+ ];
323
+ // Derived metric calculations
324
+ const metricSelects = plan.metrics.map((metric) => `${renderExpression(metric.expression)} AS ${metric.name}`);
325
+ // Build outer query with CTE
326
+ const allSelects = [...passthrough, ...metricSelects];
327
+ let outerSql = `WITH base AS (${sql}) SELECT ${allSelects.join(', ')} FROM base`;
328
+ // ORDER BY
329
+ if (plan.orderBy?.length) {
330
+ const orderClauses = plan.orderBy.map((order) => `${order.field} ${order.direction.toUpperCase()}`);
331
+ outerSql += ` ORDER BY ${orderClauses.join(', ')}`;
332
+ }
333
+ // LIMIT and OFFSET
334
+ if (plan.limit != null) {
335
+ outerSql += ` LIMIT ${plan.limit}`;
336
+ }
337
+ if (plan.offset != null) {
338
+ outerSql += ` OFFSET ${plan.offset}`;
339
+ }
340
+ return { sql: outerSql, parameters };
341
+ }
342
+ // =============================================================================
343
+ // Semantic Backend Implementation
344
+ // =============================================================================
345
+ /**
346
+ * Create ClickHouse Semantic Backend
347
+ *
348
+ * Creates a SemanticBackend implementation that translates database-agnostic
349
+ * semantic plans into ClickHouse SQL and executes them.
350
+ *
351
+ * @param config - ClickHouse connection configuration
352
+ * @returns SemanticBackend interface for executing semantic queries
353
+ */
354
+ export function createBackend(config) {
355
+ const queryBuilder = createQueryBuilder(config);
356
+ return {
357
+ /**
358
+ * Execute a semantic plan and return results
359
+ */
360
+ async execute(plan) {
361
+ const start = Date.now();
362
+ if (plan.kind === 'aggregate') {
363
+ // Base metrics: use query builder for full safety
364
+ const query = buildAggregateQuery(queryBuilder, plan);
365
+ const { sql } = query.toSQLWithParams();
366
+ const data = await query.execute();
367
+ return {
368
+ data,
369
+ meta: {
370
+ sql,
371
+ timingMs: Date.now() - start,
372
+ tenant: plan.tenant?.operator === 'eq' ? plan.tenant.value : undefined,
373
+ },
374
+ };
375
+ }
376
+ // Derived metrics: CTE query with formulas
377
+ const { sql, parameters } = buildDerivedSQL(queryBuilder, plan);
378
+ const data = await queryBuilder.rawQuery(sql, parameters);
379
+ const tenant = plan.input.kind === 'aggregate' && plan.input.tenant?.operator === 'eq'
380
+ ? plan.input.tenant.value
381
+ : undefined;
382
+ return {
383
+ data,
384
+ meta: {
385
+ sql,
386
+ timingMs: Date.now() - start,
387
+ tenant,
388
+ },
389
+ };
390
+ },
391
+ /**
392
+ * Generate SQL without executing
393
+ */
394
+ async explain(plan) {
395
+ if (plan.kind === 'aggregate') {
396
+ return { sql: buildAggregateQuery(queryBuilder, plan).toSQLWithParams().sql };
397
+ }
398
+ return { sql: buildDerivedSQL(queryBuilder, plan).sql };
399
+ },
400
+ };
401
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/clickhouse",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "ClickHouse typescript query builder",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,6 +14,11 @@
14
14
  "import": "./dist/index.js",
15
15
  "require": "./dist/index.js"
16
16
  },
17
+ "./datasets": {
18
+ "types": "./dist/datasets.d.ts",
19
+ "import": "./dist/datasets.js",
20
+ "require": "./dist/datasets.js"
21
+ },
17
22
  "./cli": {
18
23
  "types": "./dist/cli/index.d.ts",
19
24
  "import": "./dist/cli/index.js",
@@ -33,11 +38,15 @@
33
38
  "dotenv": "^16.0.0"
34
39
  },
35
40
  "peerDependencies": {
36
- "@clickhouse/client-web": "^0.2.0 || ^1.0.0"
41
+ "@clickhouse/client-web": "^0.2.0 || ^1.0.0",
42
+ "@hypequery/datasets": "^0.1.0 || ^0.2.0"
37
43
  },
38
44
  "peerDependenciesMeta": {
39
45
  "@clickhouse/client-web": {
40
46
  "optional": true
47
+ },
48
+ "@hypequery/datasets": {
49
+ "optional": true
41
50
  }
42
51
  },
43
52
  "devDependencies": {
@@ -51,7 +60,8 @@
51
60
  "typedoc-plugin-markdown": "^4.6.0",
52
61
  "typescript": "^5.7.3",
53
62
  "@vitest/coverage-v8": "^2.1.6",
54
- "vitest": "^2.1.6"
63
+ "vitest": "^2.1.6",
64
+ "@hypequery/datasets": "0.2.0"
55
65
  },
56
66
  "ts-node": {
57
67
  "esm": true,