@quark-fw/clisma 0.1.25 → 0.1.27
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/src/assert-replicated-schema.d.ts +2 -0
- package/dist/src/assert-replicated-schema.js +16 -0
- package/dist/src/build-count-query.d.ts +4 -0
- package/dist/src/build-count-query.js +13 -0
- package/dist/src/build-create-dictionary-query.d.ts +2 -0
- package/dist/src/build-create-dictionary-query.js +51 -0
- package/dist/src/build-delete-query.d.ts +4 -0
- package/dist/src/build-delete-query.js +11 -0
- package/dist/src/build-find-many-query.d.ts +3 -0
- package/dist/src/build-find-many-query.js +58 -0
- package/dist/src/build-index-sql.d.ts +2 -0
- package/dist/src/build-index-sql.js +11 -0
- package/dist/src/build-lightweight-delete-query.d.ts +4 -0
- package/dist/src/build-lightweight-delete-query.js +11 -0
- package/dist/src/build-update-query.d.ts +4 -0
- package/dist/src/build-update-query.js +13 -0
- package/dist/src/client.d.ts +36 -0
- package/dist/src/client.js +139 -0
- package/dist/src/column.d.ts +46 -0
- package/dist/src/column.js +79 -0
- package/dist/src/ddl-shared.d.ts +13 -0
- package/dist/src/ddl-shared.js +54 -0
- package/dist/src/ddl.d.ts +5 -0
- package/dist/src/ddl.js +52 -0
- package/dist/src/dictionary.d.ts +26 -0
- package/dist/src/dictionary.js +7 -0
- package/dist/src/engine.d.ts +16 -0
- package/dist/src/engine.js +26 -0
- package/dist/src/ensure-dictionaries-on-nodes.d.ts +3 -0
- package/dist/src/ensure-dictionaries-on-nodes.js +15 -0
- package/dist/src/ensure-tables-on-nodes.d.ts +11 -0
- package/dist/src/ensure-tables-on-nodes.js +39 -0
- package/dist/src/escape-id.d.ts +1 -0
- package/dist/src/escape-id.js +4 -0
- package/dist/src/expr.d.ts +5 -0
- package/dist/src/expr.js +4 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +15 -0
- package/dist/src/migrate.d.ts +14 -0
- package/dist/src/migrate.js +91 -0
- package/dist/src/migration-error.d.ts +10 -0
- package/dist/src/migration-error.js +24 -0
- package/dist/src/query.d.ts +26 -0
- package/dist/src/query.js +60 -0
- package/dist/src/rows.d.ts +2 -0
- package/dist/src/rows.js +45 -0
- package/dist/src/schema.d.ts +8 -0
- package/dist/src/schema.js +4 -0
- package/dist/src/sql-shared.d.ts +33 -0
- package/dist/src/sql-shared.js +171 -0
- package/dist/src/sql.d.ts +6 -0
- package/dist/src/sql.js +9 -0
- package/dist/src/table-client.d.ts +55 -0
- package/dist/src/table-client.js +198 -0
- package/dist/src/table.d.ts +37 -0
- package/dist/src/table.js +30 -0
- package/dist/src/types.d.ts +4 -0
- package/dist/src/types.js +2 -0
- package/dist/src/where.d.ts +24 -0
- package/dist/src/where.js +2 -0
- package/package.json +3 -3
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// src/sql-shared.ts
|
|
2
|
+
// Shared helpers and types for the SQL builders (build-*.ts).
|
|
3
|
+
import { escapeId } from "./escape-id.js";
|
|
4
|
+
import { collapsingOf, keyColumns } from "./ddl-shared.js";
|
|
5
|
+
// escapeId живёт отдельным файлом: он нужен и рендеру DDL, и билдерам
|
|
6
|
+
// запросов, а взаимный импорт между ними создал бы цикл модулей
|
|
7
|
+
export { escapeId } from "./escape-id.js";
|
|
8
|
+
// UInt64 max — used to emulate a bare OFFSET (skip without take)
|
|
9
|
+
export const NO_LIMIT = "18446744073709551615";
|
|
10
|
+
// DateTime is second-precision; the client would send Date with
|
|
11
|
+
// a fractional part, which ClickHouse rejects — send unix seconds.
|
|
12
|
+
function serialize(value) {
|
|
13
|
+
if (value instanceof Date) {
|
|
14
|
+
return Math.floor(value.getTime() / 1000);
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return value.map(serialize);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export function createBuildContext(tableName, columns) {
|
|
22
|
+
const params = {};
|
|
23
|
+
let paramIndex = 0;
|
|
24
|
+
function colRef(key) {
|
|
25
|
+
if (!columns[key]) {
|
|
26
|
+
throw new Error(`Unknown column "${key}" in table "${tableName}"`);
|
|
27
|
+
}
|
|
28
|
+
return escapeId(key);
|
|
29
|
+
}
|
|
30
|
+
function baseType(key) {
|
|
31
|
+
const chType = columns[key].clickhouseType;
|
|
32
|
+
const m = /^Nullable\((.+)\)$/.exec(chType);
|
|
33
|
+
return m ? m[1] : chType;
|
|
34
|
+
}
|
|
35
|
+
function addParam(value, type) {
|
|
36
|
+
const name = `p${paramIndex++}`;
|
|
37
|
+
params[name] = serialize(value);
|
|
38
|
+
return `{${name}:${type}}`;
|
|
39
|
+
}
|
|
40
|
+
function buildFieldFilter(key, filter) {
|
|
41
|
+
const col = colRef(key);
|
|
42
|
+
const conditions = [];
|
|
43
|
+
for (const op in filter) {
|
|
44
|
+
const value = filter[op];
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
switch (op) {
|
|
48
|
+
case "equals":
|
|
49
|
+
conditions.push(value === null
|
|
50
|
+
? `${col} IS NULL`
|
|
51
|
+
: `${col} = ${addParam(value, baseType(key))}`);
|
|
52
|
+
break;
|
|
53
|
+
case "not":
|
|
54
|
+
conditions.push(value === null
|
|
55
|
+
? `${col} IS NOT NULL`
|
|
56
|
+
: `${col} != ${addParam(value, baseType(key))}`);
|
|
57
|
+
break;
|
|
58
|
+
case "in":
|
|
59
|
+
conditions.push(value.length === 0
|
|
60
|
+
? "0"
|
|
61
|
+
: `${col} IN ${addParam(value, `Array(${baseType(key)})`)}`);
|
|
62
|
+
break;
|
|
63
|
+
case "notIn":
|
|
64
|
+
if (value.length > 0) {
|
|
65
|
+
conditions.push(`${col} NOT IN ${addParam(value, `Array(${baseType(key)})`)}`);
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
case "lt":
|
|
69
|
+
case "lte":
|
|
70
|
+
case "gt":
|
|
71
|
+
case "gte": {
|
|
72
|
+
const cmp = { lt: "<", lte: "<=", gt: ">", gte: ">=" }[op];
|
|
73
|
+
conditions.push(`${col} ${cmp} ${addParam(value, baseType(key))}`);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "contains":
|
|
77
|
+
conditions.push(`position(${col}, ${addParam(value, "String")}) > 0`);
|
|
78
|
+
break;
|
|
79
|
+
case "startsWith":
|
|
80
|
+
conditions.push(`startsWith(${col}, ${addParam(value, "String")})`);
|
|
81
|
+
break;
|
|
82
|
+
case "endsWith":
|
|
83
|
+
conditions.push(`endsWith(${col}, ${addParam(value, "String")})`);
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(`Unknown operator "${op}" for column "${key}"`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return conditions;
|
|
90
|
+
}
|
|
91
|
+
function isFilterObject(value) {
|
|
92
|
+
return (value !== null &&
|
|
93
|
+
typeof value === "object" &&
|
|
94
|
+
!(value instanceof Date) &&
|
|
95
|
+
!Array.isArray(value));
|
|
96
|
+
}
|
|
97
|
+
function whereSQL(where) {
|
|
98
|
+
const parts = [];
|
|
99
|
+
for (const key in where) {
|
|
100
|
+
const value = where[key];
|
|
101
|
+
if (value === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
if (key === "AND") {
|
|
104
|
+
const list = (Array.isArray(value) ? value : [value]);
|
|
105
|
+
for (const w of list) {
|
|
106
|
+
const c = whereSQL(w);
|
|
107
|
+
if (c)
|
|
108
|
+
parts.push(`(${c})`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (key === "OR") {
|
|
112
|
+
const conds = value
|
|
113
|
+
.map((w) => whereSQL(w))
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.map((c) => `(${c})`);
|
|
116
|
+
if (conds.length)
|
|
117
|
+
parts.push(`(${conds.join(" OR ")})`);
|
|
118
|
+
}
|
|
119
|
+
else if (key === "NOT") {
|
|
120
|
+
const list = (Array.isArray(value) ? value : [value]);
|
|
121
|
+
for (const w of list) {
|
|
122
|
+
const c = whereSQL(w);
|
|
123
|
+
if (c)
|
|
124
|
+
parts.push(`NOT (${c})`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (value === null) {
|
|
128
|
+
parts.push(`${colRef(key)} IS NULL`);
|
|
129
|
+
}
|
|
130
|
+
else if (isFilterObject(value)) {
|
|
131
|
+
parts.push(...buildFieldFilter(key, value));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
parts.push(`${colRef(key)} = ${addParam(value, baseType(key))}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return parts.join(" AND ");
|
|
138
|
+
}
|
|
139
|
+
return { params, colRef, baseType, addParam, whereSQL };
|
|
140
|
+
}
|
|
141
|
+
export function tableRef(database, tableName, table, final = false) {
|
|
142
|
+
const ref = `${escapeId(database)}.${escapeId(tableName)}`;
|
|
143
|
+
// replacing-таблицы обязаны читать последние версии строк; для остальных
|
|
144
|
+
// FINAL включается явно (например, чтобы прочитать коллапсирующую таблицу)
|
|
145
|
+
return table.mutations === "replacing" || final ? `${ref} FINAL` : ref;
|
|
146
|
+
}
|
|
147
|
+
// у коллапсирующих таблиц строка «существует», только если её знак не
|
|
148
|
+
// погашен парой -1 — count() посчитал бы обе строки пары
|
|
149
|
+
export function countExpr(table) {
|
|
150
|
+
const collapsing = collapsingOf(table.engine);
|
|
151
|
+
return collapsing ? `sum(${escapeId(collapsing.sign)})` : "count()";
|
|
152
|
+
}
|
|
153
|
+
export function checkPagination(name, value) {
|
|
154
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
155
|
+
throw new Error(`"${name}" must be a non-negative integer, got: ${value}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// shared by buildUpdateQuery and the replacing-strategy update path
|
|
159
|
+
export function validateUpdateData(table, tableName, data) {
|
|
160
|
+
const keys = Object.keys(data).filter((k) => data[k] !== undefined);
|
|
161
|
+
if (keys.length === 0) {
|
|
162
|
+
throw new Error(`updateMany requires non-empty data`);
|
|
163
|
+
}
|
|
164
|
+
for (const key of keys) {
|
|
165
|
+
if (keyColumns(table.orderBy).includes(key)) {
|
|
166
|
+
throw new Error(`Cannot update ORDER BY key column "${key}" ` +
|
|
167
|
+
`of table "${tableName}"`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return keys;
|
|
171
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
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/src/sql.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/sql.ts
|
|
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";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ClickHouseClient } from "@clickhouse/client";
|
|
2
|
+
import { AnyTable } from "./table.js";
|
|
3
|
+
import { Simplify } from "./types.js";
|
|
4
|
+
import { WhereInput } from "./where.js";
|
|
5
|
+
import { FindManyArgs } from "./sql.js";
|
|
6
|
+
export type FindManyResult<T, A> = A extends {
|
|
7
|
+
select: infer S;
|
|
8
|
+
} ? Simplify<{
|
|
9
|
+
[K in keyof S & keyof T as S[K] extends true ? K : never]: T[K];
|
|
10
|
+
}> : T;
|
|
11
|
+
type NullableKeys<T> = {
|
|
12
|
+
[K in keyof T]-?: null extends T[K] ? K : never;
|
|
13
|
+
}[keyof T];
|
|
14
|
+
type OptionalCreateKeys<T> = NullableKeys<T> | ("id" & keyof T);
|
|
15
|
+
export type CreateData<T> = Simplify<Omit<T, OptionalCreateKeys<T>> & {
|
|
16
|
+
[K in OptionalCreateKeys<T>]?: T[K];
|
|
17
|
+
}>;
|
|
18
|
+
export declare class TableClient<T extends Record<string, unknown>> {
|
|
19
|
+
private database;
|
|
20
|
+
private tableName;
|
|
21
|
+
private table;
|
|
22
|
+
private client;
|
|
23
|
+
constructor(database: string, tableName: string, table: AnyTable, client: ClickHouseClient);
|
|
24
|
+
private get tableRef();
|
|
25
|
+
private runQuery;
|
|
26
|
+
findMany<A extends FindManyArgs<T>>(args?: A): Promise<FindManyResult<T, A>[]>;
|
|
27
|
+
findFirst<A extends Omit<FindManyArgs<T>, "take">>(args?: A): Promise<FindManyResult<T, A> | null>;
|
|
28
|
+
count(args?: {
|
|
29
|
+
where?: WhereInput<T>;
|
|
30
|
+
}): Promise<number>;
|
|
31
|
+
private prepareRows;
|
|
32
|
+
private insertWire;
|
|
33
|
+
insert(rows: Partial<T> | Partial<T>[]): Promise<void>;
|
|
34
|
+
create(args: {
|
|
35
|
+
data: CreateData<T>;
|
|
36
|
+
}): Promise<T>;
|
|
37
|
+
private get strategy();
|
|
38
|
+
updateMany(args: {
|
|
39
|
+
where?: WhereInput<T>;
|
|
40
|
+
data: Partial<T>;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
count: number;
|
|
43
|
+
}>;
|
|
44
|
+
deleteMany(args?: {
|
|
45
|
+
where?: WhereInput<T>;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
count: number;
|
|
48
|
+
}>;
|
|
49
|
+
createMany(args: {
|
|
50
|
+
data: CreateData<T>[];
|
|
51
|
+
}): Promise<{
|
|
52
|
+
count: number;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// src/table-client.ts
|
|
2
|
+
import { buildFindManyQuery, buildCountQuery, buildUpdateQuery, buildDeleteQuery, buildLightweightDeleteQuery, validateUpdateData, escapeId, } from "./sql.js";
|
|
3
|
+
import { deserializeRows } from "./rows.js";
|
|
4
|
+
import { collapsingOf } from "./ddl-shared.js";
|
|
5
|
+
// process-unique id: ms timestamp in the high bits + 20-bit counter
|
|
6
|
+
let idCounter = 0n;
|
|
7
|
+
function generateId() {
|
|
8
|
+
return (BigInt(Date.now()) << 20n) | (idCounter++ & 0xfffffn);
|
|
9
|
+
}
|
|
10
|
+
export class TableClient {
|
|
11
|
+
database;
|
|
12
|
+
tableName;
|
|
13
|
+
table;
|
|
14
|
+
client;
|
|
15
|
+
constructor(database, tableName, table, client) {
|
|
16
|
+
this.database = database;
|
|
17
|
+
this.tableName = tableName;
|
|
18
|
+
this.table = table;
|
|
19
|
+
this.client = client;
|
|
20
|
+
}
|
|
21
|
+
get tableRef() {
|
|
22
|
+
return `${escapeId(this.database)}.${escapeId(this.tableName)}`;
|
|
23
|
+
}
|
|
24
|
+
async runQuery(sql, params, settings = {}) {
|
|
25
|
+
const result = await this.client.query({
|
|
26
|
+
query: sql,
|
|
27
|
+
query_params: params,
|
|
28
|
+
format: "JSONEachRow",
|
|
29
|
+
clickhouse_settings: {
|
|
30
|
+
...settings,
|
|
31
|
+
// ISO with Z: the default DateTime rendering is in server
|
|
32
|
+
// timezone with no offset, which is unparseable correctly
|
|
33
|
+
date_time_output_format: "iso",
|
|
34
|
+
// force 64-bit ints to arrive as strings so BigInt() is
|
|
35
|
+
// lossless even if the server default was changed
|
|
36
|
+
output_format_json_quote_64bit_integers: 1,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
return result.json();
|
|
40
|
+
}
|
|
41
|
+
async findMany(args) {
|
|
42
|
+
// сырые строки коллапсирующей таблицы содержат и погашенные версии —
|
|
43
|
+
// без FINAL такой результат вводит в заблуждение
|
|
44
|
+
if (collapsingOf(this.table.engine) && !args?.final) {
|
|
45
|
+
throw new Error(`Table "${this.tableName}" uses a collapsing engine: pass final: true to findMany`);
|
|
46
|
+
}
|
|
47
|
+
const { sql, params } = buildFindManyQuery(this.database, this.tableName, this.table, args);
|
|
48
|
+
const rows = await this.runQuery(sql, params, args?.settings);
|
|
49
|
+
return deserializeRows(rows, this.table.columns);
|
|
50
|
+
}
|
|
51
|
+
async findFirst(args) {
|
|
52
|
+
const rows = await this.findMany({
|
|
53
|
+
...args,
|
|
54
|
+
take: 1,
|
|
55
|
+
});
|
|
56
|
+
return rows[0] ?? null;
|
|
57
|
+
}
|
|
58
|
+
async count(args) {
|
|
59
|
+
const { sql, params } = buildCountQuery(this.database, this.tableName, this.table, args?.where);
|
|
60
|
+
const rows = await this.runQuery(sql, params);
|
|
61
|
+
return Number(rows[0].count);
|
|
62
|
+
}
|
|
63
|
+
// wire: values serialized for JSONEachRow; completed: JS values
|
|
64
|
+
// including the generated id (what create() returns)
|
|
65
|
+
prepareRows(arr) {
|
|
66
|
+
const hasIdColumn = "id" in this.table.columns;
|
|
67
|
+
const wire = [];
|
|
68
|
+
const completed = [];
|
|
69
|
+
for (const r of arr) {
|
|
70
|
+
const full = { ...r };
|
|
71
|
+
if (hasIdColumn && full.id === undefined) {
|
|
72
|
+
full.id = generateId();
|
|
73
|
+
}
|
|
74
|
+
// the returned row must match both its type and what a
|
|
75
|
+
// subsequent read returns: omitted nullable columns are null,
|
|
76
|
+
// DateTime values are stored with second precision
|
|
77
|
+
for (const colName in this.table.columns) {
|
|
78
|
+
if (full[colName] === undefined &&
|
|
79
|
+
this.table.columns[colName].nullable) {
|
|
80
|
+
full[colName] = null;
|
|
81
|
+
}
|
|
82
|
+
else if (full[colName] instanceof Date) {
|
|
83
|
+
full[colName] = new Date(Math.floor(full[colName].getTime() / 1000) *
|
|
84
|
+
1000);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const obj = {};
|
|
88
|
+
for (const k in full) {
|
|
89
|
+
const v = full[k];
|
|
90
|
+
if (v === undefined)
|
|
91
|
+
continue;
|
|
92
|
+
if (typeof v === "bigint") {
|
|
93
|
+
obj[k] = v.toString();
|
|
94
|
+
}
|
|
95
|
+
else if (v instanceof Date) {
|
|
96
|
+
// unix seconds: ISO strings are rejected by DateTime
|
|
97
|
+
// columns in the default (basic) input format
|
|
98
|
+
obj[k] = Math.floor(v.getTime() / 1000);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
obj[k] = v;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
wire.push(obj);
|
|
105
|
+
completed.push(full);
|
|
106
|
+
}
|
|
107
|
+
return { wire, completed };
|
|
108
|
+
}
|
|
109
|
+
async insertWire(wire) {
|
|
110
|
+
await this.client.insert({
|
|
111
|
+
table: this.tableRef,
|
|
112
|
+
values: wire,
|
|
113
|
+
format: "JSONEachRow",
|
|
114
|
+
clickhouse_settings: {
|
|
115
|
+
// with wait_for_async_insert=0 on the server, inserts are
|
|
116
|
+
// acked before flush: errors are swallowed and rows are not
|
|
117
|
+
// immediately readable — force read-your-writes semantics
|
|
118
|
+
wait_for_async_insert: 1,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async insert(rows) {
|
|
123
|
+
const arr = Array.isArray(rows) ? rows : [rows];
|
|
124
|
+
if (arr.length === 0)
|
|
125
|
+
return;
|
|
126
|
+
const { wire } = this.prepareRows(arr);
|
|
127
|
+
await this.insertWire(wire);
|
|
128
|
+
}
|
|
129
|
+
async create(args) {
|
|
130
|
+
const { wire, completed } = this.prepareRows([args.data]);
|
|
131
|
+
await this.insertWire(wire);
|
|
132
|
+
return completed[0];
|
|
133
|
+
}
|
|
134
|
+
get strategy() {
|
|
135
|
+
return this.table.mutations ?? "alter";
|
|
136
|
+
}
|
|
137
|
+
async updateMany(args) {
|
|
138
|
+
if (this.strategy === "replacing") {
|
|
139
|
+
const keys = validateUpdateData(this.table, this.tableName, args.data);
|
|
140
|
+
// re-insert new versions of the matching rows;
|
|
141
|
+
// FINAL on the read keeps this idempotent
|
|
142
|
+
const rows = await this.findMany({ where: args.where });
|
|
143
|
+
if (rows.length === 0)
|
|
144
|
+
return { count: 0 };
|
|
145
|
+
const patch = {};
|
|
146
|
+
for (const k of keys)
|
|
147
|
+
patch[k] = args.data[k];
|
|
148
|
+
const { wire } = this.prepareRows(rows.map((r) => ({ ...r, ...patch })));
|
|
149
|
+
await this.insertWire(wire);
|
|
150
|
+
return { count: rows.length };
|
|
151
|
+
}
|
|
152
|
+
// "alter" and "lightweight" (ClickHouse has no stable lightweight
|
|
153
|
+
// UPDATE — lightweight tables fall back to a heavy ALTER mutation)
|
|
154
|
+
const { sql, params } = buildUpdateQuery(this.database, this.tableName, this.table, args.where, args.data);
|
|
155
|
+
const count = await this.count({ where: args.where });
|
|
156
|
+
if (count === 0)
|
|
157
|
+
return { count: 0 };
|
|
158
|
+
await this.client.command({
|
|
159
|
+
query: sql,
|
|
160
|
+
query_params: params,
|
|
161
|
+
clickhouse_settings: {
|
|
162
|
+
// wait for the mutation so subsequent reads see the change
|
|
163
|
+
mutations_sync: "1",
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
return { count };
|
|
167
|
+
}
|
|
168
|
+
async deleteMany(args) {
|
|
169
|
+
const count = await this.count({ where: args?.where });
|
|
170
|
+
if (count === 0)
|
|
171
|
+
return { count: 0 };
|
|
172
|
+
if (this.strategy === "alter") {
|
|
173
|
+
const { sql, params } = buildDeleteQuery(this.database, this.tableName, this.table, args?.where);
|
|
174
|
+
await this.client.command({
|
|
175
|
+
query: sql,
|
|
176
|
+
query_params: params,
|
|
177
|
+
clickhouse_settings: { mutations_sync: "1" },
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// "lightweight" and "replacing": fast tombstone-based delete
|
|
182
|
+
const { sql, params } = buildLightweightDeleteQuery(this.database, this.tableName, this.table, args?.where);
|
|
183
|
+
await this.client.command({
|
|
184
|
+
query: sql,
|
|
185
|
+
query_params: params,
|
|
186
|
+
clickhouse_settings: { lightweight_deletes_sync: "2" },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return { count };
|
|
190
|
+
}
|
|
191
|
+
async createMany(args) {
|
|
192
|
+
if (args.data.length === 0)
|
|
193
|
+
return { count: 0 };
|
|
194
|
+
const { wire } = this.prepareRows(args.data);
|
|
195
|
+
await this.insertWire(wire);
|
|
196
|
+
return { count: args.data.length };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Column } from "./column.js";
|
|
2
|
+
import type { EngineSpec } from "./engine.js";
|
|
3
|
+
import type { KeyPart } from "./expr.js";
|
|
4
|
+
import { Simplify } from "./types.js";
|
|
5
|
+
export type MutationStrategy = "alter" | "lightweight" | "replacing";
|
|
6
|
+
export type IndexType = "bloom_filter" | "set" | "minmax" | "tokenbf_v1" | "ngrambf_v1";
|
|
7
|
+
export type IndexDefinition = {
|
|
8
|
+
name: string;
|
|
9
|
+
expr: KeyPart;
|
|
10
|
+
type: IndexType;
|
|
11
|
+
args?: (number | string)[];
|
|
12
|
+
granularity?: number;
|
|
13
|
+
};
|
|
14
|
+
export type TableDefinition<TColumns extends Record<string, Column<unknown>>> = {
|
|
15
|
+
engine: string | EngineSpec;
|
|
16
|
+
replication?: {
|
|
17
|
+
path: string;
|
|
18
|
+
replica: string;
|
|
19
|
+
};
|
|
20
|
+
orderBy: KeyPart[];
|
|
21
|
+
primaryKey?: KeyPart[];
|
|
22
|
+
partitionBy?: KeyPart;
|
|
23
|
+
sampleBy?: KeyPart;
|
|
24
|
+
indexes?: IndexDefinition[];
|
|
25
|
+
ttl?: string;
|
|
26
|
+
settings?: Record<string, string | number>;
|
|
27
|
+
comment?: string;
|
|
28
|
+
mutations?: MutationStrategy;
|
|
29
|
+
columns: TColumns;
|
|
30
|
+
};
|
|
31
|
+
export type AnyColumns = Record<string, Column<unknown>>;
|
|
32
|
+
export type AnyTable = TableDefinition<AnyColumns>;
|
|
33
|
+
export declare function table<TColumns extends Record<string, Column<unknown>>>(def: TableDefinition<TColumns>): TableDefinition<TColumns>;
|
|
34
|
+
export type InferColumns<TColumns extends Record<string, Column<unknown>>> = Simplify<{
|
|
35
|
+
[K in keyof TColumns]: TColumns[K] extends Column<infer T> ? T : never;
|
|
36
|
+
}>;
|
|
37
|
+
export type InferRow<TTable> = TTable extends TableDefinition<infer TColumns> ? InferColumns<TColumns> : never;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/table.ts
|
|
2
|
+
import { collapsingOf, isReplacingEngine, keyColumns } from "./ddl-shared.js";
|
|
3
|
+
const INTEGER_TYPE = /^(U?Int(8|16|32|64))$/;
|
|
4
|
+
export function table(def) {
|
|
5
|
+
if (def.mutations === "replacing" && !isReplacingEngine(def.engine)) {
|
|
6
|
+
throw new Error(`mutations: "replacing" requires a ReplacingMergeTree engine ` +
|
|
7
|
+
`(got "${typeof def.engine === "string" ? def.engine : def.engine.family}")`);
|
|
8
|
+
}
|
|
9
|
+
// коллапс работает только с целочисленным знаком и версией — ошибка
|
|
10
|
+
// схемы должна всплыть при объявлении, а не при первом INSERT
|
|
11
|
+
const collapsing = collapsingOf(def.engine);
|
|
12
|
+
if (collapsing) {
|
|
13
|
+
const sign = def.columns[collapsing.sign];
|
|
14
|
+
if (!sign || sign.clickhouseType !== "Int8") {
|
|
15
|
+
throw new Error(`Collapsing engine requires sign column "${collapsing.sign}" of type Int8`);
|
|
16
|
+
}
|
|
17
|
+
if (collapsing.version) {
|
|
18
|
+
const version = def.columns[collapsing.version];
|
|
19
|
+
if (!version || !INTEGER_TYPE.test(version.clickhouseType)) {
|
|
20
|
+
throw new Error(`VersionedCollapsingMergeTree requires integer version column "${collapsing.version}"`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const key of keyColumns([...def.orderBy, ...(def.primaryKey ?? [])])) {
|
|
25
|
+
if (!def.columns[key]) {
|
|
26
|
+
throw new Error(`Key column "${key}" is not declared in columns`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return def;
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type ComparableFilter<T> = {
|
|
2
|
+
lt?: T;
|
|
3
|
+
lte?: T;
|
|
4
|
+
gt?: T;
|
|
5
|
+
gte?: T;
|
|
6
|
+
};
|
|
7
|
+
export type StringFilter = {
|
|
8
|
+
contains?: string;
|
|
9
|
+
startsWith?: string;
|
|
10
|
+
endsWith?: string;
|
|
11
|
+
};
|
|
12
|
+
export type ScalarFilter<T> = {
|
|
13
|
+
equals?: T | null;
|
|
14
|
+
not?: T | null;
|
|
15
|
+
in?: T[];
|
|
16
|
+
notIn?: T[];
|
|
17
|
+
} & (T extends number | bigint | Date | string ? ComparableFilter<T> : unknown) & (T extends string ? StringFilter : unknown);
|
|
18
|
+
export type WhereInput<T> = {
|
|
19
|
+
AND?: WhereInput<T> | WhereInput<T>[];
|
|
20
|
+
OR?: WhereInput<T>[];
|
|
21
|
+
NOT?: WhereInput<T> | WhereInput<T>[];
|
|
22
|
+
} & {
|
|
23
|
+
[K in keyof T]?: T[K] extends readonly unknown[] ? never : T[K] | ScalarFilter<NonNullable<T[K]>> | null;
|
|
24
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quark-fw/clisma",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"description": "Prisma-like typed client and schema migrator for ClickHouse",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"clickhouse",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"node": ">=20"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"build": "tsc -p tsconfig.build.json",
|
|
33
|
-
"typecheck": "tsc --noEmit",
|
|
32
|
+
"build": "node ../../node_modules/tsgo/bin/tsc -p tsconfig.build.json",
|
|
33
|
+
"typecheck": "node ../../node_modules/tsgo/bin/tsc --noEmit",
|
|
34
34
|
"test:integration": "tsx --env-file=./src/test/.env ./src/test/index.ts",
|
|
35
35
|
"test": "tsx --test src/test/unit.test.ts src/test/ddl.test.ts src/test/migrate-nodes.test.ts",
|
|
36
36
|
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|