@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.
- package/README.md +34 -0
- package/dist/builders/delete.js +112 -0
- package/dist/builders/insert.d.ts +0 -91
- package/dist/builders/insert.js +393 -0
- package/dist/builders/prepared.d.ts +1 -2
- package/dist/builders/prepared.js +30 -0
- package/dist/builders/select.d.ts +0 -161
- package/dist/builders/select.js +562 -0
- package/dist/builders/select.types.js +1 -0
- package/dist/builders/update.js +136 -0
- package/dist/client.d.ts +0 -6
- package/dist/client.js +140 -0
- package/dist/codegen/zod.js +107 -0
- package/dist/column.d.ts +1 -25
- package/dist/column.js +133 -0
- package/dist/compiler.d.ts +0 -7
- package/dist/compiler.js +513 -0
- package/dist/core.js +6 -0
- package/dist/data-types.d.ts +0 -61
- package/dist/data-types.js +127 -0
- package/dist/dictionary.d.ts +0 -149
- package/dist/dictionary.js +158 -0
- package/dist/engines.d.ts +0 -385
- package/dist/engines.js +292 -0
- package/dist/expressions.d.ts +0 -10
- package/dist/expressions.js +268 -0
- package/dist/external.d.ts +0 -112
- package/dist/external.js +224 -0
- package/dist/index.d.ts +0 -51
- package/dist/index.js +139 -6853
- package/dist/logger.js +36 -0
- package/dist/materialized-views.d.ts +0 -188
- package/dist/materialized-views.js +380 -0
- package/dist/metadata.js +59 -0
- package/dist/modules/aggregates.d.ts +0 -164
- package/dist/modules/aggregates.js +121 -0
- package/dist/modules/array.d.ts +0 -98
- package/dist/modules/array.js +71 -0
- package/dist/modules/conditional.d.ts +0 -84
- package/dist/modules/conditional.js +138 -0
- package/dist/modules/conversion.d.ts +0 -147
- package/dist/modules/conversion.js +109 -0
- package/dist/modules/geo.d.ts +0 -164
- package/dist/modules/geo.js +112 -0
- package/dist/modules/hash.js +4 -0
- package/dist/modules/index.js +12 -0
- package/dist/modules/json.d.ts +0 -106
- package/dist/modules/json.js +76 -0
- package/dist/modules/math.d.ts +0 -16
- package/dist/modules/math.js +16 -0
- package/dist/modules/string.d.ts +0 -136
- package/dist/modules/string.js +89 -0
- package/dist/modules/time.d.ts +0 -123
- package/dist/modules/time.js +91 -0
- package/dist/modules/types.d.ts +0 -133
- package/dist/modules/types.js +114 -0
- package/dist/modules/window.js +140 -0
- package/dist/relational.d.ts +0 -82
- package/dist/relational.js +290 -0
- package/dist/relations.js +21 -0
- package/dist/schema-builder.d.ts +0 -90
- package/dist/schema-builder.js +140 -0
- package/dist/table.d.ts +0 -42
- package/dist/table.js +406 -0
- package/dist/utils/background-batcher.js +75 -0
- package/dist/utils/batch-transform.js +51 -0
- package/dist/utils/binary-reader.d.ts +0 -6
- package/dist/utils/binary-reader.js +334 -0
- package/dist/utils/binary-serializer.d.ts +0 -125
- package/dist/utils/binary-serializer.js +637 -0
- package/dist/utils/binary-worker-code.js +1 -0
- package/dist/utils/binary-worker-pool.d.ts +0 -34
- package/dist/utils/binary-worker-pool.js +206 -0
- package/dist/utils/binary-worker.d.ts +0 -11
- package/dist/utils/binary-worker.js +63 -0
- package/dist/utils/insert-processing.d.ts +0 -2
- package/dist/utils/insert-processing.js +163 -0
- package/dist/utils/lru-cache.js +30 -0
- 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
|
+
}
|