@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
package/dist/logger.js ADDED
@@ -0,0 +1,36 @@
1
+ export function wrapClientWithLogger(client, logger) {
2
+ if (!logger)
3
+ return client;
4
+ const extractStats = (summary) => {
5
+ if (!summary)
6
+ return undefined;
7
+ const readRows = summary.read_rows ?? summary.rows_read ?? summary.rows ?? summary.result_rows ?? 0;
8
+ const readBytes = summary.read_bytes ?? summary.bytes_read ?? summary.bytes ?? 0;
9
+ return { readRows, readBytes };
10
+ };
11
+ const withTiming = async (sql, params, fn) => {
12
+ const start = Date.now();
13
+ try {
14
+ const res = await fn();
15
+ const duration = Date.now() - start;
16
+ logger.logQuery(sql, params, duration, extractStats(res?.summary));
17
+ return res;
18
+ }
19
+ catch (err) {
20
+ logger.logError(err, sql);
21
+ throw err;
22
+ }
23
+ };
24
+ const wrapped = {
25
+ ...client,
26
+ query: async (params) => {
27
+ const sql = typeof params === 'string' ? params : params?.query || '';
28
+ return withTiming(sql, params?.query_params, () => client.query(params));
29
+ },
30
+ insert: async (params) => {
31
+ const sql = `INSERT INTO ${params?.table ?? ''}`;
32
+ return withTiming(sql, params?.values, () => client.insert(params));
33
+ }
34
+ };
35
+ return wrapped;
36
+ }
@@ -1,92 +1,23 @@
1
- /**
2
- * HouseKit Materialized Views DSL - Type-Safe Materialized Views
3
- *
4
- * Unlike generic ORMs which often treat materialized views as static SQL strings,
5
- * HouseKit allows defining MV queries using the Query Builder for
6
- * compile-time type safety. If you rename a column in the source table,
7
- * TypeScript will catch the error before deployment.
8
- */
9
1
  import { ClickHouseColumn } from './column';
10
2
  import { type TableDefinition, type TableColumns } from './table';
11
3
  import { EngineConfiguration } from './engines';
12
4
  import { type SQLExpression } from './expressions';
13
- /**
14
- * Helper type to extract columns from a TableDefinition
15
- */
16
5
  export type InferTableColumns<T> = T extends TableDefinition<infer TCols> ? TCols : TableColumns;
17
- /**
18
- * Helper type to ensure we have access to $columns
19
- */
20
6
  type TableWithColumns<TCols extends TableColumns> = {
21
7
  $table: string;
22
8
  $columns: TCols;
23
9
  };
24
- /**
25
- * Configuration for type-safe materialized views
26
- */
27
10
  export interface MaterializedViewConfig<TSource extends TableDefinition<any>, TTargetCols extends TableColumns = TableColumns> {
28
- /**
29
- * Source table(s) for the materialized view.
30
- * This is used to provide type information for the query builder.
31
- */
32
11
  source: TSource;
33
- /**
34
- * The query definition for the materialized view.
35
- * Use the query builder to ensure type safety.
36
- *
37
- * @example
38
- * ```typescript
39
- * query: (qb) => qb
40
- * .from(events)
41
- * .select({
42
- * eventType: events.event_type,
43
- * count: sql`count()`
44
- * })
45
- * .groupBy(events.event_type)
46
- * ```
47
- */
48
12
  query: (qb: MaterializedViewQueryBuilder<TSource>) => MaterializedViewQueryBuilder<any>;
49
- /**
50
- * Target table to write materialized data to.
51
- * If not specified, ClickHouse creates an internal table.
52
- *
53
- * Can be:
54
- * - A string table name
55
- * - A TableDefinition reference (type-safe)
56
- */
57
13
  toTable?: string | TableDefinition<TTargetCols>;
58
- /**
59
- * Cluster name for distributed materialized view
60
- */
61
14
  onCluster?: string;
62
- /**
63
- * Whether to populate the MV with existing data on creation.
64
- * Warning: Can be slow and resource-intensive for large tables.
65
- */
66
15
  populate?: boolean;
67
- /**
68
- * Engine configuration for the internal table (when toTable is not specified).
69
- * Uses the type-safe Engine DSL.
70
- */
71
16
  engine?: EngineConfiguration;
72
- /**
73
- * Whether to use OR REPLACE semantics
74
- */
75
17
  orReplace?: boolean;
76
- /**
77
- * Optional ORDER BY for the internal storage table
78
- */
79
18
  orderBy?: string | string[];
80
- /**
81
- * Optional partition by for the internal storage table
82
- */
83
19
  partitionBy?: string | string[];
84
20
  }
85
- /**
86
- * Simplified query builder for materialized view definitions.
87
- * This is a subset of the full ClickHouseQueryBuilder that generates
88
- * SQL without executing queries.
89
- */
90
21
  export interface MaterializedViewQueryBuilder<TTable extends TableDefinition<any>> {
91
22
  select<TSelection extends Record<string, ClickHouseColumn | SQLExpression>>(fields: TSelection): MaterializedViewQueryBuilder<TTable>;
92
23
  from<TNewTable extends TableDefinition<any>>(table: TNewTable): MaterializedViewQueryBuilder<TNewTable>;
@@ -102,10 +33,6 @@ export interface MaterializedViewQueryBuilder<TTable extends TableDefinition<any
102
33
  params: Record<string, unknown>;
103
34
  };
104
35
  }
105
- /**
106
- * Extended type for materialized views - contains table-like properties
107
- * plus MV-specific metadata for drift detection and query tracking.
108
- */
109
36
  export type MaterializedViewDefinition<TCols extends TableColumns, TSource extends TableDefinition<any>, TOptions = MaterializedViewConfig<TSource>> = {
110
37
  $table: string;
111
38
  $columns: TCols;
@@ -114,114 +41,27 @@ export type MaterializedViewDefinition<TCols extends TableColumns, TSource exten
114
41
  $source: TSource;
115
42
  $querySQL: string;
116
43
  $config: TOptions;
117
- /**
118
- * Get the SQL to create the materialized view
119
- */
120
44
  toSQL(): string;
121
- /**
122
- * Get all SQL statements (for views that need multiple statements)
123
- */
124
45
  toSQLs(): string[];
125
- /**
126
- * Create an aliased version for use in queries
127
- */
128
46
  as(alias: string): MaterializedViewDefinition<TCols, TSource, TOptions>;
129
47
  } & TCols;
130
- /**
131
- * Result type for projection definitions using query builder
132
- */
133
48
  export interface TypedProjectionDefinition<TSource extends TableDefinition<any>> {
134
49
  name: string;
135
50
  query: string;
136
51
  sourceTable: TSource;
137
52
  orderBy?: string[];
138
53
  }
139
- /**
140
- * Create a type-safe materialized view definition.
141
- * The query is defined using a query builder that validates column references
142
- * at compile time.
143
- *
144
- * @example
145
- * ```typescript
146
- * import { defineMaterializedView, text, uint64, sql, Engine } from '@housekit/orm';
147
- *
148
- * // Source table
149
- * const events = defineTable('events', {
150
- * event_type: t.text('event_type'),
151
- * user_id: t.text('user_id'),
152
- * revenue: t.uint64('revenue'),
153
- * }, { engine: Engine.MergeTree(), orderBy: 'event_type' });
154
- *
155
- * // Type-safe materialized view
156
- * export const revenueByEvent = defineMaterializedView('revenue_by_event_mv', {
157
- * event_type: t.text('event_type'),
158
- * total_revenue: t.uint64('total_revenue'),
159
- * event_count: t.uint64('event_count'),
160
- * }, {
161
- * source: events,
162
- * query: (qb) => qb
163
- * .from(events) // Type-safe: only accepts events columns
164
- * .select({
165
- * event_type: events.event_type,
166
- * total_revenue: sql`sum(${events.revenue})`,
167
- * event_count: sql`count()`,
168
- * })
169
- * .groupBy(events.event_type),
170
- * engine: Engine.SummingMergeTree(['total_revenue', 'event_count']),
171
- * orderBy: 'event_type',
172
- * });
173
- * ```
174
- */
175
54
  export declare function chMaterializedView<TCols extends TableColumns, TSource extends TableDefinition<any>, TTarget extends TableColumns = TCols>(name: string, columns: TCols, config: MaterializedViewConfig<TSource, TTarget>): MaterializedViewDefinition<TCols, TSource, MaterializedViewConfig<TSource, TTarget>>;
176
- /**
177
- * Create a type-safe projection definition.
178
- *
179
- * Projections are precomputed aggregations stored alongside the main table.
180
- * They're automatically used by ClickHouse when the query matches.
181
- *
182
- * @example
183
- * ```typescript
184
- * const events = defineTable('events', {
185
- * event_type: t.text('event_type'),
186
- * user_id: t.text('user_id'),
187
- * created_at: t.timestamp('created_at'),
188
- * }, {
189
- * engine: Engine.MergeTree(),
190
- * orderBy: ['event_type', 'created_at'],
191
- * projections: [
192
- * // Type-safe projection
193
- * chProjection('events_by_user', events, (cols) => ({
194
- * select: {
195
- * user_id: cols.user_id,
196
- * event_type: cols.event_type,
197
- * event_count: sql`count()`,
198
- * },
199
- * groupBy: [cols.user_id, cols.event_type],
200
- * orderBy: ['user_id', 'event_type'],
201
- * })),
202
- * ],
203
- * });
204
- * ```
205
- */
206
55
  export declare function chProjection<TCols extends TableColumns, TSource extends TableDefinition<TCols>>(name: string, sourceTable: TSource & TableWithColumns<TCols>, definition: (cols: TCols) => {
207
56
  select: Record<string, ClickHouseColumn | SQLExpression>;
208
57
  groupBy?: (ClickHouseColumn | SQLExpression)[];
209
58
  orderBy?: string[];
210
59
  }): TypedProjectionDefinition<TSource>;
211
- /**
212
- * Convert a TypedProjectionDefinition to the format expected by defineTable
213
- */
214
60
  export declare function toProjectionDefinition(projection: TypedProjectionDefinition<any>): {
215
61
  name: string;
216
62
  query: string;
217
63
  };
218
- /**
219
- * Normalize SQL for comparison (removes extra whitespace, normalizes case)
220
- */
221
64
  export declare function normalizeSQL(sql: string): string;
222
- /**
223
- * Compare two materialized view definitions for drift
224
- */
225
65
  export declare function detectMaterializedViewDrift(local: MaterializedViewDefinition<any, any>, remoteSQL: string): {
226
66
  hasDrift: boolean;
227
67
  localSQL: string;
@@ -229,38 +69,10 @@ export declare function detectMaterializedViewDrift(local: MaterializedViewDefin
229
69
  normalizedLocal: string;
230
70
  normalizedRemote: string;
231
71
  };
232
- /**
233
- * Extract the AS query from a CREATE MATERIALIZED VIEW statement
234
- */
235
72
  export declare function extractMVQuery(createStatement: string): string | null;
236
- /**
237
- * Generate a Blue-Green migration plan for a Materialized View update.
238
- *
239
- * Instead of just dropping and recreating, we:
240
- * 1. Create a "next" version of the target table (if using TO table)
241
- * 2. Create a "next" version of the MV
242
- * 3. Backfill data from source table using the NEW query
243
- * 4. Swap them using RENAME
244
- */
245
73
  export declare function generateBlueGreenMigration(oldMV: MaterializedViewDefinition<any, any>, newMV: MaterializedViewDefinition<any, any>, options?: {
246
74
  backfill?: boolean;
247
75
  }): string[];
248
- /**
249
- * Creates a "migration bridge" Materialized View that synchronizes data from an old table to a new one.
250
- * This is extremely useful for zero-downtime migrations where you need to pipe real-time
251
- * inserts from the v1 table into the v2 table while you perform a separate historical backfill.
252
- *
253
- * @example
254
- * ```typescript
255
- * const bridge = createMigrationBridge({
256
- * from: tableV1,
257
- * to: tableV2,
258
- * mapping: {
259
- * new_col: sql`upper(${tableV1.old_col})`
260
- * }
261
- * });
262
- * ```
263
- */
264
76
  export declare function createMigrationBridge<TSourceCols extends TableColumns, TTargetCols extends TableColumns>(options: {
265
77
  from: TableDefinition<TSourceCols>;
266
78
  to: TableDefinition<TTargetCols>;
@@ -0,0 +1,380 @@
1
+ import { ClickHouseColumn } from './column';
2
+ import { renderEngineSQL } from './engines';
3
+ class VirtualQueryBuilder {
4
+ _select = null;
5
+ _table = null;
6
+ _where = null;
7
+ _groupBy = [];
8
+ _having = null;
9
+ _orderBy = [];
10
+ _limit = null;
11
+ _joins = [];
12
+ constructor(sourceTable) {
13
+ if (sourceTable) {
14
+ this._table = sourceTable;
15
+ }
16
+ }
17
+ select(fields) {
18
+ this._select = fields;
19
+ return this;
20
+ }
21
+ from(table) {
22
+ this._table = table;
23
+ return this;
24
+ }
25
+ where(expression) {
26
+ this._where = expression;
27
+ return this;
28
+ }
29
+ groupBy(...cols) {
30
+ this._groupBy = cols;
31
+ return this;
32
+ }
33
+ having(expression) {
34
+ this._having = expression;
35
+ return this;
36
+ }
37
+ orderBy(col, dir = 'ASC') {
38
+ this._orderBy.push({ col, dir });
39
+ return this;
40
+ }
41
+ limit(val) {
42
+ this._limit = val;
43
+ return this;
44
+ }
45
+ innerJoin(table, on) {
46
+ this._joins.push({ type: 'INNER', table: table.$table, on });
47
+ return this;
48
+ }
49
+ leftJoin(table, on) {
50
+ this._joins.push({ type: 'LEFT', table: table.$table, on });
51
+ return this;
52
+ }
53
+ toSQL() {
54
+ const parts = [];
55
+ const params = {};
56
+ if (this._select) {
57
+ const selectParts = Object.entries(this._select).map(([alias, col]) => {
58
+ if (col instanceof ClickHouseColumn) {
59
+ const tableName = col.tableName ? `\`${col.tableName}\`.` : '';
60
+ return `${tableName}\`${col.name}\` AS \`${alias}\``;
61
+ }
62
+ else {
63
+ const { sql, params: exprParams } = this.resolveSQLExpression(col);
64
+ Object.assign(params, exprParams);
65
+ return `${sql} AS \`${alias}\``;
66
+ }
67
+ });
68
+ parts.push(`SELECT ${selectParts.join(', ')}`);
69
+ }
70
+ else {
71
+ parts.push('SELECT *');
72
+ }
73
+ if (this._table) {
74
+ parts.push(`FROM \`${this._table.$table}\``);
75
+ }
76
+ for (const join of this._joins) {
77
+ const { sql: onSQL } = this.resolveSQLExpression(join.on);
78
+ parts.push(`${join.type} JOIN \`${join.table}\` ON ${onSQL}`);
79
+ }
80
+ if (this._where) {
81
+ const { sql: whereSQL, params: whereParams } = this.resolveSQLExpression(this._where);
82
+ Object.assign(params, whereParams);
83
+ parts.push(`WHERE ${whereSQL}`);
84
+ }
85
+ if (this._groupBy.length > 0) {
86
+ const groupParts = this._groupBy.map(col => {
87
+ if (col instanceof ClickHouseColumn) {
88
+ const tableName = col.tableName ? `\`${col.tableName}\`.` : '';
89
+ return `${tableName}\`${col.name}\``;
90
+ }
91
+ else {
92
+ const { sql } = this.resolveSQLExpression(col);
93
+ return sql;
94
+ }
95
+ });
96
+ parts.push(`GROUP BY ${groupParts.join(', ')}`);
97
+ }
98
+ if (this._having) {
99
+ const { sql: havingSQL, params: havingParams } = this.resolveSQLExpression(this._having);
100
+ Object.assign(params, havingParams);
101
+ parts.push(`HAVING ${havingSQL}`);
102
+ }
103
+ if (this._orderBy.length > 0) {
104
+ const orderParts = this._orderBy.map(({ col, dir }) => {
105
+ if (col instanceof ClickHouseColumn) {
106
+ const tableName = col.tableName ? `\`${col.tableName}\`.` : '';
107
+ return `${tableName}\`${col.name}\` ${dir}`;
108
+ }
109
+ else {
110
+ const { sql } = this.resolveSQLExpression(col);
111
+ return `${sql} ${dir}`;
112
+ }
113
+ });
114
+ parts.push(`ORDER BY ${orderParts.join(', ')}`);
115
+ }
116
+ if (this._limit !== null) {
117
+ parts.push(`LIMIT ${this._limit}`);
118
+ }
119
+ return { query: parts.join(' '), params };
120
+ }
121
+ resolveSQLExpression(expr) {
122
+ const params = {};
123
+ if (typeof expr.toSQL === 'function') {
124
+ const result = expr.toSQL();
125
+ return { sql: result.sql || String(result), params: result.params || {} };
126
+ }
127
+ if (typeof expr.sql === 'string') {
128
+ return { sql: expr.sql, params: expr.params || {} };
129
+ }
130
+ if (Array.isArray(expr.strings)) {
131
+ const exprAny = expr;
132
+ let sql = '';
133
+ const strings = exprAny.strings;
134
+ const values = exprAny.values;
135
+ for (let i = 0; i < strings.length; i++) {
136
+ sql += strings[i];
137
+ if (i < values.length) {
138
+ const value = values[i];
139
+ if (value instanceof ClickHouseColumn) {
140
+ const tableName = value.tableName ? `\`${value.tableName}\`.` : '';
141
+ sql += `${tableName}\`${value.name}\``;
142
+ }
143
+ else if (typeof value === 'object' && value !== null && 'sql' in value) {
144
+ const nested = this.resolveSQLExpression(value);
145
+ sql += nested.sql;
146
+ Object.assign(params, nested.params);
147
+ }
148
+ else {
149
+ const paramName = `p${Object.keys(params).length}`;
150
+ params[paramName] = value;
151
+ sql += `{${paramName}: String}`;
152
+ }
153
+ }
154
+ }
155
+ return { sql, params };
156
+ }
157
+ return { sql: String(expr), params };
158
+ }
159
+ }
160
+ export function chMaterializedView(name, columns, config) {
161
+ const virtualBuilder = new VirtualQueryBuilder(config.source);
162
+ const finalBuilder = config.query(virtualBuilder);
163
+ const { query: querySQL, params } = finalBuilder.toSQL();
164
+ for (const col of Object.values(columns)) {
165
+ col.tableName = name;
166
+ }
167
+ const toSQL = () => {
168
+ const parts = [];
169
+ if (config.orReplace) {
170
+ parts.push('CREATE OR REPLACE MATERIALIZED VIEW');
171
+ }
172
+ else {
173
+ parts.push('CREATE MATERIALIZED VIEW IF NOT EXISTS');
174
+ }
175
+ parts.push(`\`${name}\``);
176
+ if (config.onCluster) {
177
+ parts.push(`ON CLUSTER ${config.onCluster}`);
178
+ }
179
+ if (config.toTable) {
180
+ const tableName = typeof config.toTable === 'string'
181
+ ? config.toTable
182
+ : config.toTable.$table;
183
+ parts.push(`TO \`${tableName}\``);
184
+ }
185
+ else if (config.engine) {
186
+ const engineSQL = renderEngineSQL(config.engine);
187
+ parts.push(`ENGINE = ${engineSQL}`);
188
+ if (config.orderBy) {
189
+ const orderBy = Array.isArray(config.orderBy)
190
+ ? config.orderBy.join(', ')
191
+ : config.orderBy;
192
+ parts.push(`ORDER BY (${orderBy})`);
193
+ }
194
+ if (config.partitionBy) {
195
+ const partitionBy = Array.isArray(config.partitionBy)
196
+ ? config.partitionBy.join(', ')
197
+ : config.partitionBy;
198
+ parts.push(`PARTITION BY (${partitionBy})`);
199
+ }
200
+ }
201
+ if (config.populate) {
202
+ parts.push('POPULATE');
203
+ }
204
+ parts.push('AS');
205
+ parts.push(querySQL);
206
+ return parts.join(' ');
207
+ };
208
+ const definition = {
209
+ $table: name,
210
+ $columns: columns,
211
+ $options: config,
212
+ $kind: 'materializedView',
213
+ $source: config.source,
214
+ $querySQL: querySQL,
215
+ $config: config,
216
+ toSQL,
217
+ toSQLs: () => [toSQL()],
218
+ as: (alias) => {
219
+ const aliased = {
220
+ $table: alias,
221
+ $columns: { ...columns },
222
+ $options: { ...config, externallyManaged: true },
223
+ $kind: 'materializedView',
224
+ toSQL: () => ''
225
+ };
226
+ for (const [key, col] of Object.entries(columns)) {
227
+ const column = col;
228
+ const cloned = Object.create(Object.getPrototypeOf(column));
229
+ Object.assign(cloned, column);
230
+ cloned.tableName = alias;
231
+ aliased[key] = cloned;
232
+ aliased.$columns[key] = cloned;
233
+ }
234
+ return aliased;
235
+ }
236
+ };
237
+ for (const [key, col] of Object.entries(columns)) {
238
+ definition[key] = col;
239
+ }
240
+ return definition;
241
+ }
242
+ export function chProjection(name, sourceTable, definition) {
243
+ const cols = sourceTable.$columns;
244
+ const def = definition(cols);
245
+ const selectParts = Object.entries(def.select).map(([alias, col]) => {
246
+ if (col instanceof ClickHouseColumn) {
247
+ return `\`${col.name}\` AS \`${alias}\``;
248
+ }
249
+ else {
250
+ const sqlExpr = col;
251
+ if (typeof sqlExpr.toSQL === 'function') {
252
+ const { sql: sqlStr } = sqlExpr.toSQL({ ignoreTablePrefix: true });
253
+ return `${sqlStr} AS \`${alias}\``;
254
+ }
255
+ return `${String(col)} AS \`${alias}\``;
256
+ }
257
+ });
258
+ let query = `SELECT ${selectParts.join(', ')}`;
259
+ if (def.groupBy && def.groupBy.length > 0) {
260
+ const groupParts = def.groupBy.map(col => {
261
+ if (col instanceof ClickHouseColumn) {
262
+ return `\`${col.name}\``;
263
+ }
264
+ const sqlExpr = col;
265
+ if (typeof sqlExpr.toSQL === 'function') {
266
+ const { sql: sqlStr } = sqlExpr.toSQL({ ignoreTablePrefix: true });
267
+ return sqlStr;
268
+ }
269
+ return String(col);
270
+ });
271
+ query += ` GROUP BY ${groupParts.join(', ')}`;
272
+ }
273
+ if (def.orderBy && def.orderBy.length > 0) {
274
+ query += ` ORDER BY ${def.orderBy.join(', ')}`;
275
+ }
276
+ return {
277
+ name,
278
+ query,
279
+ sourceTable,
280
+ orderBy: def.orderBy
281
+ };
282
+ }
283
+ export function toProjectionDefinition(projection) {
284
+ return {
285
+ name: projection.name,
286
+ query: projection.query
287
+ };
288
+ }
289
+ export function normalizeSQL(sql) {
290
+ return sql
291
+ .replace(/\s+/g, ' ')
292
+ .replace(/\(\s+/g, '(')
293
+ .replace(/\s+\)/g, ')')
294
+ .replace(/,\s+/g, ', ')
295
+ .trim()
296
+ .toUpperCase();
297
+ }
298
+ function hashString(str) {
299
+ let hash = 0;
300
+ for (let i = 0; i < str.length; i++) {
301
+ const char = str.charCodeAt(i);
302
+ hash = ((hash << 5) - hash) + char;
303
+ hash = hash & hash;
304
+ }
305
+ return Math.abs(hash).toString(16);
306
+ }
307
+ export function detectMaterializedViewDrift(local, remoteSQL) {
308
+ const localSQL = local.toSQL();
309
+ const normalizedLocal = normalizeSQL(localSQL);
310
+ const normalizedRemote = normalizeSQL(remoteSQL);
311
+ return {
312
+ hasDrift: normalizedLocal !== normalizedRemote,
313
+ localSQL,
314
+ remoteSQL,
315
+ normalizedLocal,
316
+ normalizedRemote
317
+ };
318
+ }
319
+ export function extractMVQuery(createStatement) {
320
+ const match = createStatement.match(/\bAS\s+(.+)$/is);
321
+ return match ? match[1].trim() : null;
322
+ }
323
+ export function generateBlueGreenMigration(oldMV, newMV, options = {}) {
324
+ const name = newMV.$table;
325
+ const nextName = `${name}_next`;
326
+ const targetTable = newMV.$options.toTable;
327
+ const sqls = [];
328
+ if (targetTable) {
329
+ const targetTableName = typeof targetTable === 'string' ? targetTable : targetTable.$table;
330
+ const nextTargetName = `${targetTableName}_next`;
331
+ if (typeof targetTable !== 'string') {
332
+ const nextTableSQL = targetTable.toSQL().replace(new RegExp(`\`${targetTableName}\``, 'g'), `\`${nextTargetName}\``);
333
+ sqls.push(nextTableSQL);
334
+ }
335
+ else {
336
+ sqls.push(`CREATE TABLE \`${nextTargetName}\` AS \`${targetTableName}\``);
337
+ }
338
+ const nextMVSQL = newMV.toSQL()
339
+ .replace(new RegExp(`\`${name}\``, 'g'), `\`${nextName}\``)
340
+ .replace(new RegExp(`TO \`${targetTableName}\``, 'g'), `TO \`${nextTargetName}\``);
341
+ sqls.push(nextMVSQL);
342
+ if (options.backfill) {
343
+ const querySQL = newMV.$querySQL;
344
+ sqls.push(`INSERT INTO \`${nextTargetName}\` ${querySQL}`);
345
+ }
346
+ sqls.push(`EXCHANGE TABLES \`${name}\` AND \`${nextName}\``);
347
+ sqls.push(`EXCHANGE TABLES \`${targetTableName}\` AND \`${nextTargetName}\``);
348
+ sqls.push(`DROP VIEW IF EXISTS \`${nextName}\``);
349
+ sqls.push(`DROP TABLE IF EXISTS \`${nextTargetName}\``);
350
+ }
351
+ else {
352
+ sqls.push(`DROP VIEW IF EXISTS \`${name}\``);
353
+ sqls.push(newMV.toSQL());
354
+ }
355
+ return sqls;
356
+ }
357
+ export function createMigrationBridge(options) {
358
+ const fromName = options.from.$table;
359
+ const toName = options.to.$table;
360
+ const bridgeName = options.name || `${fromName}_to_${toName}_bridge_mv`;
361
+ const selection = {};
362
+ if (options.mapping) {
363
+ Object.assign(selection, options.mapping);
364
+ }
365
+ else {
366
+ const fromCols = options.from.$columns;
367
+ const toCols = options.to.$columns;
368
+ for (const key of Object.keys(fromCols)) {
369
+ if (toCols[key]) {
370
+ selection[key] = fromCols[key];
371
+ }
372
+ }
373
+ }
374
+ return chMaterializedView(bridgeName, options.to.$columns, {
375
+ source: options.from,
376
+ toTable: options.to,
377
+ onCluster: options.onCluster,
378
+ query: (qb) => qb.from(options.from).select(selection)
379
+ });
380
+ }