@quark-fw/clisma 0.1.7 → 0.1.8
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/dist/build-count-query.d.ts +4 -0
- package/dist/build-count-query.js +12 -0
- package/dist/build-delete-query.d.ts +4 -0
- package/dist/build-delete-query.js +11 -0
- package/dist/build-find-many-query.d.ts +3 -0
- package/dist/build-find-many-query.js +57 -0
- package/dist/build-lightweight-delete-query.d.ts +4 -0
- package/dist/build-lightweight-delete-query.js +11 -0
- package/dist/build-update-query.d.ts +4 -0
- package/dist/build-update-query.js +13 -0
- package/dist/client.js +4 -4
- package/dist/column.js +1 -8
- package/dist/rows.js +8 -3
- package/dist/sql-shared.d.ts +31 -0
- package/dist/sql-shared.js +162 -0
- package/dist/sql.d.ts +6 -25
- package/dist/sql.js +8 -254
- package/dist/table-client.js +12 -11
- package/package.json +1 -1
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TableDefinition } from "./table.js";
|
|
2
|
+
import { WhereInput } from "./where.js";
|
|
3
|
+
import { SQLBuildResult } from "./sql-shared.js";
|
|
4
|
+
export declare function buildCountQuery(database: string, tableName: string, table: TableDefinition<any>, where?: WhereInput<any>): SQLBuildResult;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/build-count-query.ts
|
|
2
|
+
import { buildFindManyQuery } from "./build-find-many-query.js";
|
|
3
|
+
export function buildCountQuery(database, tableName, table, where) {
|
|
4
|
+
const inner = buildFindManyQuery(database, tableName, table, { where });
|
|
5
|
+
// with no select arg the query starts with "SELECT * FROM ..." —
|
|
6
|
+
// swap the projection for count()
|
|
7
|
+
const from = inner.sql.indexOf(" FROM ");
|
|
8
|
+
return {
|
|
9
|
+
sql: "SELECT count() AS count" + inner.sql.slice(from),
|
|
10
|
+
params: inner.params,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TableDefinition } from "./table.js";
|
|
2
|
+
import { WhereInput } from "./where.js";
|
|
3
|
+
import { SQLBuildResult } from "./sql-shared.js";
|
|
4
|
+
export declare function buildDeleteQuery(database: string, tableName: string, table: TableDefinition<any>, where?: WhereInput<any>): SQLBuildResult;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/build-delete-query.ts
|
|
2
|
+
import { createBuildContext, escapeId } from "./sql-shared.js";
|
|
3
|
+
export function buildDeleteQuery(database, tableName, table, where) {
|
|
4
|
+
const ctx = createBuildContext(tableName, table.columns);
|
|
5
|
+
const whereSQL = where ? ctx.whereSQL(where) : "";
|
|
6
|
+
return {
|
|
7
|
+
sql: `ALTER TABLE ${escapeId(database)}.${escapeId(tableName)} ` +
|
|
8
|
+
`DELETE WHERE ${whereSQL || "1"}`,
|
|
9
|
+
params: ctx.params,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/build-find-many-query.ts
|
|
2
|
+
import { checkPagination, createBuildContext, NO_LIMIT, tableRef, } from "./sql-shared.js";
|
|
3
|
+
export function buildFindManyQuery(database, tableName, table, args = {}) {
|
|
4
|
+
const ctx = createBuildContext(tableName, table.columns);
|
|
5
|
+
// SELECT
|
|
6
|
+
let selectSQL = "*";
|
|
7
|
+
if (args.select) {
|
|
8
|
+
const keys = Object.keys(args.select).filter((k) => args.select[k] === true);
|
|
9
|
+
if (keys.length) {
|
|
10
|
+
selectSQL = keys.map(ctx.colRef).join(", ");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const clauses = [
|
|
14
|
+
`SELECT ${selectSQL} FROM ${tableRef(database, tableName, table)}`,
|
|
15
|
+
];
|
|
16
|
+
// WHERE
|
|
17
|
+
if (args.where) {
|
|
18
|
+
const where = ctx.whereSQL(args.where);
|
|
19
|
+
if (where)
|
|
20
|
+
clauses.push(`WHERE ${where}`);
|
|
21
|
+
}
|
|
22
|
+
// ORDER BY
|
|
23
|
+
if (args.orderBy) {
|
|
24
|
+
const list = Array.isArray(args.orderBy)
|
|
25
|
+
? args.orderBy
|
|
26
|
+
: [args.orderBy];
|
|
27
|
+
const orders = [];
|
|
28
|
+
for (const entry of list) {
|
|
29
|
+
for (const key in entry) {
|
|
30
|
+
const dir = entry[key];
|
|
31
|
+
if (dir === undefined)
|
|
32
|
+
continue;
|
|
33
|
+
if (dir !== "asc" && dir !== "desc") {
|
|
34
|
+
throw new Error(`Invalid sort direction "${dir}" for column "${key}"`);
|
|
35
|
+
}
|
|
36
|
+
orders.push(`${ctx.colRef(key)} ${dir.toUpperCase()}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (orders.length)
|
|
40
|
+
clauses.push(`ORDER BY ${orders.join(", ")}`);
|
|
41
|
+
}
|
|
42
|
+
// LIMIT / OFFSET
|
|
43
|
+
if (args.take !== undefined)
|
|
44
|
+
checkPagination("take", args.take);
|
|
45
|
+
if (args.skip !== undefined)
|
|
46
|
+
checkPagination("skip", args.skip);
|
|
47
|
+
if (args.take !== undefined && args.skip !== undefined) {
|
|
48
|
+
clauses.push(`LIMIT ${args.take} OFFSET ${args.skip}`);
|
|
49
|
+
}
|
|
50
|
+
else if (args.take !== undefined) {
|
|
51
|
+
clauses.push(`LIMIT ${args.take}`);
|
|
52
|
+
}
|
|
53
|
+
else if (args.skip !== undefined) {
|
|
54
|
+
clauses.push(`LIMIT ${NO_LIMIT} OFFSET ${args.skip}`);
|
|
55
|
+
}
|
|
56
|
+
return { sql: clauses.join(" "), params: ctx.params };
|
|
57
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TableDefinition } from "./table.js";
|
|
2
|
+
import { WhereInput } from "./where.js";
|
|
3
|
+
import { SQLBuildResult } from "./sql-shared.js";
|
|
4
|
+
export declare function buildLightweightDeleteQuery(database: string, tableName: string, table: TableDefinition<any>, where?: WhereInput<any>): SQLBuildResult;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/build-lightweight-delete-query.ts
|
|
2
|
+
import { createBuildContext, escapeId } from "./sql-shared.js";
|
|
3
|
+
export function buildLightweightDeleteQuery(database, tableName, table, where) {
|
|
4
|
+
const ctx = createBuildContext(tableName, table.columns);
|
|
5
|
+
const whereSQL = where ? ctx.whereSQL(where) : "";
|
|
6
|
+
return {
|
|
7
|
+
sql: `DELETE FROM ${escapeId(database)}.${escapeId(tableName)} ` +
|
|
8
|
+
`WHERE ${whereSQL || "1"}`,
|
|
9
|
+
params: ctx.params,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TableDefinition } from "./table.js";
|
|
2
|
+
import { WhereInput } from "./where.js";
|
|
3
|
+
import { SQLBuildResult } from "./sql-shared.js";
|
|
4
|
+
export declare function buildUpdateQuery(database: string, tableName: string, table: TableDefinition<any>, where: WhereInput<any> | undefined, data: Record<string, any>): SQLBuildResult;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/build-update-query.ts
|
|
2
|
+
import { createBuildContext, escapeId, validateUpdateData, } from "./sql-shared.js";
|
|
3
|
+
export function buildUpdateQuery(database, tableName, table, where, data) {
|
|
4
|
+
const keys = validateUpdateData(table, tableName, data);
|
|
5
|
+
const ctx = createBuildContext(tableName, table.columns);
|
|
6
|
+
const sets = keys.map((k) => `${ctx.colRef(k)} = ${ctx.addParam(data[k], ctx.baseType(k))}`);
|
|
7
|
+
const whereSQL = where ? ctx.whereSQL(where) : "";
|
|
8
|
+
return {
|
|
9
|
+
sql: `ALTER TABLE ${escapeId(database)}.${escapeId(tableName)} ` +
|
|
10
|
+
`UPDATE ${sets.join(", ")} WHERE ${whereSQL || "1"}`,
|
|
11
|
+
params: ctx.params,
|
|
12
|
+
};
|
|
13
|
+
}
|
package/dist/client.js
CHANGED
|
@@ -9,7 +9,7 @@ export function createClient(schema, options) {
|
|
|
9
9
|
url: options.url,
|
|
10
10
|
username: options.username,
|
|
11
11
|
password: options.password,
|
|
12
|
-
database
|
|
12
|
+
database,
|
|
13
13
|
});
|
|
14
14
|
const db = {};
|
|
15
15
|
for (const tableName in schema.tables) {
|
|
@@ -20,11 +20,11 @@ export function createClient(schema, options) {
|
|
|
20
20
|
const rootClient = createCHClient({
|
|
21
21
|
url: options.url,
|
|
22
22
|
username: options.username,
|
|
23
|
-
password: options.password
|
|
23
|
+
password: options.password,
|
|
24
24
|
});
|
|
25
25
|
try {
|
|
26
26
|
await rootClient.command({
|
|
27
|
-
query: `CREATE DATABASE IF NOT EXISTS ${escapeId(database)}
|
|
27
|
+
query: `CREATE DATABASE IF NOT EXISTS ${escapeId(database)}`,
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
finally {
|
|
@@ -50,7 +50,7 @@ export function createClient(schema, options) {
|
|
|
50
50
|
ORDER BY table, position
|
|
51
51
|
`,
|
|
52
52
|
query_params: { db: database },
|
|
53
|
-
format: "JSONEachRow"
|
|
53
|
+
format: "JSONEachRow",
|
|
54
54
|
});
|
|
55
55
|
const existing = await result.json();
|
|
56
56
|
const plan = buildMigrationPlan(database, schema.tables, existing);
|
package/dist/column.js
CHANGED
|
@@ -5,13 +5,6 @@ function makeColumn(clickhouseType, nullable = false) {
|
|
|
5
5
|
nullable,
|
|
6
6
|
};
|
|
7
7
|
}
|
|
8
|
-
function uint64Id() {
|
|
9
|
-
return {
|
|
10
|
-
clickhouseType: "UInt64",
|
|
11
|
-
nullable: false,
|
|
12
|
-
autoId: true // phantom field для TS
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
8
|
export const t = {
|
|
16
9
|
string: () => makeColumn("String"),
|
|
17
10
|
uint8: () => makeColumn("UInt8"),
|
|
@@ -27,5 +20,5 @@ export const t = {
|
|
|
27
20
|
},
|
|
28
21
|
array(col) {
|
|
29
22
|
return makeColumn(`Array(${col.clickhouseType})`);
|
|
30
|
-
}
|
|
23
|
+
},
|
|
31
24
|
};
|
package/dist/rows.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// src/rows.ts
|
|
2
2
|
const BIGINT_TYPES = new Set([
|
|
3
|
-
"UInt64",
|
|
3
|
+
"UInt64",
|
|
4
|
+
"Int64",
|
|
5
|
+
"UInt128",
|
|
6
|
+
"Int128",
|
|
7
|
+
"UInt256",
|
|
8
|
+
"Int256",
|
|
4
9
|
]);
|
|
5
10
|
function convertValue(value, chType) {
|
|
6
11
|
if (value === null || value === undefined)
|
|
@@ -10,7 +15,7 @@ function convertValue(value, chType) {
|
|
|
10
15
|
return convertValue(value, nullable[1]);
|
|
11
16
|
const array = /^Array\((.+)\)$/.exec(chType);
|
|
12
17
|
if (array) {
|
|
13
|
-
return value.map(v => convertValue(v, array[1]));
|
|
18
|
+
return value.map((v) => convertValue(v, array[1]));
|
|
14
19
|
}
|
|
15
20
|
if (BIGINT_TYPES.has(chType))
|
|
16
21
|
return BigInt(value);
|
|
@@ -20,7 +25,7 @@ function convertValue(value, chType) {
|
|
|
20
25
|
return value;
|
|
21
26
|
}
|
|
22
27
|
export function deserializeRows(rows, columns) {
|
|
23
|
-
return rows.map(row => {
|
|
28
|
+
return rows.map((row) => {
|
|
24
29
|
const out = {};
|
|
25
30
|
for (const k in row) {
|
|
26
31
|
const col = columns[k];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Column } from "./column.js";
|
|
2
|
+
import { TableDefinition } from "./table.js";
|
|
3
|
+
import { WhereInput } from "./where.js";
|
|
4
|
+
export type SQLBuildResult = {
|
|
5
|
+
sql: string;
|
|
6
|
+
params: Record<string, any>;
|
|
7
|
+
};
|
|
8
|
+
export type OrderByInput<T> = {
|
|
9
|
+
[K in keyof T]?: "asc" | "desc";
|
|
10
|
+
};
|
|
11
|
+
export type FindManyArgs<T> = {
|
|
12
|
+
where?: WhereInput<T>;
|
|
13
|
+
select?: {
|
|
14
|
+
[K in keyof T]?: boolean;
|
|
15
|
+
};
|
|
16
|
+
orderBy?: OrderByInput<T> | OrderByInput<T>[];
|
|
17
|
+
take?: number;
|
|
18
|
+
skip?: number;
|
|
19
|
+
};
|
|
20
|
+
export declare function escapeId(name: string): string;
|
|
21
|
+
export declare const NO_LIMIT = "18446744073709551615";
|
|
22
|
+
export declare function createBuildContext(tableName: string, columns: Record<string, Column<any>>): {
|
|
23
|
+
params: Record<string, any>;
|
|
24
|
+
colRef: (key: string) => string;
|
|
25
|
+
baseType: (key: string) => string;
|
|
26
|
+
addParam: (value: any, type: string) => string;
|
|
27
|
+
whereSQL: (where: any) => string;
|
|
28
|
+
};
|
|
29
|
+
export declare function tableRef(database: string, tableName: string, table: TableDefinition<any>): string;
|
|
30
|
+
export declare function checkPagination(name: string, value: any): void;
|
|
31
|
+
export declare function validateUpdateData(table: TableDefinition<any>, tableName: string, data: Record<string, any>): string[];
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/sql-shared.ts
|
|
2
|
+
// Shared helpers and types for the SQL builders (build-*.ts).
|
|
3
|
+
export function escapeId(name) {
|
|
4
|
+
return "`" + name.replace(/\\/g, "\\\\").replace(/`/g, "\\`") + "`";
|
|
5
|
+
}
|
|
6
|
+
// UInt64 max — used to emulate a bare OFFSET (skip without take)
|
|
7
|
+
export const NO_LIMIT = "18446744073709551615";
|
|
8
|
+
// DateTime is second-precision; the client would send Date with
|
|
9
|
+
// a fractional part, which ClickHouse rejects — send unix seconds.
|
|
10
|
+
function serialize(value) {
|
|
11
|
+
if (value instanceof Date) {
|
|
12
|
+
return Math.floor(value.getTime() / 1000);
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return value.map(serialize);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
export function createBuildContext(tableName, columns) {
|
|
20
|
+
const params = {};
|
|
21
|
+
let paramIndex = 0;
|
|
22
|
+
function colRef(key) {
|
|
23
|
+
if (!columns[key]) {
|
|
24
|
+
throw new Error(`Unknown column "${key}" in table "${tableName}"`);
|
|
25
|
+
}
|
|
26
|
+
return escapeId(key);
|
|
27
|
+
}
|
|
28
|
+
function baseType(key) {
|
|
29
|
+
const chType = columns[key].clickhouseType;
|
|
30
|
+
const m = /^Nullable\((.+)\)$/.exec(chType);
|
|
31
|
+
return m ? m[1] : chType;
|
|
32
|
+
}
|
|
33
|
+
function addParam(value, type) {
|
|
34
|
+
const name = `p${paramIndex++}`;
|
|
35
|
+
params[name] = serialize(value);
|
|
36
|
+
return `{${name}:${type}}`;
|
|
37
|
+
}
|
|
38
|
+
function buildFieldFilter(key, filter) {
|
|
39
|
+
const col = colRef(key);
|
|
40
|
+
const conditions = [];
|
|
41
|
+
for (const op in filter) {
|
|
42
|
+
const value = filter[op];
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
switch (op) {
|
|
46
|
+
case "equals":
|
|
47
|
+
conditions.push(value === null
|
|
48
|
+
? `${col} IS NULL`
|
|
49
|
+
: `${col} = ${addParam(value, baseType(key))}`);
|
|
50
|
+
break;
|
|
51
|
+
case "not":
|
|
52
|
+
conditions.push(value === null
|
|
53
|
+
? `${col} IS NOT NULL`
|
|
54
|
+
: `${col} != ${addParam(value, baseType(key))}`);
|
|
55
|
+
break;
|
|
56
|
+
case "in":
|
|
57
|
+
conditions.push(value.length === 0
|
|
58
|
+
? "0"
|
|
59
|
+
: `${col} IN ${addParam(value, `Array(${baseType(key)})`)}`);
|
|
60
|
+
break;
|
|
61
|
+
case "notIn":
|
|
62
|
+
if (value.length > 0) {
|
|
63
|
+
conditions.push(`${col} NOT IN ${addParam(value, `Array(${baseType(key)})`)}`);
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
case "lt":
|
|
67
|
+
case "lte":
|
|
68
|
+
case "gt":
|
|
69
|
+
case "gte": {
|
|
70
|
+
const cmp = { lt: "<", lte: "<=", gt: ">", gte: ">=" }[op];
|
|
71
|
+
conditions.push(`${col} ${cmp} ${addParam(value, baseType(key))}`);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "contains":
|
|
75
|
+
conditions.push(`position(${col}, ${addParam(value, "String")}) > 0`);
|
|
76
|
+
break;
|
|
77
|
+
case "startsWith":
|
|
78
|
+
conditions.push(`startsWith(${col}, ${addParam(value, "String")})`);
|
|
79
|
+
break;
|
|
80
|
+
case "endsWith":
|
|
81
|
+
conditions.push(`endsWith(${col}, ${addParam(value, "String")})`);
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
throw new Error(`Unknown operator "${op}" for column "${key}"`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return conditions;
|
|
88
|
+
}
|
|
89
|
+
function isFilterObject(value) {
|
|
90
|
+
return (value !== null &&
|
|
91
|
+
typeof value === "object" &&
|
|
92
|
+
!(value instanceof Date) &&
|
|
93
|
+
!Array.isArray(value));
|
|
94
|
+
}
|
|
95
|
+
function whereSQL(where) {
|
|
96
|
+
const parts = [];
|
|
97
|
+
for (const key in where) {
|
|
98
|
+
const value = where[key];
|
|
99
|
+
if (value === undefined)
|
|
100
|
+
continue;
|
|
101
|
+
if (key === "AND") {
|
|
102
|
+
const list = Array.isArray(value) ? value : [value];
|
|
103
|
+
for (const w of list) {
|
|
104
|
+
const c = whereSQL(w);
|
|
105
|
+
if (c)
|
|
106
|
+
parts.push(`(${c})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (key === "OR") {
|
|
110
|
+
const conds = value
|
|
111
|
+
.map((w) => whereSQL(w))
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.map((c) => `(${c})`);
|
|
114
|
+
if (conds.length)
|
|
115
|
+
parts.push(`(${conds.join(" OR ")})`);
|
|
116
|
+
}
|
|
117
|
+
else if (key === "NOT") {
|
|
118
|
+
const list = Array.isArray(value) ? value : [value];
|
|
119
|
+
for (const w of list) {
|
|
120
|
+
const c = whereSQL(w);
|
|
121
|
+
if (c)
|
|
122
|
+
parts.push(`NOT (${c})`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (value === null) {
|
|
126
|
+
parts.push(`${colRef(key)} IS NULL`);
|
|
127
|
+
}
|
|
128
|
+
else if (isFilterObject(value)) {
|
|
129
|
+
parts.push(...buildFieldFilter(key, value));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
parts.push(`${colRef(key)} = ${addParam(value, baseType(key))}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return parts.join(" AND ");
|
|
136
|
+
}
|
|
137
|
+
return { params, colRef, baseType, addParam, whereSQL };
|
|
138
|
+
}
|
|
139
|
+
export function tableRef(database, tableName, table) {
|
|
140
|
+
const ref = `${escapeId(database)}.${escapeId(tableName)}`;
|
|
141
|
+
// replacing tables must read the latest row versions
|
|
142
|
+
return table.mutations === "replacing" ? `${ref} FINAL` : ref;
|
|
143
|
+
}
|
|
144
|
+
export function checkPagination(name, value) {
|
|
145
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
146
|
+
throw new Error(`"${name}" must be a non-negative integer, got: ${value}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// shared by buildUpdateQuery and the replacing-strategy update path
|
|
150
|
+
export function validateUpdateData(table, tableName, data) {
|
|
151
|
+
const keys = Object.keys(data).filter((k) => data[k] !== undefined);
|
|
152
|
+
if (keys.length === 0) {
|
|
153
|
+
throw new Error(`updateMany requires non-empty data`);
|
|
154
|
+
}
|
|
155
|
+
for (const key of keys) {
|
|
156
|
+
if (table.orderBy.includes(key)) {
|
|
157
|
+
throw new Error(`Cannot update ORDER BY key column "${key}" ` +
|
|
158
|
+
`of table "${tableName}"`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return keys;
|
|
162
|
+
}
|
package/dist/sql.d.ts
CHANGED
|
@@ -1,25 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
};
|
|
7
|
-
export type OrderByInput<T> = {
|
|
8
|
-
[K in keyof T]?: "asc" | "desc";
|
|
9
|
-
};
|
|
10
|
-
export type FindManyArgs<T> = {
|
|
11
|
-
where?: WhereInput<T>;
|
|
12
|
-
select?: {
|
|
13
|
-
[K in keyof T]?: boolean;
|
|
14
|
-
};
|
|
15
|
-
orderBy?: OrderByInput<T> | OrderByInput<T>[];
|
|
16
|
-
take?: number;
|
|
17
|
-
skip?: number;
|
|
18
|
-
};
|
|
19
|
-
export declare function escapeId(name: string): string;
|
|
20
|
-
export declare function buildFindManyQuery(database: string, tableName: string, table: TableDefinition<any>, args?: FindManyArgs<any>): SQLBuildResult;
|
|
21
|
-
export declare function buildCountQuery(database: string, tableName: string, table: TableDefinition<any>, where?: WhereInput<any>): SQLBuildResult;
|
|
22
|
-
export declare function validateUpdateData(table: TableDefinition<any>, tableName: string, data: Record<string, any>): string[];
|
|
23
|
-
export declare function buildUpdateQuery(database: string, tableName: string, table: TableDefinition<any>, where: WhereInput<any> | undefined, data: Record<string, any>): SQLBuildResult;
|
|
24
|
-
export declare function buildDeleteQuery(database: string, tableName: string, table: TableDefinition<any>, where?: WhereInput<any>): SQLBuildResult;
|
|
25
|
-
export declare function buildLightweightDeleteQuery(database: string, tableName: string, table: TableDefinition<any>, where?: WhereInput<any>): SQLBuildResult;
|
|
1
|
+
export { escapeId, validateUpdateData, type FindManyArgs, type OrderByInput, type SQLBuildResult, } from "./sql-shared.js";
|
|
2
|
+
export { buildFindManyQuery } from "./build-find-many-query.js";
|
|
3
|
+
export { buildCountQuery } from "./build-count-query.js";
|
|
4
|
+
export { buildUpdateQuery } from "./build-update-query.js";
|
|
5
|
+
export { buildDeleteQuery } from "./build-delete-query.js";
|
|
6
|
+
export { buildLightweightDeleteQuery } from "./build-lightweight-delete-query.js";
|
package/dist/sql.js
CHANGED
|
@@ -1,255 +1,9 @@
|
|
|
1
1
|
// src/sql.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (value instanceof Date) {
|
|
11
|
-
return Math.floor(value.getTime() / 1000);
|
|
12
|
-
}
|
|
13
|
-
if (Array.isArray(value)) {
|
|
14
|
-
return value.map(serialize);
|
|
15
|
-
}
|
|
16
|
-
return value;
|
|
17
|
-
}
|
|
18
|
-
function createBuildContext(tableName, columns) {
|
|
19
|
-
const params = {};
|
|
20
|
-
let paramIndex = 0;
|
|
21
|
-
function colRef(key) {
|
|
22
|
-
if (!columns[key]) {
|
|
23
|
-
throw new Error(`Unknown column "${key}" in table "${tableName}"`);
|
|
24
|
-
}
|
|
25
|
-
return escapeId(key);
|
|
26
|
-
}
|
|
27
|
-
function baseType(key) {
|
|
28
|
-
const chType = columns[key].clickhouseType;
|
|
29
|
-
const m = /^Nullable\((.+)\)$/.exec(chType);
|
|
30
|
-
return m ? m[1] : chType;
|
|
31
|
-
}
|
|
32
|
-
function addParam(value, type) {
|
|
33
|
-
const name = `p${paramIndex++}`;
|
|
34
|
-
params[name] = serialize(value);
|
|
35
|
-
return `{${name}:${type}}`;
|
|
36
|
-
}
|
|
37
|
-
function buildFieldFilter(key, filter) {
|
|
38
|
-
const col = colRef(key);
|
|
39
|
-
const conditions = [];
|
|
40
|
-
for (const op in filter) {
|
|
41
|
-
const value = filter[op];
|
|
42
|
-
if (value === undefined)
|
|
43
|
-
continue;
|
|
44
|
-
switch (op) {
|
|
45
|
-
case "equals":
|
|
46
|
-
conditions.push(value === null
|
|
47
|
-
? `${col} IS NULL`
|
|
48
|
-
: `${col} = ${addParam(value, baseType(key))}`);
|
|
49
|
-
break;
|
|
50
|
-
case "not":
|
|
51
|
-
conditions.push(value === null
|
|
52
|
-
? `${col} IS NOT NULL`
|
|
53
|
-
: `${col} != ${addParam(value, baseType(key))}`);
|
|
54
|
-
break;
|
|
55
|
-
case "in":
|
|
56
|
-
conditions.push(value.length === 0
|
|
57
|
-
? "0"
|
|
58
|
-
: `${col} IN ${addParam(value, `Array(${baseType(key)})`)}`);
|
|
59
|
-
break;
|
|
60
|
-
case "notIn":
|
|
61
|
-
if (value.length > 0) {
|
|
62
|
-
conditions.push(`${col} NOT IN ${addParam(value, `Array(${baseType(key)})`)}`);
|
|
63
|
-
}
|
|
64
|
-
break;
|
|
65
|
-
case "lt":
|
|
66
|
-
case "lte":
|
|
67
|
-
case "gt":
|
|
68
|
-
case "gte": {
|
|
69
|
-
const cmp = { lt: "<", lte: "<=", gt: ">", gte: ">=" }[op];
|
|
70
|
-
conditions.push(`${col} ${cmp} ${addParam(value, baseType(key))}`);
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
case "contains":
|
|
74
|
-
conditions.push(`position(${col}, ${addParam(value, "String")}) > 0`);
|
|
75
|
-
break;
|
|
76
|
-
case "startsWith":
|
|
77
|
-
conditions.push(`startsWith(${col}, ${addParam(value, "String")})`);
|
|
78
|
-
break;
|
|
79
|
-
case "endsWith":
|
|
80
|
-
conditions.push(`endsWith(${col}, ${addParam(value, "String")})`);
|
|
81
|
-
break;
|
|
82
|
-
default:
|
|
83
|
-
throw new Error(`Unknown operator "${op}" for column "${key}"`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return conditions;
|
|
87
|
-
}
|
|
88
|
-
function isFilterObject(value) {
|
|
89
|
-
return (value !== null &&
|
|
90
|
-
typeof value === "object" &&
|
|
91
|
-
!(value instanceof Date) &&
|
|
92
|
-
!Array.isArray(value));
|
|
93
|
-
}
|
|
94
|
-
function whereSQL(where) {
|
|
95
|
-
const parts = [];
|
|
96
|
-
for (const key in where) {
|
|
97
|
-
const value = where[key];
|
|
98
|
-
if (value === undefined)
|
|
99
|
-
continue;
|
|
100
|
-
if (key === "AND") {
|
|
101
|
-
const list = Array.isArray(value) ? value : [value];
|
|
102
|
-
for (const w of list) {
|
|
103
|
-
const c = whereSQL(w);
|
|
104
|
-
if (c)
|
|
105
|
-
parts.push(`(${c})`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
else if (key === "OR") {
|
|
109
|
-
const conds = value
|
|
110
|
-
.map((w) => whereSQL(w))
|
|
111
|
-
.filter(Boolean)
|
|
112
|
-
.map((c) => `(${c})`);
|
|
113
|
-
if (conds.length)
|
|
114
|
-
parts.push(`(${conds.join(" OR ")})`);
|
|
115
|
-
}
|
|
116
|
-
else if (key === "NOT") {
|
|
117
|
-
const list = Array.isArray(value) ? value : [value];
|
|
118
|
-
for (const w of list) {
|
|
119
|
-
const c = whereSQL(w);
|
|
120
|
-
if (c)
|
|
121
|
-
parts.push(`NOT (${c})`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
else if (value === null) {
|
|
125
|
-
parts.push(`${colRef(key)} IS NULL`);
|
|
126
|
-
}
|
|
127
|
-
else if (isFilterObject(value)) {
|
|
128
|
-
parts.push(...buildFieldFilter(key, value));
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
parts.push(`${colRef(key)} = ${addParam(value, baseType(key))}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return parts.join(" AND ");
|
|
135
|
-
}
|
|
136
|
-
return { params, colRef, baseType, addParam, whereSQL };
|
|
137
|
-
}
|
|
138
|
-
function tableRef(database, tableName, table) {
|
|
139
|
-
const ref = `${escapeId(database)}.${escapeId(tableName)}`;
|
|
140
|
-
// replacing tables must read the latest row versions
|
|
141
|
-
return table.mutations === "replacing" ? `${ref} FINAL` : ref;
|
|
142
|
-
}
|
|
143
|
-
function checkPagination(name, value) {
|
|
144
|
-
if (!Number.isInteger(value) || value < 0) {
|
|
145
|
-
throw new Error(`"${name}" must be a non-negative integer, got: ${value}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
export function buildFindManyQuery(database, tableName, table, args = {}) {
|
|
149
|
-
const ctx = createBuildContext(tableName, table.columns);
|
|
150
|
-
// SELECT
|
|
151
|
-
let selectSQL = "*";
|
|
152
|
-
if (args.select) {
|
|
153
|
-
const keys = Object.keys(args.select).filter(k => args.select[k] === true);
|
|
154
|
-
if (keys.length) {
|
|
155
|
-
selectSQL = keys.map(ctx.colRef).join(", ");
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const clauses = [
|
|
159
|
-
`SELECT ${selectSQL} FROM ${tableRef(database, tableName, table)}`
|
|
160
|
-
];
|
|
161
|
-
// WHERE
|
|
162
|
-
if (args.where) {
|
|
163
|
-
const where = ctx.whereSQL(args.where);
|
|
164
|
-
if (where)
|
|
165
|
-
clauses.push(`WHERE ${where}`);
|
|
166
|
-
}
|
|
167
|
-
// ORDER BY
|
|
168
|
-
if (args.orderBy) {
|
|
169
|
-
const list = Array.isArray(args.orderBy)
|
|
170
|
-
? args.orderBy
|
|
171
|
-
: [args.orderBy];
|
|
172
|
-
const orders = [];
|
|
173
|
-
for (const entry of list) {
|
|
174
|
-
for (const key in entry) {
|
|
175
|
-
const dir = entry[key];
|
|
176
|
-
if (dir === undefined)
|
|
177
|
-
continue;
|
|
178
|
-
if (dir !== "asc" && dir !== "desc") {
|
|
179
|
-
throw new Error(`Invalid sort direction "${dir}" for column "${key}"`);
|
|
180
|
-
}
|
|
181
|
-
orders.push(`${ctx.colRef(key)} ${dir.toUpperCase()}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (orders.length)
|
|
185
|
-
clauses.push(`ORDER BY ${orders.join(", ")}`);
|
|
186
|
-
}
|
|
187
|
-
// LIMIT / OFFSET
|
|
188
|
-
if (args.take !== undefined)
|
|
189
|
-
checkPagination("take", args.take);
|
|
190
|
-
if (args.skip !== undefined)
|
|
191
|
-
checkPagination("skip", args.skip);
|
|
192
|
-
if (args.take !== undefined && args.skip !== undefined) {
|
|
193
|
-
clauses.push(`LIMIT ${args.take} OFFSET ${args.skip}`);
|
|
194
|
-
}
|
|
195
|
-
else if (args.take !== undefined) {
|
|
196
|
-
clauses.push(`LIMIT ${args.take}`);
|
|
197
|
-
}
|
|
198
|
-
else if (args.skip !== undefined) {
|
|
199
|
-
clauses.push(`LIMIT ${NO_LIMIT} OFFSET ${args.skip}`);
|
|
200
|
-
}
|
|
201
|
-
return { sql: clauses.join(" "), params: ctx.params };
|
|
202
|
-
}
|
|
203
|
-
export function buildCountQuery(database, tableName, table, where) {
|
|
204
|
-
const inner = buildFindManyQuery(database, tableName, table, { where });
|
|
205
|
-
// with no select arg the query starts with "SELECT * FROM ..." —
|
|
206
|
-
// swap the projection for count()
|
|
207
|
-
const from = inner.sql.indexOf(" FROM ");
|
|
208
|
-
return {
|
|
209
|
-
sql: "SELECT count() AS count" + inner.sql.slice(from),
|
|
210
|
-
params: inner.params
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
// shared by buildUpdateQuery and the replacing-strategy update path
|
|
214
|
-
export function validateUpdateData(table, tableName, data) {
|
|
215
|
-
const keys = Object.keys(data).filter(k => data[k] !== undefined);
|
|
216
|
-
if (keys.length === 0) {
|
|
217
|
-
throw new Error(`updateMany requires non-empty data`);
|
|
218
|
-
}
|
|
219
|
-
for (const key of keys) {
|
|
220
|
-
if (table.orderBy.includes(key)) {
|
|
221
|
-
throw new Error(`Cannot update ORDER BY key column "${key}" ` +
|
|
222
|
-
`of table "${tableName}"`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return keys;
|
|
226
|
-
}
|
|
227
|
-
export function buildUpdateQuery(database, tableName, table, where, data) {
|
|
228
|
-
const keys = validateUpdateData(table, tableName, data);
|
|
229
|
-
const ctx = createBuildContext(tableName, table.columns);
|
|
230
|
-
const sets = keys.map(k => `${ctx.colRef(k)} = ${ctx.addParam(data[k], ctx.baseType(k))}`);
|
|
231
|
-
const whereSQL = where ? ctx.whereSQL(where) : "";
|
|
232
|
-
return {
|
|
233
|
-
sql: `ALTER TABLE ${escapeId(database)}.${escapeId(tableName)} ` +
|
|
234
|
-
`UPDATE ${sets.join(", ")} WHERE ${whereSQL || "1"}`,
|
|
235
|
-
params: ctx.params
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
export function buildDeleteQuery(database, tableName, table, where) {
|
|
239
|
-
const ctx = createBuildContext(tableName, table.columns);
|
|
240
|
-
const whereSQL = where ? ctx.whereSQL(where) : "";
|
|
241
|
-
return {
|
|
242
|
-
sql: `ALTER TABLE ${escapeId(database)}.${escapeId(tableName)} ` +
|
|
243
|
-
`DELETE WHERE ${whereSQL || "1"}`,
|
|
244
|
-
params: ctx.params
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
export function buildLightweightDeleteQuery(database, tableName, table, where) {
|
|
248
|
-
const ctx = createBuildContext(tableName, table.columns);
|
|
249
|
-
const whereSQL = where ? ctx.whereSQL(where) : "";
|
|
250
|
-
return {
|
|
251
|
-
sql: `DELETE FROM ${escapeId(database)}.${escapeId(tableName)} ` +
|
|
252
|
-
`WHERE ${whereSQL || "1"}`,
|
|
253
|
-
params: ctx.params
|
|
254
|
-
};
|
|
255
|
-
}
|
|
2
|
+
// Public surface of the SQL builders; implementation is split by function
|
|
3
|
+
// (one function per file), shared helpers live in sql-shared.ts.
|
|
4
|
+
export { escapeId, validateUpdateData, } from "./sql-shared.js";
|
|
5
|
+
export { buildFindManyQuery } from "./build-find-many-query.js";
|
|
6
|
+
export { buildCountQuery } from "./build-count-query.js";
|
|
7
|
+
export { buildUpdateQuery } from "./build-update-query.js";
|
|
8
|
+
export { buildDeleteQuery } from "./build-delete-query.js";
|
|
9
|
+
export { buildLightweightDeleteQuery } from "./build-lightweight-delete-query.js";
|
package/dist/table-client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/table-client.ts
|
|
2
|
-
import { buildFindManyQuery, buildCountQuery, buildUpdateQuery, buildDeleteQuery, buildLightweightDeleteQuery, validateUpdateData, escapeId } from "./sql.js";
|
|
2
|
+
import { buildFindManyQuery, buildCountQuery, buildUpdateQuery, buildDeleteQuery, buildLightweightDeleteQuery, validateUpdateData, escapeId, } from "./sql.js";
|
|
3
3
|
import { deserializeRows } from "./rows.js";
|
|
4
4
|
// process-unique id: ms timestamp in the high bits + 20-bit counter
|
|
5
5
|
let idCounter = 0n;
|
|
@@ -31,8 +31,8 @@ export class TableClient {
|
|
|
31
31
|
date_time_output_format: "iso",
|
|
32
32
|
// force 64-bit ints to arrive as strings so BigInt() is
|
|
33
33
|
// lossless even if the server default was changed
|
|
34
|
-
output_format_json_quote_64bit_integers: 1
|
|
35
|
-
}
|
|
34
|
+
output_format_json_quote_64bit_integers: 1,
|
|
35
|
+
},
|
|
36
36
|
});
|
|
37
37
|
return result.json();
|
|
38
38
|
}
|
|
@@ -65,7 +65,8 @@ export class TableClient {
|
|
|
65
65
|
// subsequent read returns: omitted nullable columns are null,
|
|
66
66
|
// DateTime values are stored with second precision
|
|
67
67
|
for (const colName in this.table.columns) {
|
|
68
|
-
if (full[colName] === undefined &&
|
|
68
|
+
if (full[colName] === undefined &&
|
|
69
|
+
this.table.columns[colName].nullable) {
|
|
69
70
|
full[colName] = null;
|
|
70
71
|
}
|
|
71
72
|
else if (full[colName] instanceof Date) {
|
|
@@ -103,8 +104,8 @@ export class TableClient {
|
|
|
103
104
|
// with wait_for_async_insert=0 on the server, inserts are
|
|
104
105
|
// acked before flush: errors are swallowed and rows are not
|
|
105
106
|
// immediately readable — force read-your-writes semantics
|
|
106
|
-
wait_for_async_insert: 1
|
|
107
|
-
}
|
|
107
|
+
wait_for_async_insert: 1,
|
|
108
|
+
},
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
async insert(rows) {
|
|
@@ -133,7 +134,7 @@ export class TableClient {
|
|
|
133
134
|
const patch = {};
|
|
134
135
|
for (const k of keys)
|
|
135
136
|
patch[k] = args.data[k];
|
|
136
|
-
const { wire } = this.prepareRows(rows.map(r => ({ ...r, ...patch })));
|
|
137
|
+
const { wire } = this.prepareRows(rows.map((r) => ({ ...r, ...patch })));
|
|
137
138
|
await this.insertWire(wire);
|
|
138
139
|
return { count: rows.length };
|
|
139
140
|
}
|
|
@@ -148,8 +149,8 @@ export class TableClient {
|
|
|
148
149
|
query_params: params,
|
|
149
150
|
clickhouse_settings: {
|
|
150
151
|
// wait for the mutation so subsequent reads see the change
|
|
151
|
-
mutations_sync: "1"
|
|
152
|
-
}
|
|
152
|
+
mutations_sync: "1",
|
|
153
|
+
},
|
|
153
154
|
});
|
|
154
155
|
return { count };
|
|
155
156
|
}
|
|
@@ -162,7 +163,7 @@ export class TableClient {
|
|
|
162
163
|
await this.client.command({
|
|
163
164
|
query: sql,
|
|
164
165
|
query_params: params,
|
|
165
|
-
clickhouse_settings: { mutations_sync: "1" }
|
|
166
|
+
clickhouse_settings: { mutations_sync: "1" },
|
|
166
167
|
});
|
|
167
168
|
}
|
|
168
169
|
else {
|
|
@@ -171,7 +172,7 @@ export class TableClient {
|
|
|
171
172
|
await this.client.command({
|
|
172
173
|
query: sql,
|
|
173
174
|
query_params: params,
|
|
174
|
-
clickhouse_settings: { lightweight_deletes_sync: "2" }
|
|
175
|
+
clickhouse_settings: { lightweight_deletes_sync: "2" },
|
|
175
176
|
});
|
|
176
177
|
}
|
|
177
178
|
return { count };
|