@hypequery/clickhouse 2.2.0 → 2.4.0
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/core/adapters/clickhouse-adapter.d.ts +2 -1
- package/dist/core/adapters/clickhouse-adapter.d.ts.map +1 -1
- package/dist/core/adapters/clickhouse-adapter.js +32 -2
- package/dist/core/adapters/database-adapter.d.ts +21 -1
- package/dist/core/adapters/database-adapter.d.ts.map +1 -1
- package/dist/core/features/aggregations.d.ts +13 -21
- package/dist/core/features/aggregations.d.ts.map +1 -1
- package/dist/core/features/aggregations.js +28 -15
- package/dist/core/features/insert-executor.d.ts +45 -0
- package/dist/core/features/insert-executor.d.ts.map +1 -0
- package/dist/core/features/insert-executor.js +144 -0
- package/dist/core/insert-builder.d.ts +72 -0
- package/dist/core/insert-builder.d.ts.map +1 -0
- package/dist/core/insert-builder.js +109 -0
- package/dist/core/query-builder.d.ts +25 -0
- package/dist/core/query-builder.d.ts.map +1 -1
- package/dist/core/query-builder.js +42 -0
- package/dist/core/query-node.d.ts +3 -1
- package/dist/core/query-node.d.ts.map +1 -1
- package/dist/core/query-node.js +26 -0
- package/dist/core/tests/integration/setup.d.ts +1 -0
- package/dist/core/tests/integration/setup.d.ts.map +1 -1
- package/dist/core/types/builder-state.d.ts +9 -0
- package/dist/core/types/builder-state.d.ts.map +1 -1
- package/dist/core/utils/insert-identifiers.d.ts +4 -0
- package/dist/core/utils/insert-identifiers.d.ts.map +1 -0
- package/dist/core/utils/insert-identifiers.js +16 -0
- package/dist/datasets.d.ts.map +1 -1
- package/dist/datasets.js +30 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/types/base.d.ts +6 -0
- package/dist/types/base.d.ts.map +1 -1
- package/dist/types/clickhouse-types.d.ts +7 -6
- package/dist/types/clickhouse-types.d.ts.map +1 -1
- package/dist/types/insert.d.ts +48 -0
- package/dist/types/insert.d.ts.map +1 -0
- package/dist/types/insert.js +1 -0
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DatabaseAdapter, QueryExecutionOptions } from './database-adapter.js';
|
|
1
|
+
import type { DatabaseAdapter, QueryExecutionOptions, InsertExecutionOptions, InsertResultSummary } from './database-adapter.js';
|
|
2
2
|
import type { ClickHouseConfig } from '../query-builder.js';
|
|
3
3
|
export declare class ClickHouseAdapter implements DatabaseAdapter {
|
|
4
4
|
private config;
|
|
@@ -8,6 +8,7 @@ export declare class ClickHouseAdapter implements DatabaseAdapter {
|
|
|
8
8
|
constructor(config: ClickHouseConfig);
|
|
9
9
|
query<T>(sql: string, params?: unknown[], options?: QueryExecutionOptions): Promise<T[]>;
|
|
10
10
|
stream<T>(sql: string, params?: unknown[], options?: QueryExecutionOptions): Promise<ReadableStream<T[]>>;
|
|
11
|
+
insert<T extends Record<string, unknown>>(table: string, rows: T[], options?: InsertExecutionOptions): Promise<InsertResultSummary>;
|
|
11
12
|
render(sql: string, params?: unknown[]): string;
|
|
12
13
|
}
|
|
13
14
|
export declare function createClickHouseAdapter(config: ClickHouseConfig): DatabaseAdapter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clickhouse-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/clickhouse-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"clickhouse-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/clickhouse-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,EACpB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAsC5D,qBAAa,iBAAkB,YAAW,eAAe;IAK3C,OAAO,CAAC,MAAM;IAJ1B,QAAQ,CAAC,IAAI,gBAAgB;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,MAAM,CAAmB;gBAEb,MAAM,EAAE,gBAAgB;IAKtC,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAW5F,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC;IAY7G,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,CAAC,EAAE,EACT,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,mBAAmB,CAAC;IAuB/B,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,MAAM;CAGpD;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,gBAAgB,GAAG,eAAe,CAEjF"}
|
|
@@ -3,6 +3,7 @@ import { substituteParameters } from '../utils.js';
|
|
|
3
3
|
import { getConnectionEndpoint } from '../utils/connection-endpoint.js';
|
|
4
4
|
import { createJsonEachRowStream } from '../utils/streaming-helpers.js';
|
|
5
5
|
import { getAutoClientModule } from '../env/auto-client.js';
|
|
6
|
+
import { assertSafeInsertIdentifiers } from '../utils/insert-identifiers.js';
|
|
6
7
|
function createClickHouseClient(config) {
|
|
7
8
|
if (isClientConfig(config)) {
|
|
8
9
|
return config.client;
|
|
@@ -19,6 +20,13 @@ function deriveNamespace(config) {
|
|
|
19
20
|
const username = 'username' in config ? config.username : 'default';
|
|
20
21
|
return `${endpoint || 'unknown-host'}|${database || 'default'}|${username || 'default'}`;
|
|
21
22
|
}
|
|
23
|
+
function jsonOutputSettings(settings) {
|
|
24
|
+
return {
|
|
25
|
+
// Matches generated Int64+ result types and prevents JSON.parse precision loss.
|
|
26
|
+
output_format_json_quote_64bit_integers: 1,
|
|
27
|
+
...settings,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
22
30
|
export class ClickHouseAdapter {
|
|
23
31
|
config;
|
|
24
32
|
name = 'clickhouse';
|
|
@@ -34,7 +42,7 @@ export class ClickHouseAdapter {
|
|
|
34
42
|
const result = await this.client.query({
|
|
35
43
|
query: finalSQL,
|
|
36
44
|
format: 'JSONEachRow',
|
|
37
|
-
clickhouse_settings: options?.clickhouseSettings,
|
|
45
|
+
clickhouse_settings: jsonOutputSettings(options?.clickhouseSettings),
|
|
38
46
|
query_id: options?.queryId,
|
|
39
47
|
});
|
|
40
48
|
return result.json();
|
|
@@ -44,12 +52,34 @@ export class ClickHouseAdapter {
|
|
|
44
52
|
const result = await this.client.query({
|
|
45
53
|
query: finalSQL,
|
|
46
54
|
format: 'JSONEachRow',
|
|
47
|
-
clickhouse_settings: options?.clickhouseSettings,
|
|
55
|
+
clickhouse_settings: jsonOutputSettings(options?.clickhouseSettings),
|
|
48
56
|
query_id: options?.queryId,
|
|
49
57
|
});
|
|
50
58
|
const stream = result.stream();
|
|
51
59
|
return createJsonEachRowStream(stream);
|
|
52
60
|
}
|
|
61
|
+
async insert(table, rows, options) {
|
|
62
|
+
assertSafeInsertIdentifiers(table, options?.columns);
|
|
63
|
+
const result = await this.client.insert({
|
|
64
|
+
table,
|
|
65
|
+
values: rows,
|
|
66
|
+
format: 'JSONEachRow',
|
|
67
|
+
...(options?.columns && options.columns.length > 0
|
|
68
|
+
? { columns: options.columns }
|
|
69
|
+
: {}),
|
|
70
|
+
clickhouse_settings: {
|
|
71
|
+
// Lets ISO-8601 timestamps (JSON.stringify'd Date values) parse into DateTime columns.
|
|
72
|
+
date_time_input_format: 'best_effort',
|
|
73
|
+
...options?.clickhouseSettings,
|
|
74
|
+
},
|
|
75
|
+
query_id: options?.queryId,
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
queryId: result.query_id,
|
|
79
|
+
executed: result.executed,
|
|
80
|
+
summary: result.summary,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
53
83
|
render(sql, params = []) {
|
|
54
84
|
return substituteParameters(sql, params);
|
|
55
85
|
}
|
|
@@ -1,13 +1,33 @@
|
|
|
1
|
-
import type { ClickHouseSettings } from '@clickhouse/client-common';
|
|
1
|
+
import type { ClickHouseSettings, ClickHouseSummary } from '@clickhouse/client-common';
|
|
2
2
|
export interface QueryExecutionOptions {
|
|
3
3
|
clickhouseSettings?: ClickHouseSettings;
|
|
4
4
|
queryId?: string;
|
|
5
5
|
}
|
|
6
|
+
export interface InsertExecutionOptions {
|
|
7
|
+
clickhouseSettings?: ClickHouseSettings;
|
|
8
|
+
queryId?: string;
|
|
9
|
+
/** Explicit column subset; adapters must preserve it so omitted columns take table DEFAULTs. */
|
|
10
|
+
columns?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface InsertResultSummary {
|
|
13
|
+
/** Empty when no request was sent or the backing engine does not provide query IDs. */
|
|
14
|
+
queryId: string;
|
|
15
|
+
executed: boolean;
|
|
16
|
+
/** Server-side insert summary when the client provides one (e.g. written_rows). */
|
|
17
|
+
summary?: ClickHouseSummary;
|
|
18
|
+
}
|
|
6
19
|
export interface DatabaseAdapter {
|
|
7
20
|
readonly name: string;
|
|
8
21
|
readonly namespace?: string;
|
|
9
22
|
query<T>(sql: string, params?: unknown[], options?: QueryExecutionOptions): Promise<T[]>;
|
|
10
23
|
stream?<T>(sql: string, params?: unknown[], options?: QueryExecutionOptions): Promise<ReadableStream<T[]>>;
|
|
24
|
+
/**
|
|
25
|
+
* Inserts rows that have already been normalized for JSONEachRow-compatible
|
|
26
|
+
* execution. The executor filters explicit empty batches before calling the
|
|
27
|
+
* adapter. When `options.columns` is present, omitted table columns must be
|
|
28
|
+
* left for the engine to fill via DEFAULT or Nullable semantics.
|
|
29
|
+
*/
|
|
30
|
+
insert?<T extends Record<string, unknown>>(table: string, rows: T[], options?: InsertExecutionOptions): Promise<InsertResultSummary>;
|
|
11
31
|
render?(sql: string, params?: unknown[]): string;
|
|
12
32
|
}
|
|
13
33
|
//# sourceMappingURL=database-adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/database-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"database-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/database-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEvF,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gGAAgG;IAChG,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,mFAAmF;IACnF,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACzF,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3G;;;;;OAKG;IACH,MAAM,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,CAAC,EAAE,EACT,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAChC,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;CAClD"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BuilderState, SchemaDefinition } from '../types/builder-state.js';
|
|
2
2
|
import { QueryBuilder } from '../query-builder.js';
|
|
3
|
-
import type { SelectQueryNode
|
|
3
|
+
import type { SelectQueryNode } from '../../types/index.js';
|
|
4
4
|
export declare class AggregationFeature<Schema extends SchemaDefinition<Schema>, State extends BuilderState<Schema, string, any, keyof Schema, Partial<Record<string, keyof Schema>>>> {
|
|
5
5
|
private builder;
|
|
6
6
|
private static readonly TRAILING_ALIAS_PATTERN;
|
|
@@ -11,31 +11,23 @@ export declare class AggregationFeature<Schema extends SchemaDefinition<Schema>,
|
|
|
11
11
|
private createAggregateSelection;
|
|
12
12
|
private shouldInferGroupByFromSelection;
|
|
13
13
|
private inferGroupBySelections;
|
|
14
|
+
/**
|
|
15
|
+
* Appends an aggregate selection (marked `isAggregate`, so GROUP BY
|
|
16
|
+
* inference never treats it as a grouping key) and infers GROUP BY from any
|
|
17
|
+
* plain selections already present.
|
|
18
|
+
*/
|
|
19
|
+
private appendAggregateSelection;
|
|
14
20
|
private createAggregation;
|
|
15
21
|
sum(column: string, alias: string): SelectQueryNode<State["output"], Schema>;
|
|
16
22
|
count(column: string, alias: string): SelectQueryNode<State["output"], Schema>;
|
|
17
23
|
avg(column: string, alias: string): SelectQueryNode<State["output"], Schema>;
|
|
18
24
|
min(column: string, alias: string): SelectQueryNode<State["output"], Schema>;
|
|
19
25
|
max(column: string, alias: string): SelectQueryNode<State["output"], Schema>;
|
|
20
|
-
countDistinct(column: string, alias: string):
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
where?: import("../../types/base.js").ExprNode;
|
|
27
|
-
groupBy?: import("../../types/base.js").GroupByItemNode[];
|
|
28
|
-
withTotals?: boolean;
|
|
29
|
-
having?: import("../../types/base.js").HavingNode[];
|
|
30
|
-
limitBy?: import("../../types/base.js").LimitByNode;
|
|
31
|
-
limit?: number;
|
|
32
|
-
offset?: number;
|
|
33
|
-
distinct?: boolean;
|
|
34
|
-
orderBy?: import("../../types/base.js").OrderByItemNode[];
|
|
35
|
-
joins?: import("../../types/base.js").JoinNode[];
|
|
36
|
-
ctes?: import("../../types/base.js").CteNode[];
|
|
37
|
-
unionQueries?: string[];
|
|
38
|
-
settings?: import("@clickhouse/client-common").ClickHouseSettings;
|
|
39
|
-
};
|
|
26
|
+
countDistinct(column: string, alias: string): SelectQueryNode<State["output"], Schema>;
|
|
27
|
+
argMax(column: string, argColumn: string, alias: string): SelectQueryNode<State["output"], Schema>;
|
|
28
|
+
argMin(column: string, argColumn: string, alias: string): SelectQueryNode<State["output"], Schema>;
|
|
29
|
+
quantile(column: string, level: number, alias: string): SelectQueryNode<State["output"], Schema>;
|
|
30
|
+
stddev(column: string, alias: string): SelectQueryNode<State["output"], Schema>;
|
|
31
|
+
variance(column: string, alias: string): SelectQueryNode<State["output"], Schema>;
|
|
40
32
|
}
|
|
41
33
|
//# sourceMappingURL=aggregations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aggregations.d.ts","sourceRoot":"","sources":["../../../src/core/features/aggregations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"aggregations.d.ts","sourceRoot":"","sources":["../../../src/core/features/aggregations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,sBAAsB,CAAC;AAE3E,qBAAa,kBAAkB,CAC7B,MAAM,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACvC,KAAK,SAAS,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC,CAAC,CAAC;IAMxF,OAAO,CAAC,OAAO;IAJ3B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,sBAAsB,CAAsC;IACpF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,8BAA8B,CACuB;gBAEzD,OAAO,EAAE,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAExD,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,wBAAwB;IAQhC,OAAO,CAAC,+BAA+B;IAYvC,OAAO,CAAC,sBAAsB;IAa9B;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IAiBhC,OAAO,CAAC,iBAAiB;IAQzB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAIjC,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAInC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAIjC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAIjC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAIjC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAI3C,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAIvD,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAIvD,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAOrD,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAIpC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAGvC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export class AggregationFeature {
|
|
2
2
|
builder;
|
|
3
3
|
static TRAILING_ALIAS_PATTERN = /\s+AS\s+[A-Za-z_][A-Za-z0-9_]*$/i;
|
|
4
|
-
static LEADING_AGGREGATE_CALL_PATTERN = /^(COUNT|SUM|AVG|MIN|MAX)\s*\(/i;
|
|
4
|
+
static LEADING_AGGREGATE_CALL_PATTERN = /^(COUNT|SUM|AVG|MIN|MAX|argMax|argMin|quantile|stddevSamp|varSamp)\s*\(/i;
|
|
5
5
|
constructor(builder) {
|
|
6
6
|
this.builder = builder;
|
|
7
7
|
}
|
|
@@ -40,8 +40,12 @@ export class AggregationFeature {
|
|
|
40
40
|
};
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Appends an aggregate selection (marked `isAggregate`, so GROUP BY
|
|
45
|
+
* inference never treats it as a grouping key) and infers GROUP BY from any
|
|
46
|
+
* plain selections already present.
|
|
47
|
+
*/
|
|
48
|
+
appendAggregateSelection(aggregationSQL) {
|
|
45
49
|
const query = this.builder.getQueryNode();
|
|
46
50
|
if (query.select) {
|
|
47
51
|
return {
|
|
@@ -55,6 +59,9 @@ export class AggregationFeature {
|
|
|
55
59
|
select: [this.createAggregateSelection(aggregationSQL)]
|
|
56
60
|
};
|
|
57
61
|
}
|
|
62
|
+
createAggregation(column, fn, alias) {
|
|
63
|
+
return this.appendAggregateSelection(`${fn}(${column}) AS ${alias}`);
|
|
64
|
+
}
|
|
58
65
|
sum(column, alias) {
|
|
59
66
|
return this.createAggregation(column, 'SUM', alias);
|
|
60
67
|
}
|
|
@@ -71,18 +78,24 @@ export class AggregationFeature {
|
|
|
71
78
|
return this.createAggregation(column, 'MAX', alias);
|
|
72
79
|
}
|
|
73
80
|
countDistinct(column, alias) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
return this.appendAggregateSelection(`COUNT(DISTINCT ${column}) AS ${alias}`);
|
|
82
|
+
}
|
|
83
|
+
argMax(column, argColumn, alias) {
|
|
84
|
+
return this.appendAggregateSelection(`argMax(${column}, ${argColumn}) AS ${alias}`);
|
|
85
|
+
}
|
|
86
|
+
argMin(column, argColumn, alias) {
|
|
87
|
+
return this.appendAggregateSelection(`argMin(${column}, ${argColumn}) AS ${alias}`);
|
|
88
|
+
}
|
|
89
|
+
quantile(column, level, alias) {
|
|
90
|
+
if (typeof level !== 'number' || !Number.isFinite(level) || level < 0 || level > 1) {
|
|
91
|
+
throw new Error(`Invalid quantile level ${level}: expected a number between 0 and 1.`);
|
|
82
92
|
}
|
|
83
|
-
return {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
};
|
|
93
|
+
return this.appendAggregateSelection(`quantile(${level})(${column}) AS ${alias}`);
|
|
94
|
+
}
|
|
95
|
+
stddev(column, alias) {
|
|
96
|
+
return this.appendAggregateSelection(`stddevSamp(${column}) AS ${alias}`);
|
|
97
|
+
}
|
|
98
|
+
variance(column, alias) {
|
|
99
|
+
return this.appendAggregateSelection(`varSamp(${column}) AS ${alias}`);
|
|
87
100
|
}
|
|
88
101
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ClickHouseSettings } from '@clickhouse/client-common';
|
|
2
|
+
import type { AnyInsertState, SchemaDefinition } from '../types/builder-state.js';
|
|
3
|
+
import type { InsertResultSummary } from '../adapters/database-adapter.js';
|
|
4
|
+
import { InsertBuilder } from '../insert-builder.js';
|
|
5
|
+
interface InsertExecutorRunOptions {
|
|
6
|
+
queryId?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Prepares rows for JSONEachRow serialization: `Date` values become ISO-8601
|
|
10
|
+
* strings and `bigint` values become decimal strings (JSON.stringify throws on
|
|
11
|
+
* bigint). Recurses through arrays and plain objects (Map columns).
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeInsertRows<T extends Record<string, unknown>>(rows: T[]): Record<string, unknown>[];
|
|
14
|
+
export interface JsonEachRowInsertOptions {
|
|
15
|
+
/** Explicit column subset; omitted columns take table DEFAULTs. */
|
|
16
|
+
columns?: string[];
|
|
17
|
+
/** Merged over the default `date_time_input_format: 'best_effort'`. */
|
|
18
|
+
clickhouseSettings?: ClickHouseSettings;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Renders a complete `INSERT INTO ... FORMAT JSONEachRow` statement with the
|
|
22
|
+
* row payload inlined as newline-delimited JSON.
|
|
23
|
+
*
|
|
24
|
+
* Exported for `DatabaseAdapter` authors (e.g. embedded engines like chDB) so
|
|
25
|
+
* a custom `insert` implementation stays byte-identical with the built-in
|
|
26
|
+
* adapter: rows are normalized the same way (see {@link normalizeInsertRows})
|
|
27
|
+
* and `date_time_input_format='best_effort'` is inlined in the SETTINGS clause
|
|
28
|
+
* by default so DateTime columns accept the normalized ISO timestamps.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* async insert(table, rows, options) {
|
|
33
|
+
* await runSql(buildJsonEachRowInsert(table, rows, options));
|
|
34
|
+
* return { queryId: '', executed: true };
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildJsonEachRowInsert(table: string, rows: ReadonlyArray<Record<string, unknown>>, options?: JsonEachRowInsertOptions): string;
|
|
39
|
+
export declare class InsertExecutorFeature<Schema extends SchemaDefinition<Schema>, State extends AnyInsertState> {
|
|
40
|
+
private builder;
|
|
41
|
+
constructor(builder: InsertBuilder<Schema, State>);
|
|
42
|
+
execute(options?: InsertExecutorRunOptions): Promise<InsertResultSummary>;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=insert-executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"insert-executor.d.ts","sourceRoot":"","sources":["../../../src/core/features/insert-executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAOrD,UAAU,wBAAwB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA+BD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnE,IAAI,EAAE,CAAC,EAAE,GACR,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAE3B;AAED,MAAM,WAAW,wBAAwB;IACvC,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAaD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC5C,OAAO,CAAC,EAAE,wBAAwB,GACjC,MAAM,CAeR;AAED,qBAAa,qBAAqB,CAChC,MAAM,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACvC,KAAK,SAAS,cAAc;IAEhB,OAAO,CAAC,OAAO;gBAAP,OAAO,EAAE,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC;IAEnD,OAAO,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAkEhF"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { logger } from '../utils/logger.js';
|
|
2
|
+
import { assertSafeIdentifier, assertSafeInsertIdentifiers, } from '../utils/insert-identifiers.js';
|
|
3
|
+
function normalizeInsertValue(value) {
|
|
4
|
+
if (value instanceof Date) {
|
|
5
|
+
return value.toISOString();
|
|
6
|
+
}
|
|
7
|
+
if (typeof value === 'bigint') {
|
|
8
|
+
return String(value);
|
|
9
|
+
}
|
|
10
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
11
|
+
// JSON.stringify would silently coerce NaN/Infinity to null.
|
|
12
|
+
throw new Error(`Cannot insert non-finite number ${value}: JSON serialization would coerce it to null.`);
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return value.map(normalizeInsertValue);
|
|
16
|
+
}
|
|
17
|
+
if (value !== null && typeof value === 'object') {
|
|
18
|
+
const proto = Object.getPrototypeOf(value);
|
|
19
|
+
if (proto === Object.prototype || proto === null) {
|
|
20
|
+
const normalized = {};
|
|
21
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
22
|
+
normalized[key] = normalizeInsertValue(entry);
|
|
23
|
+
}
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Prepares rows for JSONEachRow serialization: `Date` values become ISO-8601
|
|
31
|
+
* strings and `bigint` values become decimal strings (JSON.stringify throws on
|
|
32
|
+
* bigint). Recurses through arrays and plain objects (Map columns).
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeInsertRows(rows) {
|
|
35
|
+
return rows.map(row => normalizeInsertValue(row));
|
|
36
|
+
}
|
|
37
|
+
function renderSettingValue(value) {
|
|
38
|
+
if (typeof value === 'number') {
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === 'boolean') {
|
|
42
|
+
return value ? '1' : '0';
|
|
43
|
+
}
|
|
44
|
+
const escaped = String(value).replace(/\\/g, '\\\\').replace(/'/g, "''");
|
|
45
|
+
return `'${escaped}'`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Renders a complete `INSERT INTO ... FORMAT JSONEachRow` statement with the
|
|
49
|
+
* row payload inlined as newline-delimited JSON.
|
|
50
|
+
*
|
|
51
|
+
* Exported for `DatabaseAdapter` authors (e.g. embedded engines like chDB) so
|
|
52
|
+
* a custom `insert` implementation stays byte-identical with the built-in
|
|
53
|
+
* adapter: rows are normalized the same way (see {@link normalizeInsertRows})
|
|
54
|
+
* and `date_time_input_format='best_effort'` is inlined in the SETTINGS clause
|
|
55
|
+
* by default so DateTime columns accept the normalized ISO timestamps.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* async insert(table, rows, options) {
|
|
60
|
+
* await runSql(buildJsonEachRowInsert(table, rows, options));
|
|
61
|
+
* return { queryId: '', executed: true };
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function buildJsonEachRowInsert(table, rows, options) {
|
|
66
|
+
assertSafeInsertIdentifiers(table, options?.columns);
|
|
67
|
+
Object.keys(options?.clickhouseSettings ?? {})
|
|
68
|
+
.forEach(key => assertSafeIdentifier(key, 'setting'));
|
|
69
|
+
const normalized = normalizeInsertRows([...rows]);
|
|
70
|
+
const columnsSql = options?.columns?.length ? ` (${options.columns.join(', ')})` : '';
|
|
71
|
+
const settings = {
|
|
72
|
+
date_time_input_format: 'best_effort',
|
|
73
|
+
...options?.clickhouseSettings,
|
|
74
|
+
};
|
|
75
|
+
const settingsSql = Object.entries(settings)
|
|
76
|
+
.map(([key, value]) => `${key}=${renderSettingValue(value)}`)
|
|
77
|
+
.join(', ');
|
|
78
|
+
const payload = normalized.map(row => JSON.stringify(row)).join('\n');
|
|
79
|
+
return `INSERT INTO ${table}${columnsSql} SETTINGS ${settingsSql} FORMAT JSONEachRow\n${payload}`;
|
|
80
|
+
}
|
|
81
|
+
export class InsertExecutorFeature {
|
|
82
|
+
builder;
|
|
83
|
+
constructor(builder) {
|
|
84
|
+
this.builder = builder;
|
|
85
|
+
}
|
|
86
|
+
async execute(options) {
|
|
87
|
+
const queryNode = this.builder.getQueryNode();
|
|
88
|
+
if (!this.builder.hasValues()) {
|
|
89
|
+
throw new Error('No values provided. Call .values() before .execute().');
|
|
90
|
+
}
|
|
91
|
+
const adapter = this.builder.getAdapter();
|
|
92
|
+
if (!adapter.insert) {
|
|
93
|
+
throw new Error(`Inserts are not supported by adapter "${adapter.name}". Implement DatabaseAdapter.insert to enable them.`);
|
|
94
|
+
}
|
|
95
|
+
if (queryNode.rows.length === 0) {
|
|
96
|
+
// An explicit empty batch sends no request, mirroring the native client.
|
|
97
|
+
return { queryId: '', executed: false };
|
|
98
|
+
}
|
|
99
|
+
// Synthetic statement for logs only — row data is never serialized into logs.
|
|
100
|
+
const tableName = this.builder.getTableName();
|
|
101
|
+
assertSafeInsertIdentifiers(tableName, queryNode.columns);
|
|
102
|
+
const columnsSql = queryNode.columns?.length ? ` (${queryNode.columns.join(', ')})` : '';
|
|
103
|
+
const renderSql = `INSERT INTO ${tableName}${columnsSql} FORMAT JSONEachRow /* ${queryNode.rows.length} rows */`;
|
|
104
|
+
const startTime = Date.now();
|
|
105
|
+
logger.logQuery({
|
|
106
|
+
query: renderSql,
|
|
107
|
+
startTime,
|
|
108
|
+
status: 'started',
|
|
109
|
+
queryId: options?.queryId,
|
|
110
|
+
});
|
|
111
|
+
try {
|
|
112
|
+
const rows = normalizeInsertRows(queryNode.rows);
|
|
113
|
+
const result = await adapter.insert(tableName, rows, {
|
|
114
|
+
clickhouseSettings: queryNode.settings,
|
|
115
|
+
queryId: options?.queryId,
|
|
116
|
+
columns: queryNode.columns,
|
|
117
|
+
});
|
|
118
|
+
const endTime = Date.now();
|
|
119
|
+
logger.logQuery({
|
|
120
|
+
query: renderSql,
|
|
121
|
+
startTime,
|
|
122
|
+
endTime,
|
|
123
|
+
duration: endTime - startTime,
|
|
124
|
+
status: 'completed',
|
|
125
|
+
rowCount: queryNode.rows.length,
|
|
126
|
+
queryId: result.queryId || options?.queryId,
|
|
127
|
+
});
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const endTime = Date.now();
|
|
132
|
+
logger.logQuery({
|
|
133
|
+
query: renderSql,
|
|
134
|
+
startTime,
|
|
135
|
+
endTime,
|
|
136
|
+
duration: endTime - startTime,
|
|
137
|
+
status: 'error',
|
|
138
|
+
error: error,
|
|
139
|
+
queryId: options?.queryId,
|
|
140
|
+
});
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ClickHouseSettings } from '@clickhouse/client-common';
|
|
2
|
+
import type { DatabaseAdapter, InsertResultSummary } from './adapters/database-adapter.js';
|
|
3
|
+
import type { InsertQueryNode } from '../types/index.js';
|
|
4
|
+
import type { SchemaDefinition, InsertState, InitialInsertState, UpdateInsertRow } from './types/builder-state.js';
|
|
5
|
+
import type { InsertRowForColumns } from '../types/insert.js';
|
|
6
|
+
export interface InsertExecuteOptions {
|
|
7
|
+
queryId?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A type-safe insert builder for ClickHouse tables.
|
|
11
|
+
* The builder carries a single state object that encodes the schema, target
|
|
12
|
+
* table, and accepted row shape.
|
|
13
|
+
*
|
|
14
|
+
* Row shapes are derived from the schema: `Nullable(...)` columns are optional,
|
|
15
|
+
* every other column is required. Use {@link columns} to insert a subset of
|
|
16
|
+
* columns and let ClickHouse fill table DEFAULTs for the rest.
|
|
17
|
+
*
|
|
18
|
+
* Instances are immutable — every method returns a new builder.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* await db.insert('events')
|
|
23
|
+
* .values([{ id: 1, name: 'signup', created_at: new Date() }])
|
|
24
|
+
* .execute();
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare class InsertBuilder<Schema extends SchemaDefinition<Schema>, State extends InsertState<Schema, keyof Schema, any>> {
|
|
28
|
+
private query;
|
|
29
|
+
private tableName;
|
|
30
|
+
private state;
|
|
31
|
+
private executor;
|
|
32
|
+
private adapter;
|
|
33
|
+
private valuesProvided;
|
|
34
|
+
constructor(tableName: string, state: State, adapter: DatabaseAdapter, query?: InsertQueryNode);
|
|
35
|
+
private fork;
|
|
36
|
+
private updateQuery;
|
|
37
|
+
/**
|
|
38
|
+
* Restricts the insert to a subset of columns. Omitted columns take their
|
|
39
|
+
* table DEFAULT values. Must be called before {@link values}.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* await db.insert('events')
|
|
44
|
+
* .columns(['id', 'name'])
|
|
45
|
+
* .values([{ id: 1, name: 'a' }])
|
|
46
|
+
* .execute();
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
columns<K extends Extract<keyof Schema[State['table']], string>>(columns: readonly K[]): InsertBuilder<Schema, UpdateInsertRow<State, InsertRowForColumns<Schema[State['table']], K>>>;
|
|
50
|
+
/**
|
|
51
|
+
* Adds one row or an array of rows to insert. Can be chained; rows accumulate.
|
|
52
|
+
*
|
|
53
|
+
* An explicit empty array is a valid no-op batch: `execute()` sends no
|
|
54
|
+
* request and resolves `{ executed: false }`, mirroring the native client.
|
|
55
|
+
*/
|
|
56
|
+
values(rows: State['row'] | ReadonlyArray<State['row']>): this;
|
|
57
|
+
/**
|
|
58
|
+
* Merges ClickHouse settings applied to this insert
|
|
59
|
+
* (e.g. `{ async_insert: 1, wait_for_async_insert: 1 }`).
|
|
60
|
+
*/
|
|
61
|
+
settings(opts: ClickHouseSettings): this;
|
|
62
|
+
getTableName(): string;
|
|
63
|
+
getAdapter(): DatabaseAdapter;
|
|
64
|
+
getQueryNode(): InsertQueryNode;
|
|
65
|
+
hasValues(): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Executes the insert through the adapter's native insert path (JSONEachRow).
|
|
68
|
+
*/
|
|
69
|
+
execute(options?: InsertExecuteOptions): Promise<InsertResultSummary>;
|
|
70
|
+
}
|
|
71
|
+
export type InsertQB<Schema extends SchemaDefinition<Schema>, Table extends keyof Schema> = InsertBuilder<Schema, InitialInsertState<Schema, Table>>;
|
|
72
|
+
//# sourceMappingURL=insert-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"insert-builder.d.ts","sourceRoot":"","sources":["../../src/core/insert-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAI9D,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,aAAa,CACxB,MAAM,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACvC,KAAK,SAAS,WAAW,CAAC,MAAM,EAAE,MAAM,MAAM,EAAE,GAAG,CAAC;IAEpD,OAAO,CAAC,KAAK,CAAkB;IAC/B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,QAAQ,CAAuC;IACvD,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,cAAc,CAAS;gBAG7B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,eAAe,EACxB,KAAK,CAAC,EAAE,eAAe;IASzB,OAAO,CAAC,IAAI;IAcZ,OAAO,CAAC,WAAW;IAInB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,CAAC,SAAS,OAAO,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,EAC7D,OAAO,EAAE,SAAS,CAAC,EAAE,GACpB,aAAa,CAAC,MAAM,EAAE,eAAe,CAAC,KAAK,EAAE,mBAAmB,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAchG;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI;IAU9D;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI;IAQxC,YAAY;IAIZ,UAAU,IAAI,eAAe;IAI7B,YAAY,IAAI,eAAe;IAI/B,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,OAAO,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAGtE;AAED,MAAM,MAAM,QAAQ,CAClB,MAAM,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACvC,KAAK,SAAS,MAAM,MAAM,IACxB,aAAa,CAAC,MAAM,EAAE,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { cloneInsertQueryNode, createInsertQueryNode } from './query-node.js';
|
|
2
|
+
import { InsertExecutorFeature } from './features/insert-executor.js';
|
|
3
|
+
/**
|
|
4
|
+
* A type-safe insert builder for ClickHouse tables.
|
|
5
|
+
* The builder carries a single state object that encodes the schema, target
|
|
6
|
+
* table, and accepted row shape.
|
|
7
|
+
*
|
|
8
|
+
* Row shapes are derived from the schema: `Nullable(...)` columns are optional,
|
|
9
|
+
* every other column is required. Use {@link columns} to insert a subset of
|
|
10
|
+
* columns and let ClickHouse fill table DEFAULTs for the rest.
|
|
11
|
+
*
|
|
12
|
+
* Instances are immutable — every method returns a new builder.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* await db.insert('events')
|
|
17
|
+
* .values([{ id: 1, name: 'signup', created_at: new Date() }])
|
|
18
|
+
* .execute();
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export class InsertBuilder {
|
|
22
|
+
query;
|
|
23
|
+
tableName;
|
|
24
|
+
state;
|
|
25
|
+
executor;
|
|
26
|
+
adapter;
|
|
27
|
+
valuesProvided = false;
|
|
28
|
+
constructor(tableName, state, adapter, query) {
|
|
29
|
+
this.tableName = tableName;
|
|
30
|
+
this.query = query ? cloneInsertQueryNode(query) : createInsertQueryNode();
|
|
31
|
+
this.state = state;
|
|
32
|
+
this.adapter = adapter;
|
|
33
|
+
this.executor = new InsertExecutorFeature(this);
|
|
34
|
+
}
|
|
35
|
+
fork(state, query) {
|
|
36
|
+
const builder = new InsertBuilder(this.tableName, state, this.adapter, query);
|
|
37
|
+
builder.valuesProvided = this.valuesProvided;
|
|
38
|
+
return builder;
|
|
39
|
+
}
|
|
40
|
+
updateQuery(updater) {
|
|
41
|
+
return this.fork(this.state, updater(this.query));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Restricts the insert to a subset of columns. Omitted columns take their
|
|
45
|
+
* table DEFAULT values. Must be called before {@link values}.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* await db.insert('events')
|
|
50
|
+
* .columns(['id', 'name'])
|
|
51
|
+
* .values([{ id: 1, name: 'a' }])
|
|
52
|
+
* .execute();
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
columns(columns) {
|
|
56
|
+
if (this.query.rows.length > 0) {
|
|
57
|
+
throw new Error('Call .columns() before .values().');
|
|
58
|
+
}
|
|
59
|
+
const nextState = {
|
|
60
|
+
...this.state,
|
|
61
|
+
row: {},
|
|
62
|
+
};
|
|
63
|
+
return this.fork(nextState, { ...this.query, columns: [...columns] });
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Adds one row or an array of rows to insert. Can be chained; rows accumulate.
|
|
67
|
+
*
|
|
68
|
+
* An explicit empty array is a valid no-op batch: `execute()` sends no
|
|
69
|
+
* request and resolves `{ executed: false }`, mirroring the native client.
|
|
70
|
+
*/
|
|
71
|
+
values(rows) {
|
|
72
|
+
const added = (Array.isArray(rows) ? rows : [rows]);
|
|
73
|
+
const next = this.updateQuery(query => ({
|
|
74
|
+
...query,
|
|
75
|
+
rows: [...query.rows, ...added],
|
|
76
|
+
}));
|
|
77
|
+
next.valuesProvided = true;
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Merges ClickHouse settings applied to this insert
|
|
82
|
+
* (e.g. `{ async_insert: 1, wait_for_async_insert: 1 }`).
|
|
83
|
+
*/
|
|
84
|
+
settings(opts) {
|
|
85
|
+
return this.updateQuery(query => ({
|
|
86
|
+
...query,
|
|
87
|
+
settings: { ...query.settings, ...opts },
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
// Make needed properties accessible to features
|
|
91
|
+
getTableName() {
|
|
92
|
+
return this.tableName;
|
|
93
|
+
}
|
|
94
|
+
getAdapter() {
|
|
95
|
+
return this.adapter;
|
|
96
|
+
}
|
|
97
|
+
getQueryNode() {
|
|
98
|
+
return cloneInsertQueryNode(this.query);
|
|
99
|
+
}
|
|
100
|
+
hasValues() {
|
|
101
|
+
return this.valuesProvided;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Executes the insert through the adapter's native insert path (JSONEachRow).
|
|
105
|
+
*/
|
|
106
|
+
execute(options) {
|
|
107
|
+
return this.executor.execute(options);
|
|
108
|
+
}
|
|
109
|
+
}
|