@lossless.org/client 0.1.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/.smartconfig.json +37 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.losslessorgclient.d.ts +32 -0
- package/dist_ts/classes.losslessorgclient.js +103 -0
- package/dist_ts/core/classes.error.d.ts +16 -0
- package/dist_ts/core/classes.error.js +29 -0
- package/dist_ts/core/classes.operation.d.ts +17 -0
- package/dist_ts/core/classes.operation.js +70 -0
- package/dist_ts/core/interfaces.d.ts +18 -0
- package/dist_ts/core/interfaces.js +2 -0
- package/dist_ts/index.d.ts +4 -0
- package/dist_ts/index.js +3 -0
- package/dist_ts/nosqldb/classes.atomicdelete.d.ts +26 -0
- package/dist_ts/nosqldb/classes.atomicdelete.js +44 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +27 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +86 -0
- package/dist_ts/nosqldb/classes.atomicupdate.d.ts +26 -0
- package/dist_ts/nosqldb/classes.atomicupdate.js +55 -0
- package/dist_ts/nosqldb/classes.bootstraprecord.d.ts +14 -0
- package/dist_ts/nosqldb/classes.bootstraprecord.js +80 -0
- package/dist_ts/nosqldb/classes.bsonassertion.d.ts +2 -0
- package/dist_ts/nosqldb/classes.bsonassertion.js +29 -0
- package/dist_ts/nosqldb/classes.bsonobjectid.d.ts +2 -0
- package/dist_ts/nosqldb/classes.bsonobjectid.js +50 -0
- package/dist_ts/nosqldb/classes.bsonsnapshot.d.ts +3 -0
- package/dist_ts/nosqldb/classes.bsonsnapshot.js +126 -0
- package/dist_ts/nosqldb/classes.cached-document.d.ts +27 -0
- package/dist_ts/nosqldb/classes.cached-document.js +112 -0
- package/dist_ts/nosqldb/classes.collection.d.ts +302 -0
- package/dist_ts/nosqldb/classes.collection.js +1568 -0
- package/dist_ts/nosqldb/classes.collectionfactory.d.ts +30 -0
- package/dist_ts/nosqldb/classes.collectionfactory.js +65 -0
- package/dist_ts/nosqldb/classes.collectionlifecycle.d.ts +4 -0
- package/dist_ts/nosqldb/classes.collectionlifecycle.js +15 -0
- package/dist_ts/nosqldb/classes.collectionpreparation.d.ts +34 -0
- package/dist_ts/nosqldb/classes.collectionpreparation.js +134 -0
- package/dist_ts/nosqldb/classes.collectiontopology.d.ts +47 -0
- package/dist_ts/nosqldb/classes.collectiontopology.js +388 -0
- package/dist_ts/nosqldb/classes.connection.d.ts +18 -0
- package/dist_ts/nosqldb/classes.connection.js +23 -0
- package/dist_ts/nosqldb/classes.convenience.d.ts +1 -0
- package/dist_ts/nosqldb/classes.convenience.js +5 -0
- package/dist_ts/nosqldb/classes.cursor.d.ts +15 -0
- package/dist_ts/nosqldb/classes.cursor.js +82 -0
- package/dist_ts/nosqldb/classes.cursoroperation.d.ts +7 -0
- package/dist_ts/nosqldb/classes.cursoroperation.js +36 -0
- package/dist_ts/nosqldb/classes.db.d.ts +98 -0
- package/dist_ts/nosqldb/classes.db.js +434 -0
- package/dist_ts/nosqldb/classes.distributedcoordinator.d.ts +52 -0
- package/dist_ts/nosqldb/classes.distributedcoordinator.js +314 -0
- package/dist_ts/nosqldb/classes.doc.d.ts +722 -0
- package/dist_ts/nosqldb/classes.doc.js +2976 -0
- package/dist_ts/nosqldb/classes.easystore.d.ts +42 -0
- package/dist_ts/nosqldb/classes.easystore.js +199 -0
- package/dist_ts/nosqldb/classes.exactpersistence.d.ts +141 -0
- package/dist_ts/nosqldb/classes.exactpersistence.js +1110 -0
- package/dist_ts/nosqldb/classes.lucene.adapter.d.ts +173 -0
- package/dist_ts/nosqldb/classes.lucene.adapter.js +623 -0
- package/dist_ts/nosqldb/classes.mongoadministration.d.ts +73 -0
- package/dist_ts/nosqldb/classes.mongoadministration.js +497 -0
- package/dist_ts/nosqldb/classes.namespaceinspection.d.ts +42 -0
- package/dist_ts/nosqldb/classes.namespaceinspection.js +138 -0
- package/dist_ts/nosqldb/classes.operationbudget.d.ts +22 -0
- package/dist_ts/nosqldb/classes.operationbudget.js +65 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.d.ts +18 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.js +71 -0
- package/dist_ts/nosqldb/classes.persistence.d.ts +14 -0
- package/dist_ts/nosqldb/classes.persistence.js +34 -0
- package/dist_ts/nosqldb/classes.session.d.ts +28 -0
- package/dist_ts/nosqldb/classes.session.js +246 -0
- package/dist_ts/nosqldb/classes.storedinspection.d.ts +44 -0
- package/dist_ts/nosqldb/classes.storedinspection.js +100 -0
- package/dist_ts/nosqldb/classes.testdatabase.d.ts +56 -0
- package/dist_ts/nosqldb/classes.testdatabase.js +250 -0
- package/dist_ts/nosqldb/classes.watcher.d.ts +37 -0
- package/dist_ts/nosqldb/classes.watcher.js +98 -0
- package/dist_ts/nosqldb/index.d.ts +27 -0
- package/dist_ts/nosqldb/index.js +18 -0
- package/dist_ts/nosqldb/logging.d.ts +2 -0
- package/dist_ts/nosqldb/logging.js +3 -0
- package/dist_ts/nosqldb/plugins.d.ts +17 -0
- package/dist_ts/nosqldb/plugins.js +20 -0
- package/dist_ts/nosqldb/shim.d.ts +1 -0
- package/dist_ts/nosqldb/shim.js +8 -0
- package/dist_ts/nosqldb/testsupport.d.ts +3 -0
- package/dist_ts/nosqldb/testsupport.js +3 -0
- package/dist_ts/objectstorage/classes.bucket.d.ts +227 -0
- package/dist_ts/objectstorage/classes.bucket.js +939 -0
- package/dist_ts/objectstorage/classes.connection.d.ts +21 -0
- package/dist_ts/objectstorage/classes.connection.js +27 -0
- package/dist_ts/objectstorage/classes.directory.d.ts +124 -0
- package/dist_ts/objectstorage/classes.directory.js +335 -0
- package/dist_ts/objectstorage/classes.exactpathpurgeerror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.exactpathpurgeerror.js +18 -0
- package/dist_ts/objectstorage/classes.exactuploaderror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.exactuploaderror.js +19 -0
- package/dist_ts/objectstorage/classes.file.d.ts +91 -0
- package/dist_ts/objectstorage/classes.file.js +255 -0
- package/dist_ts/objectstorage/classes.listcursor.d.ts +41 -0
- package/dist_ts/objectstorage/classes.listcursor.js +67 -0
- package/dist_ts/objectstorage/classes.metadata.d.ts +57 -0
- package/dist_ts/objectstorage/classes.metadata.js +113 -0
- package/dist_ts/objectstorage/classes.smartbucket.d.ts +57 -0
- package/dist_ts/objectstorage/classes.smartbucket.js +357 -0
- package/dist_ts/objectstorage/classes.trash.d.ts +11 -0
- package/dist_ts/objectstorage/classes.trash.js +22 -0
- package/dist_ts/objectstorage/classes.watcher.d.ts +79 -0
- package/dist_ts/objectstorage/classes.watcher.js +249 -0
- package/dist_ts/objectstorage/helpers.d.ts +25 -0
- package/dist_ts/objectstorage/helpers.js +215 -0
- package/dist_ts/objectstorage/index.d.ts +13 -0
- package/dist_ts/objectstorage/index.js +13 -0
- package/dist_ts/objectstorage/interfaces.d.ts +276 -0
- package/dist_ts/objectstorage/interfaces.js +2 -0
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.d.ts +11 -0
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.js +1069 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.d.ts +40 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.js +382 -0
- package/dist_ts/objectstorage/internal.exactupload.operations.d.ts +11 -0
- package/dist_ts/objectstorage/internal.exactupload.operations.js +1104 -0
- package/dist_ts/objectstorage/internal.multipart.planner.d.ts +8 -0
- package/dist_ts/objectstorage/internal.multipart.planner.js +63 -0
- package/dist_ts/objectstorage/internal.multipart.stream.d.ts +17 -0
- package/dist_ts/objectstorage/internal.multipart.stream.js +440 -0
- package/dist_ts/objectstorage/internal.multipart.types.d.ts +82 -0
- package/dist_ts/objectstorage/internal.multipart.types.js +2 -0
- package/dist_ts/objectstorage/plugins.d.ts +17 -0
- package/dist_ts/objectstorage/plugins.js +23 -0
- package/dist_ts/plugins.d.ts +4 -0
- package/dist_ts/plugins.js +5 -0
- package/dist_ts/sqldb/classes.clickhouseconnection.d.ts +39 -0
- package/dist_ts/sqldb/classes.clickhouseconnection.js +225 -0
- package/dist_ts/sqldb/classes.sqlconnection.d.ts +50 -0
- package/dist_ts/sqldb/classes.sqlconnection.js +376 -0
- package/dist_ts/sqldb/classes.sqltable.d.ts +29 -0
- package/dist_ts/sqldb/classes.sqltable.js +70 -0
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.d.ts +27 -0
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.js +89 -0
- package/dist_ts/sqldb/clickhouse/index.d.ts +9 -0
- package/dist_ts/sqldb/clickhouse/index.js +13 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.d.ts +88 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +304 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.d.ts +28 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.js +46 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.d.ts +43 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.js +155 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.d.ts +13 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.js +36 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.d.ts +49 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +106 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.d.ts +51 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.js +96 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.d.ts +5 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.js +6 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.types.d.ts +53 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.types.js +41 -0
- package/dist_ts/sqldb/index.d.ts +6 -0
- package/dist_ts/sqldb/index.js +6 -0
- package/dist_ts/sqldb/interfaces.d.ts +53 -0
- package/dist_ts/sqldb/interfaces.js +19 -0
- package/dist_ts/sqldb/internal.sqlpool.d.ts +51 -0
- package/dist_ts/sqldb/internal.sqlpool.js +214 -0
- package/dist_ts/sqldb/plugins.clickhouse.d.ts +4 -0
- package/dist_ts/sqldb/plugins.clickhouse.js +5 -0
- package/dist_ts/sqldb/plugins.mariadb.d.ts +4 -0
- package/dist_ts/sqldb/plugins.mariadb.js +5 -0
- package/docs/source/smartbucket/license.md +19 -0
- package/docs/source/smartclickhouse/license.md +19 -0
- package/license.md +21 -0
- package/package.json +101 -0
- package/readme.md +157 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.losslessorgclient.ts +104 -0
- package/ts/core/classes.error.ts +31 -0
- package/ts/core/classes.operation.ts +60 -0
- package/ts/core/interfaces.ts +24 -0
- package/ts/index.ts +4 -0
- package/ts/nosqldb/classes.atomicdelete.ts +81 -0
- package/ts/nosqldb/classes.atomicfindoneandupdate.ts +152 -0
- package/ts/nosqldb/classes.atomicupdate.ts +99 -0
- package/ts/nosqldb/classes.bootstraprecord.ts +107 -0
- package/ts/nosqldb/classes.bsonassertion.ts +25 -0
- package/ts/nosqldb/classes.bsonobjectid.ts +45 -0
- package/ts/nosqldb/classes.bsonsnapshot.ts +105 -0
- package/ts/nosqldb/classes.cached-document.ts +67 -0
- package/ts/nosqldb/classes.collection.ts +2399 -0
- package/ts/nosqldb/classes.collectionfactory.ts +87 -0
- package/ts/nosqldb/classes.collectionlifecycle.ts +24 -0
- package/ts/nosqldb/classes.collectionpreparation.ts +164 -0
- package/ts/nosqldb/classes.collectiontopology.ts +645 -0
- package/ts/nosqldb/classes.connection.ts +31 -0
- package/ts/nosqldb/classes.convenience.ts +5 -0
- package/ts/nosqldb/classes.cursor.ts +88 -0
- package/ts/nosqldb/classes.cursoroperation.ts +38 -0
- package/ts/nosqldb/classes.db.ts +637 -0
- package/ts/nosqldb/classes.distributedcoordinator.ts +305 -0
- package/ts/nosqldb/classes.doc.ts +4928 -0
- package/ts/nosqldb/classes.easystore.ts +154 -0
- package/ts/nosqldb/classes.exactpersistence.ts +1974 -0
- package/ts/nosqldb/classes.lucene.adapter.ts +780 -0
- package/ts/nosqldb/classes.mongoadministration.ts +867 -0
- package/ts/nosqldb/classes.namespaceinspection.ts +199 -0
- package/ts/nosqldb/classes.operationbudget.ts +75 -0
- package/ts/nosqldb/classes.ordinarypersistence.ts +84 -0
- package/ts/nosqldb/classes.persistence.ts +62 -0
- package/ts/nosqldb/classes.session.ts +389 -0
- package/ts/nosqldb/classes.storedinspection.ts +151 -0
- package/ts/nosqldb/classes.testdatabase.ts +272 -0
- package/ts/nosqldb/classes.watcher.ts +96 -0
- package/ts/nosqldb/index.ts +51 -0
- package/ts/nosqldb/logging.ts +3 -0
- package/ts/nosqldb/plugins.ts +37 -0
- package/ts/nosqldb/shim.ts +6 -0
- package/ts/nosqldb/testsupport.ts +13 -0
- package/ts/objectstorage/classes.bucket.ts +1114 -0
- package/ts/objectstorage/classes.connection.ts +36 -0
- package/ts/objectstorage/classes.directory.ts +425 -0
- package/ts/objectstorage/classes.exactpathpurgeerror.ts +24 -0
- package/ts/objectstorage/classes.exactuploaderror.ts +25 -0
- package/ts/objectstorage/classes.file.ts +303 -0
- package/ts/objectstorage/classes.listcursor.ts +89 -0
- package/ts/objectstorage/classes.metadata.ts +134 -0
- package/ts/objectstorage/classes.smartbucket.ts +445 -0
- package/ts/objectstorage/classes.trash.ts +30 -0
- package/ts/objectstorage/classes.watcher.ts +289 -0
- package/ts/objectstorage/helpers.ts +254 -0
- package/ts/objectstorage/index.ts +13 -0
- package/ts/objectstorage/interfaces.ts +389 -0
- package/ts/objectstorage/internal.exactpathpurge.operations.ts +1936 -0
- package/ts/objectstorage/internal.exactupload.capability.ts +580 -0
- package/ts/objectstorage/internal.exactupload.operations.ts +1715 -0
- package/ts/objectstorage/internal.multipart.planner.ts +90 -0
- package/ts/objectstorage/internal.multipart.stream.ts +606 -0
- package/ts/objectstorage/internal.multipart.types.ts +106 -0
- package/ts/objectstorage/plugins.ts +35 -0
- package/ts/plugins.ts +4 -0
- package/ts/sqldb/classes.clickhouseconnection.ts +189 -0
- package/ts/sqldb/classes.sqlconnection.ts +350 -0
- package/ts/sqldb/classes.sqltable.ts +70 -0
- package/ts/sqldb/clickhouse/classes.metricwriter.ts +72 -0
- package/ts/sqldb/clickhouse/index.ts +16 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.ts +372 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.ts +52 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.ts +120 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.resultset.ts +44 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.ts +123 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.ts +114 -0
- package/ts/sqldb/clickhouse/smartclickhouse.plugins.ts +5 -0
- package/ts/sqldb/clickhouse/smartclickhouse.types.ts +134 -0
- package/ts/sqldb/index.ts +6 -0
- package/ts/sqldb/interfaces.ts +61 -0
- package/ts/sqldb/internal.sqlpool.ts +179 -0
- package/ts/sqldb/plugins.clickhouse.ts +4 -0
- package/ts/sqldb/plugins.mariadb.ts +4 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import * as plugins from './smartclickhouse.plugins.js';
|
|
2
|
+
import type { SmartClickHouseDb } from './smartclickhouse.classes.smartclickhouse.js';
|
|
3
|
+
import { ClickhouseQueryBuilder } from './smartclickhouse.classes.querybuilder.js';
|
|
4
|
+
import type {
|
|
5
|
+
IClickhouseTableOptions,
|
|
6
|
+
IColumnInfo,
|
|
7
|
+
IEngineConfig,
|
|
8
|
+
TClickhouseColumnType,
|
|
9
|
+
} from './smartclickhouse.types.js';
|
|
10
|
+
import { detectClickhouseType } from './smartclickhouse.types.js';
|
|
11
|
+
import { quoteClickHouseIdentifier as quoteIdentifier } from '../interfaces.js';
|
|
12
|
+
import { MetricWriter, type IMetricWriterOptions } from './classes.metricwriter.js';
|
|
13
|
+
import { Operation } from '../../core/classes.operation.js';
|
|
14
|
+
import { LosslessClientError } from '../../core/classes.error.js';
|
|
15
|
+
import type { IOperationOptions } from '../../core/interfaces.js';
|
|
16
|
+
|
|
17
|
+
export class ClickhouseTable<T extends Record<string, any>> {
|
|
18
|
+
// ---- STATIC FACTORY ----
|
|
19
|
+
|
|
20
|
+
public static async create<T extends Record<string, any>>(
|
|
21
|
+
db: SmartClickHouseDb,
|
|
22
|
+
options: IClickhouseTableOptions<T>,
|
|
23
|
+
): Promise<ClickhouseTable<T>> {
|
|
24
|
+
const table = new ClickhouseTable<T>(db, options);
|
|
25
|
+
await table.setup();
|
|
26
|
+
return table;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---- INSTANCE ----
|
|
30
|
+
|
|
31
|
+
public db: SmartClickHouseDb;
|
|
32
|
+
public options: IClickhouseTableOptions<T> & { database: string; engine: IEngineConfig };
|
|
33
|
+
public columns: IColumnInfo[] = [];
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
constructor(db: SmartClickHouseDb, options: IClickhouseTableOptions<T>) {
|
|
37
|
+
this.db = db;
|
|
38
|
+
this.options = {
|
|
39
|
+
autoSchemaEvolution: true,
|
|
40
|
+
...options,
|
|
41
|
+
database: options.database || db.options.database,
|
|
42
|
+
engine: options.engine || { engine: 'MergeTree' },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected get qualifiedName(): string {
|
|
47
|
+
return quoteIdentifier(this.options.database) + '.' + quoteIdentifier(this.options.tableName);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private interval(value: string): string {
|
|
51
|
+
if (!/^[1-9][0-9]* (SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|QUARTER|YEAR)$/i.test(value)) {
|
|
52
|
+
throw new RangeError('An interval must be a positive integer and a supported unit.');
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---- SCHEMA MANAGEMENT ----
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates the table if it doesn't exist and refreshes column metadata
|
|
61
|
+
*/
|
|
62
|
+
public async setup(): Promise<void> {
|
|
63
|
+
const { database, tableName, engine, orderBy, partitionBy, primaryKey, ttl, retainDataForDays, columns } = this.options;
|
|
64
|
+
|
|
65
|
+
// Build column definitions
|
|
66
|
+
let columnDefs: string;
|
|
67
|
+
if (columns && columns.length > 0) {
|
|
68
|
+
columnDefs = columns.map((col) => {
|
|
69
|
+
let def = `${quoteIdentifier(col.name)} ${col.type}`;
|
|
70
|
+
if (col.defaultExpression) def += ` DEFAULT ${col.defaultExpression}`;
|
|
71
|
+
if (col.codec) def += ` CODEC(${col.codec})`;
|
|
72
|
+
return def;
|
|
73
|
+
}).join(',\n ');
|
|
74
|
+
} else {
|
|
75
|
+
// Default minimal schema — downstream code can add columns via auto-schema evolution
|
|
76
|
+
columnDefs = `timestamp DateTime64(3, 'Europe/Berlin'),\n message String`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Build engine clause
|
|
80
|
+
if (!['MergeTree', 'ReplacingMergeTree', 'SummingMergeTree', 'AggregatingMergeTree',
|
|
81
|
+
'CollapsingMergeTree', 'VersionedCollapsingMergeTree'].includes(engine.engine)) {
|
|
82
|
+
throw new RangeError('Unsupported table engine.');
|
|
83
|
+
}
|
|
84
|
+
let engineClause: string = engine.engine;
|
|
85
|
+
if (engine.engine === 'ReplacingMergeTree' && engine.versionColumn) {
|
|
86
|
+
engineClause = `ReplacingMergeTree(${quoteIdentifier(engine.versionColumn)})`;
|
|
87
|
+
} else if (engine.engine === 'CollapsingMergeTree' && engine.signColumn) {
|
|
88
|
+
engineClause = `CollapsingMergeTree(${quoteIdentifier(engine.signColumn)})`;
|
|
89
|
+
} else if (engine.engine === 'VersionedCollapsingMergeTree' && engine.signColumn && engine.versionColumn) {
|
|
90
|
+
engineClause = `VersionedCollapsingMergeTree(${quoteIdentifier(engine.signColumn)}, ${quoteIdentifier(engine.versionColumn)})`;
|
|
91
|
+
} else {
|
|
92
|
+
engineClause = `${engine.engine}()`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build ORDER BY
|
|
96
|
+
const orderByStr = Array.isArray(orderBy) ? orderBy.map(quoteIdentifier).join(', ') : quoteIdentifier(orderBy);
|
|
97
|
+
|
|
98
|
+
let createSQL = `
|
|
99
|
+
CREATE TABLE IF NOT EXISTS ${this.qualifiedName} (
|
|
100
|
+
${columnDefs}
|
|
101
|
+
) ENGINE = ${engineClause}`;
|
|
102
|
+
|
|
103
|
+
if (partitionBy) {
|
|
104
|
+
createSQL += `\n PARTITION BY ${partitionBy}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
createSQL += `\n ORDER BY (${orderByStr})`;
|
|
108
|
+
|
|
109
|
+
if (primaryKey) {
|
|
110
|
+
const primaryKeyStr = Array.isArray(primaryKey) ? primaryKey.map(quoteIdentifier).join(', ') : quoteIdentifier(primaryKey);
|
|
111
|
+
createSQL += `\n PRIMARY KEY (${primaryKeyStr})`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await this.db.clickhouseHttpClient.queryPromise(createSQL);
|
|
115
|
+
|
|
116
|
+
// Apply TTL if configured
|
|
117
|
+
if (ttl) {
|
|
118
|
+
await this.db.clickhouseHttpClient.queryPromise(`
|
|
119
|
+
ALTER TABLE ${this.qualifiedName}
|
|
120
|
+
MODIFY TTL toDateTime(${quoteIdentifier(String(ttl.column))}) + INTERVAL ${this.interval(ttl.interval)}
|
|
121
|
+
`);
|
|
122
|
+
} else if (retainDataForDays && retainDataForDays > 0) {
|
|
123
|
+
// Legacy shorthand
|
|
124
|
+
await this.db.clickhouseHttpClient.queryPromise(`
|
|
125
|
+
ALTER TABLE ${this.qualifiedName}
|
|
126
|
+
MODIFY TTL toDateTime(timestamp) + INTERVAL ${this.interval(retainDataForDays + ' DAY')}
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await this.updateColumns();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Refresh column metadata from system.columns
|
|
135
|
+
*/
|
|
136
|
+
public async updateColumns(): Promise<IColumnInfo[]> {
|
|
137
|
+
this.columns = await this.db.clickhouseHttpClient.queryPromise(`
|
|
138
|
+
SELECT * FROM system.columns
|
|
139
|
+
WHERE database = {database:String}
|
|
140
|
+
AND table = {table:String}
|
|
141
|
+
`, { database: this.options.database, table: this.options.tableName });
|
|
142
|
+
return this.columns;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Auto-schema evolution: detect new columns from data and add them
|
|
147
|
+
*/
|
|
148
|
+
public async syncSchema(data: Record<string, any>): Promise<void> {
|
|
149
|
+
const flatData = plugins.smartobject.toFlatObject(data);
|
|
150
|
+
for (const key of Object.keys(flatData)) {
|
|
151
|
+
if (key === 'timestamp') continue;
|
|
152
|
+
const value = flatData[key];
|
|
153
|
+
const clickhouseType = detectClickhouseType(value);
|
|
154
|
+
if (!clickhouseType) continue;
|
|
155
|
+
await this.ensureColumn(key, clickhouseType);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---- INSERT ----
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Insert a single row
|
|
163
|
+
*/
|
|
164
|
+
public async insert(data: Partial<T>): Promise<void> {
|
|
165
|
+
|
|
166
|
+
const storageDoc = await this.prepareDocument(data);
|
|
167
|
+
await this.executeInsert([storageDoc]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Insert multiple rows
|
|
172
|
+
*/
|
|
173
|
+
public async insertMany(data: Partial<T>[]): Promise<void> {
|
|
174
|
+
if (data.length === 0) return;
|
|
175
|
+
|
|
176
|
+
// Schema sync across all documents
|
|
177
|
+
if (this.options.autoSchemaEvolution) {
|
|
178
|
+
const allKeys = new Set<string>();
|
|
179
|
+
const sampleValues: Record<string, any> = {};
|
|
180
|
+
for (const doc of data) {
|
|
181
|
+
const flat = plugins.smartobject.toFlatObject(doc);
|
|
182
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
183
|
+
if (!allKeys.has(key)) {
|
|
184
|
+
allKeys.add(key);
|
|
185
|
+
sampleValues[key] = value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
await this.syncSchema(sampleValues);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const storageDocs = data.map((doc) => this.flattenDocument(doc));
|
|
193
|
+
await this.executeInsert(storageDocs);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Insert in batches of configurable size
|
|
198
|
+
*/
|
|
199
|
+
public async insertBatch(data: Partial<T>[], options?: { batchSize?: number }): Promise<void> {
|
|
200
|
+
const size = options?.batchSize ?? 500;
|
|
201
|
+
if (!Number.isSafeInteger(size) || size < 1) throw new RangeError('batchSize must be positive.');
|
|
202
|
+
for (let offset = 0; offset < data.length; offset += size) {
|
|
203
|
+
await this.insertMany(data.slice(offset, offset + size));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Await write() for backpressure and close() for the final acknowledged flush. */
|
|
208
|
+
public createInsertStream(options: IMetricWriterOptions = {}): MetricWriter<Partial<T>> {
|
|
209
|
+
return this.db.ownWriter(new MetricWriter<Partial<T>>(rows => this.insertMany(rows), options));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---- QUERY ----
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Returns a fluent query builder for this table
|
|
216
|
+
*/
|
|
217
|
+
public query(): ClickhouseQueryBuilder<T> {
|
|
218
|
+
return new ClickhouseQueryBuilder<T>(
|
|
219
|
+
this.options.tableName,
|
|
220
|
+
this.options.database,
|
|
221
|
+
this.db.clickhouseHttpClient,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---- UPDATE ----
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Update rows matching a where condition (ClickHouse mutation - use sparingly)
|
|
229
|
+
*/
|
|
230
|
+
public async update(
|
|
231
|
+
set: Partial<T>,
|
|
232
|
+
whereFn: (q: ClickhouseQueryBuilder<T>) => ClickhouseQueryBuilder<T>,
|
|
233
|
+
): Promise<void> {
|
|
234
|
+
const qb = whereFn(new ClickhouseQueryBuilder<T>(this.options.tableName, this.options.database, this.db.clickhouseHttpClient));
|
|
235
|
+
const whereClause = qb.buildWhereClause();
|
|
236
|
+
if (!whereClause) {
|
|
237
|
+
throw new Error('UPDATE requires a WHERE clause. Use a where condition to target specific rows.');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const setClauses = Object.entries(set)
|
|
241
|
+
.map(([key, value]) => `${quoteIdentifier(key)} = ${qb.bind(value)}`)
|
|
242
|
+
.join(', ');
|
|
243
|
+
|
|
244
|
+
await this.db.clickhouseHttpClient.mutatePromise(
|
|
245
|
+
`ALTER TABLE ${this.qualifiedName} UPDATE ${setClauses} WHERE ${whereClause}`, qb.parameters
|
|
246
|
+
);
|
|
247
|
+
await this.waitForMutations();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---- DELETE ----
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Delete rows matching a where condition (ClickHouse mutation)
|
|
254
|
+
*/
|
|
255
|
+
public async deleteWhere(
|
|
256
|
+
whereFn: (q: ClickhouseQueryBuilder<T>) => ClickhouseQueryBuilder<T>,
|
|
257
|
+
): Promise<void> {
|
|
258
|
+
const qb = whereFn(new ClickhouseQueryBuilder<T>(this.options.tableName, this.options.database, this.db.clickhouseHttpClient));
|
|
259
|
+
const whereClause = qb.buildWhereClause();
|
|
260
|
+
if (!whereClause) {
|
|
261
|
+
throw new Error('DELETE requires a WHERE clause.');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await this.db.clickhouseHttpClient.mutatePromise(
|
|
265
|
+
`ALTER TABLE ${this.qualifiedName} DELETE WHERE ${whereClause}`, qb.parameters
|
|
266
|
+
);
|
|
267
|
+
await this.waitForMutations();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Delete entries older than a given interval on a column
|
|
272
|
+
*/
|
|
273
|
+
public async deleteOlderThan(column: keyof T & string, interval: string): Promise<void> {
|
|
274
|
+
await this.db.clickhouseHttpClient.mutatePromise(
|
|
275
|
+
`ALTER TABLE ${this.qualifiedName} DELETE WHERE ${quoteIdentifier(String(column))} < now() - INTERVAL ${this.interval(interval)}`
|
|
276
|
+
);
|
|
277
|
+
await this.waitForMutations();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Drop the entire table
|
|
282
|
+
*/
|
|
283
|
+
public async drop(): Promise<void> {
|
|
284
|
+
await this.db.clickhouseHttpClient.queryPromise(
|
|
285
|
+
`DROP TABLE IF EXISTS ${this.qualifiedName}`
|
|
286
|
+
);
|
|
287
|
+
this.columns = [];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---- UTILITIES ----
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Wait for all pending mutations on this table to complete
|
|
294
|
+
*/
|
|
295
|
+
public async waitForMutations(options: IOperationOptions = {}): Promise<void> {
|
|
296
|
+
const operation = new Operation(options, this.db.options.timeoutMs);
|
|
297
|
+
try {
|
|
298
|
+
while (true) {
|
|
299
|
+
operation.check();
|
|
300
|
+
const rows = await this.db.clickhouseHttpClient.queryPromise(
|
|
301
|
+
'SELECT count() AS cnt FROM system.mutations WHERE is_done = 0 AND database = {database:String} AND table = {table:String}',
|
|
302
|
+
{ database: this.options.database, table: this.options.tableName }, { signal: operation.signal });
|
|
303
|
+
if (BigInt(rows[0]?.cnt ?? 0) === 0n) return;
|
|
304
|
+
await plugins.smartdelay.delayFor(100);
|
|
305
|
+
}
|
|
306
|
+
} finally { operation.dispose(); }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get the total row count
|
|
311
|
+
*/
|
|
312
|
+
public async getRowCount(): Promise<number> {
|
|
313
|
+
const result = await this.db.clickhouseHttpClient.queryPromise(`
|
|
314
|
+
SELECT count() AS cnt FROM ${this.qualifiedName} FORMAT JSONEachRow
|
|
315
|
+
`);
|
|
316
|
+
const count = Number(result[0]?.cnt ?? 0);
|
|
317
|
+
if (!Number.isSafeInteger(count)) throw new RangeError('Count exceeds a safe JavaScript integer.');
|
|
318
|
+
return count;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Optimize table (useful for ReplacingMergeTree deduplication)
|
|
323
|
+
*/
|
|
324
|
+
public async optimize(final: boolean = false): Promise<void> {
|
|
325
|
+
const finalClause = final ? ' FINAL' : '';
|
|
326
|
+
await this.db.clickhouseHttpClient.queryPromise(
|
|
327
|
+
`OPTIMIZE TABLE ${this.qualifiedName}${finalClause}`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---- OBSERVATION ----
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Watch for new entries via polling. Returns an RxJS Observable.
|
|
335
|
+
*/
|
|
336
|
+
public watch(_options?: { pollInterval?: number }): plugins.smartrx.rxjs.Observable<T> {
|
|
337
|
+
return new plugins.smartrx.rxjs.Observable<T>(observer => observer.error(
|
|
338
|
+
new LosslessClientError('unsupported_capability',
|
|
339
|
+
'Timestamp polling cannot guarantee delivery of equal-timestamp or late rows. Use an explicit stable ingestion cursor before enabling a watch.')));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ---- PRIVATE HELPERS ----
|
|
343
|
+
private async ensureColumn(name: string, type: TClickhouseColumnType): Promise<void> {
|
|
344
|
+
if (this.columns.some(column => column.name === name)) return;
|
|
345
|
+
await this.db.clickhouseHttpClient.queryPromise(
|
|
346
|
+
`ALTER TABLE ${this.qualifiedName} ADD COLUMN IF NOT EXISTS ${quoteIdentifier(name)} ${type}`);
|
|
347
|
+
await this.updateColumns();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private flattenDocument(data: Partial<T>): Record<string, any> {
|
|
351
|
+
const flat = plugins.smartobject.toFlatObject(data);
|
|
352
|
+
const storageDoc: Record<string, any> = {};
|
|
353
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
354
|
+
const type = detectClickhouseType(value);
|
|
355
|
+
if (type || key === 'timestamp') {
|
|
356
|
+
storageDoc[key] = value;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return storageDoc;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private async prepareDocument(data: Partial<T>): Promise<Record<string, any>> {
|
|
363
|
+
if (this.options.autoSchemaEvolution) {
|
|
364
|
+
await this.syncSchema(data as Record<string, any>);
|
|
365
|
+
}
|
|
366
|
+
return this.flattenDocument(data);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private async executeInsert(docs: Record<string, any>[]): Promise<void> {
|
|
370
|
+
await this.db.clickhouseHttpClient.insertPromise(this.options.database, this.options.tableName, docs);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ClickHouseConnection } from '../classes.clickhouseconnection.js';
|
|
2
|
+
import type { IOperationOptions } from '../../core/interfaces.js';
|
|
3
|
+
import type { IQueryOptions } from '../interfaces.js';
|
|
4
|
+
|
|
5
|
+
export interface IClickhouseHttpClientOptions {
|
|
6
|
+
username?: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
url: string;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Migrated public transport API, using the official streaming ClickHouse connector. */
|
|
13
|
+
export class ClickhouseHttpClient {
|
|
14
|
+
public readonly connection: ClickHouseConnection;
|
|
15
|
+
private readonly ownsConnection: boolean;
|
|
16
|
+
constructor(public options: IClickhouseHttpClientOptions, connection?: ClickHouseConnection) {
|
|
17
|
+
this.ownsConnection = !connection;
|
|
18
|
+
this.connection = connection ?? new ClickHouseConnection({ ...options, backend: 'clickhouse', database: 'default' });
|
|
19
|
+
}
|
|
20
|
+
public static async createAndStart(options: IClickhouseHttpClientOptions) {
|
|
21
|
+
const instance = new ClickhouseHttpClient(options);
|
|
22
|
+
await instance.start();
|
|
23
|
+
return instance;
|
|
24
|
+
}
|
|
25
|
+
public async start(): Promise<void> { await this.connection.connect(); }
|
|
26
|
+
public async close(): Promise<void> { if (this.ownsConnection) await this.connection.close(); }
|
|
27
|
+
public async ping(options: IOperationOptions = {}): Promise<boolean> { return (await this.connection.ready(options)).ready; }
|
|
28
|
+
|
|
29
|
+
public async queryPromise(query: string, values?: Record<string, unknown>, options: IQueryOptions = {}): Promise<any[]> {
|
|
30
|
+
const sql = query.trim().replace(/\s+FORMAT\s+JSONEachRow\s*;?$/i, '');
|
|
31
|
+
if (/^(SELECT|WITH|SHOW|DESCRIBE|DESC|EXISTS|EXPLAIN)\b/i.test(sql)) {
|
|
32
|
+
return this.connection.query({ sql, values }, options);
|
|
33
|
+
}
|
|
34
|
+
await this.connection.execute({ sql, values }, options);
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
public async queryTyped<T>(query: string, values?: Record<string, unknown>, options: IQueryOptions = {}): Promise<T[]> {
|
|
38
|
+
return this.queryPromise(query, values, options) as Promise<T[]>;
|
|
39
|
+
}
|
|
40
|
+
public async insertPromise(database: string, table: string, documents: Record<string, unknown>[]) {
|
|
41
|
+
return this.connection.insertQualified(database, table, documents);
|
|
42
|
+
}
|
|
43
|
+
public async insertBatch(database: string, table: string, documents: Record<string, unknown>[], batchSize = 10_000) {
|
|
44
|
+
if (!Number.isSafeInteger(batchSize) || batchSize < 1) throw new RangeError('batchSize must be positive.');
|
|
45
|
+
for (let i = 0; i < documents.length; i += batchSize) {
|
|
46
|
+
await this.insertPromise(database, table, documents.slice(i, i + batchSize));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
public async mutatePromise(query: string, values?: Record<string, unknown>, options: IOperationOptions = {}): Promise<void> {
|
|
50
|
+
await this.connection.execute({ sql: query, values }, options);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { ClickhouseHttpClient } from './smartclickhouse.classes.httpclient.js';
|
|
2
|
+
import { ClickhouseResultSet } from './smartclickhouse.classes.resultset.js';
|
|
3
|
+
import type { TComparisonOperator } from './smartclickhouse.types.js';
|
|
4
|
+
import { quoteClickHouseIdentifier as quoteIdentifier } from '../interfaces.js';
|
|
5
|
+
import type { IClickHouseStatement, IQueryOptions } from '../interfaces.js';
|
|
6
|
+
import { LosslessClientError } from '../../core/classes.error.js';
|
|
7
|
+
|
|
8
|
+
interface IWhereClause { connector: 'AND' | 'OR'; expression: string }
|
|
9
|
+
|
|
10
|
+
/** Identifier methods quote names. Methods ending in Raw and having() accept trusted SQL expressions. */
|
|
11
|
+
export class ClickhouseQueryBuilder<T extends Record<string, any>> {
|
|
12
|
+
private selectColumns = ['*'];
|
|
13
|
+
private whereClauses: IWhereClause[] = [];
|
|
14
|
+
private orderByClauses: string[] = [];
|
|
15
|
+
private groupByClauses: string[] = [];
|
|
16
|
+
private havingClauses: string[] = [];
|
|
17
|
+
private limitValue: number | null = null;
|
|
18
|
+
private offsetValue: number | null = null;
|
|
19
|
+
private readonly values: Record<string, unknown> = {};
|
|
20
|
+
|
|
21
|
+
constructor(private tableName: string, private database: string, private httpClient: ClickhouseHttpClient) {}
|
|
22
|
+
|
|
23
|
+
public select<K extends keyof T & string>(...columns: K[]): this {
|
|
24
|
+
this.selectColumns = columns.map(quoteIdentifier); return this;
|
|
25
|
+
}
|
|
26
|
+
public selectRaw(...expressions: string[]): this { this.selectColumns = expressions; return this; }
|
|
27
|
+
public where<K extends keyof T & string>(column: K, operator: TComparisonOperator, value: unknown): this {
|
|
28
|
+
return this.and(column, operator, value);
|
|
29
|
+
}
|
|
30
|
+
public and<K extends keyof T & string>(column: K, operator: TComparisonOperator, value: unknown): this {
|
|
31
|
+
this.whereClauses.push({ connector: 'AND', expression: this.buildCondition(column, operator, value) }); return this;
|
|
32
|
+
}
|
|
33
|
+
public or<K extends keyof T & string>(column: K, operator: TComparisonOperator, value: unknown): this {
|
|
34
|
+
this.whereClauses.push({ connector: 'OR', expression: this.buildCondition(column, operator, value) }); return this;
|
|
35
|
+
}
|
|
36
|
+
public whereRaw(expression: string): this {
|
|
37
|
+
this.whereClauses.push({ connector: 'AND', expression }); return this;
|
|
38
|
+
}
|
|
39
|
+
public orderBy(column: (keyof T & string) | string, direction: 'ASC' | 'DESC' = 'ASC'): this {
|
|
40
|
+
if (!['ASC', 'DESC'].includes(direction)) throw new RangeError('Invalid sort direction.');
|
|
41
|
+
this.orderByClauses.push(quoteIdentifier(column) + ' ' + direction); return this;
|
|
42
|
+
}
|
|
43
|
+
public groupBy<K extends keyof T & string>(...columns: K[]): this {
|
|
44
|
+
this.groupByClauses.push(...columns.map(quoteIdentifier)); return this;
|
|
45
|
+
}
|
|
46
|
+
public having(expression: string): this { this.havingClauses.push(expression); return this; }
|
|
47
|
+
public limit(count: number): this { this.validateCount(count); this.limitValue = count; return this; }
|
|
48
|
+
public offset(count: number): this { this.validateCount(count); this.offsetValue = count; return this; }
|
|
49
|
+
private validateCount(count: number): void {
|
|
50
|
+
if (!Number.isSafeInteger(count) || count < 0) throw new RangeError('Limit and offset must be nonnegative safe integers.');
|
|
51
|
+
}
|
|
52
|
+
public async execute(options: IQueryOptions = {}): Promise<ClickhouseResultSet<T>> {
|
|
53
|
+
return new ClickhouseResultSet<T>(await this.httpClient.queryTyped<T>(this.toSQL(), this.parameters, options));
|
|
54
|
+
}
|
|
55
|
+
public async first(): Promise<T | null> { this.limitValue = 1; return (await this.execute()).first(); }
|
|
56
|
+
public async count(): Promise<number> {
|
|
57
|
+
const selected = this.selectColumns;
|
|
58
|
+
this.selectColumns = ['count() as _count'];
|
|
59
|
+
let sql: string;
|
|
60
|
+
try { sql = this.toSQL(); } finally { this.selectColumns = selected; }
|
|
61
|
+
const rows = await this.httpClient.queryTyped<{ _count: string }>(sql, this.parameters);
|
|
62
|
+
const count = Number(rows[0]?._count ?? 0);
|
|
63
|
+
if (!Number.isSafeInteger(count)) throw new RangeError('Count exceeds a safe JavaScript integer; select count() as a string.');
|
|
64
|
+
return count;
|
|
65
|
+
}
|
|
66
|
+
public async toArray(options: IQueryOptions = {}): Promise<T[]> { return (await this.execute(options)).toArray(); }
|
|
67
|
+
/** SQL contains typed placeholders. Pass parameters alongside it, or use toStatement(). */
|
|
68
|
+
public toSQL(): string {
|
|
69
|
+
const parts = ['SELECT ' + this.selectColumns.join(', '), 'FROM ' + quoteIdentifier(this.database) + '.' + quoteIdentifier(this.tableName)];
|
|
70
|
+
const where = this.buildWhereClause();
|
|
71
|
+
if (where) parts.push('WHERE ' + where);
|
|
72
|
+
if (this.groupByClauses.length) parts.push('GROUP BY ' + this.groupByClauses.join(', '));
|
|
73
|
+
if (this.havingClauses.length) parts.push('HAVING ' + this.havingClauses.join(' AND '));
|
|
74
|
+
if (this.orderByClauses.length) parts.push('ORDER BY ' + this.orderByClauses.join(', '));
|
|
75
|
+
if (this.limitValue !== null) parts.push('LIMIT ' + this.limitValue);
|
|
76
|
+
if (this.offsetValue !== null) parts.push('OFFSET ' + this.offsetValue);
|
|
77
|
+
return parts.join(' ');
|
|
78
|
+
}
|
|
79
|
+
public get parameters(): Record<string, unknown> { return { ...this.values }; }
|
|
80
|
+
public toStatement(): IClickHouseStatement { return { sql: this.toSQL(), values: this.parameters }; }
|
|
81
|
+
public buildWhereClause(): string {
|
|
82
|
+
return this.whereClauses.map((clause, index) => (index ? clause.connector + ' ' : '') + '(' + clause.expression + ')').join(' ');
|
|
83
|
+
}
|
|
84
|
+
/** Bind a scalar for table mutations as well as predicates. */
|
|
85
|
+
public bind(value: unknown): string {
|
|
86
|
+
let type: string;
|
|
87
|
+
if (value === null) type = 'Nullable(String)';
|
|
88
|
+
else if (typeof value === 'string') type = 'String';
|
|
89
|
+
else if (typeof value === 'boolean') type = 'Bool';
|
|
90
|
+
else if (typeof value === 'bigint') {
|
|
91
|
+
if (value < -(2n ** 255n) || value >= 2n ** 255n) throw new RangeError('Integer exceeds Int256.');
|
|
92
|
+
type = 'Int256'; value = value.toString();
|
|
93
|
+
} else if (typeof value === 'number' && Number.isFinite(value)) {
|
|
94
|
+
if (Number.isInteger(value) && !Number.isSafeInteger(value)) throw new RangeError('Use bigint for large integers.');
|
|
95
|
+
type = Number.isInteger(value) ? 'Int64' : 'Float64';
|
|
96
|
+
} else if (value instanceof Date && Number.isFinite(value.getTime())) {
|
|
97
|
+
type = "DateTime64(3, 'UTC')"; value = value.toISOString().replace('T', ' ').replace('Z', '');
|
|
98
|
+
} else throw new LosslessClientError('invalid_argument', 'Use a supported scalar query parameter.');
|
|
99
|
+
const key = 'p' + Object.keys(this.values).length;
|
|
100
|
+
this.values[key] = value;
|
|
101
|
+
return '{' + key + ':' + type + '}';
|
|
102
|
+
}
|
|
103
|
+
private buildCondition(column: string, operator: TComparisonOperator, value: unknown): string {
|
|
104
|
+
const name = quoteIdentifier(column);
|
|
105
|
+
if (!['=', '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN'].includes(operator)) {
|
|
106
|
+
throw new RangeError('Invalid comparison operator.');
|
|
107
|
+
}
|
|
108
|
+
if (value === null && (operator === '=' || operator === '!=')) return name + (operator === '=' ? ' IS NULL' : ' IS NOT NULL');
|
|
109
|
+
if (operator === 'IN' || operator === 'NOT IN') {
|
|
110
|
+
if (!Array.isArray(value)) throw new RangeError('IN requires an array.');
|
|
111
|
+
if (!value.length) return operator === 'IN' ? '0' : '1';
|
|
112
|
+
return name + ' ' + operator + ' (' + value.map(item => this.bind(item)).join(', ') + ')';
|
|
113
|
+
}
|
|
114
|
+
if (operator === 'BETWEEN') {
|
|
115
|
+
if (!Array.isArray(value) || value.length !== 2) throw new RangeError('BETWEEN requires two values.');
|
|
116
|
+
return name + ' BETWEEN ' + this.bind(value[0]) + ' AND ' + this.bind(value[1]);
|
|
117
|
+
}
|
|
118
|
+
return name + ' ' + operator + ' ' + this.bind(value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as plugins from './smartclickhouse.plugins.js';
|
|
2
|
+
|
|
3
|
+
export class ClickhouseResultSet<T> {
|
|
4
|
+
public rows: T[];
|
|
5
|
+
public rowCount: number;
|
|
6
|
+
|
|
7
|
+
constructor(rows: T[]) {
|
|
8
|
+
this.rows = rows;
|
|
9
|
+
this.rowCount = rows.length;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public first(): T | null {
|
|
13
|
+
return this.rows[0] ?? null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public last(): T | null {
|
|
17
|
+
return this.rows[this.rows.length - 1] ?? null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public isEmpty(): boolean {
|
|
21
|
+
return this.rows.length === 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public toArray(): T[] {
|
|
25
|
+
return this.rows;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public map<U>(fn: (row: T) => U): U[] {
|
|
29
|
+
return this.rows.map(fn);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public filter(fn: (row: T) => boolean): ClickhouseResultSet<T> {
|
|
33
|
+
return new ClickhouseResultSet<T>(this.rows.filter(fn));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public toObservable(): plugins.smartrx.rxjs.Observable<T> {
|
|
37
|
+
return new plugins.smartrx.rxjs.Observable<T>((observer) => {
|
|
38
|
+
for (const row of this.rows) {
|
|
39
|
+
observer.next(row);
|
|
40
|
+
}
|
|
41
|
+
observer.complete();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|