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.
Files changed (310) hide show
  1. package/CHANGELOG.md +1111 -0
  2. package/LICENSE +21 -0
  3. package/README.md +573 -0
  4. package/dist/audit/history-query.js +180 -0
  5. package/dist/audit/index.js +23 -0
  6. package/dist/audit/logger.js +236 -0
  7. package/dist/cache/cache-manager.js +84 -0
  8. package/dist/cache/index.js +16 -0
  9. package/dist/cache/redis-cluster-cache.js +557 -0
  10. package/dist/cli.js +2200 -0
  11. package/dist/compliance/audit-trail.js +68 -0
  12. package/dist/compliance/backup-verification.js +685 -0
  13. package/dist/compliance/breach-detector.js +505 -0
  14. package/dist/compliance/consent-record.js +227 -0
  15. package/dist/compliance/consent-versioning.js +331 -0
  16. package/dist/compliance/cross-border-log.js +530 -0
  17. package/dist/compliance/data-classifier.js +258 -0
  18. package/dist/compliance/data-lineage.js +565 -0
  19. package/dist/compliance/data-masker.js +303 -0
  20. package/dist/compliance/data-portability.js +265 -0
  21. package/dist/compliance/data-retention.js +195 -0
  22. package/dist/compliance/dsar-workflow.js +342 -0
  23. package/dist/compliance/field-encryption.js +223 -0
  24. package/dist/compliance/immutable-record.js +148 -0
  25. package/dist/compliance/index.js +260 -0
  26. package/dist/compliance/privacy-impact-assessment.js +364 -0
  27. package/dist/compliance/pseudonymization.js +269 -0
  28. package/dist/compliance/query-firewall.js +1182 -0
  29. package/dist/compliance/rate-limiter.js +580 -0
  30. package/dist/compliance/right-to-erasure.js +117 -0
  31. package/dist/compliance/row-level-security.js +246 -0
  32. package/dist/compliance/security-decorator.js +574 -0
  33. package/dist/compliance/security-monitor.js +525 -0
  34. package/dist/compliance/sensitive-data-discovery.js +476 -0
  35. package/dist/compliance/session-isolation.js +472 -0
  36. package/dist/compliance/tls-enforcer.js +108 -0
  37. package/dist/compliance/worm-storage.js +712 -0
  38. package/dist/connection-manager.js +198 -0
  39. package/dist/connection-pool.js +519 -0
  40. package/dist/decorators/audit.js +136 -0
  41. package/dist/decorators/belongs-to-many.js +115 -0
  42. package/dist/decorators/belongs-to.js +115 -0
  43. package/dist/decorators/check.js +435 -0
  44. package/dist/decorators/collate.js +329 -0
  45. package/dist/decorators/comment.js +205 -0
  46. package/dist/decorators/database-settings.js +236 -0
  47. package/dist/decorators/default.js +244 -0
  48. package/dist/decorators/encryption.js +235 -0
  49. package/dist/decorators/engine.js +97 -0
  50. package/dist/decorators/fk-constraints.js +594 -0
  51. package/dist/decorators/foreign-table.js +136 -0
  52. package/dist/decorators/generated.js +274 -0
  53. package/dist/decorators/has-many.js +127 -0
  54. package/dist/decorators/has-one.js +116 -0
  55. package/dist/decorators/hstore.js +129 -0
  56. package/dist/decorators/index.js +355 -0
  57. package/dist/decorators/json-column.js +83 -0
  58. package/dist/decorators/jsonb.js +101 -0
  59. package/dist/decorators/orm-decorators.js +425 -0
  60. package/dist/decorators/permissions.js +294 -0
  61. package/dist/decorators/procedure.js +210 -0
  62. package/dist/decorators/query-options.js +556 -0
  63. package/dist/decorators/range.js +167 -0
  64. package/dist/decorators/set-column.js +83 -0
  65. package/dist/decorators/spatial.js +114 -0
  66. package/dist/decorators/storage.js +580 -0
  67. package/dist/decorators/timezone.js +512 -0
  68. package/dist/decorators/trigger.js +90 -0
  69. package/dist/decorators/uuid.js +135 -0
  70. package/dist/decorators/view.js +258 -0
  71. package/dist/diagrams/chen-diagram.js +354 -0
  72. package/dist/diagrams/class-diagram.js +384 -0
  73. package/dist/diagrams/dependency-diagram.js +432 -0
  74. package/dist/diagrams/er-diagram.js +605 -0
  75. package/dist/diagrams/flow-diagram.js +394 -0
  76. package/dist/diagrams/gantt-diagram.js +411 -0
  77. package/dist/diagrams/index-diagram.js +353 -0
  78. package/dist/diagrams/index.js +184 -0
  79. package/dist/diagrams/migration-diagram.js +316 -0
  80. package/dist/diagrams/model-diagram.js +616 -0
  81. package/dist/diagrams/package-diagram.js +376 -0
  82. package/dist/diagrams/palette.js +85 -0
  83. package/dist/diagrams/relational-diagram.js +455 -0
  84. package/dist/diagrams/schemadoc-diagram.js +309 -0
  85. package/dist/diagrams/sequence-diagram.js +307 -0
  86. package/dist/diagrams/state-diagram.js +344 -0
  87. package/dist/diagrams/svg-dom.js +111 -0
  88. package/dist/diagrams/tree-diagram.js +250 -0
  89. package/dist/dialects/clickhouse/index.js +1742 -0
  90. package/dist/dialects/cockroachdb/index.js +4677 -0
  91. package/dist/dialects/cratedb/index.js +441 -0
  92. package/dist/dialects/databricks/index.js +517 -0
  93. package/dist/dialects/db2/index.js +2225 -0
  94. package/dist/dialects/dialect.js +720 -0
  95. package/dist/dialects/duckdb/index.js +2014 -0
  96. package/dist/dialects/exasol/index.js +357 -0
  97. package/dist/dialects/firebird/index.js +495 -0
  98. package/dist/dialects/greenplum/index.js +457 -0
  99. package/dist/dialects/hana/index.js +1741 -0
  100. package/dist/dialects/mariadb/index.js +3590 -0
  101. package/dist/dialects/mssql/index.js +2296 -0
  102. package/dist/dialects/mysql/index.js +4058 -0
  103. package/dist/dialects/oracle/index.js +2999 -0
  104. package/dist/dialects/postgres/index.js +5319 -0
  105. package/dist/dialects/query-stream-helper.js +152 -0
  106. package/dist/dialects/questdb/index.js +433 -0
  107. package/dist/dialects/redshift/index.js +2277 -0
  108. package/dist/dialects/singlestore/index.js +354 -0
  109. package/dist/dialects/snowflake/index.js +2082 -0
  110. package/dist/dialects/spanner/index.js +1768 -0
  111. package/dist/dialects/sqlite/index.js +3382 -0
  112. package/dist/dialects/tidb/index.js +377 -0
  113. package/dist/dialects/timescaledb/index.js +164 -0
  114. package/dist/dialects/trino/index.js +420 -0
  115. package/dist/dialects/turso/index.js +227 -0
  116. package/dist/dialects/vertica/index.js +317 -0
  117. package/dist/dialects/yugabytedb/index.js +424 -0
  118. package/dist/errors/index.js +472 -0
  119. package/dist/errors/utils.js +340 -0
  120. package/dist/errors.js +21 -0
  121. package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
  122. package/dist/extensions/catalog/mssql-features.js +147 -0
  123. package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
  124. package/dist/extensions/catalog/oracle-db2-features.js +196 -0
  125. package/dist/extensions/catalog/postgres-extensions.js +516 -0
  126. package/dist/extensions/index.js +71 -0
  127. package/dist/extensions/types.js +13 -0
  128. package/dist/foreign-data.js +173 -0
  129. package/dist/hooks/hooks-manager.js +350 -0
  130. package/dist/hooks/index.js +37 -0
  131. package/dist/index.js +463 -0
  132. package/dist/logging.js +335 -0
  133. package/dist/migrations/index.js +40 -0
  134. package/dist/migrations/migration.js +196 -0
  135. package/dist/migrations/migrator.js +411 -0
  136. package/dist/migrations/prormmigration.js +275 -0
  137. package/dist/migrations/query-interface.js +435 -0
  138. package/dist/migrations/seeder.js +353 -0
  139. package/dist/models/associations.js +852 -0
  140. package/dist/models/constraints.js +288 -0
  141. package/dist/models/data-types.js +2518 -0
  142. package/dist/models/decorators.js +445 -0
  143. package/dist/models/index.js +33 -0
  144. package/dist/models/indexes.js +531 -0
  145. package/dist/models/methods.js +382 -0
  146. package/dist/models/model-manager.js +103 -0
  147. package/dist/models/model.js +5349 -0
  148. package/dist/models/operators.js +67 -0
  149. package/dist/models/scopes.js +189 -0
  150. package/dist/models/typescript-types.js +26 -0
  151. package/dist/nosql/aerospike/index.js +205 -0
  152. package/dist/nosql/allegrograph/index.js +169 -0
  153. package/dist/nosql/arangodb/index.js +364 -0
  154. package/dist/nosql/azure-blob/index.js +206 -0
  155. package/dist/nosql/beanstalkd/index.js +231 -0
  156. package/dist/nosql/beequeue/index.js +210 -0
  157. package/dist/nosql/bigchaindb/index.js +195 -0
  158. package/dist/nosql/bigtable/index.js +224 -0
  159. package/dist/nosql/blazegraph/index.js +173 -0
  160. package/dist/nosql/bullmq/index.js +191 -0
  161. package/dist/nosql/cassandra/index.js +174 -0
  162. package/dist/nosql/chroma/index.js +190 -0
  163. package/dist/nosql/cloudflare-kv/index.js +220 -0
  164. package/dist/nosql/coherence/index.js +200 -0
  165. package/dist/nosql/cosmosdb/index.js +157 -0
  166. package/dist/nosql/couchbase/index.js +213 -0
  167. package/dist/nosql/dax/index.js +212 -0
  168. package/dist/nosql/deno-kv/index.js +206 -0
  169. package/dist/nosql/dgraph/index.js +171 -0
  170. package/dist/nosql/doris/index.js +169 -0
  171. package/dist/nosql/druid/index.js +162 -0
  172. package/dist/nosql/dynamodb/index.js +937 -0
  173. package/dist/nosql/elasticsearch/index.js +377 -0
  174. package/dist/nosql/etcd/index.js +502 -0
  175. package/dist/nosql/eventhubs/index.js +213 -0
  176. package/dist/nosql/eventstore/index.js +254 -0
  177. package/dist/nosql/faunadb/index.js +188 -0
  178. package/dist/nosql/firestore/index.js +177 -0
  179. package/dist/nosql/fluree/index.js +148 -0
  180. package/dist/nosql/fuseki/index.js +170 -0
  181. package/dist/nosql/gcs/index.js +172 -0
  182. package/dist/nosql/gearman/index.js +160 -0
  183. package/dist/nosql/geode/index.js +196 -0
  184. package/dist/nosql/graphdb/index.js +169 -0
  185. package/dist/nosql/graylog/index.js +188 -0
  186. package/dist/nosql/gridgain/index.js +171 -0
  187. package/dist/nosql/hazelcast/index.js +162 -0
  188. package/dist/nosql/hbase/index.js +230 -0
  189. package/dist/nosql/ignite/index.js +173 -0
  190. package/dist/nosql/immudb/index.js +184 -0
  191. package/dist/nosql/index.js +232 -0
  192. package/dist/nosql/infinispan/index.js +200 -0
  193. package/dist/nosql/influxdb/index.js +0 -0
  194. package/dist/nosql/kafka/index.js +234 -0
  195. package/dist/nosql/keyspaces/index.js +189 -0
  196. package/dist/nosql/kinesis/index.js +253 -0
  197. package/dist/nosql/leveldb/index.js +153 -0
  198. package/dist/nosql/lmdb/index.js +160 -0
  199. package/dist/nosql/loki/index.js +201 -0
  200. package/dist/nosql/marklogic/index.js +204 -0
  201. package/dist/nosql/materialize/index.js +145 -0
  202. package/dist/nosql/meilisearch/index.js +154 -0
  203. package/dist/nosql/memcached/index.js +223 -0
  204. package/dist/nosql/milvus/index.js +410 -0
  205. package/dist/nosql/minio/index.js +265 -0
  206. package/dist/nosql/momento/index.js +179 -0
  207. package/dist/nosql/mongodb/index.js +461 -0
  208. package/dist/nosql/nats/index.js +247 -0
  209. package/dist/nosql/nedb/index.js +164 -0
  210. package/dist/nosql/neo4j/index.js +450 -0
  211. package/dist/nosql/neptune/index.js +470 -0
  212. package/dist/nosql/nsq/index.js +200 -0
  213. package/dist/nosql/opensearch/index.js +186 -0
  214. package/dist/nosql/orientdb/index.js +175 -0
  215. package/dist/nosql/papertrail/index.js +200 -0
  216. package/dist/nosql/pinecone/index.js +0 -0
  217. package/dist/nosql/pinot/index.js +133 -0
  218. package/dist/nosql/pouchdb/index.js +172 -0
  219. package/dist/nosql/prometheus/index.js +174 -0
  220. package/dist/nosql/provendb/index.js +147 -0
  221. package/dist/nosql/pubsub/index.js +187 -0
  222. package/dist/nosql/pulsar/index.js +232 -0
  223. package/dist/nosql/qdrant/index.js +295 -0
  224. package/dist/nosql/qldb/index.js +192 -0
  225. package/dist/nosql/r2/index.js +281 -0
  226. package/dist/nosql/rabbitmq/index.js +237 -0
  227. package/dist/nosql/ravendb/index.js +175 -0
  228. package/dist/nosql/redis/index.js +607 -0
  229. package/dist/nosql/redpanda/index.js +237 -0
  230. package/dist/nosql/resque/index.js +203 -0
  231. package/dist/nosql/rethinkdb/index.js +232 -0
  232. package/dist/nosql/rocksdb/index.js +152 -0
  233. package/dist/nosql/rockset/index.js +126 -0
  234. package/dist/nosql/s3/index.js +298 -0
  235. package/dist/nosql/scylladb/index.js +178 -0
  236. package/dist/nosql/signoz/index.js +227 -0
  237. package/dist/nosql/sns/index.js +201 -0
  238. package/dist/nosql/solr/index.js +231 -0
  239. package/dist/nosql/splunk/index.js +227 -0
  240. package/dist/nosql/sqs/index.js +246 -0
  241. package/dist/nosql/stardog/index.js +169 -0
  242. package/dist/nosql/starrocks/index.js +170 -0
  243. package/dist/nosql/store.js +2 -0
  244. package/dist/nosql/sumologic/index.js +213 -0
  245. package/dist/nosql/surrealdb/index.js +179 -0
  246. package/dist/nosql/terminusdb/index.js +151 -0
  247. package/dist/nosql/tigergraph/index.js +469 -0
  248. package/dist/nosql/typesense/index.js +148 -0
  249. package/dist/nosql/unqlite/index.js +157 -0
  250. package/dist/nosql/upstash/index.js +167 -0
  251. package/dist/nosql/vercel-kv/index.js +166 -0
  252. package/dist/nosql/victoriametrics/index.js +213 -0
  253. package/dist/nosql/virtuoso/index.js +169 -0
  254. package/dist/nosql/weaviate/index.js +276 -0
  255. package/dist/operators/index.js +108 -0
  256. package/dist/operators.js +2690 -0
  257. package/dist/prisma-migrate/index.js +38 -0
  258. package/dist/prisma-migrate/migration-generator.js +125 -0
  259. package/dist/prisma-migrate/model-generator.js +172 -0
  260. package/dist/prisma-migrate/relations.js +100 -0
  261. package/dist/prisma-migrate/schema-parser.js +167 -0
  262. package/dist/prisma-migrate/type-mapper.js +40 -0
  263. package/dist/prorm.js +6832 -0
  264. package/dist/query-builders/cte-builder.js +80 -0
  265. package/dist/query-builders/functions/aggregate.js +390 -0
  266. package/dist/query-builders/functions/conditional.js +503 -0
  267. package/dist/query-builders/functions/datetime.js +695 -0
  268. package/dist/query-builders/functions/fulltext.js +439 -0
  269. package/dist/query-builders/functions/index.js +93 -0
  270. package/dist/query-builders/functions/json.js +427 -0
  271. package/dist/query-builders/functions/math.js +399 -0
  272. package/dist/query-builders/functions/string.js +518 -0
  273. package/dist/query-builders/functions/window.js +328 -0
  274. package/dist/query-builders/include-builder.js +323 -0
  275. package/dist/query-builders/index-expression-builder.js +242 -0
  276. package/dist/query-builders/index.js +161 -0
  277. package/dist/query-builders/insert-builder.js +164 -0
  278. package/dist/query-builders/model-helpers.js +38 -0
  279. package/dist/query-builders/order-limit-builder.js +239 -0
  280. package/dist/query-builders/sql-compiler.js +2040 -0
  281. package/dist/query-builders/subquery-builder.js +152 -0
  282. package/dist/query-builders/update-builder.js +182 -0
  283. package/dist/query-builders/view-builder.js +218 -0
  284. package/dist/query-builders/where-builder.js +1532 -0
  285. package/dist/query-interface.js +622 -0
  286. package/dist/query-optimizers/batch-optimizer.js +426 -0
  287. package/dist/query-optimizers/explain-plans.js +415 -0
  288. package/dist/query-optimizers/index.js +51 -0
  289. package/dist/query-optimizers/prepared-statement-cache.js +423 -0
  290. package/dist/query-optimizers/query-hints.js +438 -0
  291. package/dist/query-optimizers/query-optimizer.js +278 -0
  292. package/dist/query-optimizers/slow-query-logger.js +271 -0
  293. package/dist/replica-manager.js +507 -0
  294. package/dist/schema/index.js +15 -0
  295. package/dist/schema/migration-generator.js +374 -0
  296. package/dist/schema/schema-differ.js +549 -0
  297. package/dist/schema/types.js +6 -0
  298. package/dist/sql-constants.js +300 -0
  299. package/dist/sqlite-advanced.js +1047 -0
  300. package/dist/streams/index.js +12 -0
  301. package/dist/streams/transforms.js +178 -0
  302. package/dist/transaction.js +456 -0
  303. package/dist/types/index.js +180 -0
  304. package/dist/user-management.js +127 -0
  305. package/dist/utils/date.js +300 -0
  306. package/dist/utils/index.js +1079 -0
  307. package/dist/utils/string.js +161 -0
  308. package/dist/validators/index.js +19 -0
  309. package/dist/validators/validator.js +911 -0
  310. 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
+ }