@housekit/orm 0.1.47 → 0.1.48

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 (79) hide show
  1. package/README.md +34 -0
  2. package/dist/builders/delete.js +112 -0
  3. package/dist/builders/insert.d.ts +0 -91
  4. package/dist/builders/insert.js +393 -0
  5. package/dist/builders/prepared.d.ts +1 -2
  6. package/dist/builders/prepared.js +30 -0
  7. package/dist/builders/select.d.ts +0 -161
  8. package/dist/builders/select.js +562 -0
  9. package/dist/builders/select.types.js +1 -0
  10. package/dist/builders/update.js +136 -0
  11. package/dist/client.d.ts +0 -6
  12. package/dist/client.js +140 -0
  13. package/dist/codegen/zod.js +107 -0
  14. package/dist/column.d.ts +1 -25
  15. package/dist/column.js +133 -0
  16. package/dist/compiler.d.ts +0 -7
  17. package/dist/compiler.js +513 -0
  18. package/dist/core.js +6 -0
  19. package/dist/data-types.d.ts +0 -61
  20. package/dist/data-types.js +127 -0
  21. package/dist/dictionary.d.ts +0 -149
  22. package/dist/dictionary.js +158 -0
  23. package/dist/engines.d.ts +0 -385
  24. package/dist/engines.js +292 -0
  25. package/dist/expressions.d.ts +0 -10
  26. package/dist/expressions.js +268 -0
  27. package/dist/external.d.ts +0 -112
  28. package/dist/external.js +224 -0
  29. package/dist/index.d.ts +0 -51
  30. package/dist/index.js +139 -6853
  31. package/dist/logger.js +36 -0
  32. package/dist/materialized-views.d.ts +0 -188
  33. package/dist/materialized-views.js +380 -0
  34. package/dist/metadata.js +59 -0
  35. package/dist/modules/aggregates.d.ts +0 -164
  36. package/dist/modules/aggregates.js +121 -0
  37. package/dist/modules/array.d.ts +0 -98
  38. package/dist/modules/array.js +71 -0
  39. package/dist/modules/conditional.d.ts +0 -84
  40. package/dist/modules/conditional.js +138 -0
  41. package/dist/modules/conversion.d.ts +0 -147
  42. package/dist/modules/conversion.js +109 -0
  43. package/dist/modules/geo.d.ts +0 -164
  44. package/dist/modules/geo.js +112 -0
  45. package/dist/modules/hash.js +4 -0
  46. package/dist/modules/index.js +12 -0
  47. package/dist/modules/json.d.ts +0 -106
  48. package/dist/modules/json.js +76 -0
  49. package/dist/modules/math.d.ts +0 -16
  50. package/dist/modules/math.js +16 -0
  51. package/dist/modules/string.d.ts +0 -136
  52. package/dist/modules/string.js +89 -0
  53. package/dist/modules/time.d.ts +0 -123
  54. package/dist/modules/time.js +91 -0
  55. package/dist/modules/types.d.ts +0 -133
  56. package/dist/modules/types.js +114 -0
  57. package/dist/modules/window.js +140 -0
  58. package/dist/relational.d.ts +0 -82
  59. package/dist/relational.js +290 -0
  60. package/dist/relations.js +21 -0
  61. package/dist/schema-builder.d.ts +0 -90
  62. package/dist/schema-builder.js +140 -0
  63. package/dist/table.d.ts +0 -42
  64. package/dist/table.js +406 -0
  65. package/dist/utils/background-batcher.js +75 -0
  66. package/dist/utils/batch-transform.js +51 -0
  67. package/dist/utils/binary-reader.d.ts +0 -6
  68. package/dist/utils/binary-reader.js +334 -0
  69. package/dist/utils/binary-serializer.d.ts +0 -125
  70. package/dist/utils/binary-serializer.js +637 -0
  71. package/dist/utils/binary-worker-code.js +1 -0
  72. package/dist/utils/binary-worker-pool.d.ts +0 -34
  73. package/dist/utils/binary-worker-pool.js +206 -0
  74. package/dist/utils/binary-worker.d.ts +0 -11
  75. package/dist/utils/binary-worker.js +63 -0
  76. package/dist/utils/insert-processing.d.ts +0 -2
  77. package/dist/utils/insert-processing.js +163 -0
  78. package/dist/utils/lru-cache.js +30 -0
  79. package/package.json +68 -3
@@ -0,0 +1,140 @@
1
+ import { ClickHouseColumn } from './column';
2
+ import { chTable, chView, index, projection } from './table';
3
+ import { chMaterializedView, detectMaterializedViewDrift, extractMVQuery, createMigrationBridge, generateBlueGreenMigration } from './materialized-views';
4
+ export { detectMaterializedViewDrift, extractMVQuery, createMigrationBridge, generateBlueGreenMigration };
5
+ import { chDictionary } from './dictionary';
6
+ import { chProjection } from './materialized-views';
7
+ export { index };
8
+ function primaryUuid(name) {
9
+ const colName = (name ?? 'id');
10
+ const column = new ClickHouseColumn(colName, 'UUID')
11
+ .autoGenerate()
12
+ .primaryKey();
13
+ return {
14
+ [colName]: column
15
+ };
16
+ }
17
+ function primaryUuidV7(name) {
18
+ const colName = (name ?? 'id');
19
+ const column = new ClickHouseColumn(colName, 'UUID')
20
+ .autoGenerate({ version: 7 })
21
+ .primaryKey();
22
+ return {
23
+ [colName]: column
24
+ };
25
+ }
26
+ export const t = {
27
+ int8: (name) => new ClickHouseColumn(name, 'Int8'),
28
+ int16: (name) => new ClickHouseColumn(name, 'Int16'),
29
+ integer: (name) => new ClickHouseColumn(name, 'Int32'),
30
+ int32: (name) => new ClickHouseColumn(name, 'Int32'),
31
+ int64: (name) => new ClickHouseColumn(name, 'Int64'),
32
+ int128: (name) => new ClickHouseColumn(name, 'Int128'),
33
+ int256: (name) => new ClickHouseColumn(name, 'Int256'),
34
+ uint8: (name) => new ClickHouseColumn(name, 'UInt8'),
35
+ uint16: (name) => new ClickHouseColumn(name, 'UInt16'),
36
+ uint32: (name) => new ClickHouseColumn(name, 'UInt32'),
37
+ uint64: (name) => new ClickHouseColumn(name, 'UInt64'),
38
+ uint128: (name) => new ClickHouseColumn(name, 'UInt128'),
39
+ uint256: (name) => new ClickHouseColumn(name, 'UInt256'),
40
+ float32: (name) => new ClickHouseColumn(name, 'Float32'),
41
+ float: (name) => new ClickHouseColumn(name, 'Float64'),
42
+ float64: (name) => new ClickHouseColumn(name, 'Float64'),
43
+ bfloat16: (name) => new ClickHouseColumn(name, 'BFloat16'),
44
+ decimal: (name, precision = 18, scale = 4) => new ClickHouseColumn(name, `Decimal(${precision}, ${scale})`),
45
+ decimal32: (name, scale = 4) => new ClickHouseColumn(name, `Decimal32(${scale})`),
46
+ decimal64: (name, scale = 4) => new ClickHouseColumn(name, `Decimal64(${scale})`),
47
+ decimal128: (name, scale = 4) => new ClickHouseColumn(name, `Decimal128(${scale})`),
48
+ decimal256: (name, scale = 4) => new ClickHouseColumn(name, `Decimal256(${scale})`),
49
+ text: (name) => new ClickHouseColumn(name, 'String'),
50
+ string: (name) => new ClickHouseColumn(name, 'String'),
51
+ fixedString: (name, length) => new ClickHouseColumn(name, `FixedString(${length})`),
52
+ varchar: (name, opts) => opts?.length
53
+ ? new ClickHouseColumn(name, `FixedString(${opts.length})`)
54
+ : new ClickHouseColumn(name, 'String'),
55
+ date: (name) => new ClickHouseColumn(name, 'Date'),
56
+ date32: (name) => new ClickHouseColumn(name, 'Date32'),
57
+ timestamp: (name, timezone) => new ClickHouseColumn(name, timezone ? `DateTime('${timezone}')` : 'DateTime'),
58
+ datetime: (name, timezone) => new ClickHouseColumn(name, timezone ? `DateTime('${timezone}')` : 'DateTime'),
59
+ datetime64: (name, precision = 3, timezone) => new ClickHouseColumn(name, timezone ? `DateTime64(${precision}, '${timezone}')` : `DateTime64(${precision})`),
60
+ boolean: (name) => new ClickHouseColumn(name, 'Bool'),
61
+ bool: (name) => new ClickHouseColumn(name, 'Bool'),
62
+ uuid: (name) => new ClickHouseColumn(name, 'UUID'),
63
+ ipv4: (name) => new ClickHouseColumn(name, 'IPv4'),
64
+ ipv6: (name) => new ClickHouseColumn(name, 'IPv6'),
65
+ array: (col) => {
66
+ const isInnerComposite = col.type.startsWith('Array(') || col.type.startsWith('Map(') || col.type.startsWith('Tuple(');
67
+ const innerType = (col.isNull && !isInnerComposite) ? `Nullable(${col.type})` : col.type;
68
+ return new ClickHouseColumn(col.name, `Array(${innerType})`);
69
+ },
70
+ tuple: (name, types) => {
71
+ if (!Array.isArray(types)) {
72
+ throw new Error(`tuple() expects an array of types, but received: ${typeof types}`);
73
+ }
74
+ return new ClickHouseColumn(name, `Tuple(${types.join(', ')})`);
75
+ },
76
+ map: (name, keyType = 'String', valueType = 'String') => new ClickHouseColumn(name, `Map(${keyType}, ${valueType})`),
77
+ nested: (name, fields) => {
78
+ const fieldDefs = Object.entries(fields).map(([k, v]) => `${k} ${v}`).join(', ');
79
+ return new ClickHouseColumn(name, `Nested(${fieldDefs})`);
80
+ },
81
+ json: (name) => new ClickHouseColumn(name, 'JSON', true, { isJson: true }),
82
+ dynamic: (name, maxTypes) => new ClickHouseColumn(name, maxTypes ? `Dynamic(max_types=${maxTypes})` : 'Dynamic'),
83
+ lowCardinality: (col) => col.clone({ type: `LowCardinality(${col.type})` }),
84
+ aggregateFunction: (name, funcName, ...argTypes) => new ClickHouseColumn(name, `AggregateFunction(${funcName}${argTypes.length > 0 ? ', ' + argTypes.join(', ') : ''})`),
85
+ simpleAggregateFunction: (name, funcName, argType) => new ClickHouseColumn(name, `SimpleAggregateFunction(${funcName}, ${argType})`),
86
+ point: (name) => new ClickHouseColumn(name, 'Point'),
87
+ ring: (name) => new ClickHouseColumn(name, 'Ring'),
88
+ polygon: (name) => new ClickHouseColumn(name, 'Polygon'),
89
+ multiPolygon: (name) => new ClickHouseColumn(name, 'MultiPolygon'),
90
+ enum: (name, values) => new ClickHouseColumn(name, 'String', true, { enumValues: values }),
91
+ timestamps: () => ({
92
+ created_at: new ClickHouseColumn('created_at', 'DateTime').default('now()'),
93
+ updated_at: new ClickHouseColumn('updated_at', 'DateTime').default('now()'),
94
+ }),
95
+ primaryUuid,
96
+ primaryUuidV7,
97
+ softDeletes: () => ({
98
+ is_deleted: new ClickHouseColumn('is_deleted', 'Bool').default(false),
99
+ deleted_at: new ClickHouseColumn('deleted_at', 'DateTime').nullable(),
100
+ }),
101
+ };
102
+ export function defineTable(tableName, columnsOrCallback, options) {
103
+ const columns = typeof columnsOrCallback === 'function'
104
+ ? columnsOrCallback(t)
105
+ : columnsOrCallback;
106
+ return chTable(tableName, columns, options);
107
+ }
108
+ export const table = defineTable;
109
+ export const view = chView;
110
+ export const defineView = chView;
111
+ export const defineMaterializedView = chMaterializedView;
112
+ export const materializedView = chMaterializedView;
113
+ export const dictionary = chDictionary;
114
+ export const defineDictionary = chDictionary;
115
+ export { projection };
116
+ export { chProjection as defineProjection };
117
+ export function relations(table, relationsBuilder) {
118
+ const helpers = {
119
+ one: (targetTable, config) => ({
120
+ relation: 'one',
121
+ name: '',
122
+ table: targetTable,
123
+ fields: config.fields,
124
+ references: config.references,
125
+ }),
126
+ many: (targetTable, config) => ({
127
+ relation: 'many',
128
+ name: '',
129
+ table: targetTable,
130
+ fields: config?.fields,
131
+ references: config?.references,
132
+ }),
133
+ };
134
+ const rels = relationsBuilder(helpers);
135
+ for (const [name, rel] of Object.entries(rels)) {
136
+ rel.name = name;
137
+ }
138
+ table.$relations = rels;
139
+ return rels;
140
+ }
package/dist/table.d.ts CHANGED
@@ -3,13 +3,6 @@ import { TTLRule } from './data-types';
3
3
  import { EngineConfiguration } from './engines';
4
4
  import { type MetadataVersion } from './metadata';
5
5
  export interface TableOptions {
6
- /**
7
- * ClickHouse table engine configuration.
8
- *
9
- * // Raw string (escape hatch)
10
- * customEngine: 'MergeTree()'
11
- * ```
12
- */
13
6
  engine?: EngineConfiguration;
14
7
  customEngine?: string;
15
8
  orderBy?: string | string[];
@@ -67,9 +60,6 @@ export type RelationDefinition<TTarget extends TableDefinition<any> = TableDefin
67
60
  references?: ClickHouseColumn[];
68
61
  };
69
62
  export type TableColumns = Record<string, ClickHouseColumn<any, any, any>>;
70
- /**
71
- * Utility to force TypeScript to expand computed types for cleaner tooltips.
72
- */
73
63
  export type Prettify<T> = {
74
64
  [K in keyof T]: T[K];
75
65
  } & {};
@@ -90,20 +80,6 @@ export type InferSelectModel<T extends {
90
80
  export type InferInsertModel<T extends {
91
81
  $columns: TableColumns;
92
82
  }> = Prettify<TableInsert<T['$columns']>>;
93
- type InferInsertFromColumns<T> = T extends {
94
- $columns: infer TCols extends TableColumns;
95
- } ? TableInsert<TCols> : never;
96
- type InferSelectFromColumns<T> = T extends {
97
- $columns: infer TCols extends TableColumns;
98
- } ? InferSelectModel<{
99
- $columns: TCols;
100
- }> : never;
101
- export type CleanInsert<T> = T extends {
102
- $inferInsert: infer I;
103
- } ? I : InferInsertFromColumns<T>;
104
- export type CleanSelect<T> = T extends {
105
- $inferSelect: infer S;
106
- } ? S : InferSelectFromColumns<T>;
107
83
  export type InferInsertValue<TCols extends TableColumns, K extends keyof TCols> = TCols[K] extends ClickHouseColumn<infer Type, infer NotNull, any> ? NotNull extends true ? Type : Type | undefined | null : never;
108
84
  export type TableInsertArray<T extends TableDefinition<TableColumns>> = T['$inferInsert'][];
109
85
  export type TableModel<T extends TableDefinition<TableColumns>> = PublicSelectModel<T>;
@@ -179,17 +155,8 @@ export declare function chView<T extends Record<string, ClickHouseColumn<any, an
179
155
  }> & {
180
156
  toSQLs: () => string[];
181
157
  };
182
- /**
183
- * Define a reusable set of columns with type inference.
184
- */
185
158
  export declare function defineColumns<T extends TableColumns>(cols: T): T;
186
- /**
187
- * Compose column sets, throwing on duplicate keys to avoid accidental overrides.
188
- */
189
159
  export declare function extendColumns<Base extends TableColumns, Extra extends TableColumns>(base: Base, extra: Extra): Base & Extra;
190
- /**
191
- * Define a versioned table (e.g., base_v2) with optional view alias to point to the latest version.
192
- */
193
160
  export declare function versionedTable<T extends TableColumns>(baseName: string, version: string | number, columns: T, options?: TableOptions & {
194
161
  latestAlias?: boolean;
195
162
  aliasName?: string;
@@ -197,9 +164,6 @@ export declare function versionedTable<T extends TableColumns>(baseName: string,
197
164
  $versionMeta: VersionedMeta;
198
165
  toSQLs: () => string[];
199
166
  };
200
- /**
201
- * Define a derived (aggregated) table with a backing table and materialized view.
202
- */
203
167
  export declare function deriveTable<T extends TableColumns>(source: TableDefinition<T>, config: {
204
168
  name: string;
205
169
  groupBy: string | string[];
@@ -208,12 +172,6 @@ export declare function deriveTable<T extends TableColumns>(source: TableDefinit
208
172
  }): TableDefinition<any> & {
209
173
  toSQLs: () => string[];
210
174
  };
211
- /**
212
- * Render CREATE statements for preview.
213
- */
214
175
  export declare function renderSchema(def: TableDefinition<any>): any;
215
- /**
216
- * Generate a TypeScript type from a table definition (best effort mapping).
217
- */
218
176
  export declare function generateTypes<T extends TableColumns>(def: TableDefinition<T>, typeName?: string): string;
219
177
  export {};
package/dist/table.js ADDED
@@ -0,0 +1,406 @@
1
+ import { ClickHouseColumn } from './column';
2
+ import { renderEngineSQL, isMergeTreeFamily, getVersionColumn, Engine } from './engines';
3
+ import { assertMetadataVersion, buildHousekitMetadata, defaultMetadataVersion, getMetadataDefaults } from './metadata';
4
+ export const index = (name) => ({
5
+ on: (...cols) => ({
6
+ type: (type, granularity) => ({
7
+ name,
8
+ cols,
9
+ type,
10
+ granularity
11
+ })
12
+ })
13
+ });
14
+ export const projection = (name, query) => ({
15
+ name,
16
+ query
17
+ });
18
+ function attachTableName(tableName, columns) {
19
+ for (const col of Object.values(columns)) {
20
+ col.tableName = tableName;
21
+ }
22
+ }
23
+ function assignColumnsToTable(target, columns, tableName) {
24
+ for (const [key, col] of Object.entries(columns)) {
25
+ const column = col;
26
+ column.tableName = tableName;
27
+ target[key] = column;
28
+ }
29
+ return target;
30
+ }
31
+ export function chTable(tableName, columns, options = {}) {
32
+ attachTableName(tableName, columns);
33
+ let resolvedMetadataVersion;
34
+ if (options.metadataVersion) {
35
+ const versionStr = String(options.metadataVersion);
36
+ assertMetadataVersion(versionStr);
37
+ resolvedMetadataVersion = versionStr;
38
+ }
39
+ else {
40
+ resolvedMetadataVersion = defaultMetadataVersion;
41
+ }
42
+ const defaults = getMetadataDefaults(resolvedMetadataVersion);
43
+ const finalOptions = {
44
+ appendOnly: options.appendOnly ?? defaults.appendOnly,
45
+ metadataVersion: resolvedMetadataVersion,
46
+ readOnly: options.readOnly ?? (defaults.readOnly ?? false),
47
+ deduplicateBy: options.deduplicateBy,
48
+ customEngine: options.customEngine,
49
+ engine: options.engine,
50
+ orderBy: options.orderBy,
51
+ partitionBy: options.partitionBy,
52
+ primaryKey: options.primaryKey,
53
+ sampleBy: options.sampleBy,
54
+ logicalPrimaryKey: options.logicalPrimaryKey,
55
+ ttl: options.ttl,
56
+ versionColumn: options.versionColumn,
57
+ externallyManaged: options.externallyManaged,
58
+ onCluster: options.onCluster,
59
+ shardKey: options.shardKey,
60
+ materializedView: options.materializedView,
61
+ indices: options.indices,
62
+ projections: options.projections,
63
+ asyncInsert: options.asyncInsert
64
+ };
65
+ if (finalOptions.onCluster && !finalOptions.engine) {
66
+ finalOptions.engine = Engine.ReplicatedMergeTree();
67
+ }
68
+ const engineConfig = finalOptions.engine;
69
+ const engineStr = renderEngineSQL(engineConfig);
70
+ const isMergeTreeFamilyEngine = isMergeTreeFamily(engineConfig);
71
+ if (finalOptions.defaultFinal === undefined && /ReplacingMergeTree/i.test(engineStr)) {
72
+ finalOptions.defaultFinal = true;
73
+ }
74
+ if (!finalOptions.versionColumn) {
75
+ const extractedVersion = getVersionColumn(engineConfig);
76
+ if (extractedVersion) {
77
+ finalOptions.versionColumn = extractedVersion;
78
+ }
79
+ }
80
+ const onClusterClause = finalOptions.onCluster ? `ON CLUSTER ${finalOptions.onCluster}` : '';
81
+ const normalizeList = (val) => {
82
+ if (!val)
83
+ return [];
84
+ if (Array.isArray(val))
85
+ return val.map(v => v.toString().trim()).filter(Boolean);
86
+ return val.split(',').map(v => v.trim()).filter(Boolean);
87
+ };
88
+ const ensureColumnsExist = (label, raw) => {
89
+ const identifiers = normalizeList(raw).filter(v => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(v));
90
+ const validColumnNames = new Set(Object.values(columns).map(c => c.name));
91
+ for (const id of identifiers) {
92
+ const exists = columns[id] || validColumnNames.has(id);
93
+ if (!exists) {
94
+ if (id.includes('('))
95
+ continue;
96
+ throw new Error(`Column "${id}" referenced in ${label} does not exist in table ${tableName}`);
97
+ }
98
+ }
99
+ };
100
+ if (isMergeTreeFamilyEngine) {
101
+ if (!finalOptions.orderBy || normalizeList(finalOptions.orderBy).length === 0) {
102
+ throw new Error(`orderBy is required for MergeTree-family tables (${tableName})`);
103
+ }
104
+ ensureColumnsExist('orderBy', finalOptions.orderBy);
105
+ ensureColumnsExist('primaryKey', finalOptions.primaryKey);
106
+ ensureColumnsExist('partitionBy', finalOptions.partitionBy);
107
+ ensureColumnsExist('sampleBy', finalOptions.sampleBy);
108
+ if (engineStr.toLowerCase().includes('replacingmergetree') && !finalOptions.versionColumn) {
109
+ throw new Error(`versionColumn is required for ReplacingMergeTree in table ${tableName}`);
110
+ }
111
+ const isDeduplicationEngine = engineStr.toLowerCase().includes('replacingmergetree') ||
112
+ engineStr.toLowerCase().includes('collapsingmergetree');
113
+ if (finalOptions.engine?.type === 'MergeTree' && finalOptions.deduplicateBy) {
114
+ console.warn(`[housekit] Table "${tableName}" uses "deduplicateBy" but engine is MergeTree. Deduplication won't happen. Did you mean ReplacingMergeTree?`);
115
+ }
116
+ if (finalOptions.logicalPrimaryKey && !isDeduplicationEngine) {
117
+ console.warn(`[housekit] Table "${tableName}" uses "logicalPrimaryKey" but engine is ${finalOptions.engine?.type || 'MergeTree'}. Standard MergeTree does not enforce uniqueness. Did you mean ReplacingMergeTree or CollapsingMergeTree?`);
118
+ }
119
+ if (finalOptions.deduplicateBy) {
120
+ ensureColumnsExist('deduplicateBy', finalOptions.deduplicateBy);
121
+ if (!finalOptions.versionColumn) {
122
+ throw new Error(`versionColumn is required when deduplicateBy is set in table ${tableName}`);
123
+ }
124
+ if (!columns[finalOptions.versionColumn]) {
125
+ throw new Error(`versionColumn "${finalOptions.versionColumn}" not found in table ${tableName}`);
126
+ }
127
+ }
128
+ }
129
+ else {
130
+ ensureColumnsExist('orderBy', finalOptions.orderBy);
131
+ ensureColumnsExist('primaryKey', finalOptions.primaryKey);
132
+ ensureColumnsExist('partitionBy', finalOptions.partitionBy);
133
+ ensureColumnsExist('sampleBy', finalOptions.sampleBy);
134
+ }
135
+ const tableDefBase = {
136
+ $table: tableName,
137
+ $columns: columns,
138
+ $options: finalOptions,
139
+ toSQL: () => {
140
+ if (finalOptions.externallyManaged) {
141
+ return '';
142
+ }
143
+ const colDefs = Object.values(columns).map(col => {
144
+ return `\`${col.name}\` ${col.toSQL()}`;
145
+ });
146
+ const formatIndexExpr = (cols) => {
147
+ const parts = cols.map(c => `\`${c.name}\``);
148
+ return cols.length > 1 ? `tuple(${parts.join(', ')})` : parts[0];
149
+ };
150
+ if (finalOptions.indices?.length) {
151
+ for (const idx of finalOptions.indices) {
152
+ if (!idx.cols || idx.cols.length === 0)
153
+ continue;
154
+ const expr = formatIndexExpr(idx.cols);
155
+ const granularity = idx.granularity ?? 1;
156
+ const granularityClause = granularity ? ` GRANULARITY ${granularity}` : '';
157
+ colDefs.push(`INDEX \`${idx.name}\` ${expr} TYPE ${idx.type}${granularityClause}`);
158
+ }
159
+ }
160
+ if (finalOptions.projections?.length) {
161
+ for (const proj of finalOptions.projections) {
162
+ const query = proj.query.trim();
163
+ if (!query)
164
+ continue;
165
+ colDefs.push(`PROJECTION \`${proj.name}\` (${query})`);
166
+ }
167
+ }
168
+ const clusterClause = finalOptions.onCluster ? ` ON CLUSTER ${finalOptions.onCluster}` : '';
169
+ let finalEngineSQL = finalOptions.customEngine || engineStr;
170
+ if (finalOptions.versionColumn && !finalEngineSQL.includes('Replacing')) {
171
+ finalEngineSQL = `ReplacingMergeTree(${finalOptions.versionColumn})`;
172
+ }
173
+ let sql = `CREATE TABLE IF NOT EXISTS \`${tableName}\`${clusterClause} (${colDefs.join(', ')}) ENGINE = ${finalEngineSQL}`;
174
+ const mapColumnList = (val) => {
175
+ if (!val)
176
+ return '';
177
+ const list = Array.isArray(val) ? val : val.split(',').map(v => v.trim());
178
+ return list.map(id => {
179
+ const col = columns[id];
180
+ if (col)
181
+ return `\`${col.name}\``;
182
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(id)) {
183
+ return `\`${id}\``;
184
+ }
185
+ return id;
186
+ }).join(', ');
187
+ };
188
+ if (finalOptions.orderBy) {
189
+ const orderBy = mapColumnList(finalOptions.orderBy);
190
+ sql += ` ORDER BY (${orderBy})`;
191
+ }
192
+ else if (isMergeTreeFamilyEngine) {
193
+ if (finalOptions.primaryKey) {
194
+ const pk = mapColumnList(finalOptions.primaryKey);
195
+ sql += ` ORDER BY (${pk})`;
196
+ }
197
+ else {
198
+ sql += ` ORDER BY tuple()`;
199
+ }
200
+ }
201
+ if (finalOptions.primaryKey) {
202
+ const pk = mapColumnList(finalOptions.primaryKey);
203
+ sql += ` PRIMARY KEY (${pk})`;
204
+ }
205
+ if (finalOptions.partitionBy) {
206
+ const partition = mapColumnList(finalOptions.partitionBy);
207
+ sql += ` PARTITION BY (${partition})`;
208
+ }
209
+ if (finalOptions.sampleBy) {
210
+ const sample = mapColumnList(finalOptions.sampleBy);
211
+ sql += ` SAMPLE BY (${sample})`;
212
+ }
213
+ if (finalOptions.ttl) {
214
+ let ttlClause = '';
215
+ if (Array.isArray(finalOptions.ttl)) {
216
+ const ttlParts = finalOptions.ttl.map(ttl => {
217
+ let part = ttl.expression;
218
+ if (ttl.action === 'TO DISK' && ttl.target) {
219
+ part += ` TO DISK '${ttl.target}'`;
220
+ }
221
+ else if (ttl.action === 'TO VOLUME' && ttl.target) {
222
+ part += ` TO VOLUME '${ttl.target}'`;
223
+ }
224
+ return part;
225
+ });
226
+ ttlClause = `TTL ${ttlParts.join(', ')}`;
227
+ }
228
+ else if (typeof finalOptions.ttl === 'string') {
229
+ ttlClause = `TTL ${finalOptions.ttl}`;
230
+ }
231
+ else {
232
+ const ttl = finalOptions.ttl;
233
+ let part = ttl.expression;
234
+ if (ttl.action === 'TO DISK' && ttl.target) {
235
+ part += ` TO DISK '${ttl.target}'`;
236
+ }
237
+ else if (ttl.action === 'TO VOLUME' && ttl.target) {
238
+ part += ` TO VOLUME '${ttl.target}'`;
239
+ }
240
+ ttlClause = `TTL ${part}`;
241
+ }
242
+ sql += ` ${ttlClause}`;
243
+ }
244
+ const meta = {
245
+ housekit: buildHousekitMetadata(resolvedMetadataVersion, {
246
+ appendOnly: finalOptions.appendOnly,
247
+ readOnly: finalOptions.readOnly
248
+ })
249
+ };
250
+ const comment = JSON.stringify(meta);
251
+ sql += ` COMMENT '${comment.replace(/'/g, "\\'")}'`;
252
+ return sql;
253
+ },
254
+ toSQLs: function () {
255
+ const base = this.toSQL();
256
+ if (finalOptions.materializedView) {
257
+ const mv = finalOptions.materializedView;
258
+ const populateClause = mv.populate ? ' POPULATE' : '';
259
+ const mvSQL = `CREATE MATERIALIZED VIEW IF NOT EXISTS \`${mv.name}\`${populateClause} TO \`${mv.toTable}\` AS ${mv.query}`;
260
+ return [base, mvSQL];
261
+ }
262
+ return [base];
263
+ },
264
+ as: (alias) => {
265
+ return chTable(alias, columns, { ...finalOptions, externallyManaged: true });
266
+ }
267
+ };
268
+ return assignColumnsToTable(tableDefBase, columns, tableName);
269
+ }
270
+ export function chView(name, columns, queryOrOptions, optionsParam) {
271
+ attachTableName(name, columns);
272
+ let query;
273
+ let options;
274
+ if (typeof queryOrOptions === 'string') {
275
+ query = queryOrOptions;
276
+ options = optionsParam || {};
277
+ }
278
+ else {
279
+ query = queryOrOptions.query;
280
+ const { query: _, ...restOptions } = queryOrOptions;
281
+ options = restOptions;
282
+ }
283
+ const onClusterClause = options.onCluster ? ` ON CLUSTER ${options.onCluster}` : '';
284
+ const replaceClause = options.orReplace ? ' OR REPLACE' : '';
285
+ const ifNotExistsClause = options.orReplace ? '' : ' IF NOT EXISTS';
286
+ const toSQL = () => `CREATE${replaceClause} VIEW${ifNotExistsClause} \`${name}\`${onClusterClause} AS ${query}`;
287
+ const base = {
288
+ $table: name,
289
+ $columns: columns,
290
+ $options: { ...options, kind: 'view', query },
291
+ toSQL,
292
+ toSQLs: () => [toSQL()]
293
+ };
294
+ return assignColumnsToTable(base, columns, name);
295
+ }
296
+ export function defineColumns(cols) {
297
+ return cols;
298
+ }
299
+ export function extendColumns(base, extra) {
300
+ for (const key of Object.keys(extra)) {
301
+ if (key in base) {
302
+ throw new Error(`Column "${key}" already exists in base definition`);
303
+ }
304
+ }
305
+ return { ...base, ...extra };
306
+ }
307
+ export function versionedTable(baseName, version, columns, options = {}) {
308
+ const actualName = `${baseName}_v${version}`;
309
+ const baseTable = chTable(actualName, columns, options);
310
+ const aliasName = options.aliasName || baseName;
311
+ const latestAlias = options.latestAlias ?? true;
312
+ const viewSQL = latestAlias
313
+ ? `CREATE VIEW IF NOT EXISTS \`${aliasName}\` AS SELECT * FROM \`${actualName}\``
314
+ : null;
315
+ return {
316
+ ...baseTable,
317
+ $versionMeta: { baseName, version, aliasName: latestAlias ? aliasName : undefined },
318
+ toSQLs: () => viewSQL ? [baseTable.toSQL(), viewSQL] : [baseTable.toSQL()]
319
+ };
320
+ }
321
+ export function deriveTable(source, config) {
322
+ const groupList = Array.isArray(config.groupBy) ? config.groupBy : [config.groupBy];
323
+ const groupCols = groupList.map(g => g.trim()).filter(Boolean);
324
+ const targetColumns = {};
325
+ for (const g of groupCols) {
326
+ const col = source.$columns[g];
327
+ targetColumns[g] = col ? col : new ClickHouseColumn(g, 'String');
328
+ }
329
+ for (const [alias] of Object.entries(config.aggregates)) {
330
+ targetColumns[alias] = new ClickHouseColumn(alias, 'Float64');
331
+ }
332
+ const opts = {
333
+ engine: Engine.AggregatingMergeTree(),
334
+ orderBy: groupCols,
335
+ partitionBy: config.options?.partitionBy,
336
+ primaryKey: config.options?.primaryKey,
337
+ ttl: config.options?.ttl,
338
+ onCluster: config.options?.onCluster,
339
+ shardKey: config.options?.shardKey,
340
+ logicalPrimaryKey: config.options?.logicalPrimaryKey,
341
+ appendOnly: config.options?.appendOnly ?? true
342
+ };
343
+ const derived = chTable(config.name, targetColumns, opts);
344
+ const groupBySQL = groupCols.join(', ');
345
+ const aggSQL = Object.entries(config.aggregates)
346
+ .map(([alias, expr]) => `${expr} AS \`${alias}\``)
347
+ .join(', ');
348
+ const selectParts = [];
349
+ if (groupCols.length > 0)
350
+ selectParts.push(groupBySQL);
351
+ if (aggSQL)
352
+ selectParts.push(aggSQL);
353
+ const selectSQL = selectParts.join(', ');
354
+ const mvSQL = `CREATE MATERIALIZED VIEW IF NOT EXISTS \`${config.name}_mv\` TO \`${config.name}\` AS SELECT ${selectSQL} FROM \`${source.$table}\` GROUP BY ${groupBySQL}`;
355
+ return {
356
+ ...derived,
357
+ toSQLs: () => [derived.toSQL(), mvSQL]
358
+ };
359
+ }
360
+ export function renderSchema(def) {
361
+ if (typeof def.toSQLs === 'function') {
362
+ return def.toSQLs().join('\n\n');
363
+ }
364
+ return def.toSQL();
365
+ }
366
+ export function generateTypes(def, typeName) {
367
+ const name = typeName || pascalCase(def.$table);
368
+ const lines = Object.entries(def.$columns).map(([key, col]) => {
369
+ const tsType = clickHouseToTS(col.type);
370
+ const optional = col.isNull ? '?' : '';
371
+ return ` ${key}${optional}: ${tsType};`;
372
+ });
373
+ return `export interface ${name} {\n${lines.join('\n')}\n}`;
374
+ }
375
+ function clickHouseToTS(type) {
376
+ const normalized = type.replace(/\s+/g, '').toLowerCase();
377
+ const arrayMatch = normalized.match(/^array\((.+)\)$/);
378
+ if (arrayMatch) {
379
+ return `${clickHouseToTS(arrayMatch[1])}[]`;
380
+ }
381
+ if (normalized.startsWith('map('))
382
+ return 'Record<string, any>';
383
+ if (normalized.includes('uuid'))
384
+ return 'string';
385
+ if (normalized.startsWith('decimal')) {
386
+ return 'string';
387
+ }
388
+ if (normalized.startsWith('int') || normalized.startsWith('uint') || normalized.startsWith('float'))
389
+ return 'number';
390
+ if (normalized.startsWith('bool'))
391
+ return 'boolean';
392
+ if (normalized.startsWith('date') || normalized.startsWith('datetime'))
393
+ return 'string';
394
+ if (normalized.startsWith('enum'))
395
+ return 'string';
396
+ if (normalized.startsWith('aggregatefunction'))
397
+ return 'any';
398
+ if (normalized.startsWith('lowcardinality(')) {
399
+ const inner = normalized.slice('lowcardinality('.length, -1);
400
+ return clickHouseToTS(inner);
401
+ }
402
+ return 'string';
403
+ }
404
+ function pascalCase(name) {
405
+ return name.replace(/(^|_|-)(\w)/g, (_, _sep, c) => c.toUpperCase());
406
+ }