@housekit/orm 0.1.47 → 0.1.49
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 +120 -5
- 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
package/dist/relational.d.ts
CHANGED
|
@@ -2,10 +2,6 @@ import * as ops from './expressions';
|
|
|
2
2
|
import * as cond from './modules/conditional';
|
|
3
3
|
import { ClickHouseColumn, type RelationDefinition, type TableDefinition, type CleanSelect } from './core';
|
|
4
4
|
import type { SQLExpression } from './expressions';
|
|
5
|
-
/**
|
|
6
|
-
* Internal map of SQL operators provided to relational query callbacks.
|
|
7
|
-
* These match the standard HouseKit operators but are bundled for ergonomic access.
|
|
8
|
-
*/
|
|
9
5
|
declare const operators: {
|
|
10
6
|
eq: typeof ops.eq;
|
|
11
7
|
ne: typeof ops.ne;
|
|
@@ -37,16 +33,7 @@ type RelationsOf<TTable> = TTable extends {
|
|
|
37
33
|
} ? R : {};
|
|
38
34
|
type NormalizedRelations<TTable> = RelationsOf<TTable> extends Record<string, RelationDefinition<any>> ? RelationsOf<TTable> : {};
|
|
39
35
|
type RelationTarget<T> = T extends RelationDefinition<infer TTarget> ? TTarget : never;
|
|
40
|
-
/**
|
|
41
|
-
* Join strategy for relational queries.
|
|
42
|
-
*
|
|
43
|
-
* ClickHouse performance varies significantly depending on the join strategy used,
|
|
44
|
-
* especially in distributed environments.
|
|
45
|
-
*/
|
|
46
36
|
export type JoinStrategy = 'auto' | 'standard' | 'global' | 'any' | 'global_any';
|
|
47
|
-
/**
|
|
48
|
-
* Configuration options for the Relational Query API (`findMany`, `findFirst`).
|
|
49
|
-
*/
|
|
50
37
|
export type RelationalWith<TTable> = [
|
|
51
38
|
keyof NormalizedRelations<TTable>
|
|
52
39
|
] extends [never] ? Record<string, boolean | RelationalFindOptions<any>> : {
|
|
@@ -68,66 +55,17 @@ type WhereObject<TTable> = TTable extends TableDefinition<infer TCols> ? {
|
|
|
68
55
|
[K in keyof TCols]?: TCols[K] extends ClickHouseColumn<infer T> ? T | SQLExpression : any;
|
|
69
56
|
} : Record<string, any>;
|
|
70
57
|
export type RelationalFindOptions<TTable = any> = {
|
|
71
|
-
/**
|
|
72
|
-
* Filter rows.
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* // Object syntax (simplest)
|
|
76
|
-
* where: { email: 'a@b.com' }
|
|
77
|
-
* where: { role: 'admin', active: true }
|
|
78
|
-
*
|
|
79
|
-
* // Direct expression
|
|
80
|
-
* where: eq(users.active, true)
|
|
81
|
-
*
|
|
82
|
-
* // Callback for complex filters
|
|
83
|
-
* where: (cols, { eq, and, gt }) => and(eq(cols.role, 'admin'), gt(cols.age, 18))
|
|
84
|
-
*/
|
|
85
58
|
where?: WhereObject<TTable> | SQLExpression | ((columns: TTable extends TableDefinition<infer TCols> ? TCols : any, ops: typeof operators) => SQLExpression);
|
|
86
|
-
/** Max rows to return */
|
|
87
59
|
limit?: number;
|
|
88
|
-
/**
|
|
89
|
-
* Select specific columns. By default all columns are returned.
|
|
90
|
-
*
|
|
91
|
-
* @example
|
|
92
|
-
* columns: { id: true, email: true }
|
|
93
|
-
*/
|
|
94
60
|
columns?: TTable extends TableDefinition<infer TCols> ? {
|
|
95
61
|
[K in keyof TCols]?: boolean;
|
|
96
62
|
} : Record<string, boolean>;
|
|
97
|
-
/** Rows to skip */
|
|
98
63
|
offset?: number;
|
|
99
|
-
/**
|
|
100
|
-
* Sort results. Accepts direct value, array, or callback.
|
|
101
|
-
*
|
|
102
|
-
* @example
|
|
103
|
-
* // Direct
|
|
104
|
-
* orderBy: desc(users.createdAt)
|
|
105
|
-
*
|
|
106
|
-
* // Array
|
|
107
|
-
* orderBy: [desc(users.createdAt), asc(users.name)]
|
|
108
|
-
*
|
|
109
|
-
* // Callback
|
|
110
|
-
* orderBy: (cols, { desc }) => desc(cols.createdAt)
|
|
111
|
-
*/
|
|
112
64
|
orderBy?: OrderByValue | OrderByValue[] | ((columns: TTable extends TableDefinition<infer TCols> ? TCols : any, fns: {
|
|
113
65
|
asc: typeof ops.asc;
|
|
114
66
|
desc: typeof ops.desc;
|
|
115
67
|
}) => OrderByValue | OrderByValue[]);
|
|
116
|
-
/**
|
|
117
|
-
* Include related data. Use `true` for all columns or an object for filtering.
|
|
118
|
-
*
|
|
119
|
-
* @example
|
|
120
|
-
* with: {
|
|
121
|
-
* posts: true, // All posts
|
|
122
|
-
* comments: { limit: 5 }, // Latest 5 comments
|
|
123
|
-
* profile: { where: eq(profile.public, true) } // Only public profile
|
|
124
|
-
* }
|
|
125
|
-
*/
|
|
126
68
|
with?: RelationalWith<TTable>;
|
|
127
|
-
/**
|
|
128
|
-
* Join strategy for distributed clusters.
|
|
129
|
-
* @default 'auto'
|
|
130
|
-
*/
|
|
131
69
|
joinStrategy?: JoinStrategy;
|
|
132
70
|
};
|
|
133
71
|
type OrderByValue = {
|
|
@@ -136,30 +74,10 @@ type OrderByValue = {
|
|
|
136
74
|
};
|
|
137
75
|
export type RelationalAPI<TSchema extends Record<string, TableDefinition<any>>> = {
|
|
138
76
|
[K in keyof TSchema]: {
|
|
139
|
-
/** Find multiple records with optional filtering, pagination, and relations */
|
|
140
77
|
findMany: <TOpts extends RelationalFindOptions<TSchema[K]> | undefined>(opts?: TOpts) => Promise<Array<RelationalResult<TSchema[K], TOpts extends RelationalFindOptions<TSchema[K]> ? TOpts['with'] : undefined>>>;
|
|
141
|
-
/** Find a single record (first match) */
|
|
142
78
|
findFirst: <TOpts extends RelationalFindOptions<TSchema[K]> | undefined>(opts?: TOpts) => Promise<RelationalResult<TSchema[K], TOpts extends RelationalFindOptions<TSchema[K]> ? TOpts['with'] : undefined> | null>;
|
|
143
|
-
/**
|
|
144
|
-
* Find a record by its primary key
|
|
145
|
-
* @example
|
|
146
|
-
* const user = await db.query.users.findById('uuid-here')
|
|
147
|
-
* const user = await db.query.users.findById('uuid', { with: { posts: true } })
|
|
148
|
-
*/
|
|
149
79
|
findById: <TOpts extends Omit<RelationalFindOptions<TSchema[K]>, 'where' | 'limit'> | undefined>(id: string | number, opts?: TOpts) => Promise<RelationalResult<TSchema[K], TOpts extends RelationalFindOptions<TSchema[K]> ? TOpts['with'] : undefined> | null>;
|
|
150
80
|
};
|
|
151
81
|
};
|
|
152
|
-
/**
|
|
153
|
-
* Build a modern relational API with ClickHouse-specific optimizations.
|
|
154
|
-
*
|
|
155
|
-
* Key architectural features:
|
|
156
|
-
* - **groupUniqArray Optimization**: Instead of flat-joining which causes row explosion,
|
|
157
|
-
* HouseKit uses ClickHouse aggregation to fetch nested relations as arrays of tuples.
|
|
158
|
-
* - **Automatic Cluster Handling**: Detects sharded tables and applies GLOBAL JOINs.
|
|
159
|
-
* - **Smart Deduplication**: Merges results in-memory when flat joins are unavoidable.
|
|
160
|
-
*
|
|
161
|
-
* @param client - The ClickHouse client instance.
|
|
162
|
-
* @param schema - The shared schema definition containing all table and relation metadata.
|
|
163
|
-
*/
|
|
164
82
|
export declare function buildRelationalAPI<TSchema extends Record<string, TableDefinition<any>>>(client: any, schema?: TSchema): RelationalAPI<TSchema> | undefined;
|
|
165
83
|
export {};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { ClickHouseQueryBuilder } from './builders/select';
|
|
2
|
+
import * as ops from './expressions';
|
|
3
|
+
import * as cond from './modules/conditional';
|
|
4
|
+
const operators = {
|
|
5
|
+
eq: ops.eq,
|
|
6
|
+
ne: ops.ne,
|
|
7
|
+
gt: ops.gt,
|
|
8
|
+
gte: ops.gte,
|
|
9
|
+
lt: ops.lt,
|
|
10
|
+
lte: ops.lte,
|
|
11
|
+
inArray: ops.inArray,
|
|
12
|
+
notInArray: ops.notInArray,
|
|
13
|
+
between: ops.between,
|
|
14
|
+
notBetween: ops.notBetween,
|
|
15
|
+
has: ops.has,
|
|
16
|
+
hasAll: ops.hasAll,
|
|
17
|
+
hasAny: ops.hasAny,
|
|
18
|
+
sql: ops.sql,
|
|
19
|
+
and: cond.and,
|
|
20
|
+
or: cond.or,
|
|
21
|
+
not: cond.not,
|
|
22
|
+
isNull: cond.isNull,
|
|
23
|
+
isNotNull: cond.isNotNull,
|
|
24
|
+
asc: ops.asc,
|
|
25
|
+
desc: ops.desc,
|
|
26
|
+
};
|
|
27
|
+
function buildJoinCondition(fields, references) {
|
|
28
|
+
if (!fields || !references || fields.length === 0 || references.length === 0)
|
|
29
|
+
return null;
|
|
30
|
+
const pairs = fields.map((f, i) => ops.sql `${f} = ${references[i]}`);
|
|
31
|
+
const filtered = pairs.filter((p) => Boolean(p));
|
|
32
|
+
if (filtered.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
if (filtered.length === 1)
|
|
35
|
+
return filtered[0];
|
|
36
|
+
return cond.and(...filtered) || null;
|
|
37
|
+
}
|
|
38
|
+
function isDistributedTable(tableDef) {
|
|
39
|
+
const options = tableDef.$options;
|
|
40
|
+
return !!(options?.onCluster || options?.shardKey);
|
|
41
|
+
}
|
|
42
|
+
export function buildRelationalAPI(client, schema) {
|
|
43
|
+
if (!schema)
|
|
44
|
+
return undefined;
|
|
45
|
+
const api = {};
|
|
46
|
+
for (const [tableKey, tableDef] of Object.entries(schema)) {
|
|
47
|
+
api[tableKey] = {
|
|
48
|
+
findMany: async (opts) => {
|
|
49
|
+
let builder = new ClickHouseQueryBuilder(client).from(tableDef);
|
|
50
|
+
const selectedColumns = opts?.columns;
|
|
51
|
+
const baseColumns = selectedColumns
|
|
52
|
+
? Object.entries(tableDef.$columns).filter(([key]) => selectedColumns[key] === true)
|
|
53
|
+
: Object.entries(tableDef.$columns);
|
|
54
|
+
const relations = tableDef.$relations;
|
|
55
|
+
const requestedTopLevel = opts?.with ? Object.entries(opts.with).filter(([, v]) => v) : [];
|
|
56
|
+
const requestedRelations = requestedTopLevel
|
|
57
|
+
.map(([relName, val]) => {
|
|
58
|
+
const rel = relations?.[relName];
|
|
59
|
+
return { relName, rel, options: typeof val === 'object' ? val : {} };
|
|
60
|
+
})
|
|
61
|
+
.filter((r) => Boolean(r.rel));
|
|
62
|
+
const needsGrouping = requestedRelations.length > 0;
|
|
63
|
+
const allColumns = Object.entries(tableDef.$columns);
|
|
64
|
+
const groupByColumns = needsGrouping ? allColumns.map(([, col]) => col) : [];
|
|
65
|
+
const joinStrategy = opts?.joinStrategy || 'auto';
|
|
66
|
+
const isDistributed = isDistributedTable(tableDef);
|
|
67
|
+
const useGlobal = joinStrategy === 'global' || joinStrategy === 'global_any' || (joinStrategy === 'auto' && isDistributed);
|
|
68
|
+
const useAny = joinStrategy === 'any' || joinStrategy === 'global_any';
|
|
69
|
+
function buildNestedSelection(currentTableDef, currentWith, prefix = '', outerJoinStrategy, outerUseGlobal, outerUseAny, columnsFilter) {
|
|
70
|
+
let currentSelection = {};
|
|
71
|
+
let currentJoins = [];
|
|
72
|
+
Object.entries(currentTableDef.$columns).forEach(([key, col]) => {
|
|
73
|
+
if (!columnsFilter || columnsFilter[key] === true) {
|
|
74
|
+
currentSelection[`${prefix}${key}`] = col;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
if (!currentWith)
|
|
78
|
+
return { selection: currentSelection, joins: currentJoins };
|
|
79
|
+
const currentRelations = currentTableDef.$relations;
|
|
80
|
+
const requestedNested = Object.entries(currentWith)
|
|
81
|
+
.map(([relName, val]) => {
|
|
82
|
+
const rel = currentRelations?.[relName];
|
|
83
|
+
return { relName, rel, options: typeof val === 'object' ? val : {} };
|
|
84
|
+
})
|
|
85
|
+
.filter((r) => Boolean(r.rel));
|
|
86
|
+
for (const { relName, rel, options } of requestedNested) {
|
|
87
|
+
const newPrefix = prefix ? `${prefix}_${relName}_` : `${relName}_`;
|
|
88
|
+
let relWhere = null;
|
|
89
|
+
if (options.where) {
|
|
90
|
+
if (typeof options.where === 'function') {
|
|
91
|
+
relWhere = options.where(rel.table.$columns, operators);
|
|
92
|
+
}
|
|
93
|
+
else if (typeof options.where === 'object' && !('toSQL' in options.where)) {
|
|
94
|
+
const conditions = Object.entries(options.where).map(([key, value]) => {
|
|
95
|
+
const col = rel.table.$columns[key];
|
|
96
|
+
if (!col)
|
|
97
|
+
throw new Error(`Column '${key}' not found in relation '${relName}'`);
|
|
98
|
+
return ops.eq(col, value);
|
|
99
|
+
});
|
|
100
|
+
relWhere = conditions.length === 1 ? conditions[0] : cond.and(...conditions);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
relWhere = options.where;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
let joinCondition = buildJoinCondition(rel.fields, rel.references);
|
|
107
|
+
if (joinCondition) {
|
|
108
|
+
const joinType = (() => {
|
|
109
|
+
const relIsDistributed = isDistributedTable(rel.table);
|
|
110
|
+
const useRelGlobal = outerUseGlobal || (outerJoinStrategy === 'auto' && relIsDistributed);
|
|
111
|
+
if (useRelGlobal && outerUseAny)
|
|
112
|
+
return builder.globalAnyJoin;
|
|
113
|
+
if (useRelGlobal)
|
|
114
|
+
return builder.globalLeftJoin;
|
|
115
|
+
if (outerUseAny)
|
|
116
|
+
return builder.anyLeftJoin;
|
|
117
|
+
return builder.leftJoin;
|
|
118
|
+
})();
|
|
119
|
+
if (rel.relation === 'many' && !prefix) {
|
|
120
|
+
const relCols = Object.entries(rel.table.$columns);
|
|
121
|
+
const tupleArgs = relCols.map(([, col]) => ops.sql `${col}`);
|
|
122
|
+
if (relWhere) {
|
|
123
|
+
currentSelection[relName] = ops.sql `groupUniqArrayIf(tuple(${ops.sql.join(tupleArgs, ops.sql `, `)}), ${relWhere})`;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
currentSelection[relName] = ops.sql `groupUniqArray(tuple(${ops.sql.join(tupleArgs, ops.sql `, `)}))`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (relWhere) {
|
|
130
|
+
joinCondition = cond.and(joinCondition, relWhere);
|
|
131
|
+
}
|
|
132
|
+
currentJoins.push({ type: joinType, table: rel.table, on: joinCondition });
|
|
133
|
+
}
|
|
134
|
+
const nestedResult = buildNestedSelection(rel.table, options.with, newPrefix, outerJoinStrategy, outerUseGlobal, outerUseAny, options.columns);
|
|
135
|
+
if (!(rel.relation === 'many' && !prefix)) {
|
|
136
|
+
Object.assign(currentSelection, nestedResult.selection);
|
|
137
|
+
}
|
|
138
|
+
currentJoins.push(...nestedResult.joins);
|
|
139
|
+
}
|
|
140
|
+
return { selection: currentSelection, joins: currentJoins };
|
|
141
|
+
}
|
|
142
|
+
const { selection: flatSelection, joins: allJoins } = buildNestedSelection(tableDef, opts?.with, '', joinStrategy, useGlobal, useAny, opts?.columns);
|
|
143
|
+
for (const joinDef of allJoins) {
|
|
144
|
+
builder = joinDef.type.call(builder, joinDef.table, joinDef.on);
|
|
145
|
+
}
|
|
146
|
+
builder = builder.select(flatSelection);
|
|
147
|
+
if (needsGrouping && groupByColumns.length > 0) {
|
|
148
|
+
builder = builder.groupBy(...groupByColumns);
|
|
149
|
+
}
|
|
150
|
+
if (opts?.where) {
|
|
151
|
+
let whereValue;
|
|
152
|
+
if (typeof opts.where === 'function') {
|
|
153
|
+
whereValue = opts.where(tableDef.$columns, operators);
|
|
154
|
+
}
|
|
155
|
+
else if (opts.where && typeof opts.where === 'object' && !('toSQL' in opts.where)) {
|
|
156
|
+
const conditions = Object.entries(opts.where).map(([key, value]) => {
|
|
157
|
+
const col = tableDef.$columns[key];
|
|
158
|
+
if (!col)
|
|
159
|
+
throw new Error(`Column '${key}' not found in table`);
|
|
160
|
+
return ops.eq(col, value);
|
|
161
|
+
});
|
|
162
|
+
whereValue = conditions.length === 1 ? conditions[0] : cond.and(...conditions);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
whereValue = opts.where;
|
|
166
|
+
}
|
|
167
|
+
builder = builder.where(whereValue);
|
|
168
|
+
}
|
|
169
|
+
if (opts?.orderBy) {
|
|
170
|
+
const orderByFns = { asc: ops.asc, desc: ops.desc };
|
|
171
|
+
const orderByValue = typeof opts.orderBy === 'function'
|
|
172
|
+
? opts.orderBy(tableDef.$columns, orderByFns)
|
|
173
|
+
: opts.orderBy;
|
|
174
|
+
const orderByArray = Array.isArray(orderByValue) ? orderByValue : [orderByValue];
|
|
175
|
+
for (const ob of orderByArray) {
|
|
176
|
+
builder = builder.orderBy(ob.col, ob.dir);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (opts?.limit)
|
|
180
|
+
builder = builder.limit(opts.limit);
|
|
181
|
+
if (opts?.offset)
|
|
182
|
+
builder = builder.offset(opts.offset);
|
|
183
|
+
const rows = await builder.then();
|
|
184
|
+
function reconstructNested(row, currentTableDef, currentWith, prefix = '') {
|
|
185
|
+
const result = {};
|
|
186
|
+
Object.entries(currentTableDef.$columns).forEach(([key]) => {
|
|
187
|
+
result[key] = row[`${prefix}${key}`];
|
|
188
|
+
});
|
|
189
|
+
if (!currentWith)
|
|
190
|
+
return result;
|
|
191
|
+
const currentRelations = currentTableDef.$relations;
|
|
192
|
+
Object.entries(currentWith).filter(([, v]) => v).forEach(([relName, val]) => {
|
|
193
|
+
const rel = currentRelations?.[relName];
|
|
194
|
+
if (!rel)
|
|
195
|
+
return;
|
|
196
|
+
const options = typeof val === 'object' ? val : {};
|
|
197
|
+
const newPrefix = prefix ? `${prefix}_${relName}_` : `${relName}_`;
|
|
198
|
+
if (rel.relation === 'one') {
|
|
199
|
+
const relatedData = reconstructNested(row, rel.table, options.with, newPrefix);
|
|
200
|
+
const allNull = Object.values(relatedData).every(v => v === null || v === undefined);
|
|
201
|
+
result[relName] = allNull ? null : relatedData;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
const rawVal = row[relName];
|
|
205
|
+
if (Array.isArray(rawVal)) {
|
|
206
|
+
const relCols = Object.keys(rel.table.$columns);
|
|
207
|
+
let items = rawVal.map((tuple) => {
|
|
208
|
+
const obj = {};
|
|
209
|
+
let allNull = true;
|
|
210
|
+
relCols.forEach((colKey, i) => {
|
|
211
|
+
const v = tuple[i];
|
|
212
|
+
if (v !== null && v !== undefined)
|
|
213
|
+
allNull = false;
|
|
214
|
+
obj[colKey] = v;
|
|
215
|
+
});
|
|
216
|
+
return allNull ? null : obj;
|
|
217
|
+
}).filter(Boolean);
|
|
218
|
+
if (options.offset)
|
|
219
|
+
items = items.slice(options.offset);
|
|
220
|
+
if (options.limit)
|
|
221
|
+
items = items.slice(0, options.limit);
|
|
222
|
+
result[relName] = items;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const relatedData = reconstructNested(row, rel.table, options.with, newPrefix);
|
|
226
|
+
const allNull = Object.values(relatedData).every(v => v === null || v === undefined);
|
|
227
|
+
result[relName] = allNull ? [] : [relatedData];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
const results = rows.map((row) => reconstructNested(row, tableDef, opts?.with));
|
|
234
|
+
if (needsGrouping) {
|
|
235
|
+
const grouped = new Map();
|
|
236
|
+
const pkCols = Array.isArray(tableDef.$options.primaryKey)
|
|
237
|
+
? tableDef.$options.primaryKey
|
|
238
|
+
: [tableDef.$options.primaryKey || Object.keys(tableDef.$columns)[0]];
|
|
239
|
+
for (const item of results) {
|
|
240
|
+
const id = pkCols.map((col) => {
|
|
241
|
+
const val = item[col];
|
|
242
|
+
return val instanceof Date ? val.getTime() : String(val);
|
|
243
|
+
}).join('|');
|
|
244
|
+
if (!grouped.has(id)) {
|
|
245
|
+
grouped.set(id, item);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const existing = grouped.get(id);
|
|
249
|
+
requestedRelations.forEach(({ relName, rel, options }) => {
|
|
250
|
+
if (rel.relation === 'many' && Array.isArray(item[relName])) {
|
|
251
|
+
for (const newItem of item[relName]) {
|
|
252
|
+
const isDuplicate = existing[relName].some((x) => JSON.stringify(x) === JSON.stringify(newItem));
|
|
253
|
+
if (!isDuplicate)
|
|
254
|
+
existing[relName].push(newItem);
|
|
255
|
+
}
|
|
256
|
+
if (options.limit)
|
|
257
|
+
existing[relName] = existing[relName].slice(0, options.limit);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return Array.from(grouped.values());
|
|
263
|
+
}
|
|
264
|
+
return results;
|
|
265
|
+
},
|
|
266
|
+
findFirst: async (opts) => {
|
|
267
|
+
const rows = await api[tableKey].findMany({ ...opts, limit: 1 });
|
|
268
|
+
return rows[0] ?? null;
|
|
269
|
+
},
|
|
270
|
+
findById: async (id, opts) => {
|
|
271
|
+
const pkOption = tableDef.$options?.primaryKey;
|
|
272
|
+
const pkCols = pkOption
|
|
273
|
+
? (Array.isArray(pkOption) ? pkOption : [pkOption])
|
|
274
|
+
: [Object.keys(tableDef.$columns)[0]];
|
|
275
|
+
const pkColName = pkCols[0];
|
|
276
|
+
const pkColumn = tableDef.$columns[pkColName];
|
|
277
|
+
if (!pkColumn) {
|
|
278
|
+
throw new Error(`Primary key column '${pkColName}' not found in table '${tableKey}'`);
|
|
279
|
+
}
|
|
280
|
+
const rows = await api[tableKey].findMany({
|
|
281
|
+
...opts,
|
|
282
|
+
where: ops.eq(pkColumn, id),
|
|
283
|
+
limit: 1
|
|
284
|
+
});
|
|
285
|
+
return rows[0] ?? null;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return api;
|
|
290
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function relations(table, callback) {
|
|
2
|
+
const helpers = {
|
|
3
|
+
one: (relTable, config) => ({
|
|
4
|
+
relation: 'one',
|
|
5
|
+
name: relTable.$table,
|
|
6
|
+
table: relTable,
|
|
7
|
+
fields: config.fields,
|
|
8
|
+
references: config.references
|
|
9
|
+
}),
|
|
10
|
+
many: (relTable, config = {}) => ({
|
|
11
|
+
relation: 'many',
|
|
12
|
+
name: relTable.$table,
|
|
13
|
+
table: relTable,
|
|
14
|
+
fields: config.fields,
|
|
15
|
+
references: config.references
|
|
16
|
+
})
|
|
17
|
+
};
|
|
18
|
+
const defs = callback(helpers);
|
|
19
|
+
table.$relations = defs;
|
|
20
|
+
return defs;
|
|
21
|
+
}
|
package/dist/schema-builder.d.ts
CHANGED
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HouseKit Schema Builder - Fluent API for Table Definitions
|
|
3
|
-
*
|
|
4
|
-
* This module provides a modern fluent syntax for defining tables using
|
|
5
|
-
* a builder function pattern instead of individual imports.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```typescript
|
|
9
|
-
* import { defineTable, t } from '@housekit/orm';
|
|
10
|
-
*
|
|
11
|
-
* // Using the builder function pattern
|
|
12
|
-
* export const users = defineTable('users', (t) => ({
|
|
13
|
-
* id: t.uuid('id').primaryKey(),
|
|
14
|
-
* name: t.string('name'),
|
|
15
|
-
* email: t.string('email'),
|
|
16
|
-
* age: t.int32('age').nullable(),
|
|
17
|
-
* createdAt: t.datetime('created_at').default('now()'),
|
|
18
|
-
* }), { engine: Engine.MergeTree(), orderBy: 'createdAt' });
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
1
|
import { ClickHouseColumn } from './column';
|
|
22
2
|
import { chView, type TableOptions, type TableDefinition, type RelationDefinition, index, projection } from './table';
|
|
23
3
|
import { chMaterializedView, detectMaterializedViewDrift, extractMVQuery, createMigrationBridge, generateBlueGreenMigration } from './materialized-views';
|
|
@@ -26,46 +6,17 @@ import { chDictionary } from './dictionary';
|
|
|
26
6
|
import { chProjection } from './materialized-views';
|
|
27
7
|
import { EngineConfiguration } from './engines';
|
|
28
8
|
export { index };
|
|
29
|
-
/**
|
|
30
|
-
* Enhanced table options with column key references for orderBy, partitionBy, etc.
|
|
31
|
-
*/
|
|
32
9
|
export type EnhancedTableOptions<TColKeys extends string = string> = Omit<TableOptions, 'orderBy' | 'partitionBy' | 'primaryKey' | 'sampleBy' | 'deduplicateBy' | 'versionColumn' | 'logicalPrimaryKey' | 'engine'> & {
|
|
33
10
|
engine: EngineConfiguration;
|
|
34
11
|
customEngine?: string;
|
|
35
|
-
/**
|
|
36
|
-
* Column(s) for ORDER BY. Can use column keys from your definition.
|
|
37
|
-
* @example orderBy: 'timestamp' or orderBy: ['user_id', 'timestamp']
|
|
38
|
-
*/
|
|
39
12
|
orderBy?: TColKeys | TColKeys[] | string | string[];
|
|
40
|
-
/**
|
|
41
|
-
* Column(s) for PARTITION BY. Can use column keys from your definition.
|
|
42
|
-
*/
|
|
43
13
|
partitionBy?: TColKeys | TColKeys[] | string | string[];
|
|
44
|
-
/**
|
|
45
|
-
* Column(s) for PRIMARY KEY. Can use column keys from your definition.
|
|
46
|
-
*/
|
|
47
14
|
primaryKey?: TColKeys | TColKeys[] | string | string[];
|
|
48
|
-
/**
|
|
49
|
-
* Column(s) for SAMPLE BY. Can use column keys from your definition.
|
|
50
|
-
*/
|
|
51
15
|
sampleBy?: TColKeys | TColKeys[] | string | string[];
|
|
52
|
-
/**
|
|
53
|
-
* Column(s) for deduplication (ReplacingMergeTree).
|
|
54
|
-
*/
|
|
55
16
|
deduplicateBy?: TColKeys | TColKeys[] | string | string[];
|
|
56
|
-
/**
|
|
57
|
-
* Version column for ReplacingMergeTree.
|
|
58
|
-
*/
|
|
59
17
|
versionColumn?: TColKeys | string;
|
|
60
|
-
/**
|
|
61
|
-
* Logical primary key (for documentation, not enforced).
|
|
62
|
-
*/
|
|
63
18
|
logicalPrimaryKey?: TColKeys | TColKeys[] | string | string[];
|
|
64
19
|
};
|
|
65
|
-
/**
|
|
66
|
-
* ClickHouse data types builder.
|
|
67
|
-
* All columns are NOT NULL by default, following ClickHouse philosophy.
|
|
68
|
-
*/
|
|
69
20
|
declare function primaryUuid(): {
|
|
70
21
|
id: ClickHouseColumn<string, true, true>;
|
|
71
22
|
};
|
|
@@ -82,10 +33,6 @@ export declare const t: {
|
|
|
82
33
|
int8: (name: string) => ClickHouseColumn<number, true, false>;
|
|
83
34
|
int16: (name: string) => ClickHouseColumn<number, true, false>;
|
|
84
35
|
integer: (name: string) => ClickHouseColumn<number, true, false>;
|
|
85
|
-
/**
|
|
86
|
-
* Int32 type. Signed 32-bit integer.
|
|
87
|
-
* @range -2147483648 to 2147483647
|
|
88
|
-
*/
|
|
89
36
|
int32: (name: string) => ClickHouseColumn<number, true, false>;
|
|
90
37
|
int64: (name: string) => ClickHouseColumn<number, true, false>;
|
|
91
38
|
int128: (name: string) => ClickHouseColumn<number, true, false>;
|
|
@@ -114,10 +61,6 @@ export declare const t: {
|
|
|
114
61
|
date: (name: string) => ClickHouseColumn<string | Date, true, false>;
|
|
115
62
|
date32: (name: string) => ClickHouseColumn<string | Date, true, false>;
|
|
116
63
|
timestamp: (name: string, timezone?: string) => ClickHouseColumn<string | Date, true, false>;
|
|
117
|
-
/**
|
|
118
|
-
* DateTime type. Stores date and time.
|
|
119
|
-
* @param timezone - Optional. Example: 'UTC', 'Europe/Madrid'
|
|
120
|
-
*/
|
|
121
64
|
datetime: (name: string, timezone?: string) => ClickHouseColumn<string | Date, true, false>;
|
|
122
65
|
datetime64: (name: string, precision?: number, timezone?: string) => ClickHouseColumn<string | Date, true, false>;
|
|
123
66
|
boolean: (name: string) => ClickHouseColumn<boolean, true, false>;
|
|
@@ -131,11 +74,6 @@ export declare const t: {
|
|
|
131
74
|
nested: (name: string, fields: Record<string, string>) => ClickHouseColumn<any, true, false>;
|
|
132
75
|
json: <TSchema = Record<string, any>>(name: string) => ClickHouseColumn<TSchema, false, false>;
|
|
133
76
|
dynamic: (name: string, maxTypes?: number) => ClickHouseColumn<any, true, false>;
|
|
134
|
-
/**
|
|
135
|
-
* LowCardinality type. Optimizes columns with few unique values
|
|
136
|
-
* (typically < 10,000) for ultra-fast reading.
|
|
137
|
-
* @see https://clickhouse.com/docs/en/sql-reference/data-types/lowcardinality
|
|
138
|
-
*/
|
|
139
77
|
lowCardinality: <T, TNotNull extends boolean, TAutoGenerated extends boolean>(col: ClickHouseColumn<T, TNotNull, TAutoGenerated>) => ClickHouseColumn<T, TNotNull, TAutoGenerated>;
|
|
140
78
|
aggregateFunction: (name: string, funcName: string, ...argTypes: string[]) => ClickHouseColumn<any, true, false>;
|
|
141
79
|
simpleAggregateFunction: (name: string, funcName: string, argType: string) => ClickHouseColumn<any, true, false>;
|
|
@@ -144,44 +82,19 @@ export declare const t: {
|
|
|
144
82
|
polygon: (name: string) => ClickHouseColumn<[number, number][][], true, false>;
|
|
145
83
|
multiPolygon: (name: string) => ClickHouseColumn<[number, number][][][], true, false>;
|
|
146
84
|
enum: (name: string, values: readonly string[]) => ClickHouseColumn<string, false, false>;
|
|
147
|
-
/**
|
|
148
|
-
* Adds 'created_at' and 'updated_at' columns with default now().
|
|
149
|
-
*/
|
|
150
85
|
timestamps: () => {
|
|
151
86
|
created_at: ClickHouseColumn<string | Date, true, true>;
|
|
152
87
|
updated_at: ClickHouseColumn<string | Date, true, true>;
|
|
153
88
|
};
|
|
154
|
-
/**
|
|
155
|
-
* Adds a standard UUID primary key column (default: 'id').
|
|
156
|
-
*/
|
|
157
89
|
primaryUuid: typeof primaryUuid;
|
|
158
|
-
/**
|
|
159
|
-
* Adds a UUID v7 primary key column (default: 'id').
|
|
160
|
-
*/
|
|
161
90
|
primaryUuidV7: typeof primaryUuidV7;
|
|
162
|
-
/**
|
|
163
|
-
* Adds 'is_deleted' and 'deleted_at' columns for soft deletes.
|
|
164
|
-
*/
|
|
165
91
|
softDeletes: () => {
|
|
166
92
|
is_deleted: ClickHouseColumn<boolean, true, true>;
|
|
167
93
|
deleted_at: ClickHouseColumn<string | Date, false, false>;
|
|
168
94
|
};
|
|
169
95
|
};
|
|
170
96
|
export type ColumnBuilder = typeof t;
|
|
171
|
-
/**
|
|
172
|
-
* Define a strongly-typed ClickHouse table.
|
|
173
|
-
*
|
|
174
|
-
* @param name - Physical table name in ClickHouse.
|
|
175
|
-
* @param columns - Column definition object or callback using `t` builder.
|
|
176
|
-
* @param options - Engine configuration, sorting keys, partitioning, etc.
|
|
177
|
-
*/
|
|
178
97
|
export declare function defineTable<T extends Record<string, ClickHouseColumn<any, any, any>>>(tableName: string, columnsOrCallback: T | ((t: ColumnBuilder) => T), options: EnhancedTableOptions<keyof T & string>): TableDefinition<T, TableOptions>;
|
|
179
|
-
/**
|
|
180
|
-
* Aliases for modern API - providing both short and explicit naming.
|
|
181
|
-
*
|
|
182
|
-
* NOTE: defineTable is the preferred explicit naming for library consistency,
|
|
183
|
-
* while 'table' is provided as a shorthand similar to other ORMs.
|
|
184
|
-
*/
|
|
185
98
|
export declare const table: typeof defineTable;
|
|
186
99
|
export declare const view: typeof chView;
|
|
187
100
|
export declare const defineView: typeof chView;
|
|
@@ -191,9 +104,6 @@ export declare const dictionary: typeof chDictionary;
|
|
|
191
104
|
export declare const defineDictionary: typeof chDictionary;
|
|
192
105
|
export { projection };
|
|
193
106
|
export { chProjection as defineProjection };
|
|
194
|
-
/**
|
|
195
|
-
* Define relations for a table using a callback pattern.
|
|
196
|
-
*/
|
|
197
107
|
export declare function relations<TTable extends TableDefinition<any>, TRelations extends Record<string, RelationDefinition<any>>>(table: TTable, relationsBuilder: (helpers: {
|
|
198
108
|
one: <TTarget extends TableDefinition<any>>(table: TTarget, config: {
|
|
199
109
|
fields: ClickHouseColumn<any, any, any>[];
|