ts-prorm-orm 1.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/CHANGELOG.md +1111 -0
- package/LICENSE +21 -0
- package/README.md +573 -0
- package/dist/audit/history-query.js +180 -0
- package/dist/audit/index.js +23 -0
- package/dist/audit/logger.js +236 -0
- package/dist/cache/cache-manager.js +84 -0
- package/dist/cache/index.js +16 -0
- package/dist/cache/redis-cluster-cache.js +557 -0
- package/dist/cli.js +2200 -0
- package/dist/compliance/audit-trail.js +68 -0
- package/dist/compliance/backup-verification.js +685 -0
- package/dist/compliance/breach-detector.js +505 -0
- package/dist/compliance/consent-record.js +227 -0
- package/dist/compliance/consent-versioning.js +331 -0
- package/dist/compliance/cross-border-log.js +530 -0
- package/dist/compliance/data-classifier.js +258 -0
- package/dist/compliance/data-lineage.js +565 -0
- package/dist/compliance/data-masker.js +303 -0
- package/dist/compliance/data-portability.js +265 -0
- package/dist/compliance/data-retention.js +195 -0
- package/dist/compliance/dsar-workflow.js +342 -0
- package/dist/compliance/field-encryption.js +223 -0
- package/dist/compliance/immutable-record.js +148 -0
- package/dist/compliance/index.js +260 -0
- package/dist/compliance/privacy-impact-assessment.js +364 -0
- package/dist/compliance/pseudonymization.js +269 -0
- package/dist/compliance/query-firewall.js +1182 -0
- package/dist/compliance/rate-limiter.js +580 -0
- package/dist/compliance/right-to-erasure.js +117 -0
- package/dist/compliance/row-level-security.js +246 -0
- package/dist/compliance/security-decorator.js +574 -0
- package/dist/compliance/security-monitor.js +525 -0
- package/dist/compliance/sensitive-data-discovery.js +476 -0
- package/dist/compliance/session-isolation.js +472 -0
- package/dist/compliance/tls-enforcer.js +108 -0
- package/dist/compliance/worm-storage.js +712 -0
- package/dist/connection-manager.js +198 -0
- package/dist/connection-pool.js +519 -0
- package/dist/decorators/audit.js +136 -0
- package/dist/decorators/belongs-to-many.js +115 -0
- package/dist/decorators/belongs-to.js +115 -0
- package/dist/decorators/check.js +435 -0
- package/dist/decorators/collate.js +329 -0
- package/dist/decorators/comment.js +205 -0
- package/dist/decorators/database-settings.js +236 -0
- package/dist/decorators/default.js +244 -0
- package/dist/decorators/encryption.js +235 -0
- package/dist/decorators/engine.js +97 -0
- package/dist/decorators/fk-constraints.js +594 -0
- package/dist/decorators/foreign-table.js +136 -0
- package/dist/decorators/generated.js +274 -0
- package/dist/decorators/has-many.js +127 -0
- package/dist/decorators/has-one.js +116 -0
- package/dist/decorators/hstore.js +129 -0
- package/dist/decorators/index.js +355 -0
- package/dist/decorators/json-column.js +83 -0
- package/dist/decorators/jsonb.js +101 -0
- package/dist/decorators/orm-decorators.js +425 -0
- package/dist/decorators/permissions.js +294 -0
- package/dist/decorators/procedure.js +210 -0
- package/dist/decorators/query-options.js +556 -0
- package/dist/decorators/range.js +167 -0
- package/dist/decorators/set-column.js +83 -0
- package/dist/decorators/spatial.js +114 -0
- package/dist/decorators/storage.js +580 -0
- package/dist/decorators/timezone.js +512 -0
- package/dist/decorators/trigger.js +90 -0
- package/dist/decorators/uuid.js +135 -0
- package/dist/decorators/view.js +258 -0
- package/dist/diagrams/chen-diagram.js +354 -0
- package/dist/diagrams/class-diagram.js +384 -0
- package/dist/diagrams/dependency-diagram.js +432 -0
- package/dist/diagrams/er-diagram.js +605 -0
- package/dist/diagrams/flow-diagram.js +394 -0
- package/dist/diagrams/gantt-diagram.js +411 -0
- package/dist/diagrams/index-diagram.js +353 -0
- package/dist/diagrams/index.js +184 -0
- package/dist/diagrams/migration-diagram.js +316 -0
- package/dist/diagrams/model-diagram.js +616 -0
- package/dist/diagrams/package-diagram.js +376 -0
- package/dist/diagrams/palette.js +85 -0
- package/dist/diagrams/relational-diagram.js +455 -0
- package/dist/diagrams/schemadoc-diagram.js +309 -0
- package/dist/diagrams/sequence-diagram.js +307 -0
- package/dist/diagrams/state-diagram.js +344 -0
- package/dist/diagrams/svg-dom.js +111 -0
- package/dist/diagrams/tree-diagram.js +250 -0
- package/dist/dialects/clickhouse/index.js +1742 -0
- package/dist/dialects/cockroachdb/index.js +4677 -0
- package/dist/dialects/cratedb/index.js +441 -0
- package/dist/dialects/databricks/index.js +517 -0
- package/dist/dialects/db2/index.js +2225 -0
- package/dist/dialects/dialect.js +720 -0
- package/dist/dialects/duckdb/index.js +2014 -0
- package/dist/dialects/exasol/index.js +357 -0
- package/dist/dialects/firebird/index.js +495 -0
- package/dist/dialects/greenplum/index.js +457 -0
- package/dist/dialects/hana/index.js +1741 -0
- package/dist/dialects/mariadb/index.js +3590 -0
- package/dist/dialects/mssql/index.js +2296 -0
- package/dist/dialects/mysql/index.js +4058 -0
- package/dist/dialects/oracle/index.js +2999 -0
- package/dist/dialects/postgres/index.js +5319 -0
- package/dist/dialects/query-stream-helper.js +152 -0
- package/dist/dialects/questdb/index.js +433 -0
- package/dist/dialects/redshift/index.js +2277 -0
- package/dist/dialects/singlestore/index.js +354 -0
- package/dist/dialects/snowflake/index.js +2082 -0
- package/dist/dialects/spanner/index.js +1768 -0
- package/dist/dialects/sqlite/index.js +3382 -0
- package/dist/dialects/tidb/index.js +377 -0
- package/dist/dialects/timescaledb/index.js +164 -0
- package/dist/dialects/trino/index.js +420 -0
- package/dist/dialects/turso/index.js +227 -0
- package/dist/dialects/vertica/index.js +317 -0
- package/dist/dialects/yugabytedb/index.js +424 -0
- package/dist/errors/index.js +472 -0
- package/dist/errors/utils.js +340 -0
- package/dist/errors.js +21 -0
- package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
- package/dist/extensions/catalog/mssql-features.js +147 -0
- package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
- package/dist/extensions/catalog/oracle-db2-features.js +196 -0
- package/dist/extensions/catalog/postgres-extensions.js +516 -0
- package/dist/extensions/index.js +71 -0
- package/dist/extensions/types.js +13 -0
- package/dist/foreign-data.js +173 -0
- package/dist/hooks/hooks-manager.js +350 -0
- package/dist/hooks/index.js +37 -0
- package/dist/index.js +463 -0
- package/dist/logging.js +335 -0
- package/dist/migrations/index.js +40 -0
- package/dist/migrations/migration.js +196 -0
- package/dist/migrations/migrator.js +411 -0
- package/dist/migrations/prormmigration.js +275 -0
- package/dist/migrations/query-interface.js +435 -0
- package/dist/migrations/seeder.js +353 -0
- package/dist/models/associations.js +852 -0
- package/dist/models/constraints.js +288 -0
- package/dist/models/data-types.js +2518 -0
- package/dist/models/decorators.js +445 -0
- package/dist/models/index.js +33 -0
- package/dist/models/indexes.js +531 -0
- package/dist/models/methods.js +382 -0
- package/dist/models/model-manager.js +103 -0
- package/dist/models/model.js +5349 -0
- package/dist/models/operators.js +67 -0
- package/dist/models/scopes.js +189 -0
- package/dist/models/typescript-types.js +26 -0
- package/dist/nosql/aerospike/index.js +205 -0
- package/dist/nosql/allegrograph/index.js +169 -0
- package/dist/nosql/arangodb/index.js +364 -0
- package/dist/nosql/azure-blob/index.js +206 -0
- package/dist/nosql/beanstalkd/index.js +231 -0
- package/dist/nosql/beequeue/index.js +210 -0
- package/dist/nosql/bigchaindb/index.js +195 -0
- package/dist/nosql/bigtable/index.js +224 -0
- package/dist/nosql/blazegraph/index.js +173 -0
- package/dist/nosql/bullmq/index.js +191 -0
- package/dist/nosql/cassandra/index.js +174 -0
- package/dist/nosql/chroma/index.js +190 -0
- package/dist/nosql/cloudflare-kv/index.js +220 -0
- package/dist/nosql/coherence/index.js +200 -0
- package/dist/nosql/cosmosdb/index.js +157 -0
- package/dist/nosql/couchbase/index.js +213 -0
- package/dist/nosql/dax/index.js +212 -0
- package/dist/nosql/deno-kv/index.js +206 -0
- package/dist/nosql/dgraph/index.js +171 -0
- package/dist/nosql/doris/index.js +169 -0
- package/dist/nosql/druid/index.js +162 -0
- package/dist/nosql/dynamodb/index.js +937 -0
- package/dist/nosql/elasticsearch/index.js +377 -0
- package/dist/nosql/etcd/index.js +502 -0
- package/dist/nosql/eventhubs/index.js +213 -0
- package/dist/nosql/eventstore/index.js +254 -0
- package/dist/nosql/faunadb/index.js +188 -0
- package/dist/nosql/firestore/index.js +177 -0
- package/dist/nosql/fluree/index.js +148 -0
- package/dist/nosql/fuseki/index.js +170 -0
- package/dist/nosql/gcs/index.js +172 -0
- package/dist/nosql/gearman/index.js +160 -0
- package/dist/nosql/geode/index.js +196 -0
- package/dist/nosql/graphdb/index.js +169 -0
- package/dist/nosql/graylog/index.js +188 -0
- package/dist/nosql/gridgain/index.js +171 -0
- package/dist/nosql/hazelcast/index.js +162 -0
- package/dist/nosql/hbase/index.js +230 -0
- package/dist/nosql/ignite/index.js +173 -0
- package/dist/nosql/immudb/index.js +184 -0
- package/dist/nosql/index.js +232 -0
- package/dist/nosql/infinispan/index.js +200 -0
- package/dist/nosql/influxdb/index.js +0 -0
- package/dist/nosql/kafka/index.js +234 -0
- package/dist/nosql/keyspaces/index.js +189 -0
- package/dist/nosql/kinesis/index.js +253 -0
- package/dist/nosql/leveldb/index.js +153 -0
- package/dist/nosql/lmdb/index.js +160 -0
- package/dist/nosql/loki/index.js +201 -0
- package/dist/nosql/marklogic/index.js +204 -0
- package/dist/nosql/materialize/index.js +145 -0
- package/dist/nosql/meilisearch/index.js +154 -0
- package/dist/nosql/memcached/index.js +223 -0
- package/dist/nosql/milvus/index.js +410 -0
- package/dist/nosql/minio/index.js +265 -0
- package/dist/nosql/momento/index.js +179 -0
- package/dist/nosql/mongodb/index.js +461 -0
- package/dist/nosql/nats/index.js +247 -0
- package/dist/nosql/nedb/index.js +164 -0
- package/dist/nosql/neo4j/index.js +450 -0
- package/dist/nosql/neptune/index.js +470 -0
- package/dist/nosql/nsq/index.js +200 -0
- package/dist/nosql/opensearch/index.js +186 -0
- package/dist/nosql/orientdb/index.js +175 -0
- package/dist/nosql/papertrail/index.js +200 -0
- package/dist/nosql/pinecone/index.js +0 -0
- package/dist/nosql/pinot/index.js +133 -0
- package/dist/nosql/pouchdb/index.js +172 -0
- package/dist/nosql/prometheus/index.js +174 -0
- package/dist/nosql/provendb/index.js +147 -0
- package/dist/nosql/pubsub/index.js +187 -0
- package/dist/nosql/pulsar/index.js +232 -0
- package/dist/nosql/qdrant/index.js +295 -0
- package/dist/nosql/qldb/index.js +192 -0
- package/dist/nosql/r2/index.js +281 -0
- package/dist/nosql/rabbitmq/index.js +237 -0
- package/dist/nosql/ravendb/index.js +175 -0
- package/dist/nosql/redis/index.js +607 -0
- package/dist/nosql/redpanda/index.js +237 -0
- package/dist/nosql/resque/index.js +203 -0
- package/dist/nosql/rethinkdb/index.js +232 -0
- package/dist/nosql/rocksdb/index.js +152 -0
- package/dist/nosql/rockset/index.js +126 -0
- package/dist/nosql/s3/index.js +298 -0
- package/dist/nosql/scylladb/index.js +178 -0
- package/dist/nosql/signoz/index.js +227 -0
- package/dist/nosql/sns/index.js +201 -0
- package/dist/nosql/solr/index.js +231 -0
- package/dist/nosql/splunk/index.js +227 -0
- package/dist/nosql/sqs/index.js +246 -0
- package/dist/nosql/stardog/index.js +169 -0
- package/dist/nosql/starrocks/index.js +170 -0
- package/dist/nosql/store.js +2 -0
- package/dist/nosql/sumologic/index.js +213 -0
- package/dist/nosql/surrealdb/index.js +179 -0
- package/dist/nosql/terminusdb/index.js +151 -0
- package/dist/nosql/tigergraph/index.js +469 -0
- package/dist/nosql/typesense/index.js +148 -0
- package/dist/nosql/unqlite/index.js +157 -0
- package/dist/nosql/upstash/index.js +167 -0
- package/dist/nosql/vercel-kv/index.js +166 -0
- package/dist/nosql/victoriametrics/index.js +213 -0
- package/dist/nosql/virtuoso/index.js +169 -0
- package/dist/nosql/weaviate/index.js +276 -0
- package/dist/operators/index.js +108 -0
- package/dist/operators.js +2690 -0
- package/dist/prisma-migrate/index.js +38 -0
- package/dist/prisma-migrate/migration-generator.js +125 -0
- package/dist/prisma-migrate/model-generator.js +172 -0
- package/dist/prisma-migrate/relations.js +100 -0
- package/dist/prisma-migrate/schema-parser.js +167 -0
- package/dist/prisma-migrate/type-mapper.js +40 -0
- package/dist/prorm.js +6832 -0
- package/dist/query-builders/cte-builder.js +80 -0
- package/dist/query-builders/functions/aggregate.js +390 -0
- package/dist/query-builders/functions/conditional.js +503 -0
- package/dist/query-builders/functions/datetime.js +695 -0
- package/dist/query-builders/functions/fulltext.js +439 -0
- package/dist/query-builders/functions/index.js +93 -0
- package/dist/query-builders/functions/json.js +427 -0
- package/dist/query-builders/functions/math.js +399 -0
- package/dist/query-builders/functions/string.js +518 -0
- package/dist/query-builders/functions/window.js +328 -0
- package/dist/query-builders/include-builder.js +323 -0
- package/dist/query-builders/index-expression-builder.js +242 -0
- package/dist/query-builders/index.js +161 -0
- package/dist/query-builders/insert-builder.js +164 -0
- package/dist/query-builders/model-helpers.js +38 -0
- package/dist/query-builders/order-limit-builder.js +239 -0
- package/dist/query-builders/sql-compiler.js +2040 -0
- package/dist/query-builders/subquery-builder.js +152 -0
- package/dist/query-builders/update-builder.js +182 -0
- package/dist/query-builders/view-builder.js +218 -0
- package/dist/query-builders/where-builder.js +1532 -0
- package/dist/query-interface.js +622 -0
- package/dist/query-optimizers/batch-optimizer.js +426 -0
- package/dist/query-optimizers/explain-plans.js +415 -0
- package/dist/query-optimizers/index.js +51 -0
- package/dist/query-optimizers/prepared-statement-cache.js +423 -0
- package/dist/query-optimizers/query-hints.js +438 -0
- package/dist/query-optimizers/query-optimizer.js +278 -0
- package/dist/query-optimizers/slow-query-logger.js +271 -0
- package/dist/replica-manager.js +507 -0
- package/dist/schema/index.js +15 -0
- package/dist/schema/migration-generator.js +374 -0
- package/dist/schema/schema-differ.js +549 -0
- package/dist/schema/types.js +6 -0
- package/dist/sql-constants.js +300 -0
- package/dist/sqlite-advanced.js +1047 -0
- package/dist/streams/index.js +12 -0
- package/dist/streams/transforms.js +178 -0
- package/dist/transaction.js +456 -0
- package/dist/types/index.js +180 -0
- package/dist/user-management.js +127 -0
- package/dist/utils/date.js +300 -0
- package/dist/utils/index.js +1079 -0
- package/dist/utils/string.js +161 -0
- package/dist/validators/index.js +19 -0
- package/dist/validators/validator.js +911 -0
- package/package.json +190 -0
|
@@ -0,0 +1,1742 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ClickHouse dialect implementation for the TypeScript ORM
|
|
4
|
+
*
|
|
5
|
+
* ClickHouse is a columnar OLAP database, not an OLTP relational database like the
|
|
6
|
+
* other dialects in this codebase. Several assumptions the rest of this ORM makes
|
|
7
|
+
* about "a SQL database" do not hold for ClickHouse, and this file tries to be
|
|
8
|
+
* explicit (in comments, thrown errors, and generated SQL) about where ClickHouse
|
|
9
|
+
* genuinely behaves differently rather than papering over the differences:
|
|
10
|
+
*
|
|
11
|
+
* - `CREATE TABLE` requires an `ENGINE = ...` clause (e.g. `MergeTree()`), and
|
|
12
|
+
* MergeTree-family engines require an `ORDER BY` clause - ClickHouse's sorting
|
|
13
|
+
* key, which stands in for a traditional single-column auto-increment primary
|
|
14
|
+
* key (there is no such concept here).
|
|
15
|
+
* - There is no enforced FOREIGN KEY, UNIQUE, or (traditionally) PRIMARY KEY
|
|
16
|
+
* constraint. ClickHouse will happily store duplicate "keys" - the ORDER BY
|
|
17
|
+
* key only controls physical sort order/merges, it does not deduplicate or
|
|
18
|
+
* enforce uniqueness unless combined with a ReplacingMergeTree engine and a
|
|
19
|
+
* subsequent (asynchronous, background) merge/`OPTIMIZE ... FINAL`.
|
|
20
|
+
* - There are no synchronous, transactional UPDATE/DELETE statements. ClickHouse
|
|
21
|
+
* supports `ALTER TABLE ... UPDATE` / `ALTER TABLE ... DELETE`, but these are
|
|
22
|
+
* asynchronous *mutations*: the statement returns as soon as the mutation is
|
|
23
|
+
* scheduled, and the actual rewrite happens in the background (progress is
|
|
24
|
+
* visible in `system.mutations`).
|
|
25
|
+
* - There is no multi-statement ACID transaction support (no BEGIN/COMMIT/
|
|
26
|
+
* ROLLBACK spanning multiple statements). A single INSERT is atomic at the
|
|
27
|
+
* block level; nothing else is. `startTransaction`/`commitTransaction`/
|
|
28
|
+
* `rollbackTransaction` throw a clear error rather than silently no-op-ing,
|
|
29
|
+
* matching how this codebase signals genuinely unsupported capabilities
|
|
30
|
+
* elsewhere (see MSSQL's partition methods, or the FDW/extension stubs on
|
|
31
|
+
* non-PostgreSQL dialects).
|
|
32
|
+
* - "Upsert" is not a synchronous `ON CONFLICT` / `MERGE` statement. The
|
|
33
|
+
* idiomatic ClickHouse pattern is: insert a new row into a
|
|
34
|
+
* ReplacingMergeTree(-family) table and let ClickHouse's background merges
|
|
35
|
+
* asynchronously drop older rows sharing the same ORDER BY key (optionally
|
|
36
|
+
* keeping the row with the greatest `version` column). `buildUpsertQuery()`
|
|
37
|
+
* therefore just emits a plain INSERT and documents this loudly instead of
|
|
38
|
+
* pretending to provide synchronous upsert semantics ClickHouse doesn't have.
|
|
39
|
+
*/
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.ClickHouseDialect = void 0;
|
|
42
|
+
exports.createClickHouseDialect = createClickHouseDialect;
|
|
43
|
+
const client_1 = require("@clickhouse/client");
|
|
44
|
+
const query_stream_helper_1 = require("../query-stream-helper");
|
|
45
|
+
const prorm_1 = require("../../prorm");
|
|
46
|
+
/**
|
|
47
|
+
* ClickHouse dialect class that implements the Dialect interface.
|
|
48
|
+
*
|
|
49
|
+
* Uses the official `@clickhouse/client` (Promise-native) driver, talking to
|
|
50
|
+
* ClickHouse's HTTP interface (default port 8123).
|
|
51
|
+
*/
|
|
52
|
+
class ClickHouseDialect {
|
|
53
|
+
constructor(config) {
|
|
54
|
+
this.name = 'clickhouse';
|
|
55
|
+
this.library = '@clickhouse/client';
|
|
56
|
+
this.client = null;
|
|
57
|
+
this._isConnected = false;
|
|
58
|
+
this.config = {
|
|
59
|
+
host: 'localhost',
|
|
60
|
+
port: 8123,
|
|
61
|
+
protocol: 'http',
|
|
62
|
+
database: 'default',
|
|
63
|
+
username: 'default',
|
|
64
|
+
engine: 'MergeTree()',
|
|
65
|
+
...config,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// ==================== Connection ====================
|
|
69
|
+
async connect() {
|
|
70
|
+
try {
|
|
71
|
+
this.client = (0, client_1.createClient)({
|
|
72
|
+
url: `${this.config.protocol}://${this.config.host}:${this.config.port}`,
|
|
73
|
+
username: this.config.username,
|
|
74
|
+
password: this.config.password,
|
|
75
|
+
database: this.config.database,
|
|
76
|
+
request_timeout: this.config.requestTimeout,
|
|
77
|
+
clickhouse_settings: this.config.clickhouseSettings,
|
|
78
|
+
});
|
|
79
|
+
// Verify connectivity up front, matching the "test the connection" behavior
|
|
80
|
+
// of the pool-based dialects (pg/mysql/mariadb) in this codebase.
|
|
81
|
+
const ping = await this.client.ping();
|
|
82
|
+
if (!ping.success) {
|
|
83
|
+
throw new Error(ping.error?.message || 'ClickHouse ping failed');
|
|
84
|
+
}
|
|
85
|
+
this._isConnected = true;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
this._isConnected = false;
|
|
89
|
+
throw new Error(`Failed to connect to ClickHouse: ${error}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async disconnect() {
|
|
93
|
+
if (this.client) {
|
|
94
|
+
await this.client.close();
|
|
95
|
+
this.client = null;
|
|
96
|
+
this._isConnected = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
getConnection() {
|
|
100
|
+
return this.client;
|
|
101
|
+
}
|
|
102
|
+
isConnected() {
|
|
103
|
+
return this._isConnected && this.client !== null;
|
|
104
|
+
}
|
|
105
|
+
async getDatabaseVersion() {
|
|
106
|
+
const result = await this.query('SELECT version() AS version');
|
|
107
|
+
return result.rows[0]?.version || 'unknown';
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Stream query results by paging through `sql` via repeated
|
|
111
|
+
* dialect-appropriate LIMIT/OFFSET queries (see
|
|
112
|
+
* `createPaginatedQueryStream()` in `src/dialects/query-stream-helper.ts`)
|
|
113
|
+
* instead of loading the whole result set into memory at once.
|
|
114
|
+
* @param sql - The SELECT statement to stream
|
|
115
|
+
* @param options - Streaming options (batch size, backpressure watermark, model mapping)
|
|
116
|
+
*/
|
|
117
|
+
queryStream(sql, options) {
|
|
118
|
+
return (0, query_stream_helper_1.createPaginatedQueryStream)(this, sql, options);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Execute a raw SQL statement.
|
|
122
|
+
*
|
|
123
|
+
* `@clickhouse/client` exposes distinct methods for statements that return
|
|
124
|
+
* data (`query()`, used here for SELECT/SHOW/DESCRIBE/EXPLAIN) versus
|
|
125
|
+
* statements that don't (`command()`, used here for everything else - DDL,
|
|
126
|
+
* INSERT, and the ALTER TABLE ... UPDATE/DELETE mutations built below).
|
|
127
|
+
* `command()` does not return row data, so non-SELECT statements always come
|
|
128
|
+
* back with `rows: []`; in particular, `rowCount` after an
|
|
129
|
+
* `ALTER TABLE ... UPDATE/DELETE` mutation is NOT the number of rows the
|
|
130
|
+
* mutation will eventually touch - ClickHouse only reports that
|
|
131
|
+
* asynchronously via `system.mutations`.
|
|
132
|
+
*/
|
|
133
|
+
async query(sql, options) {
|
|
134
|
+
if (!this.client) {
|
|
135
|
+
throw new Error('Not connected to database');
|
|
136
|
+
}
|
|
137
|
+
const replacements = options?.replacements ?? options?.bindings;
|
|
138
|
+
const finalSql = replacements !== undefined ? this.replaceReplacements(sql, replacements) : sql;
|
|
139
|
+
const trimmed = finalSql.trim();
|
|
140
|
+
const upper = trimmed.toUpperCase();
|
|
141
|
+
const isSelect = upper.startsWith('SELECT') ||
|
|
142
|
+
upper.startsWith('WITH') ||
|
|
143
|
+
upper.startsWith('SHOW') ||
|
|
144
|
+
upper.startsWith('DESCRIBE') ||
|
|
145
|
+
upper.startsWith('DESC ') ||
|
|
146
|
+
upper.startsWith('EXPLAIN') ||
|
|
147
|
+
upper.startsWith('EXISTS');
|
|
148
|
+
try {
|
|
149
|
+
if (isSelect) {
|
|
150
|
+
const resultSet = await this.client.query({ query: finalSql, format: 'JSONEachRow' });
|
|
151
|
+
const rows = await resultSet.json();
|
|
152
|
+
const fields = rows.length
|
|
153
|
+
? Object.keys(rows[0]).map((name) => ({
|
|
154
|
+
name,
|
|
155
|
+
type: 'unknown',
|
|
156
|
+
length: 0,
|
|
157
|
+
tableID: 0,
|
|
158
|
+
columnID: 0,
|
|
159
|
+
nullable: true,
|
|
160
|
+
isEnum: false,
|
|
161
|
+
isPrimaryKey: false,
|
|
162
|
+
}))
|
|
163
|
+
: [];
|
|
164
|
+
return { rows, rowCount: rows.length, fields };
|
|
165
|
+
}
|
|
166
|
+
await this.client.command({ query: finalSql });
|
|
167
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
throw new Error(`ClickHouse Query error: ${error}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Substitute `?` (positional) or `:name` (named) placeholders with escaped
|
|
175
|
+
* literal values. ClickHouse's HTTP client sends a single opaque SQL string
|
|
176
|
+
* per request - unlike the pg/mysql2/mssql drivers used by the other
|
|
177
|
+
* dialects in this codebase, there is no native positional-parameter binding
|
|
178
|
+
* for arbitrary values and WHERE trees (ClickHouse's own `{name:Type}` query
|
|
179
|
+
* parameters require a separate `query_params` map and only support scalar
|
|
180
|
+
* substitution). So, exactly like the SQLite/PostgreSQL dialects already do
|
|
181
|
+
* when `options.replacements` is supplied to `query()`, this performs plain
|
|
182
|
+
* string substitution with escaped literals before the SQL is sent.
|
|
183
|
+
*/
|
|
184
|
+
replaceReplacements(sql, replacements) {
|
|
185
|
+
if (!replacements) {
|
|
186
|
+
return sql;
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(replacements)) {
|
|
189
|
+
let i = 0;
|
|
190
|
+
return sql.replace(/\?/g, () => this.escape(replacements[i++]));
|
|
191
|
+
}
|
|
192
|
+
let result = sql;
|
|
193
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
194
|
+
result = result.replace(new RegExp(`:${key}\\b`, 'g'), this.escape(value));
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
// ==================== Escaping ====================
|
|
199
|
+
escape(value) {
|
|
200
|
+
if (value instanceof prorm_1.Literal) {
|
|
201
|
+
return value.val;
|
|
202
|
+
}
|
|
203
|
+
if (value === null || value === undefined) {
|
|
204
|
+
return 'NULL';
|
|
205
|
+
}
|
|
206
|
+
if (typeof value === 'string') {
|
|
207
|
+
return `'${this.escapeString(value)}'`;
|
|
208
|
+
}
|
|
209
|
+
if (typeof value === 'number') {
|
|
210
|
+
return String(value);
|
|
211
|
+
}
|
|
212
|
+
if (typeof value === 'bigint') {
|
|
213
|
+
return value.toString();
|
|
214
|
+
}
|
|
215
|
+
if (typeof value === 'boolean') {
|
|
216
|
+
// ClickHouse's Bool type accepts/renders as true/false, but 1/0 is also
|
|
217
|
+
// valid for the underlying UInt8 representation used pre-Bool-type.
|
|
218
|
+
return value ? 'true' : 'false';
|
|
219
|
+
}
|
|
220
|
+
if (value instanceof Date) {
|
|
221
|
+
return `'${this.formatDateTime(value)}'`;
|
|
222
|
+
}
|
|
223
|
+
if (Buffer.isBuffer(value)) {
|
|
224
|
+
return `'${this.escapeString(value.toString('utf8'))}'`;
|
|
225
|
+
}
|
|
226
|
+
if (Array.isArray(value)) {
|
|
227
|
+
// ClickHouse Array literal syntax: [1, 2, 3] / ['a', 'b'].
|
|
228
|
+
return `[${value.map((v) => this.escape(v)).join(', ')}]`;
|
|
229
|
+
}
|
|
230
|
+
return `'${this.escapeString(JSON.stringify(value))}'`;
|
|
231
|
+
}
|
|
232
|
+
escapeString(str) {
|
|
233
|
+
// ClickHouse string literals use backslash escaping (like MySQL), not the
|
|
234
|
+
// SQL-standard doubled-quote escaping PostgreSQL uses.
|
|
235
|
+
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
236
|
+
}
|
|
237
|
+
formatDateTime(date) {
|
|
238
|
+
const pad = (n, len = 2) => String(n).padStart(len, '0');
|
|
239
|
+
const base = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
240
|
+
// Preserve sub-second precision (DateTime64) rather than silently truncating
|
|
241
|
+
// it. Plain `DateTime` columns in ClickHouse don't accept a fractional
|
|
242
|
+
// part, but JS `Date` only has millisecond resolution anyway, so only emit
|
|
243
|
+
// the fraction when the value actually carries one; whole-second values
|
|
244
|
+
// stay compatible with both `DateTime` and `DateTime64`.
|
|
245
|
+
const ms = date.getMilliseconds();
|
|
246
|
+
if (ms !== 0) {
|
|
247
|
+
return `${base}.${pad(ms, 3)}`;
|
|
248
|
+
}
|
|
249
|
+
return base;
|
|
250
|
+
}
|
|
251
|
+
escapeId(identifier) {
|
|
252
|
+
const id = String(identifier ?? '');
|
|
253
|
+
return `\`${id.replace(/`/g, '``')}\``;
|
|
254
|
+
}
|
|
255
|
+
quoteIdentifier(identifier) {
|
|
256
|
+
return this.escapeId(identifier);
|
|
257
|
+
}
|
|
258
|
+
quoteTable(tableName, schema) {
|
|
259
|
+
if (schema) {
|
|
260
|
+
return `${this.escapeId(schema)}.${this.escapeId(tableName)}`;
|
|
261
|
+
}
|
|
262
|
+
return this.escapeId(tableName);
|
|
263
|
+
}
|
|
264
|
+
// ==================== Schemas (ClickHouse "databases") ====================
|
|
265
|
+
async createSchema(schema) {
|
|
266
|
+
await this.query(`CREATE DATABASE IF NOT EXISTS ${this.escapeId(schema)}`);
|
|
267
|
+
}
|
|
268
|
+
async dropSchema(schema, options) {
|
|
269
|
+
let sql = 'DROP DATABASE';
|
|
270
|
+
if (options?.ifExists !== false)
|
|
271
|
+
sql += ' IF EXISTS';
|
|
272
|
+
sql += ` ${this.escapeId(schema)}`;
|
|
273
|
+
await this.query(sql);
|
|
274
|
+
void options?.cascade; // ClickHouse databases always drop their contained tables; no separate CASCADE keyword.
|
|
275
|
+
}
|
|
276
|
+
async showAllSchemas() {
|
|
277
|
+
const result = await this.query('SELECT name FROM system.databases ORDER BY name');
|
|
278
|
+
return result.rows.map((row) => row.name);
|
|
279
|
+
}
|
|
280
|
+
async listSchemas() {
|
|
281
|
+
return this.showAllSchemas();
|
|
282
|
+
}
|
|
283
|
+
createDatabaseSQL(options) {
|
|
284
|
+
// ClickHouse ignores encoding/collation/template concepts entirely; only the
|
|
285
|
+
// database engine is configurable (Atomic is the modern default).
|
|
286
|
+
return `CREATE DATABASE IF NOT EXISTS ${this.quoteIdentifier(options.name)} ENGINE = Atomic`;
|
|
287
|
+
}
|
|
288
|
+
dropDatabaseSQL(name) {
|
|
289
|
+
return `DROP DATABASE IF EXISTS ${this.quoteIdentifier(name)}`;
|
|
290
|
+
}
|
|
291
|
+
// ==================== Savepoints (unsupported: no transactions) ====================
|
|
292
|
+
createSavepointSQL(_name) {
|
|
293
|
+
throw new Error('ClickHouse does not support savepoints: it has no multi-statement transactions to save a point within.');
|
|
294
|
+
}
|
|
295
|
+
releaseSavepointSQL(_name) {
|
|
296
|
+
throw new Error('ClickHouse does not support savepoints.');
|
|
297
|
+
}
|
|
298
|
+
rollbackToSavepointSQL(_name) {
|
|
299
|
+
throw new Error('ClickHouse does not support savepoints.');
|
|
300
|
+
}
|
|
301
|
+
// ==================== Extensions (PostgreSQL-only concept) ====================
|
|
302
|
+
createExtension(_extensionName, _options) {
|
|
303
|
+
throw new Error('Extensions are not supported by ClickHouse. This is a PostgreSQL-specific feature.');
|
|
304
|
+
}
|
|
305
|
+
dropExtension(_extensionName, _options) {
|
|
306
|
+
throw new Error('Extensions are not supported by ClickHouse. This is a PostgreSQL-specific feature.');
|
|
307
|
+
}
|
|
308
|
+
getExtensions() {
|
|
309
|
+
throw new Error('Extensions are not supported by ClickHouse. This is a PostgreSQL-specific feature.');
|
|
310
|
+
}
|
|
311
|
+
hasExtension(_extensionName) {
|
|
312
|
+
throw new Error('Extensions are not supported by ClickHouse. This is a PostgreSQL-specific feature.');
|
|
313
|
+
}
|
|
314
|
+
// ==================== Tables ====================
|
|
315
|
+
/**
|
|
316
|
+
* Resolve the ORDER BY (sorting key) clause for a MergeTree-family table:
|
|
317
|
+
* an explicit `orderBy` (non-standard extension read off `options` since
|
|
318
|
+
* `TableOptions` doesn't declare it), else column(s) marked `primaryKey`,
|
|
319
|
+
* else the dialect-level default, else the first declared column, else
|
|
320
|
+
* `tuple()` (ClickHouse's explicit "no sorting key" spelling).
|
|
321
|
+
*/
|
|
322
|
+
resolveOrderBy(columns, options) {
|
|
323
|
+
const explicit = options?.orderBy;
|
|
324
|
+
if (explicit) {
|
|
325
|
+
return Array.isArray(explicit)
|
|
326
|
+
? `(${explicit.map((f) => this.escapeId(f)).join(', ')})`
|
|
327
|
+
: explicit;
|
|
328
|
+
}
|
|
329
|
+
const pkFields = Object.entries(columns)
|
|
330
|
+
.filter(([, def]) => def.primaryKey)
|
|
331
|
+
.map(([name]) => name);
|
|
332
|
+
if (pkFields.length > 0) {
|
|
333
|
+
return `(${pkFields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
334
|
+
}
|
|
335
|
+
if (this.config.orderBy) {
|
|
336
|
+
return Array.isArray(this.config.orderBy)
|
|
337
|
+
? `(${this.config.orderBy.map((f) => this.escapeId(f)).join(', ')})`
|
|
338
|
+
: this.config.orderBy;
|
|
339
|
+
}
|
|
340
|
+
const firstColumn = Object.keys(columns)[0];
|
|
341
|
+
return firstColumn ? this.escapeId(firstColumn) : 'tuple()';
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Build a validated `ENGINE = ...` clause for one of the MergeTree-family
|
|
345
|
+
* engines from a typed `MergeTreeEngineConfig`, checking the distinct
|
|
346
|
+
* required-column rules each engine variant has (see the doc comment on
|
|
347
|
+
* `MergeTreeEngineConfig` above) against the table's declared columns.
|
|
348
|
+
* Throws a descriptive error - rather than emitting SQL ClickHouse would
|
|
349
|
+
* reject at CREATE TABLE or (worse) silently misbehave on at merge time -
|
|
350
|
+
* when a required column is missing or the wrong type.
|
|
351
|
+
*/
|
|
352
|
+
buildMergeTreeEngineClause(config, columns) {
|
|
353
|
+
const resolvedType = (columnName) => {
|
|
354
|
+
const def = columns[columnName];
|
|
355
|
+
return def ? this.getDataTypeSql(def.type) : undefined;
|
|
356
|
+
};
|
|
357
|
+
const requireColumn = (columnName, engineName, role) => {
|
|
358
|
+
if (!columns[columnName]) {
|
|
359
|
+
throw new Error(`ClickHouse ${engineName} requires a '${role}' column named '${columnName}', but no such column ` +
|
|
360
|
+
'was declared on this table.');
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const requireInt8 = (columnName, engineName, role) => {
|
|
364
|
+
requireColumn(columnName, engineName, role);
|
|
365
|
+
const type = resolvedType(columnName) || '';
|
|
366
|
+
if (!/^(Nullable\()?Int8\)?$/.test(type)) {
|
|
367
|
+
throw new Error(`ClickHouse ${engineName} requires its '${role}' column ('${columnName}') to be typed Int8 ` +
|
|
368
|
+
`(ClickHouse's Sign column convention: 1 = insert, -1 = cancel), but it resolved to '${type}'.`);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
const requireNumeric = (columnName, engineName, role) => {
|
|
372
|
+
requireColumn(columnName, engineName, role);
|
|
373
|
+
const type = resolvedType(columnName) || '';
|
|
374
|
+
if (!/^(Nullable\()?(U?Int\d+|Float(32|64)|Decimal)/.test(type)) {
|
|
375
|
+
throw new Error(`ClickHouse ${engineName} requires its '${role}' column ('${columnName}') to be a numeric type, ` +
|
|
376
|
+
`but it resolved to '${type}'.`);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
switch (config.type) {
|
|
380
|
+
case 'MergeTree':
|
|
381
|
+
return 'MergeTree()';
|
|
382
|
+
case 'ReplacingMergeTree': {
|
|
383
|
+
const args = [];
|
|
384
|
+
if (config.version) {
|
|
385
|
+
requireColumn(config.version, 'ReplacingMergeTree', 'version');
|
|
386
|
+
args.push(this.escapeId(config.version));
|
|
387
|
+
}
|
|
388
|
+
if (config.isDeleted) {
|
|
389
|
+
if (!config.version) {
|
|
390
|
+
throw new Error('ClickHouse ReplacingMergeTree: `isDeleted` requires `version` to also be provided ' +
|
|
391
|
+
'(is_deleted only makes sense paired with a version column).');
|
|
392
|
+
}
|
|
393
|
+
requireColumn(config.isDeleted, 'ReplacingMergeTree', 'is_deleted');
|
|
394
|
+
args.push(this.escapeId(config.isDeleted));
|
|
395
|
+
}
|
|
396
|
+
return `ReplacingMergeTree(${args.join(', ')})`;
|
|
397
|
+
}
|
|
398
|
+
case 'SummingMergeTree': {
|
|
399
|
+
if (config.columns && config.columns.length > 0) {
|
|
400
|
+
for (const col of config.columns) {
|
|
401
|
+
requireNumeric(col, 'SummingMergeTree', 'summed');
|
|
402
|
+
}
|
|
403
|
+
return `SummingMergeTree((${config.columns.map((c) => this.escapeId(c)).join(', ')}))`;
|
|
404
|
+
}
|
|
405
|
+
return 'SummingMergeTree()';
|
|
406
|
+
}
|
|
407
|
+
case 'AggregatingMergeTree': {
|
|
408
|
+
const hasAggregateColumn = Object.values(columns).some((def) => {
|
|
409
|
+
const type = this.getDataTypeSql(def.type);
|
|
410
|
+
return /^(Simple)?AggregateFunction\(/.test(type);
|
|
411
|
+
});
|
|
412
|
+
if (!hasAggregateColumn) {
|
|
413
|
+
throw new Error('ClickHouse AggregatingMergeTree requires at least one AggregateFunction(...) or ' +
|
|
414
|
+
'SimpleAggregateFunction(...) column to merge partial aggregate states into; none was found ' +
|
|
415
|
+
'on this table. Pass a raw type string like `{ type: \'AggregateFunction(sum, UInt64)\' }` for ' +
|
|
416
|
+
"the column(s) that hold aggregate state (this dialect's DataType abstraction has no dedicated " +
|
|
417
|
+
'variant for it, so it must be supplied as a literal type string).');
|
|
418
|
+
}
|
|
419
|
+
return 'AggregatingMergeTree()';
|
|
420
|
+
}
|
|
421
|
+
case 'CollapsingMergeTree': {
|
|
422
|
+
requireInt8(config.sign, 'CollapsingMergeTree', 'Sign');
|
|
423
|
+
return `CollapsingMergeTree(${this.escapeId(config.sign)})`;
|
|
424
|
+
}
|
|
425
|
+
case 'VersionedCollapsingMergeTree': {
|
|
426
|
+
requireInt8(config.sign, 'VersionedCollapsingMergeTree', 'Sign');
|
|
427
|
+
requireNumeric(config.version, 'VersionedCollapsingMergeTree', 'Version');
|
|
428
|
+
return `VersionedCollapsingMergeTree(${this.escapeId(config.sign)}, ${this.escapeId(config.version)})`;
|
|
429
|
+
}
|
|
430
|
+
default: {
|
|
431
|
+
const exhaustive = config;
|
|
432
|
+
throw new Error(`ClickHouse: unknown MergeTree engine config: ${JSON.stringify(exhaustive)}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Build a projection body - `name (SELECT ... [GROUP BY ...] [ORDER BY ...])`
|
|
438
|
+
* - shared by the inline `PROJECTION` clause in `CREATE TABLE` and
|
|
439
|
+
* `ALTER TABLE ... ADD PROJECTION`, since both use the exact same grammar.
|
|
440
|
+
*/
|
|
441
|
+
buildProjectionClause(name, def) {
|
|
442
|
+
if (!def.select) {
|
|
443
|
+
throw new Error(`ClickHouse: projection '${name}' requires a 'select' column list/expression.`);
|
|
444
|
+
}
|
|
445
|
+
let body = `SELECT ${def.select}`;
|
|
446
|
+
if (def.groupBy) {
|
|
447
|
+
body += ` GROUP BY ${Array.isArray(def.groupBy) ? def.groupBy.join(', ') : def.groupBy}`;
|
|
448
|
+
}
|
|
449
|
+
if (def.orderBy) {
|
|
450
|
+
body += ` ORDER BY ${Array.isArray(def.orderBy) ? def.orderBy.join(', ') : def.orderBy}`;
|
|
451
|
+
}
|
|
452
|
+
return `${this.escapeId(name)} (${body})`;
|
|
453
|
+
}
|
|
454
|
+
getColumnDefinitionSql(columnName, definition) {
|
|
455
|
+
let type = this.getDataTypeSql(definition.type);
|
|
456
|
+
// ClickHouse columns are NOT NULL by default (the opposite default of
|
|
457
|
+
// standard SQL); Nullable(T) must be requested explicitly, and it disables
|
|
458
|
+
// some storage optimizations, so we only add it when allowNull isn't false.
|
|
459
|
+
if (definition.allowNull !== false && !/^(Array|Nullable|LowCardinality|Map|Tuple)\(/.test(type)) {
|
|
460
|
+
type = `Nullable(${type})`;
|
|
461
|
+
}
|
|
462
|
+
let sql = `${this.escapeId(columnName)} ${type}`;
|
|
463
|
+
if (definition.defaultValue !== undefined) {
|
|
464
|
+
sql += ` DEFAULT ${this.getDefaultValue(definition.defaultValue)}`;
|
|
465
|
+
}
|
|
466
|
+
if (definition.autoIncrement) {
|
|
467
|
+
// eslint-disable-next-line no-console
|
|
468
|
+
console.warn(`ClickHouse: column '${columnName}' requested autoIncrement, but ClickHouse has no native ` +
|
|
469
|
+
`auto-increment/IDENTITY mechanism. Consider a DEFAULT of generateUUIDv4() (for UUID columns) ` +
|
|
470
|
+
`or an application-maintained counter; the option is ignored here.`);
|
|
471
|
+
}
|
|
472
|
+
if (definition.primaryKey || definition.unique || definition.references) {
|
|
473
|
+
// eslint-disable-next-line no-console
|
|
474
|
+
console.warn(`ClickHouse: column '${columnName}' has primaryKey/unique/references set, but ClickHouse does ` +
|
|
475
|
+
`not enforce PRIMARY KEY, UNIQUE, or FOREIGN KEY constraints. 'primaryKey' only feeds this ` +
|
|
476
|
+
`dialect's ORDER BY (sorting key) inference; 'unique'/'references' are ignored entirely.`);
|
|
477
|
+
}
|
|
478
|
+
if (definition.comment) {
|
|
479
|
+
sql += ` COMMENT ${this.escape(definition.comment)}`;
|
|
480
|
+
}
|
|
481
|
+
// Column-level TTL: e.g. `TTL created_at + INTERVAL 30 DAY` (optionally
|
|
482
|
+
// `... TO DISK 'cold'` / `... TO VOLUME 'cold_volume'` to tier the column
|
|
483
|
+
// to different storage rather than dropping it). Not part of the shared
|
|
484
|
+
// `ColumnDefinition` type, so read off as a loosely-typed extension the
|
|
485
|
+
// same way `orderBy`/`partitionBy` are elsewhere in this file.
|
|
486
|
+
const columnTtl = definition.ttl;
|
|
487
|
+
if (columnTtl) {
|
|
488
|
+
sql += ` TTL ${columnTtl}`;
|
|
489
|
+
}
|
|
490
|
+
return sql;
|
|
491
|
+
}
|
|
492
|
+
getDefaultValue(value) {
|
|
493
|
+
if (value === null)
|
|
494
|
+
return 'NULL';
|
|
495
|
+
if (typeof value === 'object' &&
|
|
496
|
+
value !== null &&
|
|
497
|
+
typeof value.toDefaultValue === 'function') {
|
|
498
|
+
return value.toDefaultValue();
|
|
499
|
+
}
|
|
500
|
+
if (typeof value === 'string') {
|
|
501
|
+
if (/^(now|today|generateUUIDv4)\s*\(/i.test(value) || value.toUpperCase() === 'NULL') {
|
|
502
|
+
return value;
|
|
503
|
+
}
|
|
504
|
+
return `'${this.escapeString(value)}'`;
|
|
505
|
+
}
|
|
506
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
507
|
+
return String(value);
|
|
508
|
+
if (value instanceof Date)
|
|
509
|
+
return `'${this.formatDateTime(value)}'`;
|
|
510
|
+
return `'${this.escapeString(String(value))}'`;
|
|
511
|
+
}
|
|
512
|
+
async createTable(tableName, columns, options) {
|
|
513
|
+
const columnDefs = Object.entries(columns).map(([name, def]) => this.getColumnDefinitionSql(name, def));
|
|
514
|
+
if (options?.constraints?.some((c) => c.type !== 'CHECK')) {
|
|
515
|
+
// eslint-disable-next-line no-console
|
|
516
|
+
console.warn('ClickHouse: PRIMARY KEY/UNIQUE/FOREIGN KEY table constraints are not enforced and are ignored ' +
|
|
517
|
+
"(similar gotcha to Redshift). Only CHECK constraints map to real ClickHouse syntax; use ORDER BY " +
|
|
518
|
+
'and a ReplacingMergeTree engine if you need "latest row per key" semantics instead of uniqueness.');
|
|
519
|
+
}
|
|
520
|
+
if (options?.uniqueKeys) {
|
|
521
|
+
// eslint-disable-next-line no-console
|
|
522
|
+
console.warn('ClickHouse: uniqueKeys are not enforced by ClickHouse and are ignored.');
|
|
523
|
+
}
|
|
524
|
+
for (const constraint of options?.constraints ?? []) {
|
|
525
|
+
if (constraint.type === 'CHECK' && constraint.check) {
|
|
526
|
+
columnDefs.push(`CONSTRAINT ${this.escapeId(constraint.name || `${tableName}_check`)} CHECK (${constraint.check})`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Inline `PROJECTION name (SELECT ... [GROUP BY ...] [ORDER BY ...])`
|
|
530
|
+
// clause(s), one per entry in the loosely-typed `projections` extension
|
|
531
|
+
// (like `mergeTreeEngine`/`orderBy`/`ttl` above, read off `options`
|
|
532
|
+
// since `TableOptions` doesn't declare it) keyed by projection name.
|
|
533
|
+
// Equivalent to calling `addProjection()` once per projection right after
|
|
534
|
+
// creation, but lets ClickHouse build the projection alongside the table
|
|
535
|
+
// instead of as a separate mutation against (still-empty) data.
|
|
536
|
+
const projections = options?.projections;
|
|
537
|
+
if (projections) {
|
|
538
|
+
for (const [name, def] of Object.entries(projections)) {
|
|
539
|
+
columnDefs.push(`PROJECTION ${this.buildProjectionClause(name, def)}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
let sql = 'CREATE TABLE';
|
|
543
|
+
if (options?.ifNotExists)
|
|
544
|
+
sql += ' IF NOT EXISTS';
|
|
545
|
+
sql += ` ${this.quoteTable(tableName)} (${columnDefs.join(', ')})`;
|
|
546
|
+
// ENGINE is mandatory in ClickHouse; MergeTree() is the sane general-purpose default.
|
|
547
|
+
// `mergeTreeEngine` (a typed `MergeTreeEngineConfig`) takes precedence over a raw
|
|
548
|
+
// `engine` string when supplied, since it gets validated required-column checks
|
|
549
|
+
// (e.g. CollapsingMergeTree's `Sign Int8` column) that a hand-written engine
|
|
550
|
+
// string cannot. Read off as a loosely-typed extension (like `orderBy`/`ttl`
|
|
551
|
+
// elsewhere in this file) since `TableOptions` doesn't declare it.
|
|
552
|
+
const mergeTreeEngine = options?.mergeTreeEngine;
|
|
553
|
+
const engineClause = mergeTreeEngine
|
|
554
|
+
? this.buildMergeTreeEngineClause(mergeTreeEngine, columns)
|
|
555
|
+
: options?.engine || this.config.engine || 'MergeTree()';
|
|
556
|
+
sql += ` ENGINE = ${engineClause}`;
|
|
557
|
+
if (options?.partitionBy) {
|
|
558
|
+
sql += ` PARTITION BY ${options.partitionBy}`;
|
|
559
|
+
}
|
|
560
|
+
// ORDER BY is mandatory for MergeTree-family engines - ClickHouse's stand-in
|
|
561
|
+
// for a traditional primary key (it controls physical sort/merge order, not
|
|
562
|
+
// uniqueness).
|
|
563
|
+
sql += ` ORDER BY ${this.resolveOrderBy(columns, options)}`;
|
|
564
|
+
// Table-level TTL: ClickHouse's automatic data expiration/tiered-storage
|
|
565
|
+
// mechanism, e.g. `TTL created_at + INTERVAL 30 DAY` (rows older than the
|
|
566
|
+
// expression are dropped on the next merge) or
|
|
567
|
+
// `TTL created_at + INTERVAL 30 DAY TO VOLUME 'cold'` (moved to a
|
|
568
|
+
// different storage volume/disk instead of deleted). Read off as a
|
|
569
|
+
// loosely-typed extension (like `partitionBy`/`orderBy` above) since
|
|
570
|
+
// `TableOptions` doesn't declare it.
|
|
571
|
+
const tableTtl = options?.ttl;
|
|
572
|
+
if (tableTtl) {
|
|
573
|
+
sql += ` TTL ${tableTtl}`;
|
|
574
|
+
}
|
|
575
|
+
if (options?.comment) {
|
|
576
|
+
sql += ` COMMENT ${this.escape(options.comment)}`;
|
|
577
|
+
}
|
|
578
|
+
if (options?.rowFormat) {
|
|
579
|
+
// eslint-disable-next-line no-console
|
|
580
|
+
console.warn(`ClickHouse: rowFormat option '${options.rowFormat}' has no equivalent and is ignored.`);
|
|
581
|
+
}
|
|
582
|
+
if (options?.initialAutoIncrement) {
|
|
583
|
+
// eslint-disable-next-line no-console
|
|
584
|
+
console.warn('ClickHouse: initialAutoIncrement has no equivalent (no auto-increment support) and is ignored.');
|
|
585
|
+
}
|
|
586
|
+
await this.query(sql);
|
|
587
|
+
if (options?.indexes) {
|
|
588
|
+
for (const index of options.indexes) {
|
|
589
|
+
await this.addIndex(tableName, index.name || `idx_${tableName}_${index.fields.join('_')}`, index.fields, { unique: index.unique, type: index.type, using: index.using, where: index.where });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async dropTable(tableName, options) {
|
|
594
|
+
let sql = 'DROP TABLE';
|
|
595
|
+
if (options?.ifExists)
|
|
596
|
+
sql += ' IF EXISTS';
|
|
597
|
+
sql += ` ${this.quoteTable(tableName)}`;
|
|
598
|
+
if (options?.cascade) {
|
|
599
|
+
// eslint-disable-next-line no-console
|
|
600
|
+
console.warn('ClickHouse: DROP TABLE has no CASCADE keyword (there are no FK constraints to cascade); ignored.');
|
|
601
|
+
}
|
|
602
|
+
await this.query(sql);
|
|
603
|
+
}
|
|
604
|
+
// ==================== Partitions ====================
|
|
605
|
+
//
|
|
606
|
+
// ClickHouse's PARTITION BY groups rows *within a single table* by an
|
|
607
|
+
// expression (e.g. `toYYYYMM(date)`); it is not PostgreSQL-style declarative
|
|
608
|
+
// partitioning where each partition is a separate child table you create,
|
|
609
|
+
// attach, or detach independently ahead of time. Partitions come into
|
|
610
|
+
// existence automatically the first time a row with a given partition-key
|
|
611
|
+
// value is inserted.
|
|
612
|
+
async createPartitionedTable(tableName, columns, options) {
|
|
613
|
+
const partitionColumn = Array.isArray(options?.partitionBy?.column)
|
|
614
|
+
? `(${options.partitionBy.column.map((c) => this.escapeId(c)).join(', ')})`
|
|
615
|
+
: this.escapeId(options?.partitionBy?.column);
|
|
616
|
+
await this.createTable(tableName, columns, { ...options, partitionBy: partitionColumn });
|
|
617
|
+
if (options?.partitions && options.partitions.length > 0) {
|
|
618
|
+
// eslint-disable-next-line no-console
|
|
619
|
+
console.warn('ClickHouse: pre-declared partitions are ignored. Unlike PostgreSQL, ClickHouse partitions are ' +
|
|
620
|
+
'created automatically the first time a row with a given PARTITION BY value is inserted - there ' +
|
|
621
|
+
'is no way to declare an empty partition ahead of time.');
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
async createPartition(_options) {
|
|
625
|
+
throw new Error('ClickHouse does not support explicitly pre-creating a partition. Partitions are automatically ' +
|
|
626
|
+
'materialized based on the PARTITION BY expression of rows you INSERT; there is no equivalent of ' +
|
|
627
|
+
"PostgreSQL's CREATE TABLE ... PARTITION OF.");
|
|
628
|
+
}
|
|
629
|
+
async attachPartition(options) {
|
|
630
|
+
// Unlike detachPartition/dropPartition below, AttachPartitionOptions does
|
|
631
|
+
// carry a parentTable, so this can map onto real ClickHouse syntax.
|
|
632
|
+
const sql = `ALTER TABLE ${this.quoteTable(options.parentTable)} ATTACH PARTITION ${this.formatPartitionExpr(options.partitionName)}`;
|
|
633
|
+
await this.query(sql);
|
|
634
|
+
}
|
|
635
|
+
async detachPartition(_options) {
|
|
636
|
+
throw new Error("ClickHouse's ALTER TABLE ... DETACH PARTITION requires the table name, but this ORM's " +
|
|
637
|
+
'DetachPartitionOptions (modeled on PostgreSQL, where a partition is itself a table) only carries ' +
|
|
638
|
+
"a partition name. Call query(`ALTER TABLE <table> DETACH PARTITION <expr>`) directly instead.");
|
|
639
|
+
}
|
|
640
|
+
async dropPartition(_partitionName, _options) {
|
|
641
|
+
throw new Error("ClickHouse's ALTER TABLE ... DROP PARTITION requires the table name, but this ORM's dropPartition " +
|
|
642
|
+
"signature (modeled on PostgreSQL, where a partition is itself a droppable table) doesn't carry one. " +
|
|
643
|
+
'Call query(`ALTER TABLE <table> DROP PARTITION <expr>`) directly instead.');
|
|
644
|
+
}
|
|
645
|
+
/** Format a partition value/expression for ATTACH/DETACH/DROP PARTITION. */
|
|
646
|
+
formatPartitionExpr(partitionName) {
|
|
647
|
+
// A bare hex partition ID (as reported by system.parts.partition_id) is
|
|
648
|
+
// referenced via `PARTITION ID '...'`; anything else is treated as an
|
|
649
|
+
// already-formatted partition expression/value (e.g. "'2024-01-01'" or "(1,2)").
|
|
650
|
+
if (/^[0-9a-f]{8,}(_[0-9a-f]+)*$/i.test(partitionName)) {
|
|
651
|
+
return `ID '${this.escapeString(partitionName)}'`;
|
|
652
|
+
}
|
|
653
|
+
return partitionName;
|
|
654
|
+
}
|
|
655
|
+
// ==================== Views ====================
|
|
656
|
+
async createView(viewName, query, options) {
|
|
657
|
+
let sql = options?.replace ? 'CREATE OR REPLACE VIEW' : 'CREATE VIEW';
|
|
658
|
+
sql += ` ${this.quoteTable(viewName, options?.schema)} AS ${query}`;
|
|
659
|
+
await this.query(sql);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* `CREATE MATERIALIZED VIEW ... TO <target> AS SELECT ...` - ClickHouse's
|
|
663
|
+
* real materialized-view syntax. Unlike a plain (or most other databases')
|
|
664
|
+
* view, a ClickHouse materialized view is a push-based, insert-triggered
|
|
665
|
+
* transform: every time a row is INSERTed into the underlying source
|
|
666
|
+
* table(s) referenced by `query`'s FROM clause, ClickHouse runs `query`
|
|
667
|
+
* against just the newly-inserted block and appends the result into the
|
|
668
|
+
* `to` target table - it is NOT recomputed at read time the way a normal
|
|
669
|
+
* view or a re-run query would be, and it does not "backfill" existing
|
|
670
|
+
* rows already in the source table unless `populate: true` is given.
|
|
671
|
+
*
|
|
672
|
+
* `showViews()` already queries `system.tables` for `engine =
|
|
673
|
+
* 'MaterializedView'`, so this is the corresponding write-side method that
|
|
674
|
+
* was previously entirely missing (only plain, non-materialized
|
|
675
|
+
* `CREATE VIEW` existed via `createView()` above).
|
|
676
|
+
*
|
|
677
|
+
* Two real ClickHouse forms are supported:
|
|
678
|
+
* - `{ to: 'target_table' }`: the common, recommended form - the target
|
|
679
|
+
* table is created separately (with its own ENGINE/ORDER BY, e.g. an
|
|
680
|
+
* AggregatingMergeTree for rollups) and the materialized view just
|
|
681
|
+
* describes the transform feeding it.
|
|
682
|
+
* - Omitting `to` (with an inline `engine`/`orderBy0`): ClickHouse also
|
|
683
|
+
* allows a materialized view to own an implicit hidden target table
|
|
684
|
+
* (`CREATE MATERIALIZED VIEW ... ENGINE = ... AS SELECT ...`); provide
|
|
685
|
+
* `engine`/`orderBy` in that case instead of `to`.
|
|
686
|
+
*/
|
|
687
|
+
async createMaterializedView(viewName, query, options) {
|
|
688
|
+
if (!options?.to && !options?.engine) {
|
|
689
|
+
throw new Error('ClickHouse createMaterializedView(): either `to` (a target table to write into) or `engine` ' +
|
|
690
|
+
'(so the materialized view can own an implicit hidden target table) must be provided.');
|
|
691
|
+
}
|
|
692
|
+
let sql = options?.replace ? 'CREATE OR REPLACE MATERIALIZED VIEW' : 'CREATE MATERIALIZED VIEW';
|
|
693
|
+
if (options?.ifNotExists)
|
|
694
|
+
sql += ' IF NOT EXISTS';
|
|
695
|
+
sql += ` ${this.quoteTable(viewName, options?.schema)}`;
|
|
696
|
+
if (options?.to) {
|
|
697
|
+
sql += ` TO ${this.quoteTable(options.to, options.schema)}`;
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
sql += ` ENGINE = ${options?.engine}`;
|
|
701
|
+
if (options?.orderBy) {
|
|
702
|
+
const orderBy = Array.isArray(options.orderBy)
|
|
703
|
+
? `(${options.orderBy.map((f) => this.escapeId(f)).join(', ')})`
|
|
704
|
+
: options.orderBy;
|
|
705
|
+
sql += ` ORDER BY ${orderBy}`;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// POPULATE backfills existing source-table rows into the target at
|
|
709
|
+
// creation time; without it, the view only sees rows inserted AFTER
|
|
710
|
+
// creation. ClickHouse's own docs warn POPULATE races with concurrent
|
|
711
|
+
// inserts (rows written during the backfill may be double-counted or
|
|
712
|
+
// missed), so this is opt-in, not the default.
|
|
713
|
+
if (options?.populate) {
|
|
714
|
+
sql += ' POPULATE';
|
|
715
|
+
}
|
|
716
|
+
sql += ` AS ${query}`;
|
|
717
|
+
await this.query(sql);
|
|
718
|
+
}
|
|
719
|
+
async dropView(viewName, options) {
|
|
720
|
+
let sql = 'DROP VIEW';
|
|
721
|
+
if (options?.ifExists)
|
|
722
|
+
sql += ' IF EXISTS';
|
|
723
|
+
sql += ` ${this.quoteTable(viewName)}`;
|
|
724
|
+
await this.query(sql);
|
|
725
|
+
void options?.cascade; // no dependent-object cascading concept for views in ClickHouse
|
|
726
|
+
}
|
|
727
|
+
async showViews() {
|
|
728
|
+
const result = await this.query(`SELECT name FROM system.tables WHERE database = currentDatabase() AND engine IN ('View', 'MaterializedView') ORDER BY name`);
|
|
729
|
+
return result.rows.map((row) => row.name);
|
|
730
|
+
}
|
|
731
|
+
async showTables() {
|
|
732
|
+
const result = await this.query(`SELECT name FROM system.tables WHERE database = currentDatabase() AND engine NOT IN ('View', 'MaterializedView') ORDER BY name`);
|
|
733
|
+
return result.rows.map((row) => row.name);
|
|
734
|
+
}
|
|
735
|
+
// ==================== Columns ====================
|
|
736
|
+
async addColumn(tableName, columnName, definition) {
|
|
737
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD COLUMN ${this.getColumnDefinitionSql(columnName, definition)}`);
|
|
738
|
+
}
|
|
739
|
+
async removeColumn(tableName, columnName) {
|
|
740
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} DROP COLUMN ${this.escapeId(columnName)}`);
|
|
741
|
+
}
|
|
742
|
+
async changeColumn(tableName, columnName, definition) {
|
|
743
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} MODIFY COLUMN ${this.getColumnDefinitionSql(columnName, definition)}`);
|
|
744
|
+
}
|
|
745
|
+
async renameColumn(tableName, oldColumnName, newColumnName) {
|
|
746
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} RENAME COLUMN ${this.escapeId(oldColumnName)} TO ${this.escapeId(newColumnName)}`);
|
|
747
|
+
}
|
|
748
|
+
async addForeignKey(_tableName, _columnName, _referencedTableName, _referencedColumnName, _options) {
|
|
749
|
+
throw new Error('ClickHouse does not support FOREIGN KEY constraints (similar gotcha to Redshift). Referential ' +
|
|
750
|
+
'integrity, if needed, must be enforced by the application.');
|
|
751
|
+
}
|
|
752
|
+
// ==================== Bulk insert ====================
|
|
753
|
+
async bulkInsert(tableName, records, _options) {
|
|
754
|
+
if (records.length === 0) {
|
|
755
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
756
|
+
}
|
|
757
|
+
if (!this.client) {
|
|
758
|
+
throw new Error('Not connected to database');
|
|
759
|
+
}
|
|
760
|
+
// Use the driver's native `insert()` (JSONEachRow streaming) instead of
|
|
761
|
+
// building one giant literal-escaped `INSERT ... VALUES (...), (...)` SQL
|
|
762
|
+
// string. The hand-built-SQL approach risks exceeding ClickHouse's max
|
|
763
|
+
// query size (and holding the whole batch as one string in memory) for
|
|
764
|
+
// large batches, and duplicates format handling the client already does
|
|
765
|
+
// correctly (chunked/streamed encoding).
|
|
766
|
+
const columns = Object.keys(records[0]);
|
|
767
|
+
const values = records.map((record) => {
|
|
768
|
+
const row = {};
|
|
769
|
+
for (const column of columns) {
|
|
770
|
+
const value = record[column];
|
|
771
|
+
if (value instanceof prorm_1.Literal) {
|
|
772
|
+
row[column] = value.val;
|
|
773
|
+
}
|
|
774
|
+
else if (value instanceof Date) {
|
|
775
|
+
// JSONEachRow expects ClickHouse's own `YYYY-MM-DD HH:MM:SS[.fff]`
|
|
776
|
+
// wire format for DateTime/DateTime64, not JS's default ISO-8601
|
|
777
|
+
// (with a `T`/`Z`) that `JSON.stringify` would otherwise produce.
|
|
778
|
+
row[column] = this.formatDateTime(value);
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
row[column] = value;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return row;
|
|
785
|
+
});
|
|
786
|
+
try {
|
|
787
|
+
await this.client.insert({
|
|
788
|
+
table: this.quoteTable(tableName),
|
|
789
|
+
values,
|
|
790
|
+
format: 'JSONEachRow',
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
throw new Error(`ClickHouse Query error: ${error}`);
|
|
795
|
+
}
|
|
796
|
+
return { rows: [], rowCount: records.length, fields: [] };
|
|
797
|
+
}
|
|
798
|
+
// ==================== Constraints ====================
|
|
799
|
+
//
|
|
800
|
+
// ClickHouse only supports CHECK constraints (validated on INSERT only, not
|
|
801
|
+
// retroactively on existing data). PRIMARY KEY/UNIQUE/FOREIGN KEY constraints
|
|
802
|
+
// don't exist and are rejected here rather than silently accepted and ignored,
|
|
803
|
+
// since - unlike optional table/column decorations - callers relying on these
|
|
804
|
+
// for integrity need to find out immediately.
|
|
805
|
+
async showConstraints(_tableName) {
|
|
806
|
+
// No dedicated, version-stable system table exposes CHECK constraints, and
|
|
807
|
+
// there are no PK/FK/UNIQUE constraints to report.
|
|
808
|
+
return [];
|
|
809
|
+
}
|
|
810
|
+
async addConstraint(tableName, options) {
|
|
811
|
+
if (options.type !== 'CHECK') {
|
|
812
|
+
throw new Error(`ClickHouse does not enforce ${options.type} constraints (no traditional PK/UNIQUE/FK support).`);
|
|
813
|
+
}
|
|
814
|
+
const name = options.name || `${tableName}_check`;
|
|
815
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD CONSTRAINT ${this.escapeId(name)} CHECK (${options.check})`);
|
|
816
|
+
}
|
|
817
|
+
async removeConstraint(tableName, constraintName) {
|
|
818
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.escapeId(constraintName)}`);
|
|
819
|
+
}
|
|
820
|
+
async createConstraint(tableName, constraintDef) {
|
|
821
|
+
if (constraintDef.type !== 'CHECK') {
|
|
822
|
+
throw new Error(`ClickHouse does not enforce ${constraintDef.type} constraints (no traditional PK/UNIQUE/FK support).`);
|
|
823
|
+
}
|
|
824
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD CONSTRAINT ${this.escapeId(constraintDef.name)} CHECK (${constraintDef.check})`);
|
|
825
|
+
}
|
|
826
|
+
async dropConstraint(tableName, constraintName, _options) {
|
|
827
|
+
await this.removeConstraint(tableName, constraintName);
|
|
828
|
+
}
|
|
829
|
+
// ==================== Indexes ====================
|
|
830
|
+
//
|
|
831
|
+
// ClickHouse's `ADD INDEX` creates a lightweight "data skipping index" (e.g.
|
|
832
|
+
// minmax/set/bloom_filter), which lets scans skip whole granules that can't
|
|
833
|
+
// match a predicate. This is NOT a traditional B-Tree index that guarantees
|
|
834
|
+
// O(log n) point lookups the way a PostgreSQL/MySQL index does.
|
|
835
|
+
async showIndexes(tableName) {
|
|
836
|
+
const result = await this.query(`SELECT name, type, expr, granularity FROM system.data_skipping_indices WHERE table = ${this.escape(tableName)} AND database = currentDatabase()`);
|
|
837
|
+
return result.rows;
|
|
838
|
+
}
|
|
839
|
+
async addIndex(tableName, indexName, fields = [], options) {
|
|
840
|
+
const expr = options?.expression || (fields.length > 1 ? `(${fields.map((f) => this.escapeId(f)).join(', ')})` : this.escapeId(fields[0]));
|
|
841
|
+
const type = options?.type || 'minmax';
|
|
842
|
+
const granularity = 1;
|
|
843
|
+
let sql = 'ALTER TABLE';
|
|
844
|
+
sql += ` ${this.quoteTable(tableName)} ADD INDEX`;
|
|
845
|
+
if (options?.ifNotExists)
|
|
846
|
+
sql += ' IF NOT EXISTS';
|
|
847
|
+
sql += ` ${this.escapeId(indexName)} ${expr} TYPE ${type} GRANULARITY ${granularity}`;
|
|
848
|
+
await this.query(sql);
|
|
849
|
+
}
|
|
850
|
+
async removeIndex(tableName, indexName) {
|
|
851
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} DROP INDEX ${this.escapeId(indexName)}`);
|
|
852
|
+
}
|
|
853
|
+
async createIndex(tableName, indexDef) {
|
|
854
|
+
await this.addIndex(tableName, indexDef.name, indexDef.fields, {
|
|
855
|
+
type: indexDef.type,
|
|
856
|
+
using: indexDef.using,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
async dropIndex(tableName, indexName, options) {
|
|
860
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} DROP INDEX`;
|
|
861
|
+
if (options?.ifExists)
|
|
862
|
+
sql += ' IF EXISTS';
|
|
863
|
+
sql += ` ${this.escapeId(indexName)}`;
|
|
864
|
+
await this.query(sql);
|
|
865
|
+
}
|
|
866
|
+
// ==================== Projections ====================
|
|
867
|
+
//
|
|
868
|
+
// A MergeTree-family "projection" is an alternate, ClickHouse-maintained
|
|
869
|
+
// physical layout of (a subset of) a table's columns - its own sort order
|
|
870
|
+
// and, optionally, its own pre-aggregation - kept in sync automatically on
|
|
871
|
+
// every INSERT/merge. ClickHouse's query planner transparently substitutes
|
|
872
|
+
// a matching projection in place of the base table when doing so answers a
|
|
873
|
+
// query more cheaply, without the caller having to name it in the query.
|
|
874
|
+
// This is not part of the shared `Dialect` interface - other dialects have
|
|
875
|
+
// no equivalent concept - so `addProjection`/`dropProjection` are exposed
|
|
876
|
+
// as ClickHouse-specific extra public methods, the same way `uniq()`/
|
|
877
|
+
// `quantile()`/etc. are below.
|
|
878
|
+
/**
|
|
879
|
+
* `ALTER TABLE table ADD PROJECTION name (SELECT ... [GROUP BY ...] [ORDER BY ...])`.
|
|
880
|
+
*
|
|
881
|
+
* Adding a projection only registers its definition; ClickHouse does not
|
|
882
|
+
* retroactively backfill it for existing parts. Call
|
|
883
|
+
* `materializeProjection()` (`ALTER TABLE ... MATERIALIZE PROJECTION`)
|
|
884
|
+
* afterwards to build it for data that already exists - new
|
|
885
|
+
* inserts/merges are kept in sync automatically from this point on
|
|
886
|
+
* either way.
|
|
887
|
+
*/
|
|
888
|
+
async addProjection(tableName, projectionName, definition) {
|
|
889
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD PROJECTION ${this.buildProjectionClause(projectionName, definition)}`);
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* `ALTER TABLE table DROP PROJECTION [IF EXISTS] name`.
|
|
893
|
+
*
|
|
894
|
+
* Implemented as a mutation: removes the projection's definition from the
|
|
895
|
+
* table's metadata and deletes its files from disk.
|
|
896
|
+
*/
|
|
897
|
+
async dropProjection(tableName, projectionName, options) {
|
|
898
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} DROP PROJECTION`;
|
|
899
|
+
if (options?.ifExists)
|
|
900
|
+
sql += ' IF EXISTS';
|
|
901
|
+
sql += ` ${this.escapeId(projectionName)}`;
|
|
902
|
+
await this.query(sql);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* `ALTER TABLE table MATERIALIZE PROJECTION [IF EXISTS] name [IN PARTITION partition]`.
|
|
906
|
+
*
|
|
907
|
+
* Builds (or rebuilds) a projection for existing data. Needed after
|
|
908
|
+
* `addProjection()` on a table that already has rows - a freshly-added
|
|
909
|
+
* projection definition otherwise only applies to data inserted/merged
|
|
910
|
+
* after it was added.
|
|
911
|
+
*/
|
|
912
|
+
async materializeProjection(tableName, projectionName, options) {
|
|
913
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} MATERIALIZE PROJECTION`;
|
|
914
|
+
if (options?.ifExists)
|
|
915
|
+
sql += ' IF EXISTS';
|
|
916
|
+
sql += ` ${this.escapeId(projectionName)}`;
|
|
917
|
+
if (options?.partition)
|
|
918
|
+
sql += ` IN PARTITION ${options.partition}`;
|
|
919
|
+
await this.query(sql);
|
|
920
|
+
}
|
|
921
|
+
async createFulltextIndex(tableName, indexName, fields, _options) {
|
|
922
|
+
// ClickHouse's closest equivalent is the experimental `inverted`/`ngrambf_v1`
|
|
923
|
+
// data-skipping index type, not a real full-text index engine.
|
|
924
|
+
const expr = fields.length > 1 ? `(${fields.map((f) => this.escapeId(f)).join(', ')})` : this.escapeId(fields[0]);
|
|
925
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD INDEX ${this.escapeId(indexName)} ${expr} TYPE inverted GRANULARITY 1`);
|
|
926
|
+
}
|
|
927
|
+
async createSpatialIndex(_tableName, _indexName, _fields, _options) {
|
|
928
|
+
throw new Error('ClickHouse has geo functions (pointInPolygon, geoDistance, etc.) but no dedicated spatial index type.');
|
|
929
|
+
}
|
|
930
|
+
// ==================== Describe / rename ====================
|
|
931
|
+
async describeTable(tableName) {
|
|
932
|
+
const result = await this.query(`DESCRIBE TABLE ${this.quoteTable(tableName)}`);
|
|
933
|
+
const description = {};
|
|
934
|
+
for (const row of result.rows) {
|
|
935
|
+
const r = row;
|
|
936
|
+
const isNullable = /^Nullable\(/.test(r.type || '');
|
|
937
|
+
description[r.name] = {
|
|
938
|
+
type: r.type,
|
|
939
|
+
allowNull: isNullable,
|
|
940
|
+
defaultValue: r.default_expression || null,
|
|
941
|
+
primaryKey: false,
|
|
942
|
+
autoIncrement: false,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
const sortingKeyResult = await this.query(`SELECT sorting_key FROM system.tables WHERE database = currentDatabase() AND name = ${this.escape(tableName)}`);
|
|
946
|
+
const sortingKey = sortingKeyResult.rows[0]?.sorting_key || '';
|
|
947
|
+
for (const column of sortingKey.split(',').map((c) => c.trim())) {
|
|
948
|
+
if (description[column]) {
|
|
949
|
+
description[column].primaryKey = true;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return description;
|
|
953
|
+
}
|
|
954
|
+
async renameTable(oldName, newName) {
|
|
955
|
+
await this.query(`RENAME TABLE ${this.quoteTable(oldName)} TO ${this.quoteTable(newName)}`);
|
|
956
|
+
}
|
|
957
|
+
async changeOwner(_newOwner, _tableName) {
|
|
958
|
+
throw new Error('ClickHouse has no per-table ownership model to change (access is role/user-grant based).');
|
|
959
|
+
}
|
|
960
|
+
// ==================== Row policies (ClickHouse's real RLS feature) ====================
|
|
961
|
+
async createSecurityPolicy(policyName, tableName, options) {
|
|
962
|
+
const predicate = options?.predicate || '1';
|
|
963
|
+
await this.query(`CREATE ROW POLICY IF NOT EXISTS ${this.escapeId(policyName)} ON ${this.quoteTable(tableName)} USING ${predicate} TO ALL`);
|
|
964
|
+
}
|
|
965
|
+
async dropSecurityPolicy(policyName, tableName) {
|
|
966
|
+
await this.query(`DROP ROW POLICY IF EXISTS ${this.escapeId(policyName)} ON ${this.quoteTable(tableName)}`);
|
|
967
|
+
}
|
|
968
|
+
// ==================== Transactions (unsupported) ====================
|
|
969
|
+
//
|
|
970
|
+
// ClickHouse has no multi-statement, cross-request ACID transactions: no
|
|
971
|
+
// BEGIN/COMMIT/ROLLBACK spanning more than one statement. A single INSERT is
|
|
972
|
+
// atomic at the block level, and that's the extent of it. Rather than
|
|
973
|
+
// silently no-op-ing (which would let application code believe a rollback
|
|
974
|
+
// undid earlier statements when nothing of the sort happened), these throw -
|
|
975
|
+
// matching this codebase's convention for a genuinely unsupported capability
|
|
976
|
+
// (e.g. MSSQL's partition methods, or FDW/extension stubs on non-PostgreSQL
|
|
977
|
+
// dialects).
|
|
978
|
+
async startTransaction(_options) {
|
|
979
|
+
throw new Error('ClickHouse does not support multi-statement transactions (no BEGIN/COMMIT/ROLLBACK spanning ' +
|
|
980
|
+
'multiple statements). Only a single INSERT is atomic (at the block level). Do not rely on ' +
|
|
981
|
+
'startTransaction()/commitTransaction()/rollbackTransaction() with this dialect.');
|
|
982
|
+
}
|
|
983
|
+
async commitTransaction(_transaction) {
|
|
984
|
+
throw new Error('ClickHouse does not support transactions; there is nothing to commit.');
|
|
985
|
+
}
|
|
986
|
+
async rollbackTransaction(_transaction) {
|
|
987
|
+
throw new Error('ClickHouse does not support transactions; there is nothing to roll back.');
|
|
988
|
+
}
|
|
989
|
+
// ==================== Data types ====================
|
|
990
|
+
getDataTypeSql(dataType) {
|
|
991
|
+
if (typeof dataType === 'string')
|
|
992
|
+
return dataType;
|
|
993
|
+
if (!dataType || typeof dataType !== 'object')
|
|
994
|
+
return 'String';
|
|
995
|
+
const dt = dataType;
|
|
996
|
+
switch (dt.key) {
|
|
997
|
+
case 'STRING':
|
|
998
|
+
case 'CHAR':
|
|
999
|
+
case 'TEXT':
|
|
1000
|
+
return 'String';
|
|
1001
|
+
case 'INTEGER':
|
|
1002
|
+
return dt.unsigned ? 'UInt32' : 'Int32';
|
|
1003
|
+
case 'BIGINT':
|
|
1004
|
+
return dt.unsigned ? 'UInt64' : 'Int64';
|
|
1005
|
+
case 'FLOAT':
|
|
1006
|
+
return 'Float32';
|
|
1007
|
+
case 'DOUBLE':
|
|
1008
|
+
return 'Float64';
|
|
1009
|
+
case 'DECIMAL':
|
|
1010
|
+
return `Decimal(${dt.precision || 10}, ${dt.scale || 0})`;
|
|
1011
|
+
case 'BOOLEAN':
|
|
1012
|
+
return 'Bool';
|
|
1013
|
+
case 'DATE':
|
|
1014
|
+
return dt.precision ? `DateTime64(${dt.precision})` : 'DateTime';
|
|
1015
|
+
case 'DATEONLY':
|
|
1016
|
+
return 'Date';
|
|
1017
|
+
case 'TIME':
|
|
1018
|
+
// ClickHouse has no dedicated TIME type.
|
|
1019
|
+
return 'String';
|
|
1020
|
+
case 'BLOB':
|
|
1021
|
+
return 'String';
|
|
1022
|
+
case 'ENUM': {
|
|
1023
|
+
const values = dt.values || [];
|
|
1024
|
+
// ClickHouse `Enum8` is backed by Int8 and can only represent up to
|
|
1025
|
+
// 127 distinct values; `Enum16` (Int16-backed) supports up to 32767.
|
|
1026
|
+
// Emitting `Enum8` unconditionally silently overflows/fails for larger
|
|
1027
|
+
// enum sets, so pick the right width based on the number of values.
|
|
1028
|
+
const enumType = values.length > 127 ? 'Enum16' : 'Enum8';
|
|
1029
|
+
return `${enumType}(${values.map((v, i) => `'${this.escapeString(v)}' = ${i + 1}`).join(', ')})`;
|
|
1030
|
+
}
|
|
1031
|
+
case 'JSON':
|
|
1032
|
+
case 'JSONB':
|
|
1033
|
+
// The native `JSON` column type is still experimental across ClickHouse
|
|
1034
|
+
// versions; `String` (storing serialized JSON, queried with JSON*
|
|
1035
|
+
// functions) is the safe, version-stable default.
|
|
1036
|
+
return 'String';
|
|
1037
|
+
case 'UUID':
|
|
1038
|
+
return 'UUID';
|
|
1039
|
+
case 'INET':
|
|
1040
|
+
// `DataTypes.INET` defaults to IPv4 (matching the previous
|
|
1041
|
+
// unconditional mapping below) but ClickHouse has a genuinely
|
|
1042
|
+
// distinct fixed-width `IPv6` column type; `DataTypes.INET({ version: 6 })`
|
|
1043
|
+
// / `DataTypes.INET6` opts into it.
|
|
1044
|
+
return dt.version === 6 ? 'IPv6' : 'IPv4';
|
|
1045
|
+
case 'CIDR':
|
|
1046
|
+
case 'MACADDR':
|
|
1047
|
+
case 'HSTORE':
|
|
1048
|
+
case 'RANGE':
|
|
1049
|
+
case 'GEOMETRY':
|
|
1050
|
+
return 'String';
|
|
1051
|
+
case 'ARRAY': {
|
|
1052
|
+
const elementType = dt.type || dt.subtype;
|
|
1053
|
+
return elementType ? `Array(${this.getDataTypeSql(elementType)})` : 'Array(String)';
|
|
1054
|
+
}
|
|
1055
|
+
case 'VIRTUAL':
|
|
1056
|
+
return '';
|
|
1057
|
+
default:
|
|
1058
|
+
return 'String';
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
// ==================== Approximate aggregate function helpers ====================
|
|
1062
|
+
//
|
|
1063
|
+
// ClickHouse's approximate/probabilistic aggregate functions - cardinality
|
|
1064
|
+
// estimation (`uniq*`) and percentile estimation (`quantile*`) at scale -
|
|
1065
|
+
// are central to its analytics use case. Nothing in this dialect (or any
|
|
1066
|
+
// other dialect in this codebase) previously surfaced them as first-class,
|
|
1067
|
+
// typed helpers; callers had to know the exact ClickHouse function name and
|
|
1068
|
+
// hand-write it into a raw `attributes`/`group` string. These return a
|
|
1069
|
+
// `Literal` (this ORM's existing "raw SQL fragment" escape hatch, see
|
|
1070
|
+
// `prorm.ts`) with the column/level correctly identifier-escaped, so
|
|
1071
|
+
// the result can be dropped directly into `attributes`, e.g.
|
|
1072
|
+
// `attributes: [dialect.uniqCombined('user_id', 'unique_users')]`.
|
|
1073
|
+
/**
|
|
1074
|
+
* `uniq(column)` - ClickHouse's default approximate distinct-count
|
|
1075
|
+
* (adaptive sampling, small constant memory, ~1-2% error). The fastest and
|
|
1076
|
+
* least memory-hungry of the `uniq*` family; prefer this unless you need
|
|
1077
|
+
* `uniqExact`'s precision or `uniqCombined`'s tunable accuracy/memory
|
|
1078
|
+
* trade-off.
|
|
1079
|
+
*/
|
|
1080
|
+
uniq(column, alias) {
|
|
1081
|
+
return new prorm_1.Literal(`uniq(${this.escapeId(column)})${alias ? ` AS ${this.escapeId(alias)}` : ''}`);
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* `uniqCombined([precision])(column)` - approximate distinct count that
|
|
1085
|
+
* blends an array, a hash table, and HyperLogLog depending on cardinality,
|
|
1086
|
+
* giving better accuracy-per-byte than plain `uniq` at the cost of a bit
|
|
1087
|
+
* more CPU. `precision` (`HLL_precision`, default 17) trades memory for
|
|
1088
|
+
* accuracy: higher precision = more memory, lower error.
|
|
1089
|
+
*/
|
|
1090
|
+
uniqCombined(column, alias, precision) {
|
|
1091
|
+
const fn = precision !== undefined ? `uniqCombined(${precision})` : 'uniqCombined';
|
|
1092
|
+
return new prorm_1.Literal(`${fn}(${this.escapeId(column)})${alias ? ` AS ${this.escapeId(alias)}` : ''}`);
|
|
1093
|
+
}
|
|
1094
|
+
/** `uniqHLL12(column)` - classic HyperLogLog-based approximate distinct count. */
|
|
1095
|
+
uniqHLL12(column, alias) {
|
|
1096
|
+
return new prorm_1.Literal(`uniqHLL12(${this.escapeId(column)})${alias ? ` AS ${this.escapeId(alias)}` : ''}`);
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* `uniqExact(column)` - EXACT distinct count (not approximate - despite
|
|
1100
|
+
* living in the `uniq*` family). Included here for symmetry/discoverability
|
|
1101
|
+
* since callers reaching for `uniq*` helpers need an easy way to opt into
|
|
1102
|
+
* exactness when cardinality is low enough that the memory/CPU cost is
|
|
1103
|
+
* acceptable.
|
|
1104
|
+
*/
|
|
1105
|
+
uniqExact(column, alias) {
|
|
1106
|
+
return new prorm_1.Literal(`uniqExact(${this.escapeId(column)})${alias ? ` AS ${this.escapeId(alias)}` : ''}`);
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* `quantile(level)(column)` - approximate percentile/quantile estimation
|
|
1110
|
+
* (reservoir sampling) - e.g. `quantile(0.99)(latency_ms)` for p99 latency.
|
|
1111
|
+
* `level` must be in `[0, 1]`.
|
|
1112
|
+
*/
|
|
1113
|
+
quantile(level, column, alias) {
|
|
1114
|
+
this.assertQuantileLevel(level);
|
|
1115
|
+
return new prorm_1.Literal(`quantile(${level})(${this.escapeId(column)})${alias ? ` AS ${this.escapeId(alias)}` : ''}`);
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* `quantiles(level1, level2, ...)(column)` - like `quantile()`, but computes
|
|
1119
|
+
* several quantiles in one pass (e.g. p50/p90/p99 together is cheaper than
|
|
1120
|
+
* three separate `quantile()` calls since ClickHouse shares the sample).
|
|
1121
|
+
* Returns an Array(Float64) column.
|
|
1122
|
+
*/
|
|
1123
|
+
quantiles(levels, column, alias) {
|
|
1124
|
+
if (!levels || levels.length === 0) {
|
|
1125
|
+
throw new Error('ClickHouse quantiles(): at least one quantile level must be provided.');
|
|
1126
|
+
}
|
|
1127
|
+
levels.forEach((l) => this.assertQuantileLevel(l));
|
|
1128
|
+
return new prorm_1.Literal(`quantiles(${levels.join(', ')})(${this.escapeId(column)})${alias ? ` AS ${this.escapeId(alias)}` : ''}`);
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* `quantileExact(level)(column)` - EXACT quantile (loads all values into
|
|
1132
|
+
* memory; use for small-to-medium datasets where precision matters more
|
|
1133
|
+
* than the approximate functions' bounded memory usage).
|
|
1134
|
+
*/
|
|
1135
|
+
quantileExact(level, column, alias) {
|
|
1136
|
+
this.assertQuantileLevel(level);
|
|
1137
|
+
return new prorm_1.Literal(`quantileExact(${level})(${this.escapeId(column)})${alias ? ` AS ${this.escapeId(alias)}` : ''}`);
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* `quantileTDigest(level)(column)` - approximate quantile via the t-digest
|
|
1141
|
+
* algorithm: lower memory than `quantileExact`, better accuracy at extreme
|
|
1142
|
+
* percentiles (e.g. p99.9) than the default reservoir-sampling `quantile()`.
|
|
1143
|
+
*/
|
|
1144
|
+
quantileTDigest(level, column, alias) {
|
|
1145
|
+
this.assertQuantileLevel(level);
|
|
1146
|
+
return new prorm_1.Literal(`quantileTDigest(${level})(${this.escapeId(column)})${alias ? ` AS ${this.escapeId(alias)}` : ''}`);
|
|
1147
|
+
}
|
|
1148
|
+
assertQuantileLevel(level) {
|
|
1149
|
+
if (typeof level !== 'number' || Number.isNaN(level) || level < 0 || level > 1) {
|
|
1150
|
+
throw new Error(`ClickHouse quantile level must be a number in [0, 1], got ${level}.`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// ==================== Query builders ====================
|
|
1154
|
+
//
|
|
1155
|
+
// buildWhereClause/buildOrderClause/buildInsertQuery/buildUpdateQuery/
|
|
1156
|
+
// buildDeleteQuery/buildSelectQuery/buildUpsertQuery/buildIncrementQuery all
|
|
1157
|
+
// embed escaped literal values directly into the returned `sql` string
|
|
1158
|
+
// rather than returning `?`/`$N`-style placeholders (see the comment on
|
|
1159
|
+
// replaceReplacements() above for why: the ClickHouse HTTP client has no
|
|
1160
|
+
// native positional-parameter binding for arbitrary values). Each returned
|
|
1161
|
+
// `sql` is therefore directly executable via `query(sql)`; the returned
|
|
1162
|
+
// `values` arrays are kept empty for shape-parity with the Dialect interface.
|
|
1163
|
+
buildWhereClause(where, _options) {
|
|
1164
|
+
if (!where || Object.keys(where).length === 0) {
|
|
1165
|
+
return { sql: '', values: [] };
|
|
1166
|
+
}
|
|
1167
|
+
const buildCondition = (condition) => {
|
|
1168
|
+
if (!condition)
|
|
1169
|
+
return '';
|
|
1170
|
+
if (typeof condition !== 'object')
|
|
1171
|
+
return this.escape(condition);
|
|
1172
|
+
const cond = condition;
|
|
1173
|
+
if (cond.$and || cond.$or || cond.$not) {
|
|
1174
|
+
const parts = [];
|
|
1175
|
+
if (cond.$and) {
|
|
1176
|
+
parts.push(`(${cond.$and.map(buildCondition).join(' AND ')})`);
|
|
1177
|
+
}
|
|
1178
|
+
if (cond.$or) {
|
|
1179
|
+
parts.push(`(${cond.$or.map(buildCondition).join(' OR ')})`);
|
|
1180
|
+
}
|
|
1181
|
+
if (cond.$not) {
|
|
1182
|
+
parts.push(`NOT (${buildCondition(cond.$not)})`);
|
|
1183
|
+
}
|
|
1184
|
+
return parts.join(' AND ');
|
|
1185
|
+
}
|
|
1186
|
+
const fieldConditions = [];
|
|
1187
|
+
for (const [key, value] of Object.entries(cond)) {
|
|
1188
|
+
if (key.startsWith('$'))
|
|
1189
|
+
continue;
|
|
1190
|
+
const col = this.escapeId(key);
|
|
1191
|
+
if (value && typeof value === 'object' && !(value instanceof Date) && !Array.isArray(value)) {
|
|
1192
|
+
const v = value;
|
|
1193
|
+
if (v.$eq !== undefined)
|
|
1194
|
+
fieldConditions.push(`${col} = ${this.escape(v.$eq)}`);
|
|
1195
|
+
else if (v.$ne !== undefined)
|
|
1196
|
+
fieldConditions.push(`${col} != ${this.escape(v.$ne)}`);
|
|
1197
|
+
else if (v.$gt !== undefined)
|
|
1198
|
+
fieldConditions.push(`${col} > ${this.escape(v.$gt)}`);
|
|
1199
|
+
else if (v.$gte !== undefined)
|
|
1200
|
+
fieldConditions.push(`${col} >= ${this.escape(v.$gte)}`);
|
|
1201
|
+
else if (v.$lt !== undefined)
|
|
1202
|
+
fieldConditions.push(`${col} < ${this.escape(v.$lt)}`);
|
|
1203
|
+
else if (v.$lte !== undefined)
|
|
1204
|
+
fieldConditions.push(`${col} <= ${this.escape(v.$lte)}`);
|
|
1205
|
+
else if (v.$like !== undefined)
|
|
1206
|
+
fieldConditions.push(`${col} LIKE ${this.escape(v.$like)}`);
|
|
1207
|
+
else if (v.$notLike !== undefined)
|
|
1208
|
+
fieldConditions.push(`${col} NOT LIKE ${this.escape(v.$notLike)}`);
|
|
1209
|
+
else if (v.$startsWith !== undefined)
|
|
1210
|
+
fieldConditions.push(`${col} LIKE ${this.escape(`${v.$startsWith}%`)}`);
|
|
1211
|
+
else if (v.$endsWith !== undefined)
|
|
1212
|
+
fieldConditions.push(`${col} LIKE ${this.escape(`%${v.$endsWith}`)}`);
|
|
1213
|
+
else if (v.$substring !== undefined)
|
|
1214
|
+
fieldConditions.push(`${col} LIKE ${this.escape(`%${v.$substring}%`)}`);
|
|
1215
|
+
else if (v.$in) {
|
|
1216
|
+
const vals = v.$in.map((x) => this.escape(x)).join(', ');
|
|
1217
|
+
fieldConditions.push(`${col} IN (${vals})`);
|
|
1218
|
+
}
|
|
1219
|
+
else if (v.$notIn) {
|
|
1220
|
+
const vals = v.$notIn.map((x) => this.escape(x)).join(', ');
|
|
1221
|
+
fieldConditions.push(`${col} NOT IN (${vals})`);
|
|
1222
|
+
}
|
|
1223
|
+
else if (v.$between) {
|
|
1224
|
+
const [a, b] = v.$between;
|
|
1225
|
+
fieldConditions.push(`${col} BETWEEN ${this.escape(a)} AND ${this.escape(b)}`);
|
|
1226
|
+
}
|
|
1227
|
+
else if (v.$notBetween) {
|
|
1228
|
+
const [a, b] = v.$notBetween;
|
|
1229
|
+
fieldConditions.push(`${col} NOT BETWEEN ${this.escape(a)} AND ${this.escape(b)}`);
|
|
1230
|
+
}
|
|
1231
|
+
else if (v.$isNull !== undefined) {
|
|
1232
|
+
fieldConditions.push(v.$isNull ? `${col} IS NULL` : `${col} IS NOT NULL`);
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
fieldConditions.push(`${col} = ${this.escape(value)}`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
else if (value === null) {
|
|
1239
|
+
fieldConditions.push(`${col} IS NULL`);
|
|
1240
|
+
}
|
|
1241
|
+
else if (Array.isArray(value)) {
|
|
1242
|
+
fieldConditions.push(`${col} IN (${value.map((v) => this.escape(v)).join(', ')})`);
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
fieldConditions.push(`${col} = ${this.escape(value)}`);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return fieldConditions.join(' AND ');
|
|
1249
|
+
};
|
|
1250
|
+
return { sql: buildCondition(where), values: [] };
|
|
1251
|
+
}
|
|
1252
|
+
buildOrderClause(order, _options) {
|
|
1253
|
+
if (!order || (Array.isArray(order) && order.length === 0))
|
|
1254
|
+
return '';
|
|
1255
|
+
const parts = [];
|
|
1256
|
+
const processItem = (item) => {
|
|
1257
|
+
if (typeof item === 'string') {
|
|
1258
|
+
const [field, dir] = item.split(' ');
|
|
1259
|
+
parts.push(dir ? `${this.escapeId(field)} ${dir.toUpperCase()}` : this.escapeId(field));
|
|
1260
|
+
}
|
|
1261
|
+
else if (Array.isArray(item)) {
|
|
1262
|
+
const [field, dir] = item;
|
|
1263
|
+
parts.push(dir ? `${this.escapeId(field)} ${dir.toUpperCase()}` : this.escapeId(field));
|
|
1264
|
+
}
|
|
1265
|
+
else if (item && typeof item === 'object') {
|
|
1266
|
+
for (const [key, value] of Object.entries(item)) {
|
|
1267
|
+
parts.push(`${this.escapeId(key)} ${String(value).toUpperCase()}`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
if (Array.isArray(order))
|
|
1272
|
+
order.forEach(processItem);
|
|
1273
|
+
else
|
|
1274
|
+
processItem(order);
|
|
1275
|
+
return parts.length > 0 ? `ORDER BY ${parts.join(', ')}` : '';
|
|
1276
|
+
}
|
|
1277
|
+
buildLimitOffset(limit, offset) {
|
|
1278
|
+
let sql = '';
|
|
1279
|
+
if (limit !== undefined)
|
|
1280
|
+
sql += ` LIMIT ${Number(limit)}`;
|
|
1281
|
+
if (offset !== undefined)
|
|
1282
|
+
sql += ` OFFSET ${Number(offset)}`;
|
|
1283
|
+
return sql;
|
|
1284
|
+
}
|
|
1285
|
+
buildInsertQuery(tableName, values, options) {
|
|
1286
|
+
const columns = Object.keys(values);
|
|
1287
|
+
const rendered = columns.map((c) => {
|
|
1288
|
+
const v = values[c];
|
|
1289
|
+
return v instanceof prorm_1.Literal ? v.val : this.escape(v);
|
|
1290
|
+
});
|
|
1291
|
+
let sql = `INSERT INTO ${this.quoteTable(tableName, options?.schema)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${rendered.join(', ')})`;
|
|
1292
|
+
if (options?.returning) {
|
|
1293
|
+
// eslint-disable-next-line no-console
|
|
1294
|
+
console.warn('ClickHouse: INSERT has no RETURNING clause; the `returning` option is ignored.');
|
|
1295
|
+
}
|
|
1296
|
+
if (options?.upsert) {
|
|
1297
|
+
// eslint-disable-next-line no-console
|
|
1298
|
+
console.warn('ClickHouse: INSERT has no ON CONFLICT/ON DUPLICATE KEY UPDATE clause. Emitting a plain INSERT; ' +
|
|
1299
|
+
'use buildUpsertQuery()/a ReplacingMergeTree engine for "last write wins" semantics instead.');
|
|
1300
|
+
}
|
|
1301
|
+
return { sql, values: [] };
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* ClickHouse has no synchronous UPSERT / `ON CONFLICT` / `MERGE` statement.
|
|
1305
|
+
* The idiomatic ClickHouse pattern is: insert a new row into a
|
|
1306
|
+
* ReplacingMergeTree(-family) table and let ClickHouse's background merges
|
|
1307
|
+
* asynchronously drop older rows sharing the same ORDER BY key (optionally
|
|
1308
|
+
* keeping the row with the greatest `version` column, if the engine and
|
|
1309
|
+
* table were set up with one). Until that merge runs, duplicate rows for
|
|
1310
|
+
* the same key coexist, and reads must use `SELECT ... FINAL` (or the table
|
|
1311
|
+
* must be `OPTIMIZE ... FINAL`-ed) to see "the latest" value.
|
|
1312
|
+
*
|
|
1313
|
+
* This method therefore only ever builds a plain INSERT - `conflictFields`/
|
|
1314
|
+
* `updateOnDuplicate` are accepted (to satisfy the shared Dialect signature)
|
|
1315
|
+
* but silently have no effect, and that is called out loudly below rather
|
|
1316
|
+
* than pretending this performs a real, synchronous upsert.
|
|
1317
|
+
*
|
|
1318
|
+
* IMPORTANT: pass `{ final: true }` to `buildSelectQuery()` on subsequent
|
|
1319
|
+
* reads of this table (`SELECT ... FROM <table> FINAL`) to force ClickHouse
|
|
1320
|
+
* to apply ReplacingMergeTree's merge-time dedup logic at query time -
|
|
1321
|
+
* otherwise a plain SELECT can return duplicate/stale rows for the same
|
|
1322
|
+
* ORDER BY key until a background merge happens. See the doc comment on
|
|
1323
|
+
* `buildSelectQuery()`'s `final` handling for why this is opt-in rather
|
|
1324
|
+
* than automatic.
|
|
1325
|
+
*/
|
|
1326
|
+
buildUpsertQuery(tableName, values, options) {
|
|
1327
|
+
// eslint-disable-next-line no-console
|
|
1328
|
+
console.warn('ClickHouse buildUpsertQuery(): emitting a plain INSERT. ClickHouse has no ON CONFLICT/MERGE ' +
|
|
1329
|
+
"statement; conflictFields/updateOnDuplicate are ignored. Use a ReplacingMergeTree table and query " +
|
|
1330
|
+
'with FINAL (or run OPTIMIZE TABLE ... FINAL) to actually deduplicate by the ORDER BY key.');
|
|
1331
|
+
return this.buildInsertQuery(tableName, values, { schema: options?.schema, returning: options?.returning });
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* `ALTER TABLE ... UPDATE` is an asynchronous ClickHouse *mutation*, not a
|
|
1335
|
+
* synchronous, transactional UPDATE. The statement returns as soon as the
|
|
1336
|
+
* mutation is scheduled; ClickHouse rewrites the affected data parts in the
|
|
1337
|
+
* background (progress/completion is visible via `system.mutations`), so
|
|
1338
|
+
* `query()`'s `rowCount` for this statement is always 0 - it reflects "rows
|
|
1339
|
+
* returned by this HTTP request", not "rows the mutation will touch".
|
|
1340
|
+
*/
|
|
1341
|
+
buildUpdateQuery(tableName, values, where, options) {
|
|
1342
|
+
const setClauses = Object.entries(values).map(([key, value]) => `${this.escapeId(key)} = ${value instanceof prorm_1.Literal ? value.val : this.escape(value)}`);
|
|
1343
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} UPDATE ${setClauses.join(', ')}`;
|
|
1344
|
+
const whereClause = this.buildWhereClause(where);
|
|
1345
|
+
if (whereClause.sql)
|
|
1346
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1347
|
+
if (options?.limit) {
|
|
1348
|
+
// eslint-disable-next-line no-console
|
|
1349
|
+
console.warn('ClickHouse: ALTER TABLE ... UPDATE has no LIMIT clause; the `limit` option is ignored.');
|
|
1350
|
+
}
|
|
1351
|
+
if (options?.returning) {
|
|
1352
|
+
// eslint-disable-next-line no-console
|
|
1353
|
+
console.warn('ClickHouse: ALTER TABLE ... UPDATE has no RETURNING clause; the `returning` option is ignored.');
|
|
1354
|
+
}
|
|
1355
|
+
return { sql, values: [] };
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* `ALTER TABLE ... DELETE` is likewise an asynchronous mutation (see
|
|
1359
|
+
* buildUpdateQuery's doc comment above) - not a synchronous, transactional
|
|
1360
|
+
* DELETE. ClickHouse also offers a syntactically lighter `DELETE FROM ...
|
|
1361
|
+
* WHERE ...` ("lightweight delete", available since v23.3) which marks rows
|
|
1362
|
+
* deleted immediately for query purposes while physically removing them in
|
|
1363
|
+
* the background; this builder intentionally uses the classic, more widely
|
|
1364
|
+
* supported `ALTER TABLE ... DELETE` mutation form instead.
|
|
1365
|
+
*/
|
|
1366
|
+
buildDeleteQuery(tableName, where, options) {
|
|
1367
|
+
if (options?.truncate && (!where || Object.keys(where).length === 0)) {
|
|
1368
|
+
return { sql: `TRUNCATE TABLE ${this.quoteTable(tableName)}`, values: [] };
|
|
1369
|
+
}
|
|
1370
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} DELETE`;
|
|
1371
|
+
const whereClause = this.buildWhereClause(where);
|
|
1372
|
+
// ClickHouse requires a WHERE clause on ALTER TABLE ... DELETE; "delete
|
|
1373
|
+
// everything" should go through TRUNCATE TABLE instead (see above).
|
|
1374
|
+
sql += whereClause.sql ? ` WHERE ${whereClause.sql}` : ' WHERE 1';
|
|
1375
|
+
if (options?.limit) {
|
|
1376
|
+
// eslint-disable-next-line no-console
|
|
1377
|
+
console.warn('ClickHouse: ALTER TABLE ... DELETE has no LIMIT clause; the `limit` option is ignored.');
|
|
1378
|
+
}
|
|
1379
|
+
if (options?.returning) {
|
|
1380
|
+
// eslint-disable-next-line no-console
|
|
1381
|
+
console.warn('ClickHouse: ALTER TABLE ... DELETE has no RETURNING clause; the `returning` option is ignored.');
|
|
1382
|
+
}
|
|
1383
|
+
return { sql, values: [] };
|
|
1384
|
+
}
|
|
1385
|
+
buildSelectQuery(options) {
|
|
1386
|
+
let selectSql = '*';
|
|
1387
|
+
if (options.attributes) {
|
|
1388
|
+
if (Array.isArray(options.attributes)) {
|
|
1389
|
+
// A `Literal` (e.g. the output of `uniq()`/`quantile()`/etc. below, or any
|
|
1390
|
+
// other raw-SQL escape hatch) is passed through verbatim rather than
|
|
1391
|
+
// identifier-escaped, since it's already a complete SQL expression
|
|
1392
|
+
// (function call + optional `AS alias`), not a bare column name.
|
|
1393
|
+
selectSql = options.attributes
|
|
1394
|
+
.map((a) => (a === '*' ? a : a instanceof prorm_1.Literal ? a.val : this.escapeId(a)))
|
|
1395
|
+
.join(', ');
|
|
1396
|
+
}
|
|
1397
|
+
else if (options.attributes.include) {
|
|
1398
|
+
selectSql = options.attributes.include.map((a) => this.escapeId(a)).join(', ');
|
|
1399
|
+
}
|
|
1400
|
+
else if (options.attributes.exclude) {
|
|
1401
|
+
// ClickHouse supports `SELECT * EXCEPT (...)` natively, same as SQLite's dialect uses.
|
|
1402
|
+
selectSql = `* EXCEPT (${options.attributes.exclude.map((a) => this.escapeId(a)).join(', ')})`;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
const distinct = options.distinct ? (options.col ? `DISTINCT ${this.escapeId(options.col)} ` : 'DISTINCT ') : '';
|
|
1406
|
+
let sql = `SELECT ${distinct}${selectSql} FROM ${this.quoteTable(options.tableName, options.schema)}`;
|
|
1407
|
+
/**
|
|
1408
|
+
* `FINAL` fixes the correctness bug documented on `buildUpsertQuery()`
|
|
1409
|
+
* above: without it, a `SELECT` against a ReplacingMergeTree(-family)
|
|
1410
|
+
* table can return duplicate rows for the same ORDER BY key (or rows that
|
|
1411
|
+
* a later-arriving "replacement" row was supposed to supersede) for as
|
|
1412
|
+
* long as ClickHouse's background merges haven't run - which can be
|
|
1413
|
+
* minutes to indefinitely on a lightly-written table. Appending `FINAL`
|
|
1414
|
+
* right after the table reference (ClickHouse's real syntax - it goes
|
|
1415
|
+
* before any join/alias, not at the end of the query) forces ClickHouse
|
|
1416
|
+
* to perform the merge-time dedup logic at query time instead.
|
|
1417
|
+
*
|
|
1418
|
+
* Deliberately OPT-IN, not auto-detected from the table's engine:
|
|
1419
|
+
* - This dialect does not persist per-table engine metadata anywhere
|
|
1420
|
+
* (see `resolveOrderBy()`/`createTable()` above - `options.engine` is
|
|
1421
|
+
* used once, at CREATE TABLE time, and never stored). Auto-applying
|
|
1422
|
+
* FINAL would require an extra round-trip query against
|
|
1423
|
+
* `system.tables` on every single SELECT to look up the engine,
|
|
1424
|
+
* which is both a synchronous-API mismatch (this method is
|
|
1425
|
+
* synchronous and returns plain SQL text - it cannot await a
|
|
1426
|
+
* metadata lookup) and a real latency/load cost on hot paths.
|
|
1427
|
+
* - FINAL is also not free even when it IS what you want: it forces a
|
|
1428
|
+
* merge-on-read, which can be significantly slower than a plain
|
|
1429
|
+
* SELECT and doesn't scale well to very large tables/parts counts.
|
|
1430
|
+
* Silently forcing it on every read against something that merely
|
|
1431
|
+
* *might* be a ReplacingMergeTree table would be a surprising,
|
|
1432
|
+
* hard-to-diagnose performance regression for callers who don't
|
|
1433
|
+
* need dedup for a given query (e.g. aggregates that tolerate
|
|
1434
|
+
* eventual consistency).
|
|
1435
|
+
* - An explicit `final: true` is the same shape as the loud
|
|
1436
|
+
* documentation already on `buildUpsertQuery()`: callers who choose
|
|
1437
|
+
* ReplacingMergeTree + this upsert pattern must consciously opt in
|
|
1438
|
+
* to FINAL on the reads that require deduplicated/latest-row
|
|
1439
|
+
* semantics, rather than have correctness depend on an inference
|
|
1440
|
+
* this dialect cannot safely make at this layer.
|
|
1441
|
+
*/
|
|
1442
|
+
if (options.final) {
|
|
1443
|
+
sql += ' FINAL';
|
|
1444
|
+
}
|
|
1445
|
+
if (options.include && options.include.length > 0) {
|
|
1446
|
+
for (const include of options.include) {
|
|
1447
|
+
const joinType = include.required ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1448
|
+
const joinModel = include.model;
|
|
1449
|
+
const joinTable = joinModel.tableName || String(include.model);
|
|
1450
|
+
const alias = include.as || joinTable;
|
|
1451
|
+
sql += ` ${joinType} ${this.quoteTable(joinTable)} AS ${this.escapeId(alias)}`;
|
|
1452
|
+
if (include.where) {
|
|
1453
|
+
const onClause = this.buildWhereClause(include.where);
|
|
1454
|
+
if (onClause.sql)
|
|
1455
|
+
sql += ` ON ${onClause.sql}`;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* ClickHouse's `ARRAY JOIN` / `LEFT ARRAY JOIN` clause "unnests" Array-typed
|
|
1461
|
+
* columns into multiple rows (roughly analogous to PostgreSQL's `unnest()`,
|
|
1462
|
+
* but its own clause with different NULL-handling: `LEFT ARRAY JOIN` keeps a
|
|
1463
|
+
* source row with an empty array by producing one row with the array
|
|
1464
|
+
* element's default value, whereas plain `ARRAY JOIN` drops it). It is
|
|
1465
|
+
* positioned after FROM/JOIN and before WHERE, matching ClickHouse's real
|
|
1466
|
+
* grammar. `(options as any).arrayJoin` accepts either a single expression
|
|
1467
|
+
* string (optionally `'col AS alias'`) or an array of such expressions;
|
|
1468
|
+
* `(options as any).leftArrayJoin` selects the LEFT variant.
|
|
1469
|
+
*/
|
|
1470
|
+
const arrayJoinExprs = options.arrayJoin ?? options.leftArrayJoin;
|
|
1471
|
+
if (arrayJoinExprs) {
|
|
1472
|
+
const exprs = Array.isArray(arrayJoinExprs) ? arrayJoinExprs : [arrayJoinExprs];
|
|
1473
|
+
const keyword = options.leftArrayJoin ? 'LEFT ARRAY JOIN' : 'ARRAY JOIN';
|
|
1474
|
+
sql += ` ${keyword} ${exprs.join(', ')}`;
|
|
1475
|
+
}
|
|
1476
|
+
if (options.where) {
|
|
1477
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
1478
|
+
if (whereClause.sql)
|
|
1479
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1480
|
+
}
|
|
1481
|
+
if (options.group) {
|
|
1482
|
+
const groupBy = Array.isArray(options.group) ? options.group : [options.group];
|
|
1483
|
+
sql += ` GROUP BY ${groupBy.map((g) => this.escapeId(g)).join(', ')}`;
|
|
1484
|
+
if (options.having) {
|
|
1485
|
+
const havingClause = this.buildWhereClause(options.having);
|
|
1486
|
+
if (havingClause.sql)
|
|
1487
|
+
sql += ` HAVING ${havingClause.sql}`;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (options.order) {
|
|
1491
|
+
const orderClause = this.buildOrderClause(options.order);
|
|
1492
|
+
if (orderClause)
|
|
1493
|
+
sql += ` ${orderClause}`;
|
|
1494
|
+
}
|
|
1495
|
+
sql += this.buildLimitOffset(options.limit, options.offset);
|
|
1496
|
+
if (options.union && options.union.length > 0) {
|
|
1497
|
+
const unionType = options.unionType || 'UNION';
|
|
1498
|
+
for (const unionQuery of options.union) {
|
|
1499
|
+
const unionModel = unionQuery.model;
|
|
1500
|
+
const unionTableName = unionModel.tableName || unionModel.name || '';
|
|
1501
|
+
let unionSql = `SELECT * FROM ${this.escapeId(unionTableName)}`;
|
|
1502
|
+
if (unionQuery.where) {
|
|
1503
|
+
const whereClause = this.buildWhereClause(unionQuery.where);
|
|
1504
|
+
if (whereClause.sql)
|
|
1505
|
+
unionSql += ` WHERE ${whereClause.sql}`;
|
|
1506
|
+
}
|
|
1507
|
+
if (unionQuery.order) {
|
|
1508
|
+
const orderClause = this.buildOrderClause(unionQuery.order);
|
|
1509
|
+
if (orderClause)
|
|
1510
|
+
unionSql += ` ${orderClause}`;
|
|
1511
|
+
}
|
|
1512
|
+
unionSql += this.buildLimitOffset(unionQuery.limit, unionQuery.offset);
|
|
1513
|
+
sql += ` ${unionType} ${unionSql}`;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return { sql, values: [] };
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Increment/decrement is built on the same asynchronous ALTER TABLE ...
|
|
1520
|
+
* UPDATE mutation mechanism as buildUpdateQuery() - see its doc comment.
|
|
1521
|
+
*/
|
|
1522
|
+
buildIncrementQuery(tableName, fields, where, options) {
|
|
1523
|
+
const by = options?.by ?? 1;
|
|
1524
|
+
let setClauses;
|
|
1525
|
+
if (typeof fields === 'string') {
|
|
1526
|
+
setClauses = [`${this.escapeId(fields)} = ${this.escapeId(fields)} + ${by}`];
|
|
1527
|
+
}
|
|
1528
|
+
else if (Array.isArray(fields)) {
|
|
1529
|
+
setClauses = fields.map((f) => `${this.escapeId(f)} = ${this.escapeId(f)} + ${by}`);
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
setClauses = Object.entries(fields).map(([f, v]) => `${this.escapeId(f)} = ${this.escapeId(f)} + ${v}`);
|
|
1533
|
+
}
|
|
1534
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} UPDATE ${setClauses.join(', ')}`;
|
|
1535
|
+
const whereClause = this.buildWhereClause(where);
|
|
1536
|
+
if (whereClause.sql)
|
|
1537
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1538
|
+
return { sql, values: [] };
|
|
1539
|
+
}
|
|
1540
|
+
// ==================== Foreign Data Wrappers (unsupported) ====================
|
|
1541
|
+
//
|
|
1542
|
+
// ClickHouse has table functions/engines for federation (mysql(), postgresql(),
|
|
1543
|
+
// s3(), etc.), but nothing matching PostgreSQL's FDW/foreign-server/user-mapping
|
|
1544
|
+
// object model that this part of the Dialect interface is shaped around.
|
|
1545
|
+
fdwUnsupported() {
|
|
1546
|
+
throw new Error('Foreign Data Wrappers are not supported by ClickHouse. This is a PostgreSQL-specific feature. ' +
|
|
1547
|
+
'ClickHouse offers table functions/engines for federation instead (e.g. mysql(), postgresql(), s3()).');
|
|
1548
|
+
}
|
|
1549
|
+
buildCreateServerQuery(_name, _opts) {
|
|
1550
|
+
return this.fdwUnsupported();
|
|
1551
|
+
}
|
|
1552
|
+
buildAlterServerQuery(_name, _opts) {
|
|
1553
|
+
return this.fdwUnsupported();
|
|
1554
|
+
}
|
|
1555
|
+
buildDropServerQuery(_name, _opts) {
|
|
1556
|
+
return this.fdwUnsupported();
|
|
1557
|
+
}
|
|
1558
|
+
buildCreateUserMappingQuery(_opts) {
|
|
1559
|
+
return this.fdwUnsupported();
|
|
1560
|
+
}
|
|
1561
|
+
buildAlterUserMappingQuery(_opts) {
|
|
1562
|
+
return this.fdwUnsupported();
|
|
1563
|
+
}
|
|
1564
|
+
buildDropUserMappingQuery(_serverName, _user, _opts) {
|
|
1565
|
+
return this.fdwUnsupported();
|
|
1566
|
+
}
|
|
1567
|
+
buildCreateForeignTableQuery(_tableName, _opts) {
|
|
1568
|
+
return this.fdwUnsupported();
|
|
1569
|
+
}
|
|
1570
|
+
buildDropForeignTableQuery(_tableName, _opts) {
|
|
1571
|
+
return this.fdwUnsupported();
|
|
1572
|
+
}
|
|
1573
|
+
buildImportForeignSchemaQuery(_remoteSchema, _serverName, _opts) {
|
|
1574
|
+
return this.fdwUnsupported();
|
|
1575
|
+
}
|
|
1576
|
+
getServersQuery() {
|
|
1577
|
+
return this.fdwUnsupported();
|
|
1578
|
+
}
|
|
1579
|
+
async createForeignDataWrapper(_fdwName, _options) {
|
|
1580
|
+
this.fdwUnsupported();
|
|
1581
|
+
}
|
|
1582
|
+
async dropForeignDataWrapper(_fdwName, _options) {
|
|
1583
|
+
this.fdwUnsupported();
|
|
1584
|
+
}
|
|
1585
|
+
async createForeignServer(_serverName, _fdwName, _options) {
|
|
1586
|
+
this.fdwUnsupported();
|
|
1587
|
+
}
|
|
1588
|
+
async dropForeignServer(_serverName, _options) {
|
|
1589
|
+
this.fdwUnsupported();
|
|
1590
|
+
}
|
|
1591
|
+
async createForeignTable(_tableName, _columns, _options) {
|
|
1592
|
+
this.fdwUnsupported();
|
|
1593
|
+
}
|
|
1594
|
+
async createUserMapping(_userName, _serverName, _options) {
|
|
1595
|
+
this.fdwUnsupported();
|
|
1596
|
+
}
|
|
1597
|
+
async dropUserMapping(_userName, _serverName, _options) {
|
|
1598
|
+
this.fdwUnsupported();
|
|
1599
|
+
}
|
|
1600
|
+
// ==================== User / role / privilege management ====================
|
|
1601
|
+
//
|
|
1602
|
+
// ClickHouse does support SQL-driven access control (CREATE USER/ROLE, GRANT/
|
|
1603
|
+
// REVOKE) when `access_management` is enabled server-side; the syntax below
|
|
1604
|
+
// uses ClickHouse's real grammar (distinct from MySQL/PostgreSQL's).
|
|
1605
|
+
buildCreateUserQuery(username, options) {
|
|
1606
|
+
let sql = `CREATE USER${options?.ifNotExists ? ' IF NOT EXISTS' : ''} ${this.escapeId(username)}`;
|
|
1607
|
+
if (options?.password) {
|
|
1608
|
+
sql += ` IDENTIFIED WITH plaintext_password BY '${this.escapeString(options.password)}'`;
|
|
1609
|
+
}
|
|
1610
|
+
return sql;
|
|
1611
|
+
}
|
|
1612
|
+
buildAlterUserQuery(username, options) {
|
|
1613
|
+
let sql = `ALTER USER ${this.escapeId(username)}`;
|
|
1614
|
+
if (options?.password) {
|
|
1615
|
+
sql += ` IDENTIFIED WITH plaintext_password BY '${this.escapeString(options.password)}'`;
|
|
1616
|
+
}
|
|
1617
|
+
return sql;
|
|
1618
|
+
}
|
|
1619
|
+
buildDropUserQuery(username, options) {
|
|
1620
|
+
return `DROP USER${options?.ifExists ? ' IF EXISTS' : ''} ${this.escapeId(username)}`;
|
|
1621
|
+
}
|
|
1622
|
+
getUsersQuery() {
|
|
1623
|
+
return 'SELECT name, storage, auth_type FROM system.users';
|
|
1624
|
+
}
|
|
1625
|
+
buildGrantQuery(options) {
|
|
1626
|
+
const privileges = (options.privileges || []).join(', ');
|
|
1627
|
+
const scope = options.on?.level === 'table'
|
|
1628
|
+
? `${options.on.database ? `${this.escapeId(options.on.database)}.` : ''}${this.escapeId(options.on.table)}`
|
|
1629
|
+
: options.on?.level === 'database'
|
|
1630
|
+
? `${this.escapeId(options.on.database)}.*`
|
|
1631
|
+
: '*.*';
|
|
1632
|
+
const recipients = (Array.isArray(options.to) ? options.to : [options.to]).map((r) => this.escapeId(r)).join(', ');
|
|
1633
|
+
let sql = `GRANT ${privileges} ON ${scope} TO ${recipients}`;
|
|
1634
|
+
if (options.withGrantOption)
|
|
1635
|
+
sql += ' WITH GRANT OPTION';
|
|
1636
|
+
return sql;
|
|
1637
|
+
}
|
|
1638
|
+
buildRevokeQuery(options) {
|
|
1639
|
+
const privileges = (options.privileges || []).join(', ');
|
|
1640
|
+
const scope = options.on?.level === 'table'
|
|
1641
|
+
? `${options.on.database ? `${this.escapeId(options.on.database)}.` : ''}${this.escapeId(options.on.table)}`
|
|
1642
|
+
: options.on?.level === 'database'
|
|
1643
|
+
? `${this.escapeId(options.on.database)}.*`
|
|
1644
|
+
: '*.*';
|
|
1645
|
+
const targets = (Array.isArray(options.from) ? options.from : [options.from]).map((r) => this.escapeId(r)).join(', ');
|
|
1646
|
+
return `REVOKE ${privileges} ON ${scope} FROM ${targets}`;
|
|
1647
|
+
}
|
|
1648
|
+
buildShowGrantsQuery(username, _host) {
|
|
1649
|
+
return `SHOW GRANTS FOR ${this.escapeId(username)}`;
|
|
1650
|
+
}
|
|
1651
|
+
buildFlushPrivilegesQuery() {
|
|
1652
|
+
// ClickHouse re-reads users.xml/access-storage automatically; there is no
|
|
1653
|
+
// FLUSH PRIVILEGES statement. SYSTEM RELOAD USERS is the closest analog.
|
|
1654
|
+
return 'SYSTEM RELOAD USERS';
|
|
1655
|
+
}
|
|
1656
|
+
buildCreateRoleQuery(roleName, options) {
|
|
1657
|
+
return `CREATE ROLE${options?.ifNotExists ? ' IF NOT EXISTS' : ''} ${this.escapeId(roleName)}`;
|
|
1658
|
+
}
|
|
1659
|
+
buildDropRoleQuery(roleName, options) {
|
|
1660
|
+
return `DROP ROLE${options?.ifExists ? ' IF EXISTS' : ''} ${this.escapeId(roleName)}`;
|
|
1661
|
+
}
|
|
1662
|
+
buildGrantRoleQuery(role, to, _options) {
|
|
1663
|
+
const recipients = (Array.isArray(to) ? to : [to]).map((r) => this.escapeId(r)).join(', ');
|
|
1664
|
+
return `GRANT ${this.escapeId(role)} TO ${recipients}`;
|
|
1665
|
+
}
|
|
1666
|
+
buildRevokeRoleQuery(role, from, _options) {
|
|
1667
|
+
const targets = (Array.isArray(from) ? from : [from]).map((r) => this.escapeId(r)).join(', ');
|
|
1668
|
+
return `REVOKE ${this.escapeId(role)} FROM ${targets}`;
|
|
1669
|
+
}
|
|
1670
|
+
getRolesQuery() {
|
|
1671
|
+
return 'SELECT name FROM system.roles';
|
|
1672
|
+
}
|
|
1673
|
+
// ==================== Stored procedures (unsupported) ====================
|
|
1674
|
+
//
|
|
1675
|
+
// ClickHouse has no stored procedure language. The closest analogs are
|
|
1676
|
+
// materialized views (for insert-triggered transformations) and user-defined
|
|
1677
|
+
// functions (SQL or executable UDFs), neither of which offers arbitrary
|
|
1678
|
+
// procedural logic or resembles this interface's shape.
|
|
1679
|
+
async createStoredProcedure(_options) {
|
|
1680
|
+
throw new Error('ClickHouse has no stored procedure language. Consider a materialized view (for insert-triggered ' +
|
|
1681
|
+
'transformations) or a user-defined function (CREATE FUNCTION) instead.');
|
|
1682
|
+
}
|
|
1683
|
+
async createProcedure(options) {
|
|
1684
|
+
return this.createStoredProcedure(options);
|
|
1685
|
+
}
|
|
1686
|
+
async dropStoredProcedure(_procedureName, _options) {
|
|
1687
|
+
throw new Error('ClickHouse has no stored procedure language.');
|
|
1688
|
+
}
|
|
1689
|
+
async dropProcedure(procedureName, options) {
|
|
1690
|
+
return this.dropStoredProcedure(procedureName, options);
|
|
1691
|
+
}
|
|
1692
|
+
async executeStoredProcedure(_options) {
|
|
1693
|
+
throw new Error('ClickHouse has no stored procedure language.');
|
|
1694
|
+
}
|
|
1695
|
+
async hasStoredProcedure(_procedureName, _schema) {
|
|
1696
|
+
return false;
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Build an expression to shred a JSON document/array into relational rows.
|
|
1700
|
+
* ClickHouse has no `JSON_TABLE`/`OPENJSON` function; the real equivalent composes
|
|
1701
|
+
* `JSONExtractArrayRaw` (split a JSON array into raw per-element JSON strings) with
|
|
1702
|
+
* `arrayJoin` (explode an array into rows), then per-column `JSONExtract*` functions
|
|
1703
|
+
* (chosen by the requested column type) to pull typed values out of each element.
|
|
1704
|
+
* ClickHouse: (SELECT JSONExtractString(elem, 'name') AS name, ... FROM
|
|
1705
|
+
* (SELECT arrayJoin(JSONExtractArrayRaw(expr, 'rowPath')) AS elem)) AS alias
|
|
1706
|
+
*/
|
|
1707
|
+
buildJsonTable(jsonExpression, rowPath, columns, alias) {
|
|
1708
|
+
const jsonExtractFn = {
|
|
1709
|
+
string: 'JSONExtractString',
|
|
1710
|
+
varchar: 'JSONExtractString',
|
|
1711
|
+
text: 'JSONExtractString',
|
|
1712
|
+
int: 'JSONExtractInt',
|
|
1713
|
+
integer: 'JSONExtractInt',
|
|
1714
|
+
bigint: 'JSONExtractInt',
|
|
1715
|
+
uint64: 'JSONExtractUInt',
|
|
1716
|
+
float: 'JSONExtractFloat',
|
|
1717
|
+
float64: 'JSONExtractFloat',
|
|
1718
|
+
double: 'JSONExtractFloat',
|
|
1719
|
+
decimal: 'JSONExtractFloat',
|
|
1720
|
+
bool: 'JSONExtractBool',
|
|
1721
|
+
boolean: 'JSONExtractBool',
|
|
1722
|
+
};
|
|
1723
|
+
const normalizedRowPath = rowPath ? rowPath.replace(/^\$\.?/, '') : '';
|
|
1724
|
+
const rowPathArg = normalizedRowPath ? `, '${normalizedRowPath}'` : '';
|
|
1725
|
+
const selectCols = columns
|
|
1726
|
+
.map((col) => {
|
|
1727
|
+
if (col.forOrdinality) {
|
|
1728
|
+
return `rowNumberInBlock() AS ${this.escapeId(col.name)}`;
|
|
1729
|
+
}
|
|
1730
|
+
const key = (col.path || col.name).replace(/^\$\.?/, '');
|
|
1731
|
+
const fn = (col.type && jsonExtractFn[col.type.toLowerCase()]) || 'JSONExtractRaw';
|
|
1732
|
+
return `${fn}(elem, '${key}') AS ${this.escapeId(col.name)}`;
|
|
1733
|
+
})
|
|
1734
|
+
.join(', ');
|
|
1735
|
+
return (`(SELECT ${selectCols} FROM ` +
|
|
1736
|
+
`(SELECT arrayJoin(JSONExtractArrayRaw(${jsonExpression}${rowPathArg})) AS elem)) AS ${this.escapeId(alias)}`);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
exports.ClickHouseDialect = ClickHouseDialect;
|
|
1740
|
+
function createClickHouseDialect(config) {
|
|
1741
|
+
return new ClickHouseDialect(config);
|
|
1742
|
+
}
|