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,4677 @@
1
+ "use strict";
2
+ /**
3
+ * CockroachDB dialect implementation for the TypeScript ORM
4
+ *
5
+ * CockroachDB speaks the CockroachDB wire protocol and is largely SQL-compatible
6
+ * with CockroachDB, so this dialect reuses the `pg` driver (same as the Postgres
7
+ * dialect) and mirrors most of its query-building behavior. Notable differences
8
+ * from the Postgres dialect:
9
+ *
10
+ * - Default port is 26257 (not 5432).
11
+ * - showTables/describeTable/showIndexes/showConstraints prefer CockroachDB's
12
+ * `SHOW TABLES` / `SHOW COLUMNS FROM` / `SHOW INDEXES FROM` / `SHOW CONSTRAINTS FROM`
13
+ * statements instead of querying information_schema/pg_catalog directly.
14
+ * - `CREATE INDEX ... CONCURRENTLY` is not supported (and not needed) in CockroachDB
15
+ * because index creation is online/non-blocking by default; the `concurrently`
16
+ * option is ignored with a warning instead of being emitted in the SQL.
17
+ * - Interleaved tables (CockroachDB's now-deprecated `INTERLEAVE IN PARENT` syntax)
18
+ * are intentionally NOT implemented.
19
+ * - SERIAL/BIGSERIAL auto-increment columns are kept for parity with the Postgres
20
+ * dialect; CockroachDB accepts these as a Postgres-compatibility shorthand that
21
+ * maps to an implicit sequence internally (CockroachDB's default unique ID
22
+ * generation otherwise uses `unique_rowid()`).
23
+ * - `buildUpsertQuery`/`buildInsertQuery` default to Postgres-style `ON CONFLICT
24
+ * ... DO UPDATE` for consistency with the Postgres dialect's style, but
25
+ * accept a `nativeUpsert` option to opt into CockroachDB's native
26
+ * `UPSERT INTO ... VALUES (...)` shorthand, which replaces the whole row
27
+ * without requiring a conflict target.
28
+ * - Adds `isRetryableError()` to detect CockroachDB's serialization failure
29
+ * SQLSTATE (40001), which requires the client to retry the transaction.
30
+ * `runTransaction()` uses it to implement CockroachDB's client-side
31
+ * transaction retry loop (`SAVEPOINT cockroach_restart`), and it's also
32
+ * folded into the default query-retry match lists so single-statement
33
+ * retries cover it too.
34
+ */
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.CockroachDBTransaction = exports.CockroachDBDialect = void 0;
40
+ exports.createCockroachDBDialect = createCockroachDBDialect;
41
+ const pg_1 = __importDefault(require("pg"));
42
+ const prorm_1 = require("../../prorm");
43
+ const { Pool: PgPool } = pg_1.default;
44
+ /**
45
+ * Build a CockroachDB OPTIONS (...) clause from a plain object.
46
+ * Single-quotes each value and escapes existing single-quotes.
47
+ */
48
+ function buildPgOptionsClause(opts) {
49
+ const pairs = Object.entries(opts).map(([k, v]) => `${k} '${v.replace(/'/g, "''")}'`);
50
+ return `OPTIONS (${pairs.join(', ')})`;
51
+ }
52
+ /**
53
+ * Default retry options for CockroachDB connection
54
+ */
55
+ const DEFAULT_RETRY_OPTIONS = {
56
+ max: 3,
57
+ timeout: 1000,
58
+ match: [
59
+ 'ECONNREFUSED',
60
+ 'ENOTFOUND',
61
+ 'ETIMEDOUT',
62
+ 'connection timeout',
63
+ 'connect timeout',
64
+ 'too many connections',
65
+ 'remaining connection slots are reserved',
66
+ 'Lock not available',
67
+ // CockroachDB's SQLSTATE 40001 ("serialization_failure"), surfaced to
68
+ // clients as a "restart transaction" error, is a routine occurrence
69
+ // under SERIALIZABLE isolation when transactions conflict. It's part of
70
+ // CockroachDB's documented client contract that these be retried, so
71
+ // single-statement retries (see query()/executeWithRetry()) need to
72
+ // match on it too.
73
+ '40001',
74
+ 'serialization_failure',
75
+ 'restart transaction',
76
+ ],
77
+ backoff: false,
78
+ backoffMultiplier: 2,
79
+ backoffMax: 10000,
80
+ };
81
+ /**
82
+ * Default pool options for CockroachDB
83
+ */
84
+ const DEFAULT_POOL_OPTIONS = {
85
+ max: 20,
86
+ min: 0,
87
+ acquire: 60000,
88
+ idle: 30000,
89
+ evict: 1000,
90
+ handleDisconnects: false,
91
+ onConnect: undefined,
92
+ onDisconnect: undefined,
93
+ enableEvents: false,
94
+ logPoolOperations: false,
95
+ };
96
+ /**
97
+ * CockroachDB dialect class that implements the Dialect interface
98
+ */
99
+ class CockroachDBDialect {
100
+ constructor(config) {
101
+ this.name = 'cockroachdb';
102
+ this.library = 'pg';
103
+ this.pool = null;
104
+ this._isConnected = false;
105
+ this.transactionDepth = 0;
106
+ this.config = {
107
+ host: 'localhost',
108
+ // CockroachDB's default SQL port is 26257 (not Postgres's 5432).
109
+ port: 26257,
110
+ max: 20,
111
+ idleTimeoutMillis: 30000,
112
+ connectionTimeoutMillis: 2000,
113
+ ...config,
114
+ };
115
+ }
116
+ /**
117
+ * Determine whether an error is retryable, i.e. whether the client should
118
+ * retry the transaction that produced it.
119
+ *
120
+ * CockroachDB uses SQLSTATE 40001 ("serialization_failure", surfaced as the
121
+ * "restart transaction" error) to signal that a transaction could not be
122
+ * committed due to a conflict with a concurrent transaction under
123
+ * SERIALIZABLE isolation, and that the client should retry it from the
124
+ * beginning. See: https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference
125
+ *
126
+ * @param error - The error to check (expected to expose a `.code` property,
127
+ * as errors from the `pg` driver do).
128
+ * @returns True if the error's SQLSTATE is 40001.
129
+ */
130
+ isRetryableError(error) {
131
+ if (!error) {
132
+ return false;
133
+ }
134
+ if (error.code === '40001') {
135
+ return true;
136
+ }
137
+ // Some call paths (e.g. this.query()'s own error handling, or drivers/
138
+ // wrappers that stringify the original pg error) lose the `.code`
139
+ // property along the way. Fall back to checking nested causes and the
140
+ // error message text for CockroachDB's retry signal so callers using
141
+ // those paths can still detect a retryable error.
142
+ if (error.cause && error.cause !== error && this.isRetryableError(error.cause)) {
143
+ return true;
144
+ }
145
+ const message = typeof error.message === 'string' ? error.message : String(error);
146
+ const normalized = message.toLowerCase();
147
+ return (message.includes('40001') ||
148
+ normalized.includes('serialization_failure') ||
149
+ normalized.includes('restart transaction'));
150
+ }
151
+ /**
152
+ * Connect to the CockroachDB database
153
+ */
154
+ async connect() {
155
+ try {
156
+ this.pool = new PgPool({
157
+ host: this.config.host,
158
+ port: this.config.port,
159
+ user: this.config.username,
160
+ password: this.config.password,
161
+ database: this.config.database,
162
+ max: this.config.max,
163
+ idleTimeoutMillis: this.config.idleTimeoutMillis,
164
+ connectionTimeoutMillis: this.config.connectionTimeoutMillis,
165
+ ssl: this.config.ssl,
166
+ statement_timeout: this.config.statementTimeout,
167
+ query_timeout: this.config.queryTimeout,
168
+ });
169
+ // Test the connection
170
+ const client = await this.pool.connect();
171
+ client.release();
172
+ this._isConnected = true;
173
+ }
174
+ catch (error) {
175
+ throw new Error(`Failed to connect to CockroachDB database: ${error}`);
176
+ }
177
+ }
178
+ /**
179
+ * Disconnect from the CockroachDB database
180
+ */
181
+ async disconnect() {
182
+ if (this.pool) {
183
+ await this.pool.end();
184
+ this.pool = null;
185
+ this._isConnected = false;
186
+ }
187
+ }
188
+ /**
189
+ * Get the current connection pool
190
+ */
191
+ getConnection() {
192
+ return this.pool;
193
+ }
194
+ /**
195
+ * Check if connected
196
+ */
197
+ isConnected() {
198
+ return this._isConnected && this.pool !== null;
199
+ }
200
+ /**
201
+ * Execute a raw SQL query with retry support
202
+ */
203
+ async query(sql, options) {
204
+ // Default retry options for queries
205
+ const defaultQueryRetryOptions = {
206
+ max: 3,
207
+ timeout: 1000,
208
+ match: [
209
+ 'ECONNREFUSED',
210
+ 'ENOTFOUND',
211
+ 'ETIMEDOUT',
212
+ 'connection timeout',
213
+ 'connect timeout',
214
+ 'too many connections',
215
+ 'remaining connection slots are reserved',
216
+ 'Lock not available',
217
+ 'deadlock',
218
+ // See DEFAULT_RETRY_OPTIONS above: CockroachDB's 40001/
219
+ // serialization_failure ("restart transaction") error is the
220
+ // standard signal that a single statement should be retried.
221
+ '40001',
222
+ 'serialization_failure',
223
+ 'restart transaction',
224
+ ],
225
+ backoff: false,
226
+ backoffMultiplier: 2,
227
+ backoffMax: 10000,
228
+ };
229
+ const retryOptions = options?.retry;
230
+ const effectiveRetry = retryOptions
231
+ ? {
232
+ ...defaultQueryRetryOptions,
233
+ ...retryOptions,
234
+ match: retryOptions.match || defaultQueryRetryOptions.match,
235
+ }
236
+ : null;
237
+ const executeQuery = async () => {
238
+ if (!this.pool) {
239
+ throw new Error('Not connected to database');
240
+ }
241
+ const isSelect = sql.trim().toUpperCase().startsWith('SELECT') ||
242
+ sql.trim().toUpperCase().startsWith('WITH') ||
243
+ sql.trim().toUpperCase().startsWith('SHOW') ||
244
+ sql.trim().toUpperCase().startsWith('EXPLAIN') ||
245
+ sql.trim().toUpperCase().startsWith('DESCRIBE');
246
+ try {
247
+ // Apply any $1/$2 or :name replacements before sending the query. `replacements`
248
+ // and `bindings` are both used across this file to pass values for placeholder
249
+ // substitution (see replaceReplacements()); without this the SQL is sent with
250
+ // literal, unbound placeholders and CockroachDB raises "there is no parameter $N".
251
+ const replacements = options?.replacements ?? options?.bindings;
252
+ const finalSql = replacements !== undefined ? this.replaceReplacements(sql, replacements) : sql;
253
+ const result = (await this.pool.query(finalSql));
254
+ // Transform fields
255
+ const fieldInfo = result.fields
256
+ ? result.fields.map((field) => ({
257
+ name: field.name,
258
+ type: field.dataTypeID.toString(),
259
+ length: field.dataTypeModifier || 0,
260
+ tableID: 0,
261
+ columnID: 0,
262
+ nullable: true,
263
+ isEnum: field.dataTypeID === 114 /* OID for enum */,
264
+ isPrimaryKey: false,
265
+ }))
266
+ : [];
267
+ if (isSelect) {
268
+ return {
269
+ rows: result.rows,
270
+ rowCount: result.rowCount || result.rows.length,
271
+ fields: fieldInfo,
272
+ };
273
+ }
274
+ else {
275
+ // For INSERT, UPDATE, DELETE - return row count
276
+ const rowCount = result.rowCount || 0;
277
+ // Handle RETURNING clause
278
+ if (result.rows && result.rows.length > 0) {
279
+ return {
280
+ rows: result.rows,
281
+ rowCount: result.rows.length,
282
+ fields: fieldInfo,
283
+ };
284
+ }
285
+ return {
286
+ rows: [],
287
+ rowCount,
288
+ fields: fieldInfo,
289
+ };
290
+ }
291
+ }
292
+ catch (error) {
293
+ throw new Error(`CockroachDB Query error: ${error}`);
294
+ }
295
+ };
296
+ // If retry options are provided, use retry logic
297
+ if (effectiveRetry && effectiveRetry.max > 0) {
298
+ return this.executeWithRetry(executeQuery, effectiveRetry);
299
+ }
300
+ return executeQuery();
301
+ }
302
+ /**
303
+ * Execute a query and stream results using CockroachDB cursor
304
+ * Uses server-side cursors for efficient memory usage with large datasets
305
+ * @param sql - The SQL query string
306
+ * @param options - Stream options
307
+ * @returns Readable stream for streaming results
308
+ */
309
+ queryStream(sql, options) {
310
+ const { Readable } = require('stream');
311
+ const batchSize = options?.batchSize || 1000;
312
+ const highWaterMark = options?.highWaterMark || 1000;
313
+ // Create a custom readable stream
314
+ const stream = new Readable({
315
+ objectMode: true,
316
+ highWaterMark,
317
+ read() { },
318
+ });
319
+ // Use cursor for streaming - requires a client from the pool
320
+ if (!this.pool) {
321
+ stream.destroy(new Error('Not connected to database'));
322
+ return stream;
323
+ }
324
+ // Create a client for cursor operations
325
+ this.pool.connect((err, client, done) => {
326
+ if (err) {
327
+ stream.destroy(err);
328
+ return;
329
+ }
330
+ // Use a cursor for streaming
331
+ const cursorName = `cursor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
332
+ const cursorSql = sql.replace(/\bLIMIT\s+\d+\s*$/i, '').trim();
333
+ // Declare cursor with the query
334
+ const declareSql = `DECLARE ${cursorName} CURSOR FOR ${cursorSql}`;
335
+ client.query(declareSql, (declareErr) => {
336
+ if (declareErr) {
337
+ done();
338
+ stream.destroy(declareErr);
339
+ return;
340
+ }
341
+ // Fetch rows in batches
342
+ const fetchBatch = () => {
343
+ client.query(`FETCH ${batchSize} FROM ${cursorName}`, (fetchErr, result) => {
344
+ if (fetchErr) {
345
+ done();
346
+ stream.destroy(fetchErr);
347
+ return;
348
+ }
349
+ const rows = result?.rows || [];
350
+ if (rows.length === 0) {
351
+ // No more rows - close cursor and release client
352
+ client.query(`CLOSE ${cursorName}`, () => {
353
+ done();
354
+ stream.push(null);
355
+ });
356
+ }
357
+ else {
358
+ // Push each row to the stream
359
+ for (const row of rows) {
360
+ if (options?.mapToModel && options?.model) {
361
+ // Map to model instance if requested
362
+ const instance = new options.model();
363
+ Object.assign(instance, row);
364
+ stream.push(instance);
365
+ }
366
+ else {
367
+ stream.push(row);
368
+ }
369
+ }
370
+ // Continue fetching
371
+ process.nextTick(fetchBatch);
372
+ }
373
+ });
374
+ };
375
+ // Start fetching
376
+ fetchBatch();
377
+ });
378
+ });
379
+ return stream;
380
+ }
381
+ /**
382
+ * Execute a function with retry logic for query execution
383
+ */
384
+ async executeWithRetry(fn, retryOptions) {
385
+ let lastError;
386
+ const { backoff, backoffMultiplier, backoffMax } = retryOptions;
387
+ for (let attempt = 0; attempt <= retryOptions.max; attempt++) {
388
+ try {
389
+ return await fn();
390
+ }
391
+ catch (error) {
392
+ lastError = error;
393
+ const errorMessage = lastError.message;
394
+ // Check if this error should be retried
395
+ const shouldRetry = retryOptions.match.some((pattern) => errorMessage.toLowerCase().includes(pattern.toLowerCase()));
396
+ if (!shouldRetry || attempt === retryOptions.max) {
397
+ throw lastError;
398
+ }
399
+ // Calculate delay - use exponential backoff if enabled
400
+ let delay;
401
+ if (backoff) {
402
+ delay = retryOptions.timeout * Math.pow(backoffMultiplier || 2, attempt);
403
+ delay = Math.min(delay, backoffMax || 10000);
404
+ }
405
+ else {
406
+ delay = retryOptions.timeout;
407
+ }
408
+ // Add some jitter to avoid thundering herd
409
+ const jitter = Math.random() * 100;
410
+ await new Promise((resolve) => setTimeout(resolve, delay + jitter));
411
+ }
412
+ }
413
+ throw lastError;
414
+ }
415
+ /**
416
+ * Escape a value for use in a query
417
+ */
418
+ escape(value) {
419
+ // Handle Literal (raw SQL) values - insert directly without escaping
420
+ if (value instanceof prorm_1.Literal) {
421
+ return value.val;
422
+ }
423
+ if (value === null) {
424
+ return 'NULL';
425
+ }
426
+ if (typeof value === 'string') {
427
+ return `'${this.escapeString(value)}'`;
428
+ }
429
+ if (typeof value === 'number') {
430
+ return String(value);
431
+ }
432
+ if (typeof value === 'bigint') {
433
+ return value.toString();
434
+ }
435
+ if (typeof value === 'boolean') {
436
+ return value ? 'TRUE' : 'FALSE';
437
+ }
438
+ if (value instanceof Date) {
439
+ return `'${this.formatDate(value)}'`;
440
+ }
441
+ if (Buffer.isBuffer(value)) {
442
+ return `E'${value.toString('hex')}'`;
443
+ }
444
+ if (Array.isArray(value)) {
445
+ // Use CockroachDB ARRAY[...] literal syntax, not JSON, so the value is
446
+ // usable directly in an array-typed column/expression.
447
+ return this.escapeArray(value);
448
+ }
449
+ // JSON stringify objects
450
+ return `'${this.escapeString(JSON.stringify(value))}'`;
451
+ }
452
+ /**
453
+ * Escape a string for SQL
454
+ */
455
+ escapeString(str) {
456
+ return str.replace(/'/g, "''").replace(/\\/g, '\\\\');
457
+ }
458
+ /**
459
+ * Format a date for CockroachDB
460
+ */
461
+ formatDate(date) {
462
+ const year = date.getFullYear();
463
+ const month = String(date.getMonth() + 1).padStart(2, '0');
464
+ const day = String(date.getDate()).padStart(2, '0');
465
+ const hours = String(date.getHours()).padStart(2, '0');
466
+ const minutes = String(date.getMinutes()).padStart(2, '0');
467
+ const seconds = String(date.getSeconds()).padStart(2, '0');
468
+ const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
469
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
470
+ }
471
+ /**
472
+ * Escape an identifier (table name, column name, etc.)
473
+ * CockroachDB uses double quotes for identifiers
474
+ */
475
+ escapeId(identifier) {
476
+ const id = String(identifier ?? '');
477
+ return `"${id.replace(/"/g, '""')}"`;
478
+ }
479
+ /**
480
+ * Quote an identifier (column name, table name)
481
+ * CockroachDB uses double quotes for identifiers
482
+ */
483
+ quoteIdentifier(identifier) {
484
+ return this.escapeId(identifier);
485
+ }
486
+ /**
487
+ * Quote a table name
488
+ * Includes schema prefix if provided
489
+ */
490
+ quoteTable(tableName, schema) {
491
+ if (schema && schema !== 'public') {
492
+ return `${this.escapeId(schema)}.${this.escapeId(tableName)}`;
493
+ }
494
+ return this.escapeId(tableName);
495
+ }
496
+ /**
497
+ * Get the database version
498
+ */
499
+ async getDatabaseVersion() {
500
+ if (!this.pool) {
501
+ throw new Error('Not connected to database');
502
+ }
503
+ const result = await this.query('SELECT version() as version');
504
+ return result.rows[0]?.version || 'Unknown';
505
+ }
506
+ /**
507
+ * Create a database schema
508
+ * @param schema - The schema name to create
509
+ */
510
+ async createSchema(schema) {
511
+ await this.query(`CREATE SCHEMA ${this.escapeId(schema)}`);
512
+ }
513
+ /**
514
+ * Drop a database schema
515
+ * @param schema - The schema name to drop
516
+ * @param options - Drop options (e.g., cascade)
517
+ */
518
+ async dropSchema(schema, options) {
519
+ let sql = 'DROP SCHEMA';
520
+ if (options?.ifExists) {
521
+ sql += ' IF EXISTS';
522
+ }
523
+ sql += ` ${this.escapeId(schema)}`;
524
+ if (options?.cascade) {
525
+ sql += ' CASCADE';
526
+ }
527
+ await this.query(sql);
528
+ }
529
+ /**
530
+ * Show all schemas in the database
531
+ */
532
+ async showAllSchemas() {
533
+ const result = await this.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('pg_catalog', 'information_schema') ORDER BY schema_name`, { raw: true });
534
+ return result.rows?.map((row) => row.schema_name) || [];
535
+ }
536
+ /**
537
+ * List all schemas in the database
538
+ */
539
+ async listSchemas() {
540
+ return this.showAllSchemas();
541
+ }
542
+ /**
543
+ * Replace placeholders in SQL with actual values
544
+ * @param sql - SQL string with placeholders
545
+ * @param replacements - Object or array of replacement values
546
+ */
547
+ replaceReplacements(sql, replacements) {
548
+ if (!replacements) {
549
+ return sql;
550
+ }
551
+ if (Array.isArray(replacements)) {
552
+ // Array-based replacements: $1, $2, etc.
553
+ let result = sql;
554
+ replacements.forEach((value, index) => {
555
+ const placeholder = `$${index + 1}`;
556
+ if (value === null) {
557
+ result = result.replace(new RegExp(placeholder.replace('$', '\\$'), 'g'), 'NULL');
558
+ }
559
+ else if (typeof value === 'string') {
560
+ result = result.replace(new RegExp(placeholder.replace('$', '\\$'), 'g'), `'${value.replace(/'/g, "''")}'`);
561
+ }
562
+ else if (value instanceof Date) {
563
+ result = result.replace(new RegExp(placeholder.replace('$', '\\$'), 'g'), `'${value.toISOString()}'`);
564
+ }
565
+ else {
566
+ result = result.replace(new RegExp(placeholder.replace('$', '\\$'), 'g'), String(value));
567
+ }
568
+ });
569
+ return result;
570
+ }
571
+ else {
572
+ // Object-based replacements: :name, :value, etc.
573
+ let result = sql;
574
+ for (const [key, value] of Object.entries(replacements)) {
575
+ const placeholder = `:${key}`;
576
+ const regex = new RegExp(placeholder.replace(/:/g, '\\:'), 'g');
577
+ if (value === null) {
578
+ result = result.replace(regex, 'NULL');
579
+ }
580
+ else if (typeof value === 'string') {
581
+ result = result.replace(regex, `'${value.replace(/'/g, "''")}'`);
582
+ }
583
+ else if (value instanceof Date) {
584
+ result = result.replace(regex, `'${value.toISOString()}'`);
585
+ }
586
+ else {
587
+ result = result.replace(regex, String(value));
588
+ }
589
+ }
590
+ return result;
591
+ }
592
+ }
593
+ /**
594
+ * Create a new table
595
+ */
596
+ async createTable(tableName, columns, options) {
597
+ const columnDefs = [];
598
+ for (const [columnName, definition] of Object.entries(columns)) {
599
+ columnDefs.push(this.getColumnDefinitionSql(columnName, definition));
600
+ }
601
+ // Add constraints from options
602
+ if (options?.constraints) {
603
+ for (const constraint of options.constraints) {
604
+ const constraintSql = this.buildConstraintSql(constraint);
605
+ if (constraintSql) {
606
+ columnDefs.push(constraintSql);
607
+ }
608
+ }
609
+ }
610
+ // Column families (CockroachDB storage-layout hint): `FAMILY name (col1, col2)`.
611
+ // CockroachDB groups columns into families to control how they're
612
+ // co-located in the underlying key-value store; explicit families let
613
+ // callers separate hot/cold columns for wide tables.
614
+ // See: https://www.cockroachlabs.com/docs/stable/column-families
615
+ if (options?.families && options.families.length > 0) {
616
+ for (const family of options.families) {
617
+ if (!family.columns || family.columns.length === 0) {
618
+ throw new Error('CockroachDB column family must specify at least one column.');
619
+ }
620
+ const namePart = family.name ? `${this.escapeId(family.name)} ` : '';
621
+ const cols = family.columns.map((c) => this.escapeId(c)).join(', ');
622
+ columnDefs.push(`FAMILY ${namePart}(${cols})`);
623
+ }
624
+ }
625
+ let sql = 'CREATE TABLE';
626
+ if (options?.ifNotExists) {
627
+ sql += ' IF NOT EXISTS';
628
+ }
629
+ sql += ` ${this.escapeId(tableName)} (${columnDefs.join(', ')})`;
630
+ // Handle CockroachDB-specific options
631
+ // Tablespace (CockroachDB-specific)
632
+ if (options?.tablespace) {
633
+ sql += ` TABLESPACE ${options.tablespace}`;
634
+ }
635
+ // rowFormat is not supported in CockroachDB
636
+ if (options?.rowFormat) {
637
+ console.warn(`CockroachDB: rowFormat option '${options.rowFormat}' is not supported and will be ignored`);
638
+ }
639
+ // engine and charset are not supported in CockroachDB
640
+ if (options?.engine) {
641
+ console.warn(`CockroachDB: engine option '${options.engine}' is not supported and will be ignored`);
642
+ }
643
+ if (options?.charset) {
644
+ console.warn(`CockroachDB: charset option '${options.charset}' is not supported and will be ignored`);
645
+ }
646
+ // Inherit from parent table (CockroachDB-specific)
647
+ if (options?.inherit) {
648
+ sql += ` INHERITS (${this.escapeId(options.inherit)})`;
649
+ }
650
+ // Partition by clause (CockroachDB)
651
+ if (options?.partitionBy) {
652
+ sql += ` PARTITION BY ${options.partitionBy}`;
653
+ }
654
+ // Multi-region table locality (CockroachDB): `LOCALITY GLOBAL` /
655
+ // `LOCALITY REGIONAL BY TABLE [IN region]` / `LOCALITY REGIONAL BY ROW [AS column]`.
656
+ // Only valid against a multi-region database (one with a primary region set).
657
+ // See: https://www.cockroachlabs.com/docs/stable/table-localities
658
+ if (options?.locality) {
659
+ sql += ` ${this.buildLocalityClause(options.locality)}`;
660
+ }
661
+ // Handle charset and collate (limited support in CockroachDB - affects default for new columns)
662
+ if (options?.collate) {
663
+ sql += ` COLLATE ${options.collate}`;
664
+ }
665
+ await this.query(sql);
666
+ // Handle uniqueKeys (CockroachDB supports unique constraints)
667
+ // Support both old format (Record<string, string[]>) and new format (UniqueKeyOptions[])
668
+ if (options?.uniqueKeys) {
669
+ // Check if it's the new array format
670
+ if (Array.isArray(options.uniqueKeys)) {
671
+ for (const uk of options.uniqueKeys) {
672
+ const constraintName = uk.name || `${tableName}_${uk.fields.join('_')}_key`;
673
+ const constraintSql = `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} UNIQUE (${uk.fields.map((f) => this.escapeId(f)).join(', ')})`;
674
+ await this.query(constraintSql);
675
+ }
676
+ }
677
+ else {
678
+ // Legacy object format
679
+ for (const [keyName, fields] of Object.entries(options.uniqueKeys)) {
680
+ const constraintName = keyName || `${tableName}_${fields.join('_')}_key`;
681
+ const constraintSql = `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} UNIQUE (${fields.map((f) => this.escapeId(f)).join(', ')})`;
682
+ await this.query(constraintSql);
683
+ }
684
+ }
685
+ }
686
+ // Add table comment (CockroachDB)
687
+ if (options?.comment) {
688
+ await this.query(`COMMENT ON TABLE ${this.escapeId(tableName)} IS '${this.escapeString(options.comment)}'`);
689
+ }
690
+ // Handle initialAutoIncrement for CockroachDB - set sequence start value
691
+ if (options?.initialAutoIncrement) {
692
+ // Find auto-increment column and set its sequence
693
+ for (const [columnName, definition] of Object.entries(columns)) {
694
+ if (definition.autoIncrement) {
695
+ const sequenceName = `${tableName}_${columnName}_seq`;
696
+ await this.query(`ALTER SEQUENCE ${this.escapeId(sequenceName)} START WITH ${options.initialAutoIncrement}`);
697
+ }
698
+ }
699
+ }
700
+ // Create indexes after table creation
701
+ if (options?.indexes) {
702
+ for (const index of options.indexes) {
703
+ await this.addIndex(tableName, index.name || `idx_${tableName}_${index.fields.join('_')}`, index.fields, {
704
+ unique: index.unique,
705
+ type: index.type,
706
+ using: index.using,
707
+ where: index.where,
708
+ expression: index.expression,
709
+ include: index.include,
710
+ });
711
+ }
712
+ }
713
+ }
714
+ /**
715
+ * Build constraint SQL for table creation
716
+ */
717
+ buildConstraintSql(constraint) {
718
+ const name = constraint.name ? `"${constraint.name}"` : '';
719
+ const fields = constraint.fields?.map((f) => this.escapeId(f)).join(', ') || '';
720
+ switch (constraint.type) {
721
+ case 'PRIMARY KEY':
722
+ return `${name ? name + ' ' : ''}PRIMARY KEY (${fields})`;
723
+ case 'UNIQUE':
724
+ return `${name ? name + ' ' : ''}UNIQUE (${fields})`;
725
+ case 'FOREIGN KEY':
726
+ if (!constraint.references)
727
+ return null;
728
+ // Handle composite foreign keys (string | string[])
729
+ const refField = constraint.references.field;
730
+ const refFieldSql = Array.isArray(refField)
731
+ ? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
732
+ : `(${this.escapeId(refField)})`;
733
+ let fkSql = `${name ? name + ' ' : ''}FOREIGN KEY (${fields}) `;
734
+ fkSql += `REFERENCES ${this.escapeId(constraint.references.table)}${refFieldSql}`;
735
+ if (constraint.references.onDelete) {
736
+ fkSql += ` ON DELETE ${constraint.references.onDelete}`;
737
+ }
738
+ if (constraint.references.onUpdate) {
739
+ fkSql += ` ON UPDATE ${constraint.references.onUpdate}`;
740
+ }
741
+ return fkSql;
742
+ case 'CHECK':
743
+ if (!constraint.check)
744
+ return null;
745
+ return `${name ? name + ' ' : ''}CHECK (${constraint.check})`;
746
+ default:
747
+ return null;
748
+ }
749
+ }
750
+ /**
751
+ * Generate column definition SQL for CockroachDB
752
+ */
753
+ getColumnDefinitionSql(columnName, definition) {
754
+ // Check if this is an auto-increment column and use SERIAL/BIGSERIAL
755
+ const dt = definition.type;
756
+ if (definition.autoIncrement && (dt.key === 'INTEGER' || !dt.key)) {
757
+ // For auto-increment INTEGER columns, use SERIAL
758
+ let sql = `${this.escapeId(columnName)} SERIAL`;
759
+ if (definition.primaryKey) {
760
+ sql += ' PRIMARY KEY';
761
+ }
762
+ if (definition.allowNull === false) {
763
+ sql += ' NOT NULL';
764
+ }
765
+ if (definition.unique) {
766
+ sql += ' UNIQUE';
767
+ }
768
+ if (definition.references) {
769
+ // Handle composite foreign keys (string | string[])
770
+ const refField3 = definition.references.field;
771
+ const refFieldSql3 = Array.isArray(refField3)
772
+ ? `(${refField3.map((f) => this.escapeId(f)).join(', ')})`
773
+ : this.escapeId(refField3);
774
+ sql += ` REFERENCES ${this.escapeId(definition.references.table)}(${refFieldSql3})`;
775
+ if (definition.references.onDelete) {
776
+ sql += ` ON DELETE ${definition.references.onDelete}`;
777
+ }
778
+ if (definition.references.onUpdate) {
779
+ sql += ` ON UPDATE ${definition.references.onUpdate}`;
780
+ }
781
+ }
782
+ return sql;
783
+ }
784
+ let sql = `${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type)}`;
785
+ if (definition.allowNull === false) {
786
+ sql += ' NOT NULL';
787
+ }
788
+ if (definition.defaultValue !== undefined) {
789
+ sql += ` DEFAULT ${this.getDefaultValue(definition.defaultValue)}`;
790
+ }
791
+ if (definition.primaryKey) {
792
+ sql += ' PRIMARY KEY';
793
+ }
794
+ if (definition.unique) {
795
+ if (typeof definition.unique === 'string') {
796
+ sql += ` UNIQUE (${this.escapeId(definition.unique)})`;
797
+ }
798
+ else {
799
+ sql += ' UNIQUE';
800
+ }
801
+ }
802
+ if (definition.references) {
803
+ // Handle composite foreign keys (string | string[])
804
+ const refField2 = definition.references.field;
805
+ const refFieldSql2 = Array.isArray(refField2)
806
+ ? `(${refField2.map((f) => this.escapeId(f)).join(', ')})`
807
+ : this.escapeId(refField2);
808
+ sql += ` REFERENCES ${this.escapeId(definition.references.table)}(${refFieldSql2})`;
809
+ if (definition.references.onDelete) {
810
+ sql += ` ON DELETE ${definition.references.onDelete}`;
811
+ }
812
+ if (definition.references.onUpdate) {
813
+ sql += ` ON UPDATE ${definition.references.onUpdate}`;
814
+ }
815
+ }
816
+ if (definition.comment) {
817
+ sql += ` COMMENT '${this.escapeString(definition.comment)}'`;
818
+ }
819
+ return sql;
820
+ }
821
+ /**
822
+ * Get default value SQL
823
+ */
824
+ getDefaultValue(value) {
825
+ if (value === null) {
826
+ return 'NULL';
827
+ }
828
+ if (typeof value === 'string') {
829
+ // Check for CockroachDB functions
830
+ if (value.toUpperCase().includes('CURRENT_') || value.toUpperCase() === 'NULL') {
831
+ return value;
832
+ }
833
+ return `'${this.escapeString(value)}'`;
834
+ }
835
+ if (typeof value === 'number' || typeof value === 'boolean') {
836
+ return String(value);
837
+ }
838
+ if (value instanceof Date) {
839
+ return `'${this.formatDate(value)}'`;
840
+ }
841
+ // Handle DataType instances with toDefaultValue method (e.g., DataTypes.NOW)
842
+ if (typeof value === 'object' &&
843
+ value !== null &&
844
+ typeof value.toDefaultValue === 'function') {
845
+ return value.toDefaultValue();
846
+ }
847
+ return `'${this.escapeString(String(value))}'`;
848
+ }
849
+ /**
850
+ * Drop a table
851
+ */
852
+ async dropTable(tableName, options) {
853
+ let sql = 'DROP TABLE';
854
+ if (options?.ifExists) {
855
+ sql += ' IF EXISTS';
856
+ }
857
+ sql += ` ${this.escapeId(tableName)}`;
858
+ if (options?.cascade) {
859
+ sql += ' CASCADE';
860
+ }
861
+ await this.query(sql);
862
+ }
863
+ /**
864
+ * Create a partitioned table (CockroachDB)
865
+ *
866
+ * @param tableName - Name of the table to create
867
+ * @param columns - Column definitions
868
+ * @param options - Table options including partition configuration
869
+ */
870
+ async createPartitionedTable(tableName, columns, options) {
871
+ const columnDefs = [];
872
+ for (const [columnName, definition] of Object.entries(columns)) {
873
+ columnDefs.push(this.getColumnDefinitionSql(columnName, definition));
874
+ }
875
+ // Add constraints from options
876
+ if (options?.constraints) {
877
+ for (const constraint of options.constraints) {
878
+ const constraintSql = this.buildConstraintSql(constraint);
879
+ if (constraintSql) {
880
+ columnDefs.push(constraintSql);
881
+ }
882
+ }
883
+ }
884
+ let sql = 'CREATE TABLE';
885
+ if (options?.ifNotExists) {
886
+ sql += ' IF NOT EXISTS';
887
+ }
888
+ sql += ` ${this.escapeId(tableName)} (${columnDefs.join(', ')})`;
889
+ // Partition by clause (CockroachDB)
890
+ if (options?.partitionBy) {
891
+ const columns = Array.isArray(options.partitionBy.column)
892
+ ? options.partitionBy.column.join(', ')
893
+ : options.partitionBy.column;
894
+ sql += ` PARTITION BY ${options.partitionBy.type.toUpperCase()} (${columns})`;
895
+ }
896
+ await this.query(sql);
897
+ // Create initial partitions if specified
898
+ if (options?.partitions && options.partitions.length > 0) {
899
+ for (const partition of options.partitions) {
900
+ await this.createPartition({
901
+ parentTable: tableName,
902
+ name: partition.name,
903
+ bound: partition.bound,
904
+ tablespace: partition.tablespace,
905
+ storageParameters: partition.storageParameters,
906
+ });
907
+ }
908
+ }
909
+ }
910
+ /**
911
+ * Create a partition for an existing partitioned table (CockroachDB)
912
+ *
913
+ * @param options - Partition creation options
914
+ */
915
+ async createPartition(options) {
916
+ let sql = `CREATE TABLE ${this.escapeId(options.name)} PARTITION OF ${this.escapeId(options.parentTable)}`;
917
+ if (options.bound) {
918
+ const bound = options.bound;
919
+ // Check for list partition
920
+ if ('values' in bound && bound.values && Array.isArray(bound.values)) {
921
+ const values = bound.values.map((v) => (typeof v === 'string' ? `'${v}'` : v)).join(', ');
922
+ sql += ` FOR VALUES IN (${values})`;
923
+ }
924
+ // Check for hash partition
925
+ else if ('modulus' in bound && bound.modulus !== undefined) {
926
+ const hashBound = bound;
927
+ sql += ` FOR VALUES WITH (MODULUS ${hashBound.modulus}, REMAINDER ${hashBound.remainder})`;
928
+ }
929
+ // Default to range partition
930
+ else {
931
+ const rangeBound = bound;
932
+ const from = rangeBound.from;
933
+ const to = rangeBound.to || 'MAXVALUE';
934
+ const fromStr = from instanceof Date ? from.toISOString().split('T')[0] : from;
935
+ const toStr = to instanceof Date ? to.toISOString().split('T')[0] : to;
936
+ sql += ` FOR VALUES FROM (${fromStr}) TO (${toStr})`;
937
+ }
938
+ }
939
+ if (options.tablespace) {
940
+ sql += ` TABLESPACE ${options.tablespace}`;
941
+ }
942
+ if (options.storageParameters) {
943
+ const params = Object.entries(options.storageParameters)
944
+ .map(([key, value]) => `${key} = ${value}`)
945
+ .join(', ');
946
+ sql += ` WITH (${params})`;
947
+ }
948
+ await this.query(sql);
949
+ }
950
+ /**
951
+ * Attach a partition to a partitioned table (CockroachDB)
952
+ *
953
+ * @param options - Partition attachment options
954
+ */
955
+ async attachPartition(options) {
956
+ const sql = `ALTER TABLE ${this.escapeId(options.parentTable)} ATTACH PARTITION ${this.escapeId(options.partitionName)}`;
957
+ await this.query(sql);
958
+ }
959
+ /**
960
+ * Detach a partition from a partitioned table (CockroachDB)
961
+ *
962
+ * @param options - Partition detachment options
963
+ */
964
+ async detachPartition(options) {
965
+ let sql = `ALTER TABLE ${this.escapeId(options.partitionName)} DETACH PARTITION`;
966
+ if (options.validate === false) {
967
+ sql += ' NOT VALIDATE';
968
+ }
969
+ await this.query(sql);
970
+ }
971
+ /**
972
+ * Drop a partition (CockroachDB)
973
+ *
974
+ * @param partitionName - Name of the partition to drop
975
+ * @param options - Drop options
976
+ */
977
+ async dropPartition(partitionName, options) {
978
+ let sql = 'DROP TABLE';
979
+ if (options?.ifExists) {
980
+ sql += ' IF EXISTS';
981
+ }
982
+ sql += ` ${this.escapeId(partitionName)}`;
983
+ if (options?.cascade) {
984
+ sql += ' CASCADE';
985
+ }
986
+ await this.query(sql);
987
+ }
988
+ /**
989
+ * Add a partition to an existing partitioned table (CockroachDB)
990
+ * @param tableName - Name of the partitioned table
991
+ * @param partitionName - Name for the new partition
992
+ * @param partitionSpec - Partition specification
993
+ */
994
+ async addPartition(tableName, partitionName, partitionSpec) {
995
+ let sql = `ALTER TABLE ${this.escapeId(tableName)} ATTACH PARTITION ${this.escapeId(partitionName)}`;
996
+ if (partitionSpec.values) {
997
+ sql += ` FOR VALUES ${partitionSpec.values}`;
998
+ }
999
+ else if (partitionSpec.forValues) {
1000
+ sql += ` FOR VALUES ${partitionSpec.forValues}`;
1001
+ }
1002
+ await this.query(sql);
1003
+ }
1004
+ // ==================== Multi-region (LOCALITY) ====================
1005
+ /**
1006
+ * Build the `LOCALITY ...` clause fragment (without a leading space, but
1007
+ * including the `LOCALITY` keyword itself) for a given locality setting.
1008
+ * Shared by `createTable` and `setTableLocality`.
1009
+ */
1010
+ buildLocalityClause(locality) {
1011
+ switch (locality.type) {
1012
+ case 'global':
1013
+ return 'LOCALITY GLOBAL';
1014
+ case 'regional-by-table':
1015
+ return locality.region
1016
+ ? `LOCALITY REGIONAL BY TABLE IN ${this.escapeId(locality.region)}`
1017
+ : 'LOCALITY REGIONAL BY TABLE';
1018
+ case 'regional-by-row':
1019
+ return locality.column
1020
+ ? `LOCALITY REGIONAL BY ROW AS ${this.escapeId(locality.column)}`
1021
+ : 'LOCALITY REGIONAL BY ROW';
1022
+ default:
1023
+ throw new Error(`Unknown CockroachDB locality type: ${locality.type}`);
1024
+ }
1025
+ }
1026
+ /**
1027
+ * Change the multi-region locality of an existing table
1028
+ * (`ALTER TABLE ... SET LOCALITY ...`).
1029
+ *
1030
+ * Requires the database to already be multi-region (see
1031
+ * `setPrimaryRegion`/`addRegion`).
1032
+ *
1033
+ * @see https://www.cockroachlabs.com/docs/stable/table-localities
1034
+ */
1035
+ async setTableLocality(tableName, locality) {
1036
+ const sql = `ALTER TABLE ${this.escapeId(tableName)} SET ${this.buildLocalityClause(locality)}`;
1037
+ await this.query(sql);
1038
+ }
1039
+ /**
1040
+ * Add a region to a multi-region database (`ALTER DATABASE ... ADD REGION ...`).
1041
+ *
1042
+ * @param region - Region name (e.g. `'us-east1'`)
1043
+ * @param options - `database` to target a database other than the current one
1044
+ */
1045
+ async addRegion(region, options) {
1046
+ const dbPart = options?.database ? `${this.escapeId(options.database)} ` : '';
1047
+ const ifNotExists = options?.ifNotExists ? 'IF NOT EXISTS ' : '';
1048
+ const sql = `ALTER DATABASE ${dbPart}ADD REGION ${ifNotExists}${this.escapeId(region)}`;
1049
+ await this.query(sql);
1050
+ }
1051
+ /**
1052
+ * Remove a region from a multi-region database (`ALTER DATABASE ... DROP REGION ...`).
1053
+ */
1054
+ async dropRegion(region, options) {
1055
+ const dbPart = options?.database ? `${this.escapeId(options.database)} ` : '';
1056
+ const sql = `ALTER DATABASE ${dbPart}DROP REGION ${this.escapeId(region)}`;
1057
+ await this.query(sql);
1058
+ }
1059
+ /**
1060
+ * Set (or change) the primary region of a database
1061
+ * (`ALTER DATABASE ... {SET|PRIMARY REGION} ...`). Setting a primary
1062
+ * region for the first time promotes the database to a multi-region
1063
+ * database; CockroachDB automatically adds the region as a side effect if
1064
+ * it hasn't been added yet.
1065
+ *
1066
+ * @see https://www.cockroachlabs.com/docs/stable/multiregion-overview
1067
+ */
1068
+ async setPrimaryRegion(region, options) {
1069
+ const dbPart = options?.database ? `${this.escapeId(options.database)} ` : '';
1070
+ const sql = `ALTER DATABASE ${dbPart}SET PRIMARY REGION ${this.escapeId(region)}`;
1071
+ await this.query(sql);
1072
+ }
1073
+ /**
1074
+ * Set the survival goal of a multi-region database
1075
+ * (`ALTER DATABASE ... SURVIVE {ZONE|REGION} FAILURE`).
1076
+ */
1077
+ async setSurvivalGoal(goal, options) {
1078
+ const dbPart = options?.database ? `${this.escapeId(options.database)} ` : '';
1079
+ const failureClause = goal === 'zone' ? 'ZONE FAILURE' : 'REGION FAILURE';
1080
+ const sql = `ALTER DATABASE ${dbPart}SURVIVE ${failureClause}`;
1081
+ await this.query(sql);
1082
+ }
1083
+ // ==================== Zone configuration ====================
1084
+ /**
1085
+ * Resolve a `CockroachZoneTarget` to the SQL fragment that follows
1086
+ * `ALTER ...` in `CONFIGURE ZONE`/`SPLIT AT`/`UNSPLIT AT` statements,
1087
+ * e.g. `TABLE "orders"`, `INDEX "orders"@"idx_name"`, or
1088
+ * `PARTITION "p1" OF TABLE "orders"`.
1089
+ */
1090
+ resolveZoneTarget(target) {
1091
+ switch (target.kind) {
1092
+ case 'table':
1093
+ return `TABLE ${this.escapeId(target.name)}`;
1094
+ case 'index':
1095
+ return `INDEX ${this.escapeId(target.table)}@${this.escapeId(target.index)}`;
1096
+ case 'partition': {
1097
+ const ofClause = target.index
1098
+ ? `INDEX ${this.escapeId(target.table)}@${this.escapeId(target.index)}`
1099
+ : `TABLE ${this.escapeId(target.table)}`;
1100
+ return `PARTITION ${this.escapeId(target.name)} OF ${ofClause}`;
1101
+ }
1102
+ case 'database':
1103
+ return `DATABASE ${this.escapeId(target.name)}`;
1104
+ default:
1105
+ throw new Error(`Unknown CockroachDB zone target kind: ${target.kind}`);
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Configure replication/placement settings for a table, index, partition,
1110
+ * or database (`ALTER ... CONFIGURE ZONE USING ...`).
1111
+ *
1112
+ * Exposes the subset of zone-configuration variables that are reasonable
1113
+ * for an ORM to surface: replica count, placement constraints, lease
1114
+ * preferences, GC TTL, and range size bounds. For anything more advanced,
1115
+ * callers can fall back to raw `query()`.
1116
+ *
1117
+ * @see https://www.cockroachlabs.com/docs/stable/configure-zone
1118
+ */
1119
+ async configureZone(target, options) {
1120
+ const assignments = [];
1121
+ if (options.numReplicas !== undefined) {
1122
+ assignments.push(`num_replicas = ${options.numReplicas}`);
1123
+ }
1124
+ if (options.numVoters !== undefined) {
1125
+ assignments.push(`num_voters = ${options.numVoters}`);
1126
+ }
1127
+ if (options.constraints !== undefined) {
1128
+ assignments.push(`constraints = '${this.escapeString(options.constraints)}'`);
1129
+ }
1130
+ if (options.leasePreferences !== undefined) {
1131
+ const prefs = `[${options.leasePreferences.map((p) => `'${this.escapeString(p)}'`).join(', ')}]`;
1132
+ assignments.push(`lease_preferences = ${prefs}`);
1133
+ }
1134
+ if (options.gcTtlSeconds !== undefined) {
1135
+ assignments.push(`gc.ttlseconds = ${options.gcTtlSeconds}`);
1136
+ }
1137
+ if (options.rangeMinBytes !== undefined) {
1138
+ assignments.push(`range_min_bytes = ${options.rangeMinBytes}`);
1139
+ }
1140
+ if (options.rangeMaxBytes !== undefined) {
1141
+ assignments.push(`range_max_bytes = ${options.rangeMaxBytes}`);
1142
+ }
1143
+ if (assignments.length === 0) {
1144
+ throw new Error('configureZone() requires at least one zone-configuration option.');
1145
+ }
1146
+ const sql = `ALTER ${this.resolveZoneTarget(target)} CONFIGURE ZONE USING ${assignments.join(', ')}`;
1147
+ await this.query(sql);
1148
+ }
1149
+ /**
1150
+ * Reset a zone configuration back to its inherited default
1151
+ * (`ALTER ... CONFIGURE ZONE DISCARD`).
1152
+ */
1153
+ async resetZoneConfig(target) {
1154
+ const sql = `ALTER ${this.resolveZoneTarget(target)} CONFIGURE ZONE DISCARD`;
1155
+ await this.query(sql);
1156
+ }
1157
+ // ==================== Manual range splitting ====================
1158
+ /**
1159
+ * Format a single split/unsplit key value for use inside a `VALUES (...)`
1160
+ * tuple: quote strings, pass numbers/booleans through, and stringify
1161
+ * `Date`s as ISO timestamps.
1162
+ */
1163
+ formatSplitValue(value) {
1164
+ if (value === null || value === undefined) {
1165
+ return 'NULL';
1166
+ }
1167
+ if (value instanceof Date) {
1168
+ return `'${value.toISOString()}'`;
1169
+ }
1170
+ if (typeof value === 'number' || typeof value === 'boolean') {
1171
+ return String(value);
1172
+ }
1173
+ return `'${this.escapeString(String(value))}'`;
1174
+ }
1175
+ /**
1176
+ * Manually split a range at the given key value(s)
1177
+ * (`ALTER TABLE/INDEX ... SPLIT AT VALUES (...)`), to pre-emptively
1178
+ * distribute a hot/monotonically-growing key range across nodes before
1179
+ * CockroachDB's automatic range-size-based splitting would kick in.
1180
+ *
1181
+ * `values` is an array of row tuples matching the index prefix; pass
1182
+ * multiple tuples to create multiple split points in one call.
1183
+ *
1184
+ * @see https://www.cockroachlabs.com/docs/stable/alter-table#split-at
1185
+ */
1186
+ async splitAt(target, values, options) {
1187
+ if (!values || values.length === 0) {
1188
+ throw new Error('splitAt() requires at least one value tuple.');
1189
+ }
1190
+ const tuples = values
1191
+ .map((tuple) => `(${tuple.map((v) => this.formatSplitValue(v)).join(', ')})`)
1192
+ .join(', ');
1193
+ let sql = `ALTER ${this.resolveZoneTarget(target)} SPLIT AT VALUES ${tuples}`;
1194
+ if (options?.expiration) {
1195
+ sql += ` WITH EXPIRATION ${this.formatSplitValue(options.expiration)}`;
1196
+ }
1197
+ await this.query(sql);
1198
+ }
1199
+ /**
1200
+ * Undo a manual (or expired) range split
1201
+ * (`ALTER TABLE/INDEX ... UNSPLIT AT VALUES (...)`), or unsplit every
1202
+ * manually-created split point on the target when `values` is omitted
1203
+ * (`UNSPLIT ALL`).
1204
+ *
1205
+ * @see https://www.cockroachlabs.com/docs/stable/alter-table#unsplit-at
1206
+ */
1207
+ async unsplitAt(target, values) {
1208
+ if (!values || values.length === 0) {
1209
+ const sql = `ALTER ${this.resolveZoneTarget(target)} UNSPLIT ALL`;
1210
+ await this.query(sql);
1211
+ return;
1212
+ }
1213
+ const tuples = values
1214
+ .map((tuple) => `(${tuple.map((v) => this.formatSplitValue(v)).join(', ')})`)
1215
+ .join(', ');
1216
+ const sql = `ALTER ${this.resolveZoneTarget(target)} UNSPLIT AT VALUES ${tuples}`;
1217
+ await this.query(sql);
1218
+ }
1219
+ /**
1220
+ * Create a database view
1221
+ */
1222
+ async createView(viewName, query, options) {
1223
+ const viewNameWithSchema = options?.schema
1224
+ ? `${this.escapeId(options.schema)}.${this.escapeId(viewName)}`
1225
+ : this.escapeId(viewName);
1226
+ let sql = 'CREATE VIEW';
1227
+ if (options?.replace) {
1228
+ sql = 'CREATE OR REPLACE VIEW';
1229
+ }
1230
+ sql += ` ${viewNameWithSchema} AS ${query}`;
1231
+ await this.query(sql);
1232
+ if (options?.comment) {
1233
+ const commentSql = `COMMENT ON VIEW ${viewNameWithSchema} IS ${this.escape(options.comment)}`;
1234
+ await this.query(commentSql);
1235
+ }
1236
+ }
1237
+ /**
1238
+ * Drop a database view
1239
+ */
1240
+ async dropView(viewName, options) {
1241
+ let sql = 'DROP VIEW';
1242
+ if (options?.ifExists) {
1243
+ sql += ' IF EXISTS';
1244
+ }
1245
+ sql += ` ${this.escapeId(viewName)}`;
1246
+ if (options?.cascade) {
1247
+ sql += ' CASCADE';
1248
+ }
1249
+ await this.query(sql);
1250
+ }
1251
+ /**
1252
+ * Show all views in the database
1253
+ */
1254
+ async showViews() {
1255
+ const result = await this.query(`SELECT table_name FROM information_schema.views WHERE table_schema = 'public' ORDER BY table_name`, { raw: true });
1256
+ return result.rows?.map((row) => row.table_name) || [];
1257
+ }
1258
+ /**
1259
+ * Create a materialized view
1260
+ * @param options - Materialized view options
1261
+ */
1262
+ async createMaterializedView(options) {
1263
+ const viewNameWithSchema = options.schema
1264
+ ? `${this.escapeId(options.schema)}.${this.escapeId(options.name)}`
1265
+ : this.escapeId(options.name);
1266
+ let sql = 'CREATE MATERIALIZED VIEW';
1267
+ if (options.replace) {
1268
+ sql = 'CREATE OR REPLACE MATERIALIZED VIEW';
1269
+ }
1270
+ else if (options.ifNotExists) {
1271
+ sql = 'CREATE MATERIALIZED VIEW IF NOT EXISTS';
1272
+ }
1273
+ sql += ` ${viewNameWithSchema} AS ${options.query}`;
1274
+ if (!options.withData) {
1275
+ sql += ' WITH NO DATA';
1276
+ }
1277
+ await this.query(sql);
1278
+ // Create unique index if provided (required for CONCURRENTLY refresh)
1279
+ if (options.uniqueIndex) {
1280
+ const indexSql = `CREATE UNIQUE INDEX ${this.escapeId(options.uniqueIndex)} ON ${viewNameWithSchema} (${options.uniqueIndex})`;
1281
+ await this.query(indexSql);
1282
+ }
1283
+ if (options.comment) {
1284
+ const commentSql = `COMMENT ON MATERIALIZED VIEW ${viewNameWithSchema} IS ${this.escape(options.comment)}`;
1285
+ await this.query(commentSql);
1286
+ }
1287
+ }
1288
+ /**
1289
+ * Refresh a materialized view
1290
+ * @param viewName - Name of the materialized view to refresh
1291
+ * @param options - Refresh options
1292
+ */
1293
+ async refreshMaterializedView(viewName, options) {
1294
+ let sql = 'REFRESH MATERIALIZED VIEW';
1295
+ if (options?.concurrently) {
1296
+ sql += ' CONCURRENTLY';
1297
+ }
1298
+ sql += ` ${this.escapeId(viewName)}`;
1299
+ if (options?.withNoData) {
1300
+ sql += ' WITH NO DATA';
1301
+ }
1302
+ await this.query(sql);
1303
+ }
1304
+ /**
1305
+ * Drop a materialized view
1306
+ * @param viewName - Name of the materialized view to drop
1307
+ * @param options - Drop options
1308
+ */
1309
+ async dropMaterializedView(viewName, options) {
1310
+ let sql = 'DROP MATERIALIZED VIEW';
1311
+ if (options?.ifExists) {
1312
+ sql += ' IF EXISTS';
1313
+ }
1314
+ sql += ` ${this.escapeId(viewName)}`;
1315
+ if (options?.cascade) {
1316
+ sql += ' CASCADE';
1317
+ }
1318
+ await this.query(sql);
1319
+ }
1320
+ /**
1321
+ * Check if a materialized view exists
1322
+ *
1323
+ * Queries `pg_class`/`pg_namespace` directly (filtering on
1324
+ * `relkind = 'm'`) rather than the derived `pg_matviews` view. CockroachDB's
1325
+ * `pg_catalog` compatibility layer is partial, and `pg_matviews` — itself
1326
+ * normally just a view defined on top of `pg_class` in real Postgres — is
1327
+ * not guaranteed to exist/be populated the same way on CockroachDB.
1328
+ * `pg_class` is the more fundamental catalog and a safer bet for accurate
1329
+ * results (an absent/empty `pg_matviews` would otherwise silently report a
1330
+ * false "materialized view doesn't exist").
1331
+ * @param viewName - Name of the materialized view
1332
+ * @returns True if the materialized view exists
1333
+ */
1334
+ async hasMaterializedView(viewName) {
1335
+ const result = await this.query(`SELECT 1 FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid ` +
1336
+ `WHERE c.relkind = 'm' AND c.relname = ${this.escape(viewName)} AND n.nspname = 'public'`, { raw: true });
1337
+ return result.rows?.length > 0;
1338
+ }
1339
+ /**
1340
+ * Show all materialized views in the database
1341
+ *
1342
+ * See `hasMaterializedView` for why this queries `pg_class`/`pg_namespace`
1343
+ * (`relkind = 'm'`) instead of the `pg_matviews` compatibility view.
1344
+ */
1345
+ async showMaterializedViews() {
1346
+ const result = await this.query(`SELECT c.relname AS matviewname FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid ` +
1347
+ `WHERE c.relkind = 'm' AND n.nspname = 'public' ORDER BY c.relname`, { raw: true });
1348
+ return result.rows?.map((row) => row.matviewname) || [];
1349
+ }
1350
+ // ==================== Stored Procedures ====================
1351
+ /**
1352
+ * Create a stored procedure (CockroachDB)
1353
+ */
1354
+ async createStoredProcedure(options) {
1355
+ const schema = options.schema || 'public';
1356
+ const procName = `${this.escapeId(schema)}.${this.escapeId(options.name)}`;
1357
+ let sql = 'CREATE PROCEDURE';
1358
+ if (options.replace) {
1359
+ sql = 'CREATE OR REPLACE PROCEDURE';
1360
+ }
1361
+ else if (options.ifNotExists) {
1362
+ sql = 'CREATE PROCEDURE IF NOT EXISTS';
1363
+ }
1364
+ // Add parameters
1365
+ if (options.params && options.params.length > 0) {
1366
+ const params = options.params
1367
+ .map((p) => {
1368
+ const mode = p.mode ? `${p.mode} ` : '';
1369
+ const defaultPart = p.defaultValue !== undefined ? ` DEFAULT ${this.escape(p.defaultValue)}` : '';
1370
+ return `${mode}${this.escapeId(p.name)} ${p.type}${defaultPart}`;
1371
+ })
1372
+ .join(', ');
1373
+ sql += ` ${procName} (${params})`;
1374
+ }
1375
+ else {
1376
+ sql += ` ${procName}`;
1377
+ }
1378
+ sql += ` LANGUAGE plpgsql\n${options.body}`;
1379
+ await this.query(sql);
1380
+ }
1381
+ /**
1382
+ * Create a foreign data wrapper — not supported in CockroachDB.
1383
+ * @param _fdwName - Foreign data wrapper name (unused; kept for interface parity)
1384
+ * @param _options - Options (unused; kept for interface parity)
1385
+ */
1386
+ async createForeignDataWrapper(_fdwName, _options) {
1387
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
1388
+ }
1389
+ async dropForeignDataWrapper(_fdwName, _options) {
1390
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
1391
+ }
1392
+ async createForeignServer(_serverName, _fdwName, _options) {
1393
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
1394
+ }
1395
+ async dropForeignServer(_serverName, _options) {
1396
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
1397
+ }
1398
+ async createForeignTable(_tableName, _columns, _options) {
1399
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
1400
+ }
1401
+ async changeOwner(newOwner, tableName) {
1402
+ const sql = `ALTER TABLE ${this.escapeId(tableName)} OWNER TO ${this.escapeId(newOwner)}`;
1403
+ await this.query(sql);
1404
+ }
1405
+ async addConstraint(tableName, options) {
1406
+ const constraintName = options.name || `${tableName}_${options.fields.join('_')}_${options.type.toLowerCase()}`;
1407
+ const fieldsSql = options.fields.map((f) => this.escapeId(f)).join(', ');
1408
+ let sql = `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} `;
1409
+ switch (options.type) {
1410
+ case 'UNIQUE':
1411
+ sql += `UNIQUE (${fieldsSql})`;
1412
+ break;
1413
+ case 'PRIMARY KEY':
1414
+ sql += `PRIMARY KEY (${fieldsSql})`;
1415
+ break;
1416
+ case 'FOREIGN KEY':
1417
+ sql += `FOREIGN KEY (${fieldsSql}) REFERENCES ${this.escapeId(options.references.table)} (${options.references.fields.map((f) => this.escapeId(f)).join(', ')})`;
1418
+ break;
1419
+ case 'CHECK':
1420
+ sql += `CHECK (${options.check})`;
1421
+ break;
1422
+ }
1423
+ await this.query(sql);
1424
+ }
1425
+ async removeConstraint(tableName, constraintName) {
1426
+ const sql = `ALTER TABLE ${this.escapeId(tableName)} DROP CONSTRAINT ${this.escapeId(constraintName)}`;
1427
+ await this.query(sql);
1428
+ }
1429
+ async createSecurityPolicy(_policyName, _tableName, _options) {
1430
+ throw new Error('Security policies are not supported in CockroachDB');
1431
+ }
1432
+ async dropSecurityPolicy(_policyName, _tableName) {
1433
+ throw new Error('Security policies are not supported in CockroachDB');
1434
+ }
1435
+ /**
1436
+ * Drop a stored procedure
1437
+ */
1438
+ async dropStoredProcedure(procedureName, options) {
1439
+ const schema = options?.schema || 'public';
1440
+ const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
1441
+ const cascade = options?.cascade ? ' CASCADE' : '';
1442
+ const sql = `DROP PROCEDURE ${ifExists}${this.escapeId(schema)}.${this.escapeId(procedureName)}${cascade}`;
1443
+ await this.query(sql);
1444
+ }
1445
+ /**
1446
+ * Drop a stored procedure (alias for dropStoredProcedure)
1447
+ */
1448
+ async dropProcedure(procedureName, options) {
1449
+ return this.dropStoredProcedure(procedureName, options);
1450
+ }
1451
+ /**
1452
+ * Create a stored procedure (alias for createStoredProcedure)
1453
+ */
1454
+ async createProcedure(options) {
1455
+ return this.createStoredProcedure(options);
1456
+ }
1457
+ /**
1458
+ * Execute a stored procedure
1459
+ */
1460
+ async executeStoredProcedure(options) {
1461
+ const schema = options.schema || 'public';
1462
+ const procName = `${this.escapeId(schema)}.${this.escapeId(options.procedureName)}`;
1463
+ let sql = `CALL ${procName}`;
1464
+ if (options.params && Object.keys(options.params).length > 0) {
1465
+ const params = Object.entries(options.params)
1466
+ .map(([name, value]) => `${this.escapeId(name)} => ${this.escape(value)}`)
1467
+ .join(', ');
1468
+ sql += `(${params})`;
1469
+ }
1470
+ return this.query(sql, { timeout: options.timeout });
1471
+ }
1472
+ /**
1473
+ * Check if a stored procedure exists
1474
+ */
1475
+ async hasStoredProcedure(procedureName, schema) {
1476
+ const db = schema || 'public';
1477
+ const sql = `SELECT proname FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = '${db}' AND proname = '${procedureName}' AND prokind = 'p'`;
1478
+ const result = await this.query(sql, { plain: true });
1479
+ return !!result;
1480
+ }
1481
+ // ==================== User-Defined Functions (UDFs) ====================
1482
+ /**
1483
+ * Create a user-defined function (CockroachDB)
1484
+ *
1485
+ * CockroachDB has supported `CREATE FUNCTION` for SQL and PL/pgSQL
1486
+ * functions since v22.2/23.1, and unlike `CREATE PROCEDURE` (see
1487
+ * `createStoredProcedure`), functions can be used directly in SELECT
1488
+ * lists, computed column expressions, and CHECK constraints. A function
1489
+ * must declare a `RETURNS` type, and may optionally declare a volatility
1490
+ * category:
1491
+ * - `VOLATILE` (the default when omitted): may modify the database
1492
+ * and/or return different results on successive calls with the same
1493
+ * arguments.
1494
+ * - `STABLE`: cannot modify the database and returns the same result
1495
+ * for the same arguments within a single statement; lets the
1496
+ * optimizer avoid re-evaluating it per-row when safe.
1497
+ * - `IMMUTABLE`: cannot modify the database and always returns the same
1498
+ * result for the same arguments. Required for a function to be used
1499
+ * in a computed column expression.
1500
+ * @see https://www.cockroachlabs.com/docs/stable/create-function
1501
+ */
1502
+ async createFunction(options) {
1503
+ const schema = options.schema || 'public';
1504
+ const funcName = `${this.escapeId(schema)}.${this.escapeId(options.name)}`;
1505
+ let sql = 'CREATE FUNCTION';
1506
+ if (options.replace) {
1507
+ sql = 'CREATE OR REPLACE FUNCTION';
1508
+ }
1509
+ else if (options.ifNotExists) {
1510
+ sql = 'CREATE FUNCTION IF NOT EXISTS';
1511
+ }
1512
+ // Add parameters
1513
+ if (options.params && options.params.length > 0) {
1514
+ const params = options.params
1515
+ .map((p) => {
1516
+ const mode = p.mode && p.mode !== 'IN' ? `${p.mode} ` : '';
1517
+ const defaultPart = p.defaultValue !== undefined ? ` DEFAULT ${this.escape(p.defaultValue)}` : '';
1518
+ return `${mode}${this.escapeId(p.name)} ${p.type}${defaultPart}`;
1519
+ })
1520
+ .join(', ');
1521
+ sql += ` ${funcName} (${params})`;
1522
+ }
1523
+ else {
1524
+ sql += ` ${funcName} ()`;
1525
+ }
1526
+ sql += ` RETURNS ${options.returnType}`;
1527
+ if (options.volatility) {
1528
+ sql += ` ${options.volatility}`;
1529
+ }
1530
+ const language = options.language || 'SQL';
1531
+ sql += ` LANGUAGE ${language} AS $$\n${options.body}\n$$`;
1532
+ await this.query(sql);
1533
+ }
1534
+ /**
1535
+ * Drop a user-defined function
1536
+ */
1537
+ async dropFunction(functionName, options) {
1538
+ const schema = options?.schema || 'public';
1539
+ const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
1540
+ const cascade = options?.cascade ? ' CASCADE' : '';
1541
+ const paramTypes = options?.paramTypes && options.paramTypes.length > 0
1542
+ ? `(${options.paramTypes.join(', ')})`
1543
+ : '';
1544
+ const sql = `DROP FUNCTION ${ifExists}${this.escapeId(schema)}.${this.escapeId(functionName)}${paramTypes}${cascade}`;
1545
+ await this.query(sql);
1546
+ }
1547
+ /**
1548
+ * Check if a user-defined function exists
1549
+ */
1550
+ async hasFunction(functionName, schema) {
1551
+ const db = schema || 'public';
1552
+ const sql = `SELECT proname FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = '${db}' AND proname = '${functionName}' AND prokind = 'f'`;
1553
+ const result = await this.query(sql, { plain: true });
1554
+ return !!result;
1555
+ }
1556
+ // ==================== Triggers ====================
1557
+ /**
1558
+ * Create a trigger
1559
+ *
1560
+ * CockroachDB's trigger support (added as a preview feature and
1561
+ * subsequently stabilized) is deliberately narrower than Postgres':
1562
+ * - Only row-level triggers are supported (`FOR EACH ROW`); there is no
1563
+ * `FOR EACH STATEMENT` execution.
1564
+ * - `INSTEAD OF` triggers are not supported (CockroachDB views are not
1565
+ * updatable in the way Postgres views can be).
1566
+ * - The `REFERENCING` clause (transition tables, e.g.
1567
+ * `REFERENCING NEW TABLE AS ...`) is not supported.
1568
+ * - Constraint triggers (`CREATE CONSTRAINT TRIGGER`) are not supported.
1569
+ * Rather than silently emitting Postgres-only DDL that will fail (or
1570
+ * behave unexpectedly) against CockroachDB, these unsupported options
1571
+ * throw a clear error.
1572
+ *
1573
+ * @see https://www.cockroachlabs.com/docs/stable/triggers
1574
+ */
1575
+ async createTrigger(options) {
1576
+ const level = options.level || 'ROW';
1577
+ if (level !== 'ROW') {
1578
+ throw new Error('CockroachDB only supports row-level triggers (FOR EACH ROW); ' +
1579
+ 'FOR EACH STATEMENT triggers are not supported.');
1580
+ }
1581
+ if (options.timing === 'INSTEAD OF') {
1582
+ throw new Error('CockroachDB does not support INSTEAD OF triggers.');
1583
+ }
1584
+ if (options.referencing) {
1585
+ throw new Error('CockroachDB does not support the REFERENCING clause (transition tables) for triggers.');
1586
+ }
1587
+ if (options.constraint) {
1588
+ throw new Error('CockroachDB does not support constraint triggers.');
1589
+ }
1590
+ const timing = options.timing;
1591
+ const events = (options.events || []).join(' OR ');
1592
+ const tableName = options.schema
1593
+ ? `${this.escapeId(options.schema)}.${this.escapeId(options.tableName)}`
1594
+ : this.quoteTable(options.tableName);
1595
+ let sql = `CREATE TRIGGER ${this.escapeId(options.name)} ${timing} ${events} ON ${tableName} FOR EACH ROW`;
1596
+ sql += `\nEXECUTE FUNCTION ${options.body}`;
1597
+ if (options.replace) {
1598
+ sql = sql.replace('CREATE TRIGGER', 'CREATE OR REPLACE TRIGGER');
1599
+ }
1600
+ await this.query(sql);
1601
+ }
1602
+ /**
1603
+ * Drop a trigger
1604
+ */
1605
+ async dropTrigger(triggerName, tableName, options) {
1606
+ const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
1607
+ const cascade = options?.cascade ? ' CASCADE' : '';
1608
+ const schemaPart = options?.schema ? `${this.escapeId(options.schema)}.` : '';
1609
+ const sql = `DROP TRIGGER ${ifExists}${this.escapeId(triggerName)} ON ${schemaPart}${this.escapeId(tableName)}${cascade}`;
1610
+ await this.query(sql);
1611
+ }
1612
+ /**
1613
+ * List triggers defined on a table.
1614
+ *
1615
+ * Uses CockroachDB's own `SHOW TRIGGERS FROM <table>` introspection
1616
+ * statement (in the same family as `SHOW INDEXES FROM`/`SHOW CONSTRAINTS
1617
+ * FROM` used elsewhere in this dialect) rather than querying the
1618
+ * `pg_trigger` catalog directly. CockroachDB's `pg_catalog` compatibility
1619
+ * layer is partial, and there is no guarantee `pg_trigger` is populated
1620
+ * for CockroachDB-native triggers the same way it is in real Postgres, so
1621
+ * relying on it risks silently reporting "no trigger" when one exists.
1622
+ */
1623
+ async showTriggers(tableName) {
1624
+ const sql = `SHOW TRIGGERS FROM ${this.escapeId(tableName)}`;
1625
+ const result = await this.query(sql);
1626
+ return result.rows;
1627
+ }
1628
+ /**
1629
+ * Check if a trigger exists
1630
+ */
1631
+ async hasTrigger(triggerName, tableName) {
1632
+ const triggers = await this.showTriggers(tableName);
1633
+ return triggers.some((row) => row.trigger_name === triggerName || row.name === triggerName);
1634
+ }
1635
+ // ==================== Sequences ====================
1636
+ /**
1637
+ * Create a sequence
1638
+ */
1639
+ async createSequence(options) {
1640
+ const schema = options.schema || 'public';
1641
+ const seqName = `${this.escapeId(schema)}.${this.escapeId(options.name)}`;
1642
+ let sql = 'CREATE SEQUENCE';
1643
+ if (options.replace) {
1644
+ sql = 'CREATE OR REPLACE SEQUENCE';
1645
+ }
1646
+ else if (options.ifNotExists) {
1647
+ sql = 'CREATE SEQUENCE IF NOT EXISTS';
1648
+ }
1649
+ if (options.temporary) {
1650
+ sql = sql.replace('CREATE', 'CREATE TEMPORARY SEQUENCE');
1651
+ }
1652
+ sql += ` ${seqName}`;
1653
+ if (options.startWith)
1654
+ sql += ` START WITH ${options.startWith}`;
1655
+ if (options.incrementBy)
1656
+ sql += ` INCREMENT BY ${options.incrementBy}`;
1657
+ if (options.minvalue)
1658
+ sql += ` MINVALUE ${options.minvalue}`;
1659
+ if (options.maxvalue)
1660
+ sql += ` MAXVALUE ${options.maxvalue}`;
1661
+ if (options.cycle)
1662
+ sql += ` CYCLE`;
1663
+ if (options.cache)
1664
+ sql += ` CACHE ${options.cache}`;
1665
+ if (options.ownedBy)
1666
+ sql += ` OWNED BY ${options.ownedBy}`;
1667
+ await this.query(sql);
1668
+ }
1669
+ /**
1670
+ * Drop a sequence
1671
+ */
1672
+ async dropSequence(sequenceName, options) {
1673
+ const schema = options?.schema || 'public';
1674
+ const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
1675
+ const cascade = options?.cascade ? ' CASCADE' : '';
1676
+ const sql = `DROP SEQUENCE ${ifExists}${this.escapeId(schema)}.${this.escapeId(sequenceName)}${cascade}`;
1677
+ await this.query(sql);
1678
+ }
1679
+ /**
1680
+ * Get next value from a sequence
1681
+ */
1682
+ async nextSequenceValue(sequenceName) {
1683
+ const sql = `SELECT nextval(${this.escape(sequenceName)}) as value`;
1684
+ const result = await this.query(sql, { raw: true });
1685
+ return result.rows?.[0]?.value;
1686
+ }
1687
+ /**
1688
+ * Check if a sequence exists
1689
+ */
1690
+ async hasSequence(sequenceName) {
1691
+ const sql = `SELECT sequence_name FROM information_schema.sequences WHERE sequence_name = '${sequenceName}'`;
1692
+ const result = await this.query(sql, { plain: true });
1693
+ return !!result;
1694
+ }
1695
+ /**
1696
+ * List all sequences in the database (CockroachDB)
1697
+ * @returns Array of sequence names
1698
+ */
1699
+ async listSequences() {
1700
+ const sql = `SELECT sequence_name FROM information_schema.sequences ORDER BY sequence_name`;
1701
+ const result = await this.query(sql);
1702
+ return result.rows.map((row) => row.sequence_name);
1703
+ }
1704
+ // ==================== Row-Level Security (RLS) ====================
1705
+ /**
1706
+ * Create a policy (CockroachDB RLS)
1707
+ */
1708
+ async createPolicy(options) {
1709
+ const schema = options.schema || 'public';
1710
+ const tableName = `${this.escapeId(schema)}.${this.escapeId(options.tableName)}`;
1711
+ const policyName = this.escapeId(options.name);
1712
+ let sql = `CREATE POLICY ${policyName} ON ${tableName}`;
1713
+ if (options.permissive) {
1714
+ sql += ' FOR SELECT USING (true)';
1715
+ }
1716
+ if (options.roles && options.roles.length > 0) {
1717
+ sql += ` TO ${options.roles.join(', ')}`;
1718
+ }
1719
+ if (options.using) {
1720
+ sql += ` USING (${options.using})`;
1721
+ }
1722
+ if (options.withCheck) {
1723
+ sql += ` WITH CHECK (${options.withCheck})`;
1724
+ }
1725
+ await this.query(sql);
1726
+ }
1727
+ /**
1728
+ * Drop a policy (CockroachDB RLS)
1729
+ */
1730
+ async dropPolicy(policyName, tableName, options) {
1731
+ const schema = options?.schema || 'public';
1732
+ const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
1733
+ const cascade = options?.cascade ? ' CASCADE' : '';
1734
+ const sql = `DROP POLICY ${ifExists}${this.escapeId(policyName)} ON ${this.escapeId(schema)}.${this.escapeId(tableName)}${cascade}`;
1735
+ await this.query(sql);
1736
+ }
1737
+ /**
1738
+ * Enable row-level security on a table (CockroachDB)
1739
+ */
1740
+ async enableRLS(tableName, schema) {
1741
+ const schemaName = schema || 'public';
1742
+ const sql = `ALTER TABLE ${this.escapeId(schemaName)}.${this.escapeId(tableName)} ENABLE ROW LEVEL SECURITY`;
1743
+ await this.query(sql);
1744
+ }
1745
+ /**
1746
+ * Enable row-level security on a table (alias for enableRLS)
1747
+ */
1748
+ async enableRowLevelSecurity(tableName, schema) {
1749
+ return this.enableRLS(tableName, schema);
1750
+ }
1751
+ /**
1752
+ * Disable row-level security on a table (CockroachDB)
1753
+ */
1754
+ async disableRLS(tableName, schema) {
1755
+ const schemaName = schema || 'public';
1756
+ const sql = `ALTER TABLE ${this.escapeId(schemaName)}.${this.escapeId(tableName)} DISABLE ROW LEVEL SECURITY`;
1757
+ await this.query(sql);
1758
+ }
1759
+ /**
1760
+ * Check if a policy exists (CockroachDB RLS)
1761
+ */
1762
+ async hasPolicy(policyName, tableName) {
1763
+ const sql = `SELECT policyname FROM pg_policies WHERE policyname = '${policyName}' AND tablename = '${tableName}'`;
1764
+ const result = await this.query(sql, { plain: true });
1765
+ return !!result;
1766
+ }
1767
+ // ==================== Comments ====================
1768
+ /**
1769
+ * Add comment to a table
1770
+ */
1771
+ async commentTable(tableName, comment) {
1772
+ const sql = `COMMENT ON TABLE ${this.quoteTable(tableName)} IS ${this.escape(comment)}`;
1773
+ await this.query(sql);
1774
+ }
1775
+ /**
1776
+ * Add comment to a column
1777
+ */
1778
+ async commentColumn(tableName, columnName, comment) {
1779
+ const sql = `COMMENT ON COLUMN ${this.quoteTable(tableName)}.${this.quoteIdentifier(columnName)} IS ${this.escape(comment)}`;
1780
+ await this.query(sql);
1781
+ }
1782
+ // ==================== Advanced Indexes ====================
1783
+ /**
1784
+ * Create a partial index (index with WHERE clause)
1785
+ */
1786
+ async createPartialIndex(tableName, indexName, fields, where, options) {
1787
+ const fieldsSql = fields.map((f) => this.escapeId(f)).join(', ');
1788
+ let sql = `CREATE INDEX ${this.escapeId(indexName)} ON ${this.quoteTable(tableName)} (${fieldsSql}) WHERE ${where}`;
1789
+ if (options?.unique)
1790
+ sql = sql.replace('CREATE INDEX', 'CREATE UNIQUE INDEX');
1791
+ if (options?.ifNotExists)
1792
+ sql = sql.replace('CREATE INDEX', 'CREATE INDEX IF NOT EXISTS');
1793
+ await this.query(sql);
1794
+ }
1795
+ /**
1796
+ * Create an expression index
1797
+ */
1798
+ async createExpressionIndex(tableName, indexName, expression, options) {
1799
+ let sql = `CREATE INDEX ${this.escapeId(indexName)} ON ${this.quoteTable(tableName)} ((${expression}))`;
1800
+ if (options?.unique)
1801
+ sql = sql.replace('CREATE INDEX', 'CREATE UNIQUE INDEX');
1802
+ if (options?.ifNotExists)
1803
+ sql = sql.replace('CREATE INDEX', 'CREATE INDEX IF NOT EXISTS');
1804
+ await this.query(sql);
1805
+ }
1806
+ /**
1807
+ * Create a fulltext index (CockroachDB)
1808
+ * Uses GIN index for fulltext search
1809
+ * @param tableName - Table name
1810
+ * @param indexName - Index name
1811
+ * @param fields - Fields to index
1812
+ * @param options - Fulltext index options
1813
+ */
1814
+ async createFulltextIndex(tableName, indexName, fields, options) {
1815
+ const usingClause = options?.parser ? ` USING gin(to_tsvector(${options.parser}, ` : ' USING gin(to_tsvector(';
1816
+ const sql = `CREATE INDEX ${this.escapeId(indexName)} ON ${this.quoteTable(tableName)}${usingClause}${fields.map((f) => `${this.escapeId(f)}`).join(' || ')}))`;
1817
+ await this.query(sql);
1818
+ }
1819
+ /**
1820
+ * Create a spatial index (CockroachDB)
1821
+ * Uses GIST index for spatial data
1822
+ * @param tableName - Table name
1823
+ * @param indexName - Index name
1824
+ * @param fields - Fields to index
1825
+ * @param options - Spatial index options
1826
+ */
1827
+ async createSpatialIndex(tableName, indexName, fields, options) {
1828
+ const sql = `CREATE INDEX ${this.escapeId(indexName)} ON ${this.quoteTable(tableName)} USING gist(${fields.map((f) => this.escapeId(f)).join(', ')})`;
1829
+ await this.query(sql);
1830
+ }
1831
+ /**
1832
+ * Bulk insert records into a table
1833
+ */
1834
+ async bulkInsert(tableName, records, _options) {
1835
+ if (records.length === 0) {
1836
+ return { rows: [], rowCount: 0, fields: [] };
1837
+ }
1838
+ const values = [];
1839
+ const placeholders = [];
1840
+ const columns = Object.keys(records[0]);
1841
+ for (const record of records) {
1842
+ const rowPlaceholders = [];
1843
+ for (let i = 0; i < columns.length; i++) {
1844
+ rowPlaceholders.push(`$${values.length + i + 1}`);
1845
+ values.push(record[columns[i]]);
1846
+ }
1847
+ placeholders.push(`(${rowPlaceholders.join(', ')})`);
1848
+ }
1849
+ const sql = `INSERT INTO ${this.quoteTable(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES ${placeholders.join(', ')}`;
1850
+ return this.query(sql, { replacements: values });
1851
+ }
1852
+ /**
1853
+ * Add a foreign key to a table
1854
+ */
1855
+ async addForeignKey(tableName, columnName, referencedTableName, referencedColumnName, options) {
1856
+ const constraintName = options?.name || `${tableName}_${columnName}_fkey`;
1857
+ let sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} FOREIGN KEY (${this.escapeId(columnName)}) REFERENCES ${this.quoteTable(referencedTableName)}(${this.escapeId(referencedColumnName)})`;
1858
+ const clauses = [];
1859
+ if (options?.onDelete) {
1860
+ clauses.push(`ON DELETE ${options.onDelete}`);
1861
+ }
1862
+ if (options?.onUpdate) {
1863
+ clauses.push(`ON UPDATE ${options.onUpdate}`);
1864
+ }
1865
+ if (clauses.length > 0) {
1866
+ sql += ' ' + clauses.join(' ');
1867
+ }
1868
+ await this.query(sql);
1869
+ }
1870
+ /**
1871
+ * Rename a column
1872
+ */
1873
+ async renameColumn(tableName, oldColumnName, newColumnName) {
1874
+ const sql = `ALTER TABLE ${this.quoteTable(tableName)} RENAME COLUMN ${this.escapeId(oldColumnName)} TO ${this.escapeId(newColumnName)}`;
1875
+ await this.query(sql);
1876
+ }
1877
+ // ==================== Identity & Computed Columns ====================
1878
+ /**
1879
+ * Create an identity column
1880
+ */
1881
+ async createIdentityColumn(tableName, columnName, options) {
1882
+ let sql = `ALTER TABLE ${this.quoteTable(tableName)} ALTER COLUMN ${this.escapeId(columnName)} ADD GENERATED ALWAYS AS IDENTITY`;
1883
+ if (options?.startWith || options?.incrementBy) {
1884
+ const startWith = options.startWith || 1;
1885
+ const incrementBy = options.incrementBy || 1;
1886
+ sql = `ALTER TABLE ${this.quoteTable(tableName)} ALTER COLUMN ${this.escapeId(columnName)} ADD GENERATED ALWAYS AS IDENTITY (START WITH ${startWith} INCREMENT BY ${incrementBy})`;
1887
+ }
1888
+ await this.query(sql);
1889
+ }
1890
+ /**
1891
+ * Create a computed column (generated column)
1892
+ */
1893
+ async createComputedColumn(tableName, columnName, expression, options) {
1894
+ const persisted = options?.persisted ? 'STORED' : 'VIRTUAL';
1895
+ const type = options?.type || 'ANY';
1896
+ const sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD COLUMN ${this.escapeId(columnName)} ${type} GENERATED ALWAYS AS (${expression}) ${persisted}`;
1897
+ await this.query(sql);
1898
+ }
1899
+ assertExtensionShimSupported(extensionName) {
1900
+ const normalized = extensionName.toLowerCase();
1901
+ if (!CockroachDBDialect.SUPPORTED_EXTENSION_SHIMS.has(normalized)) {
1902
+ throw new Error(`Extension '${extensionName}' is not supported by CockroachDB. CockroachDB only has partial ` +
1903
+ 'PostgreSQL extension compatibility: CREATE/DROP EXTENSION is accepted as a no-op compatibility ' +
1904
+ `shim for a small allowlist of extension names (${Array.from(CockroachDBDialect.SUPPORTED_EXTENSION_SHIMS).join(', ')}), and no actual third-party extension code can be loaded.`);
1905
+ }
1906
+ }
1907
+ /**
1908
+ * Create a CockroachDB extension (no-op compatibility shim; see
1909
+ * SUPPORTED_EXTENSION_SHIMS above for CockroachDB's partial extension compatibility)
1910
+ * @param extensionName - Name of the extension to create
1911
+ * @param options - Extension options
1912
+ */
1913
+ async createExtension(extensionName, options) {
1914
+ this.assertExtensionShimSupported(extensionName);
1915
+ const ifNotExists = options?.ifNotExists ? 'IF NOT EXISTS ' : '';
1916
+ const schema = options?.schema ? ` SCHEMA ${this.escapeId(options.schema)}` : '';
1917
+ const version = options?.version ? ` VERSION ${options.version}` : '';
1918
+ const sql = `CREATE EXTENSION ${ifNotExists}${this.escapeId(extensionName)}${schema}${version}`;
1919
+ await this.query(sql);
1920
+ }
1921
+ /**
1922
+ * Drop a CockroachDB extension (no-op compatibility shim; see
1923
+ * SUPPORTED_EXTENSION_SHIMS above for CockroachDB's partial extension compatibility)
1924
+ * @param extensionName - Name of the extension to drop
1925
+ * @param options - Drop options
1926
+ */
1927
+ async dropExtension(extensionName, options) {
1928
+ this.assertExtensionShimSupported(extensionName);
1929
+ const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
1930
+ const cascade = options?.cascade ? ' CASCADE' : '';
1931
+ const sql = `DROP EXTENSION ${ifExists}${this.escapeId(extensionName)}${cascade}`;
1932
+ await this.query(sql);
1933
+ }
1934
+ /**
1935
+ * Get all installed CockroachDB extensions
1936
+ * @returns Array of extension information
1937
+ */
1938
+ async getExtensions() {
1939
+ const sql = `
1940
+ SELECT
1941
+ e.extname as name,
1942
+ e.extversion as installed_version,
1943
+ obj_description(e.oid, 'pg_extension') as comment
1944
+ FROM pg_extension e
1945
+ WHERE e.extname NOT IN ('plpgsql')
1946
+ ORDER BY e.extname
1947
+ `;
1948
+ const result = await this.query(sql, { raw: true });
1949
+ return (result.rows?.map((row) => ({
1950
+ name: row.name,
1951
+ defaultVersion: null,
1952
+ installedVersion: row.installed_version,
1953
+ comment: row.comment,
1954
+ })) || []);
1955
+ }
1956
+ /**
1957
+ * Check if a CockroachDB extension is installed. Extensions outside
1958
+ * CockroachDB's small no-op compatibility allowlist (see
1959
+ * SUPPORTED_EXTENSION_SHIMS above) can never be "installed" on CockroachDB,
1960
+ * so this returns false for them without a round-trip to the server.
1961
+ * @param extensionName - Name of the extension
1962
+ * @returns True if the extension is installed
1963
+ */
1964
+ async hasExtension(extensionName) {
1965
+ if (!CockroachDBDialect.SUPPORTED_EXTENSION_SHIMS.has(extensionName.toLowerCase())) {
1966
+ return false;
1967
+ }
1968
+ const sql = `
1969
+ SELECT 1 FROM pg_extension WHERE extname = $1
1970
+ `;
1971
+ const result = await this.query(sql, { replacements: [extensionName] });
1972
+ return (result.rows?.length ?? 0) > 0;
1973
+ }
1974
+ // ---------------------------------------------------------------------------
1975
+ // Foreign Data Wrapper SQL builders
1976
+ //
1977
+ // CockroachDB implements none of the Postgres FDW machinery (no
1978
+ // `CREATE FOREIGN SERVER`/`CREATE USER MAPPING`/`CREATE FOREIGN TABLE`/
1979
+ // `IMPORT FOREIGN SCHEMA`, and no `pg_foreign_server`/
1980
+ // `pg_foreign_data_wrapper` catalogs). These builders are part of the
1981
+ // shared `Dialect` interface (implemented for real by the Postgres
1982
+ // dialect); rather than returning Postgres-only DDL/catalog queries that
1983
+ // would fail against a real CockroachDB cluster, they throw a clear
1984
+ // "not supported" error instead.
1985
+ // ---------------------------------------------------------------------------
1986
+ buildCreateServerQuery(_name, _opts) {
1987
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
1988
+ }
1989
+ buildAlterServerQuery(_name, _opts) {
1990
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
1991
+ }
1992
+ buildDropServerQuery(_name, _opts) {
1993
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
1994
+ }
1995
+ /**
1996
+ * Return the SQL to list all foreign servers from the catalog — not
1997
+ * supported in CockroachDB (no `pg_foreign_server`/`pg_foreign_data_wrapper`).
1998
+ */
1999
+ getServersQuery() {
2000
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
2001
+ }
2002
+ buildCreateUserMappingQuery(_opts) {
2003
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
2004
+ }
2005
+ buildAlterUserMappingQuery(_opts) {
2006
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
2007
+ }
2008
+ buildDropUserMappingQuery(_serverName, _user, _opts) {
2009
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
2010
+ }
2011
+ /**
2012
+ * Create a user mapping for a foreign server — not supported in CockroachDB.
2013
+ */
2014
+ async createUserMapping(_userName, _serverName, _options) {
2015
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
2016
+ }
2017
+ /**
2018
+ * Drop a user mapping for a foreign server — not supported in CockroachDB.
2019
+ */
2020
+ async dropUserMapping(_userName, _serverName, _options) {
2021
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
2022
+ }
2023
+ buildCreateForeignTableQuery(_tableName, _opts) {
2024
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
2025
+ }
2026
+ buildDropForeignTableQuery(_tableName, _opts) {
2027
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
2028
+ }
2029
+ buildImportForeignSchemaQuery(_remoteSchema, _serverName, _opts) {
2030
+ throw new Error(CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE);
2031
+ }
2032
+ /**
2033
+ * Add a column to a table
2034
+ */
2035
+ async addColumn(tableName, columnName, definition) {
2036
+ const sql = `ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${this.getColumnDefinitionSql(columnName, definition)}`;
2037
+ await this.query(sql);
2038
+ }
2039
+ /**
2040
+ * Remove a column from a table
2041
+ */
2042
+ async removeColumn(tableName, columnName) {
2043
+ const sql = `ALTER TABLE ${this.escapeId(tableName)} DROP COLUMN ${this.escapeId(columnName)}`;
2044
+ await this.query(sql);
2045
+ }
2046
+ /**
2047
+ * Change a column definition
2048
+ */
2049
+ async changeColumn(tableName, columnName, definition) {
2050
+ const sql = `ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} TYPE ${this.getDataTypeSql(definition.type)}`;
2051
+ await this.query(sql);
2052
+ if (definition.allowNull === false) {
2053
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} SET NOT NULL`);
2054
+ }
2055
+ else if (definition.allowNull === true) {
2056
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} DROP NOT NULL`);
2057
+ }
2058
+ if (definition.defaultValue !== undefined) {
2059
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} SET DEFAULT ${this.getDefaultValue(definition.defaultValue)}`);
2060
+ }
2061
+ }
2062
+ /**
2063
+ * Show all tables in the database
2064
+ *
2065
+ * Uses CockroachDB's `SHOW TABLES` statement (rather than querying
2066
+ * information_schema directly, as the Postgres dialect does) so this
2067
+ * dialect exercises CockroachDB-specific introspection syntax. `SHOW
2068
+ * TABLES` returns rows shaped like
2069
+ * `{ schema_name, table_name, type, owner, estimated_row_count, locality }`
2070
+ * for the current database's `public` schema by default.
2071
+ */
2072
+ async showTables() {
2073
+ const sql = 'SHOW TABLES';
2074
+ const result = await this.query(sql);
2075
+ return result.rows
2076
+ .filter((row) => !row.type || row.type === 'table')
2077
+ .map((row) => row.table_name);
2078
+ }
2079
+ /**
2080
+ * Get table status (CockroachDB implementation)
2081
+ */
2082
+ async getTableStatus(tableName) {
2083
+ const sql = tableName
2084
+ ? `SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1`
2085
+ : `SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = 'public'`;
2086
+ const result = tableName
2087
+ ? await this.query(sql, { bindings: [tableName] })
2088
+ : await this.query(sql);
2089
+ return result.rows;
2090
+ }
2091
+ /**
2092
+ * Get table create statement (CockroachDB implementation)
2093
+ */
2094
+ async getCreateTable(tableName) {
2095
+ const sql = `SELECT pg_get_constraintdef(oid) as definition FROM pg_class WHERE relname = $1`;
2096
+ // For full CREATE TABLE statement, use pg_dump or similar
2097
+ const tableSql = `SELECT pg_get_table_def(oid) as create_sql FROM pg_class WHERE relname = $1`;
2098
+ const result = await this.query(tableSql, { bindings: [tableName] });
2099
+ return result.rows[0]?.create_sql || '';
2100
+ }
2101
+ /**
2102
+ * Check if a table has partitions (CockroachDB implementation)
2103
+ */
2104
+ async hasPartition(tableName) {
2105
+ const sql = `
2106
+ SELECT 1 FROM pg_tables
2107
+ WHERE tablename = $1 AND schemaname = 'public'
2108
+ `;
2109
+ const result = await this.query(sql, { bindings: [tableName] });
2110
+ // Check if it's a partitioned table
2111
+ const partSql = `
2112
+ SELECT 1 FROM pg_partitions
2113
+ WHERE tablename = $1 AND schemaname = 'public'
2114
+ `;
2115
+ const partResult = await this.query(partSql, { bindings: [tableName] });
2116
+ return partResult.rows.length > 0;
2117
+ }
2118
+ /**
2119
+ * Show constraints for a table
2120
+ *
2121
+ * Uses CockroachDB's `SHOW CONSTRAINTS FROM <table>` statement, which
2122
+ * returns rows shaped like
2123
+ * `{ table_name, constraint_name, constraint_type, details, validated }`.
2124
+ */
2125
+ async showConstraints(tableName) {
2126
+ const sql = `SHOW CONSTRAINTS FROM ${this.escapeId(tableName)}`;
2127
+ const result = await this.query(sql);
2128
+ return result.rows;
2129
+ }
2130
+ /**
2131
+ * Show indexes for a table
2132
+ *
2133
+ * Uses CockroachDB's `SHOW INDEXES FROM <table>` statement, which returns
2134
+ * rows shaped like
2135
+ * `{ table_name, index_name, non_unique, seq_in_index, column_name,
2136
+ * direction, storing, implicit }`.
2137
+ */
2138
+ async showIndexes(tableName) {
2139
+ const sql = `SHOW INDEXES FROM ${this.escapeId(tableName)}`;
2140
+ const result = await this.query(sql);
2141
+ return result.rows;
2142
+ }
2143
+ /**
2144
+ * Describe a table (get column information)
2145
+ *
2146
+ * Uses CockroachDB's `SHOW COLUMNS FROM <table>` statement, which returns
2147
+ * rows shaped like
2148
+ * `{ column_name, data_type, is_nullable, column_default,
2149
+ * generation_expression, indices, is_hidden }`, plus
2150
+ * `SHOW CONSTRAINTS FROM <table>` to determine which column(s) form the
2151
+ * primary key.
2152
+ */
2153
+ async describeTable(tableName) {
2154
+ const sql = `SHOW COLUMNS FROM ${this.escapeId(tableName)}`;
2155
+ const result = await this.query(sql);
2156
+ const description = {};
2157
+ for (const row of result.rows) {
2158
+ const rowData = row;
2159
+ const columnName = rowData.column_name;
2160
+ if (!columnName)
2161
+ continue;
2162
+ // Check if it's an auto-increment column (SERIAL/BIGSERIAL columns
2163
+ // default to `unique_rowid()` or `nextval(...)` in CockroachDB).
2164
+ const isSerial = typeof rowData.column_default === 'string' &&
2165
+ (rowData.column_default.includes('nextval') ||
2166
+ rowData.column_default.includes('unique_rowid'));
2167
+ description[columnName] = {
2168
+ type: rowData.data_type,
2169
+ allowNull: rowData.is_nullable === true || rowData.is_nullable === 'true',
2170
+ defaultValue: rowData.column_default,
2171
+ primaryKey: false, // Determined below via SHOW CONSTRAINTS
2172
+ autoIncrement: !!isSerial,
2173
+ };
2174
+ }
2175
+ // Check for primary key via SHOW CONSTRAINTS FROM <table>
2176
+ const constraints = await this.showConstraints(tableName);
2177
+ for (const constraint of constraints) {
2178
+ const c = constraint;
2179
+ if (c.constraint_type !== 'PRIMARY KEY')
2180
+ continue;
2181
+ // `details` is typically formatted like `PRIMARY KEY (id ASC)`; extract
2182
+ // the column name(s) between the parentheses.
2183
+ const match = typeof c.details === 'string' ? c.details.match(/\(([^)]+)\)/) : null;
2184
+ if (match) {
2185
+ for (const part of match[1].split(',')) {
2186
+ const colName = part.trim().split(/\s+/)[0];
2187
+ if (description[colName]) {
2188
+ description[colName].primaryKey = true;
2189
+ }
2190
+ }
2191
+ }
2192
+ }
2193
+ return description;
2194
+ }
2195
+ /**
2196
+ * Rename a table
2197
+ */
2198
+ async renameTable(oldName, newName) {
2199
+ const sql = `ALTER TABLE ${this.escapeId(oldName)} RENAME TO ${this.escapeId(newName)}`;
2200
+ await this.query(sql);
2201
+ }
2202
+ /**
2203
+ * Resolve and validate the requested hash-sharded index bucket count
2204
+ * (`bucketCount`/`shard`) from an options object. Returns `undefined` if
2205
+ * neither is set.
2206
+ */
2207
+ resolveHashBucketCount(options) {
2208
+ const bucketCount = options?.bucketCount ?? options?.shard;
2209
+ if (bucketCount === undefined) {
2210
+ return undefined;
2211
+ }
2212
+ if (!Number.isInteger(bucketCount) || bucketCount < 2) {
2213
+ throw new Error(`Invalid hash-sharded index bucket count: ${bucketCount}. Must be an integer >= 2.`);
2214
+ }
2215
+ return bucketCount;
2216
+ }
2217
+ /**
2218
+ * Add an index to a table
2219
+ *
2220
+ * Note: unlike Postgres, CockroachDB does not support (or need)
2221
+ * `CREATE INDEX CONCURRENTLY` — index backfills are already online and
2222
+ * non-blocking by default. If a caller requests `concurrently`, it is
2223
+ * ignored (with a warning) rather than emitted in the SQL.
2224
+ */
2225
+ async addIndex(tableName, indexName, fields = [], options) {
2226
+ if (options?.concurrently) {
2227
+ console.warn('CockroachDB: CREATE INDEX CONCURRENTLY is not supported (or needed) — ' +
2228
+ 'CockroachDB index creation is online by default. The `concurrently` option will be ignored.');
2229
+ }
2230
+ let sql = 'CREATE';
2231
+ if (options?.unique) {
2232
+ sql += ' UNIQUE';
2233
+ }
2234
+ if (options?.type) {
2235
+ sql += ` INDEX ${options.type}`;
2236
+ }
2237
+ else {
2238
+ sql += ' INDEX';
2239
+ }
2240
+ sql += ` ${this.escapeId(indexName)} ON ${this.escapeId(tableName)}`;
2241
+ // Handle expression index
2242
+ if (options?.expression) {
2243
+ sql += ` ((${options.expression}))`;
2244
+ }
2245
+ else {
2246
+ sql += ` (${fields.map((f) => this.escapeId(f)).join(', ')})`;
2247
+ }
2248
+ // Hash-sharded index (CockroachDB-specific hotspot-avoidance mechanism
2249
+ // for sequential/monotonic keys): `USING HASH WITH (bucket_count = N)`.
2250
+ // See: https://www.cockroachlabs.com/docs/stable/hash-sharded-indexes
2251
+ const bucketCount = this.resolveHashBucketCount(options);
2252
+ if (bucketCount !== undefined) {
2253
+ sql += ` USING HASH WITH (bucket_count = ${bucketCount})`;
2254
+ }
2255
+ else if (options?.using) {
2256
+ sql += ` USING ${options.using}`;
2257
+ }
2258
+ // Add INCLUDE columns for covering (storing) indexes
2259
+ if (options?.include && options.include.length > 0) {
2260
+ sql += ` INCLUDE (${options.include.map((f) => this.escapeId(f)).join(', ')})`;
2261
+ }
2262
+ if (options?.where) {
2263
+ const whereClause = this.buildWhereClause(options.where);
2264
+ sql += ` WHERE ${whereClause.sql}`;
2265
+ }
2266
+ await this.query(sql);
2267
+ }
2268
+ /**
2269
+ * Remove an index from a table
2270
+ */
2271
+ async removeIndex(tableName, indexName) {
2272
+ const sql = `DROP INDEX ${this.escapeId(indexName)}`;
2273
+ await this.query(sql);
2274
+ }
2275
+ /**
2276
+ * Create an index on a table with full options support
2277
+ *
2278
+ * Note: CockroachDB does not support `CREATE INDEX CONCURRENTLY` — index
2279
+ * creation is already online/non-blocking, so a `concurrently` flag (if
2280
+ * present on `indexDef`) is ignored with a warning instead of being
2281
+ * emitted in the SQL.
2282
+ */
2283
+ async createIndex(tableName, indexDef) {
2284
+ if (indexDef.concurrently) {
2285
+ console.warn('CockroachDB: CREATE INDEX CONCURRENTLY is not supported (or needed) — ' +
2286
+ 'CockroachDB index creation is online by default. The `concurrently` option will be ignored.');
2287
+ }
2288
+ const fields = indexDef.fields || [];
2289
+ let sql = 'CREATE';
2290
+ if (indexDef.unique) {
2291
+ sql += ' UNIQUE';
2292
+ }
2293
+ if (indexDef.type) {
2294
+ sql += ` INDEX ${indexDef.type}`;
2295
+ }
2296
+ else {
2297
+ sql += ' INDEX';
2298
+ }
2299
+ sql += ` ${this.escapeId(indexDef.name)} ON ${this.escapeId(tableName)}`;
2300
+ // Handle expression index
2301
+ if (indexDef.expression) {
2302
+ sql += ` ((${indexDef.expression}))`;
2303
+ }
2304
+ else {
2305
+ sql += ` (${fields.map((f) => this.escapeId(f)).join(', ')})`;
2306
+ }
2307
+ // Hash-sharded index (CockroachDB-specific hotspot-avoidance mechanism
2308
+ // for sequential/monotonic keys): `USING HASH WITH (bucket_count = N)`.
2309
+ const bucketCount = this.resolveHashBucketCount(indexDef);
2310
+ if (bucketCount !== undefined) {
2311
+ sql += ` USING HASH WITH (bucket_count = ${bucketCount})`;
2312
+ }
2313
+ else if (indexDef.using) {
2314
+ sql += ` USING ${indexDef.using}`;
2315
+ }
2316
+ // Add INCLUDE columns for covering index (CockroachDB)
2317
+ if (indexDef.include && indexDef.include.length > 0) {
2318
+ sql += ` INCLUDE (${indexDef.include.map((f) => this.escapeId(f)).join(', ')})`;
2319
+ }
2320
+ if (indexDef.where && Object.keys(indexDef.where).length > 0) {
2321
+ const whereClause = this.buildWhereClause(indexDef.where);
2322
+ sql += ` WHERE ${whereClause.sql}`;
2323
+ }
2324
+ await this.query(sql);
2325
+ }
2326
+ /**
2327
+ * Create a GIN index for full-text search
2328
+ * CockroachDB-specific GIN (Generalized Inverted Index) for tsvector
2329
+ */
2330
+ async createGINIndex(tableName, indexName, column, options) {
2331
+ const config = options?.config || 'english';
2332
+ const sql = `CREATE INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)} USING GIN(to_tsvector('${config}', ${this.escapeId(column)}))${options?.storageParameters
2333
+ ? ' WITH (' +
2334
+ Object.entries(options.storageParameters)
2335
+ .map(([k, v]) => `${k} = ${v}`)
2336
+ .join(', ') +
2337
+ ')'
2338
+ : ''}`;
2339
+ await this.query(sql);
2340
+ }
2341
+ /**
2342
+ * Build a to_tsvector expression
2343
+ * SQL: to_tsvector([config,] document)
2344
+ */
2345
+ buildTsVector(column, config) {
2346
+ const cfg = config || 'english';
2347
+ const cols = Array.isArray(column) ? column : [column];
2348
+ const columnList = cols.map((c) => this.escapeId(c)).join(" || ' ' || ");
2349
+ return `to_tsvector('${cfg}', ${columnList})`;
2350
+ }
2351
+ /**
2352
+ * Build a to_tsquery expression
2353
+ * SQL: to_tsquery([config,] query)
2354
+ */
2355
+ buildTsQuery(query, config) {
2356
+ const cfg = config || 'english';
2357
+ return `to_tsquery('${cfg}', ${this.escape(query)})`;
2358
+ }
2359
+ /**
2360
+ * Build a plainto_tsquery expression (for phrase searching)
2361
+ * SQL: plainto_tsquery([config,] query)
2362
+ * Converts a phrase into a tsquery that matches documents containing all the words
2363
+ */
2364
+ buildPlainTsQuery(query, config) {
2365
+ const cfg = config || 'english';
2366
+ return `plainto_tsquery('${cfg}', ${this.escape(query)})`;
2367
+ }
2368
+ /**
2369
+ * Build a phraseto_tsquery expression (for exact phrase matching)
2370
+ * SQL: phraseto_tsquery([config,] query)
2371
+ * Converts a phrase into a tsquery that matches documents containing the exact phrase
2372
+ */
2373
+ buildPhraseTsQuery(query, config) {
2374
+ const cfg = config || 'english';
2375
+ return `phraseto_tsquery('${cfg}', ${this.escape(query)})`;
2376
+ }
2377
+ /**
2378
+ * Build a websearch_to_tsquery expression (for web search style queries)
2379
+ * SQL: websearch_to_tsquery([config,] query)
2380
+ * Supports web search operators like +, -, "quotes", etc.
2381
+ */
2382
+ buildWebsearchTsQuery(query, config) {
2383
+ const cfg = config || 'english';
2384
+ return `websearch_to_tsquery('${cfg}', ${this.escape(query)})`;
2385
+ }
2386
+ /**
2387
+ * Build a ts_rank expression for ranking full-text search results
2388
+ * SQL: ts_rank([weights,] vector, query [, normalization])
2389
+ */
2390
+ buildTsRank(vector, query, options) {
2391
+ let sql = 'ts_rank(';
2392
+ if (options?.weights) {
2393
+ sql += `'{${options.weights.join(',')}}', `;
2394
+ }
2395
+ sql += `${vector}, to_tsquery('english', ${this.escape(query)})`;
2396
+ if (options?.normalization !== undefined) {
2397
+ sql += `, ${options.normalization}`;
2398
+ }
2399
+ sql += ')';
2400
+ return sql;
2401
+ }
2402
+ // ============================================
2403
+ // PostGIS spatial functions
2404
+ // ============================================
2405
+ /**
2406
+ * Create a GIST index for spatial data
2407
+ * CockroachDB GIST (Generalized Search Tree) index for geometry/geography columns
2408
+ */
2409
+ async createGISTIndex(tableName, indexName, column, options) {
2410
+ let sql = `CREATE INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)} USING GIST (${this.escapeId(column)})`;
2411
+ // Add constraints if specified
2412
+ if (options?.geometryType || options?.srid) {
2413
+ const constraints = [];
2414
+ if (options.geometryType) {
2415
+ constraints.push(`geometry_type = '${options.geometryType}'`);
2416
+ }
2417
+ if (options.srid) {
2418
+ constraints.push(`srid = ${options.srid}`);
2419
+ }
2420
+ if (constraints.length > 0) {
2421
+ sql += ` WHERE ${constraints.join(' AND ')}`;
2422
+ }
2423
+ }
2424
+ await this.query(sql);
2425
+ }
2426
+ /**
2427
+ * Create a GIN index for spatial data (PostGIS 2.1+)
2428
+ * GIN indexes are slower to build but faster for very large datasets with many overlapping geometries
2429
+ */
2430
+ async createGINSpatialIndex(tableName, indexName, column) {
2431
+ const sql = `CREATE INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)} USING GIN (${this.escapeId(column)})`;
2432
+ await this.query(sql);
2433
+ }
2434
+ /**
2435
+ * ST_Distance - calculate distance between two geometries
2436
+ * For geography, returns distance in meters
2437
+ */
2438
+ stDistance(geom1, geom2, useGeography) {
2439
+ if (useGeography) {
2440
+ return `ST_Distance(${geom1}::geography, ${geom2}::geography)`;
2441
+ }
2442
+ return `ST_Distance(${geom1}, ${geom2})`;
2443
+ }
2444
+ /**
2445
+ * ST_DWithin - check if geometries are within a given distance
2446
+ * For geography, distance is in meters
2447
+ */
2448
+ stDWithin(geom1, geom2, distance, useGeography) {
2449
+ if (useGeography) {
2450
+ return `ST_DWithin(${geom1}::geography, ${geom2}::geography, ${distance})`;
2451
+ }
2452
+ return `ST_DWithin(${geom1}, ${geom2}, ${distance})`;
2453
+ }
2454
+ /**
2455
+ * ST_Within - check if geometry A is within geometry B
2456
+ */
2457
+ stWithin(geom1, geom2) {
2458
+ return `ST_Within(${geom1}, ${geom2})`;
2459
+ }
2460
+ /**
2461
+ * ST_Contains - check if geometry A contains geometry B
2462
+ */
2463
+ stContains(geom1, geom2) {
2464
+ return `ST_Contains(${geom1}, ${geom2})`;
2465
+ }
2466
+ /**
2467
+ * ST_Intersects - check if two geometries intersect
2468
+ */
2469
+ stIntersects(geom1, geom2) {
2470
+ return `ST_Intersects(${geom1}, ${geom2})`;
2471
+ }
2472
+ /**
2473
+ * ST_Crosses - check if two geometries cross
2474
+ */
2475
+ stCrosses(geom1, geom2) {
2476
+ return `ST_Crosses(${geom1}, ${geom2})`;
2477
+ }
2478
+ /**
2479
+ * ST_Overlaps - check if two geometries overlap
2480
+ */
2481
+ stOverlaps(geom1, geom2) {
2482
+ return `ST_Overlaps(${geom1}, ${geom2})`;
2483
+ }
2484
+ /**
2485
+ * ST_Touches - check if two geometries touch
2486
+ */
2487
+ stTouches(geom1, geom2) {
2488
+ return `ST_Touches(${geom1}, ${geom2})`;
2489
+ }
2490
+ /**
2491
+ * ST_Equals - check if two geometries are equal
2492
+ */
2493
+ stEquals(geom1, geom2) {
2494
+ return `ST_Equals(${geom1}, ${geom2})`;
2495
+ }
2496
+ /**
2497
+ * ST_IsValid - check if a geometry is valid
2498
+ */
2499
+ stIsValid(geom) {
2500
+ return `ST_IsValid(${geom})`;
2501
+ }
2502
+ /**
2503
+ * ST_GeomFromText - create geometry from WKT text
2504
+ */
2505
+ stGeomFromText(wkt, srid) {
2506
+ if (srid) {
2507
+ return `ST_GeomFromText('${wkt}', ${srid})`;
2508
+ }
2509
+ return `ST_GeomFromText('${wkt}')`;
2510
+ }
2511
+ /**
2512
+ * ST_GeomFromGeoJSON - create geometry from GeoJSON
2513
+ */
2514
+ stGeomFromGeoJSON(geojson, srid) {
2515
+ if (srid) {
2516
+ return `ST_GeomFromGeoJSON('${geojson}'${srid})`;
2517
+ }
2518
+ return `ST_GeomFromGeoJSON('${geojson}')`;
2519
+ }
2520
+ /**
2521
+ * ST_AsGeoJSON - convert geometry to GeoJSON
2522
+ */
2523
+ stAsGeoJSON(geom, options) {
2524
+ let sql = `ST_AsGeoJSON(${geom}`;
2525
+ if (options?.precision !== undefined) {
2526
+ sql += `, ${options.precision}`;
2527
+ }
2528
+ if (options?.longCRS) {
2529
+ sql += ', 0, 1';
2530
+ }
2531
+ sql += ')';
2532
+ return sql;
2533
+ }
2534
+ /**
2535
+ * ST_AsText - convert geometry to WKT
2536
+ */
2537
+ stAsText(geom) {
2538
+ return `ST_AsText(${geom})`;
2539
+ }
2540
+ /**
2541
+ * ST_Centroid - get the centroid of a geometry
2542
+ */
2543
+ stCentroid(geom) {
2544
+ return `ST_Centroid(${geom})`;
2545
+ }
2546
+ /**
2547
+ * ST_Area - calculate the area of a polygon
2548
+ * For geography, returns area in square meters
2549
+ */
2550
+ stArea(geom, useGeography) {
2551
+ if (useGeography) {
2552
+ return `ST_Area(${geom}::geography)`;
2553
+ }
2554
+ return `ST_Area(${geom})`;
2555
+ }
2556
+ /**
2557
+ * ST_Length - calculate the length of a line
2558
+ * For geography, returns length in meters
2559
+ */
2560
+ stLength(geom, useGeography) {
2561
+ if (useGeography) {
2562
+ return `ST_Length(${geom}::geography)`;
2563
+ }
2564
+ return `ST_Length(${geom})`;
2565
+ }
2566
+ /**
2567
+ * ST_Point - create a point from coordinates
2568
+ */
2569
+ stPoint(long, lat) {
2570
+ return `ST_Point(${long}, ${lat})`;
2571
+ }
2572
+ /**
2573
+ * ST_SetSRID - set the SRID of a geometry
2574
+ */
2575
+ stSetSRID(geom, srid) {
2576
+ return `ST_SetSRID(${geom}, ${srid})`;
2577
+ }
2578
+ /**
2579
+ * ST_SRID - get the SRID of a geometry
2580
+ */
2581
+ stSRID(geom) {
2582
+ return `ST_SRID(${geom})`;
2583
+ }
2584
+ /**
2585
+ * ST_X - get the X coordinate of a point
2586
+ */
2587
+ stX(geom) {
2588
+ return `ST_X(${geom})`;
2589
+ }
2590
+ /**
2591
+ * ST_Y - get the Y coordinate of a point
2592
+ */
2593
+ stY(geom) {
2594
+ return `ST_Y(${geom})`;
2595
+ }
2596
+ /**
2597
+ * ST_Distance_Sphere - calculate distance using sphere (less accurate but faster)
2598
+ * Returns distance in meters
2599
+ */
2600
+ stDistanceSphere(geom1, geom2) {
2601
+ return `ST_Distance_Sphere(${geom1}, ${geom2})`;
2602
+ }
2603
+ /**
2604
+ * ST_Distance_Spheroid - calculate distance using spheroid (accurate)
2605
+ * Returns distance in meters
2606
+ */
2607
+ stDistanceSpheroid(geom1, geom2) {
2608
+ return `ST_Distance_Spheroid(${geom1}, ${geom2}, 'SPHEROID["WGS 84",6378137,298.257223563]')`;
2609
+ }
2610
+ /**
2611
+ * Drop an index from a table
2612
+ */
2613
+ async dropIndex(tableName, indexName, options) {
2614
+ let sql = 'DROP INDEX';
2615
+ if (options?.ifExists) {
2616
+ sql += ' IF EXISTS';
2617
+ }
2618
+ sql += ` ${this.escapeId(indexName)}`;
2619
+ if (options?.cascade) {
2620
+ sql += ' CASCADE';
2621
+ }
2622
+ await this.query(sql);
2623
+ }
2624
+ /**
2625
+ * Create a constraint on a table
2626
+ */
2627
+ async createConstraint(tableName, constraintDef) {
2628
+ const fields = constraintDef.fields?.map((f) => this.escapeId(f)).join(', ');
2629
+ switch (constraintDef.type) {
2630
+ case 'PRIMARY KEY':
2631
+ if (!fields) {
2632
+ throw new Error('Primary key constraint requires fields');
2633
+ }
2634
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintDef.name)} PRIMARY KEY (${fields})`);
2635
+ return;
2636
+ case 'UNIQUE':
2637
+ if (!fields) {
2638
+ throw new Error('Unique constraint requires fields');
2639
+ }
2640
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintDef.name)} UNIQUE (${fields})`);
2641
+ return;
2642
+ case 'FOREIGN KEY': {
2643
+ if (!fields || !constraintDef.references) {
2644
+ throw new Error('Foreign key constraint requires fields and references');
2645
+ }
2646
+ // Handle composite foreign keys (string | string[])
2647
+ const refField = constraintDef.references.field;
2648
+ const refFieldSql = Array.isArray(refField)
2649
+ ? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
2650
+ : `(${this.escapeId(refField)})`;
2651
+ let sql = `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintDef.name)} `;
2652
+ sql += `FOREIGN KEY (${fields}) REFERENCES ${this.escapeId(constraintDef.references.table)}${refFieldSql}`;
2653
+ if (constraintDef.references.onDelete) {
2654
+ sql += ` ON DELETE ${constraintDef.references.onDelete}`;
2655
+ }
2656
+ if (constraintDef.references.onUpdate) {
2657
+ sql += ` ON UPDATE ${constraintDef.references.onUpdate}`;
2658
+ }
2659
+ if (constraintDef.deferrable) {
2660
+ sql += ` DEFERRABLE ${constraintDef.deferrable}`;
2661
+ }
2662
+ await this.query(sql);
2663
+ return;
2664
+ }
2665
+ case 'CHECK':
2666
+ if (!constraintDef.check) {
2667
+ throw new Error('Check constraint requires a check expression');
2668
+ }
2669
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintDef.name)} CHECK (${constraintDef.check})`);
2670
+ return;
2671
+ default:
2672
+ throw new Error(`Unknown constraint type: ${constraintDef.type}`);
2673
+ }
2674
+ }
2675
+ /**
2676
+ * Drop a constraint from a table
2677
+ */
2678
+ async dropConstraint(tableName, constraintName, options) {
2679
+ let sql = 'ALTER TABLE';
2680
+ if (options?.ifExists) {
2681
+ sql += ' IF EXISTS';
2682
+ }
2683
+ sql += ` ${this.escapeId(tableName)} DROP CONSTRAINT ${this.escapeId(constraintName)}`;
2684
+ if (options?.cascade) {
2685
+ sql += ' CASCADE';
2686
+ }
2687
+ await this.query(sql);
2688
+ }
2689
+ /**
2690
+ * Begin a new transaction
2691
+ */
2692
+ async startTransaction(options) {
2693
+ if (!this.pool) {
2694
+ throw new Error('Not connected to database');
2695
+ }
2696
+ const client = await this.pool.connect();
2697
+ if (options?.isolationLevel) {
2698
+ const isolationLevelSql = this.getIsolationLevelSql(options.isolationLevel);
2699
+ if (isolationLevelSql) {
2700
+ await client.query(`BEGIN ${isolationLevelSql}`);
2701
+ }
2702
+ else {
2703
+ await client.query('BEGIN');
2704
+ }
2705
+ }
2706
+ else {
2707
+ await client.query('BEGIN');
2708
+ }
2709
+ const transaction = new CockroachDBTransaction(client, this.transactionDepth++, options);
2710
+ return transaction;
2711
+ }
2712
+ /**
2713
+ * Run a function inside a CockroachDB transaction, automatically retrying
2714
+ * the *entire transaction body* when it fails with a serialization failure
2715
+ * (SQLSTATE `40001`, surfaced as a "restart transaction" error).
2716
+ *
2717
+ * Under SERIALIZABLE isolation (CockroachDB's default and only isolation
2718
+ * level), conflicting concurrent transactions routinely abort with
2719
+ * `40001`; CockroachDB's documented client contract is to retry the whole
2720
+ * transaction from the beginning rather than treating it as a fatal error.
2721
+ * This implements that contract using the standard
2722
+ * `SAVEPOINT cockroach_restart` pattern: a savepoint is created right after
2723
+ * `BEGIN`, and on a retryable error the transaction is rolled back to that
2724
+ * savepoint (instead of being aborted outright) so `fn` can be re-run.
2725
+ *
2726
+ * @see https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference
2727
+ * @see https://www.cockroachlabs.com/docs/stable/transactions#client-side-intervention
2728
+ *
2729
+ * @param fn - Callback that receives the transaction and performs the work
2730
+ * to (re)try. It may be invoked more than once, so it should be
2731
+ * idempotent / avoid externally-visible side effects other than through
2732
+ * the database.
2733
+ * @param options - Transaction options, plus an optional `maxRetries`
2734
+ * (defaults to 5) capping how many times `fn` will be retried after a
2735
+ * `40001` before the error is rethrown.
2736
+ */
2737
+ async runTransaction(fn, options) {
2738
+ const maxRetries = options?.maxRetries ?? 5;
2739
+ const transaction = (await this.startTransaction(options));
2740
+ const client = transaction.client;
2741
+ if (!client) {
2742
+ throw new Error('Not connected to database');
2743
+ }
2744
+ const restartSavepoint = 'cockroach_restart';
2745
+ try {
2746
+ await transaction.createSavepoint(restartSavepoint);
2747
+ for (let attempt = 0;; attempt++) {
2748
+ try {
2749
+ const result = await fn(transaction);
2750
+ await transaction.releaseSavepoint(restartSavepoint);
2751
+ await client.query('COMMIT');
2752
+ transaction.finished = true;
2753
+ return result;
2754
+ }
2755
+ catch (error) {
2756
+ if (this.isRetryableError(error) && attempt < maxRetries) {
2757
+ await transaction.rollbackToSavepoint(restartSavepoint);
2758
+ continue;
2759
+ }
2760
+ try {
2761
+ await client.query('ROLLBACK');
2762
+ }
2763
+ catch {
2764
+ // The connection/transaction may already be unusable; ignore the
2765
+ // rollback failure and propagate the original error instead.
2766
+ }
2767
+ transaction.finished = true;
2768
+ throw error;
2769
+ }
2770
+ }
2771
+ }
2772
+ finally {
2773
+ transaction.finished = true;
2774
+ client.release();
2775
+ }
2776
+ }
2777
+ /**
2778
+ * Commit a transaction
2779
+ */
2780
+ async commitTransaction(transaction) {
2781
+ const postgresTx = transaction;
2782
+ if (postgresTx.client) {
2783
+ await postgresTx.client.query('COMMIT');
2784
+ postgresTx.client.release();
2785
+ postgresTx.finished = true;
2786
+ }
2787
+ }
2788
+ /**
2789
+ * Rollback a transaction
2790
+ */
2791
+ async rollbackTransaction(transaction) {
2792
+ const postgresTx = transaction;
2793
+ if (postgresTx.client) {
2794
+ await postgresTx.client.query('ROLLBACK');
2795
+ postgresTx.client.release();
2796
+ postgresTx.finished = true;
2797
+ }
2798
+ }
2799
+ /**
2800
+ * Get the SQL for a data type
2801
+ */
2802
+ getDataTypeSql(dataType) {
2803
+ if (typeof dataType === 'string') {
2804
+ return dataType;
2805
+ }
2806
+ if (!dataType || typeof dataType !== 'object') {
2807
+ return 'VARCHAR(255)';
2808
+ }
2809
+ const dt = dataType;
2810
+ switch (dt.key) {
2811
+ case 'STRING':
2812
+ return `VARCHAR(${dt.length || 255})`;
2813
+ case 'CHAR':
2814
+ return `CHAR(${dt.length || 1})`;
2815
+ case 'TEXT':
2816
+ if (dt.length) {
2817
+ if (dt.length === 256)
2818
+ return 'TEXT';
2819
+ if (dt.length === 65535)
2820
+ return 'TEXT';
2821
+ return `VARCHAR(${dt.length})`;
2822
+ }
2823
+ return 'TEXT';
2824
+ case 'INTEGER':
2825
+ // Handle SERIAL for auto-increment
2826
+ if (dt.autoIncrement) {
2827
+ return 'SERIAL';
2828
+ }
2829
+ return 'INTEGER';
2830
+ case 'BIGINT':
2831
+ if (dt.autoIncrement) {
2832
+ return 'BIGSERIAL';
2833
+ }
2834
+ return 'BIGINT';
2835
+ case 'FLOAT':
2836
+ return 'REAL';
2837
+ case 'DOUBLE':
2838
+ return 'DOUBLE PRECISION';
2839
+ case 'DECIMAL':
2840
+ return `DECIMAL(${dt.precision || 10},${dt.scale || 0})`;
2841
+ case 'BOOLEAN':
2842
+ return 'BOOLEAN';
2843
+ case 'DATE':
2844
+ return dt.precision ? `TIMESTAMP(${dt.precision})` : 'TIMESTAMP';
2845
+ case 'DATEONLY':
2846
+ return 'DATE';
2847
+ case 'TIME':
2848
+ return dt.precision ? `TIME(${dt.precision})` : 'TIME';
2849
+ case 'BLOB':
2850
+ return 'BYTEA';
2851
+ case 'ENUM':
2852
+ const values = dt.values || [];
2853
+ return `ENUM(${values.map((v) => `'${v}'`).join(',')})`;
2854
+ case 'JSON':
2855
+ return 'JSON';
2856
+ case 'JSONB':
2857
+ return 'JSONB';
2858
+ case 'UUID':
2859
+ return 'UUID';
2860
+ case 'HSTORE':
2861
+ return 'HSTORE';
2862
+ case 'RANGE':
2863
+ const rangeSubtype = dt.subtype || 'int4range';
2864
+ return rangeSubtype;
2865
+ case 'INET':
2866
+ return 'INET';
2867
+ case 'CIDR':
2868
+ return 'CIDR';
2869
+ case 'MACADDR':
2870
+ return 'MACADDR';
2871
+ case 'GEOMETRY':
2872
+ const geoType = dt.type || 'GEOMETRY';
2873
+ const srid = dt.srid;
2874
+ if (srid) {
2875
+ return `${geoType}(${srid})`;
2876
+ }
2877
+ return geoType;
2878
+ case 'GEOGRAPHY':
2879
+ const geoogType = dt.type || 'GEOGRAPHY';
2880
+ const geoSrid = dt.srid || 4326;
2881
+ return `${geoogType}(POINT, ${geoSrid})`;
2882
+ case 'ARRAY':
2883
+ // Support both 'type' (for DataTypes.ARRAY(DataTypes.INTEGER)) and 'subtype' (for { type: 'ARRAY', subtype: 'INTEGER' })
2884
+ const arrayType = dt.type || dt.subtype;
2885
+ if (arrayType) {
2886
+ const baseType = this.getDataTypeSql(arrayType);
2887
+ return `${baseType}[]`;
2888
+ }
2889
+ return 'ARRAY';
2890
+ case 'GEOGRAPHY':
2891
+ const geogType = dt.type || 'GEOGRAPHY';
2892
+ const geogSrid = dt.srid || 4326;
2893
+ return `${geogType}(${geogSrid})`;
2894
+ case 'VIRTUAL':
2895
+ // Virtual fields don't create a database column - return empty string
2896
+ return '';
2897
+ default:
2898
+ return 'VARCHAR(255)';
2899
+ }
2900
+ }
2901
+ /**
2902
+ * Get the SQL for an ARRAY type
2903
+ */
2904
+ getArrayTypeSql(elementType) {
2905
+ return `${this.getDataTypeSql(elementType)}[]`;
2906
+ }
2907
+ /**
2908
+ * Get the isolation level SQL
2909
+ */
2910
+ getIsolationLevelSql(isolationLevel) {
2911
+ if (!isolationLevel) {
2912
+ return '';
2913
+ }
2914
+ const levels = {
2915
+ 'READ UNCOMMITTED': 'READ UNCOMMITTED',
2916
+ 'READ COMMITTED': 'READ COMMITTED',
2917
+ 'REPEATABLE READ': 'REPEATABLE READ',
2918
+ SERIALIZABLE: 'SERIALIZABLE',
2919
+ };
2920
+ const level = levels[isolationLevel];
2921
+ return level ? `ISOLATION LEVEL ${level}` : '';
2922
+ }
2923
+ /**
2924
+ * Build a WHERE clause from a WhereOptions object
2925
+ */
2926
+ buildWhereClause(where, options) {
2927
+ const values = [];
2928
+ if (!where || Object.keys(where).length === 0) {
2929
+ return { sql: '', values };
2930
+ }
2931
+ const buildCondition = (condition) => {
2932
+ if (!condition) {
2933
+ return { sql: '', values: [] };
2934
+ }
2935
+ if (typeof condition !== 'object') {
2936
+ values.push(condition);
2937
+ return { sql: '$' + values.length, values: [condition] };
2938
+ }
2939
+ const cond = condition;
2940
+ // Handle logical operators
2941
+ if (cond.$and || cond.$or || cond.$not) {
2942
+ const conditions = [];
2943
+ if (cond.$and) {
2944
+ const andConditions = cond.$and.map((c) => {
2945
+ const result = buildCondition(c);
2946
+ return result.sql;
2947
+ });
2948
+ conditions.push(`(${andConditions.join(' AND ')})`);
2949
+ }
2950
+ if (cond.$or) {
2951
+ const orConditions = cond.$or.map((c) => {
2952
+ const result = buildCondition(c);
2953
+ return result.sql;
2954
+ });
2955
+ conditions.push(`(${orConditions.join(' OR ')})`);
2956
+ }
2957
+ if (cond.$not) {
2958
+ const result = buildCondition(cond.$not);
2959
+ conditions.push(`NOT (${result.sql})`);
2960
+ }
2961
+ return { sql: conditions.join(' AND '), values };
2962
+ }
2963
+ // Handle field conditions
2964
+ const fieldConditions = [];
2965
+ for (const [key, value] of Object.entries(cond)) {
2966
+ if (key.startsWith('$'))
2967
+ continue;
2968
+ if (value && typeof value === 'object') {
2969
+ const valueObj = value;
2970
+ if (valueObj.$eq !== undefined) {
2971
+ values.push(valueObj.$eq);
2972
+ fieldConditions.push(`${this.escapeId(key)} = $${values.length}`);
2973
+ }
2974
+ else if (valueObj.$ne !== undefined) {
2975
+ values.push(valueObj.$ne);
2976
+ fieldConditions.push(`${this.escapeId(key)} != $${values.length}`);
2977
+ }
2978
+ else if (valueObj.$gt !== undefined) {
2979
+ values.push(valueObj.$gt);
2980
+ fieldConditions.push(`${this.escapeId(key)} > $${values.length}`);
2981
+ }
2982
+ else if (valueObj.$gte !== undefined) {
2983
+ values.push(valueObj.$gte);
2984
+ fieldConditions.push(`${this.escapeId(key)} >= $${values.length}`);
2985
+ }
2986
+ else if (valueObj.$lt !== undefined) {
2987
+ values.push(valueObj.$lt);
2988
+ fieldConditions.push(`${this.escapeId(key)} < $${values.length}`);
2989
+ }
2990
+ else if (valueObj.$lte !== undefined) {
2991
+ values.push(valueObj.$lte);
2992
+ fieldConditions.push(`${this.escapeId(key)} <= $${values.length}`);
2993
+ }
2994
+ else if (valueObj.$like !== undefined) {
2995
+ values.push(valueObj.$like);
2996
+ fieldConditions.push(`${this.escapeId(key)} LIKE $${values.length}`);
2997
+ }
2998
+ else if (valueObj.$notLike !== undefined) {
2999
+ values.push(valueObj.$notLike);
3000
+ fieldConditions.push(`${this.escapeId(key)} NOT LIKE $${values.length}`);
3001
+ }
3002
+ else if (valueObj.$iLike !== undefined) {
3003
+ values.push(valueObj.$iLike);
3004
+ fieldConditions.push(`${this.escapeId(key)} ILIKE $${values.length}`);
3005
+ }
3006
+ else if (valueObj.$notILike !== undefined) {
3007
+ values.push(valueObj.$notILike);
3008
+ fieldConditions.push(`${this.escapeId(key)} NOT ILIKE $${values.length}`);
3009
+ }
3010
+ else if (valueObj.$startsWith !== undefined) {
3011
+ values.push(valueObj.$startsWith + '%');
3012
+ fieldConditions.push(`${this.escapeId(key)} ILIKE $${values.length}`);
3013
+ }
3014
+ else if (valueObj.$notStartsWith !== undefined) {
3015
+ values.push(valueObj.$notStartsWith + '%');
3016
+ fieldConditions.push(`${this.escapeId(key)} NOT ILIKE $${values.length}`);
3017
+ }
3018
+ else if (valueObj.$endsWith !== undefined) {
3019
+ values.push('%' + valueObj.$endsWith);
3020
+ fieldConditions.push(`${this.escapeId(key)} ILIKE $${values.length}`);
3021
+ }
3022
+ else if (valueObj.$notEndsWith !== undefined) {
3023
+ values.push('%' + valueObj.$notEndsWith);
3024
+ fieldConditions.push(`${this.escapeId(key)} NOT ILIKE $${values.length}`);
3025
+ }
3026
+ else if (valueObj.$substring !== undefined) {
3027
+ values.push('%' + valueObj.$substring + '%');
3028
+ fieldConditions.push(`${this.escapeId(key)} ILIKE $${values.length}`);
3029
+ }
3030
+ else if (valueObj.$notSubstring !== undefined) {
3031
+ values.push('%' + valueObj.$notSubstring + '%');
3032
+ fieldConditions.push(`${this.escapeId(key)} NOT ILIKE $${values.length}`);
3033
+ }
3034
+ else if (valueObj.$any !== undefined) {
3035
+ const anyValues = Array.isArray(valueObj.$any) ? valueObj.$any : [valueObj.$any];
3036
+ values.push(anyValues);
3037
+ fieldConditions.push(`${this.escapeId(key)} = ANY($${values.length})`);
3038
+ }
3039
+ else if (valueObj.$all !== undefined) {
3040
+ const allValues = Array.isArray(valueObj.$all) ? valueObj.$all : [valueObj.$all];
3041
+ values.push(allValues);
3042
+ fieldConditions.push(`${this.escapeId(key)} = ALL($${values.length})`);
3043
+ }
3044
+ else if (valueObj.$in) {
3045
+ const inValues = valueObj.$in;
3046
+ values.push(...inValues);
3047
+ const placeholders = inValues
3048
+ .map((_, i) => `$${values.length - inValues.length + i + 1}`)
3049
+ .join(',');
3050
+ fieldConditions.push(`${this.escapeId(key)} IN (${placeholders})`);
3051
+ }
3052
+ else if (valueObj.$notIn) {
3053
+ const notInValues = valueObj.$notIn;
3054
+ values.push(...notInValues);
3055
+ const placeholders = notInValues
3056
+ .map((_, i) => `$${values.length - notInValues.length + i + 1}`)
3057
+ .join(',');
3058
+ fieldConditions.push(`${this.escapeId(key)} NOT IN (${placeholders})`);
3059
+ }
3060
+ else if (valueObj.$between) {
3061
+ const between = valueObj.$between;
3062
+ values.push(between[0], between[1]);
3063
+ fieldConditions.push(`${this.escapeId(key)} BETWEEN $${values.length - 1} AND $${values.length}`);
3064
+ }
3065
+ else if (valueObj.$notBetween) {
3066
+ const notBetween = valueObj.$notBetween;
3067
+ values.push(notBetween[0], notBetween[1]);
3068
+ fieldConditions.push(`${this.escapeId(key)} NOT BETWEEN $${values.length - 1} AND $${values.length}`);
3069
+ }
3070
+ else if (valueObj.$isNull !== undefined) {
3071
+ if (valueObj.$isNull) {
3072
+ fieldConditions.push(`${this.escapeId(key)} IS NULL`);
3073
+ }
3074
+ else {
3075
+ fieldConditions.push(`${this.escapeId(key)} IS NOT NULL`);
3076
+ }
3077
+ }
3078
+ else if (valueObj.$exists !== undefined) {
3079
+ if (valueObj.$exists) {
3080
+ fieldConditions.push(`EXISTS (${valueObj.$query})`);
3081
+ }
3082
+ else {
3083
+ fieldConditions.push(`NOT EXISTS (${valueObj.$query})`);
3084
+ }
3085
+ }
3086
+ else if (valueObj.$jsonConcat !== undefined) {
3087
+ // JSONB concatenation (||)
3088
+ values.push(JSON.stringify(valueObj.$jsonConcat));
3089
+ fieldConditions.push(`${this.escapeId(key)} || $${values.length}`);
3090
+ }
3091
+ else if (valueObj.$jsonDelete !== undefined) {
3092
+ // JSONB delete key (-)
3093
+ fieldConditions.push(`${this.escapeId(key)} - '${valueObj.$jsonDelete}'`);
3094
+ }
3095
+ else if (valueObj.$jsonDeletePath !== undefined) {
3096
+ // JSONB delete by path (#-)
3097
+ const deletePath = valueObj.$jsonDeletePath;
3098
+ const pgPath = Array.isArray(deletePath)
3099
+ ? `{${deletePath.join(', ')}}`
3100
+ : `{${deletePath}}`;
3101
+ fieldConditions.push(`${this.escapeId(key)} #- '${pgPath}'`);
3102
+ }
3103
+ else if (valueObj.$jsonPathExists !== undefined) {
3104
+ // JSON path exists (@?)
3105
+ fieldConditions.push(`${this.escapeId(key)} @? '${valueObj.$jsonPathExists}'`);
3106
+ }
3107
+ else if (valueObj.$jsonPathQuery !== undefined) {
3108
+ // JSON path query (@@)
3109
+ fieldConditions.push(`${this.escapeId(key)} @@ '${valueObj.$jsonPathQuery}'`);
3110
+ }
3111
+ else if (valueObj.$jsonTypeOf !== undefined) {
3112
+ // JSON type of (json_typeof)
3113
+ values.push(valueObj.$jsonTypeOf);
3114
+ fieldConditions.push(`json_typeof(${this.escapeId(key)}) = $${values.length}`);
3115
+ }
3116
+ else {
3117
+ values.push(value);
3118
+ fieldConditions.push(`${this.escapeId(key)} = $${values.length}`);
3119
+ }
3120
+ }
3121
+ else {
3122
+ values.push(value);
3123
+ fieldConditions.push(`${this.escapeId(key)} = $${values.length}`);
3124
+ }
3125
+ }
3126
+ return { sql: fieldConditions.join(' AND '), values };
3127
+ };
3128
+ const result = buildCondition(where);
3129
+ return { sql: result.sql, values: result.values };
3130
+ }
3131
+ /**
3132
+ * Build an ORDER BY clause
3133
+ */
3134
+ buildOrderClause(order, options) {
3135
+ const orderArray = order;
3136
+ if (!orderArray || !Array.isArray(orderArray) || orderArray.length === 0) {
3137
+ return '';
3138
+ }
3139
+ const orderParts = [];
3140
+ for (const item of orderArray) {
3141
+ if (Array.isArray(item)) {
3142
+ const field = typeof item[0] === 'string' ? this.escapeId(item[0]) : item[0];
3143
+ const direction = item[1] ? ` ${item[1]}` : '';
3144
+ orderParts.push(`${field}${direction}`);
3145
+ }
3146
+ else if (typeof item === 'string') {
3147
+ orderParts.push(item);
3148
+ }
3149
+ else if (item && typeof item === 'object') {
3150
+ const itemArr = item;
3151
+ const [modelOrString, field] = itemArr;
3152
+ const model = typeof modelOrString === 'string'
3153
+ ? modelOrString
3154
+ : modelOrString.tableName || '';
3155
+ const fieldStr = typeof field === 'string' ? field : '';
3156
+ orderParts.push(`${model ? `${this.escapeId(model)}.` : ''}${this.escapeId(fieldStr)}`);
3157
+ }
3158
+ }
3159
+ return orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
3160
+ }
3161
+ /**
3162
+ * Build a LIMIT/OFFSET clause
3163
+ */
3164
+ buildLimitOffset(limit, offset) {
3165
+ let sql = '';
3166
+ if (limit !== undefined) {
3167
+ sql += ` LIMIT ${Number(limit)}`;
3168
+ if (offset !== undefined) {
3169
+ sql += ` OFFSET ${Number(offset)}`;
3170
+ }
3171
+ }
3172
+ return sql;
3173
+ }
3174
+ /**
3175
+ * Build an INSERT query with RETURNING support
3176
+ */
3177
+ buildInsertQuery(tableName, values, options) {
3178
+ const columns = Object.keys(values);
3179
+ const processedValues = [];
3180
+ const placeholders = [];
3181
+ let paramIndex = 1;
3182
+ // Process each value - handle Literal differently
3183
+ for (const value of Object.values(values)) {
3184
+ if (value instanceof prorm_1.Literal) {
3185
+ // For Literal values, inline the SQL directly
3186
+ placeholders.push(value.val);
3187
+ }
3188
+ else {
3189
+ // For regular values, use parameterized placeholder
3190
+ placeholders.push(`$${paramIndex}`);
3191
+ processedValues.push(value);
3192
+ paramIndex++;
3193
+ }
3194
+ }
3195
+ // CockroachDB's native `UPSERT INTO` shorthand replaces the row matching
3196
+ // the primary key wholesale and needs no conflict target, unlike
3197
+ // `INSERT ... ON CONFLICT ... DO UPDATE`.
3198
+ // See: https://www.cockroachlabs.com/docs/stable/upsert
3199
+ const useNativeUpsert = !!(options?.upsert && options.nativeUpsert);
3200
+ let sql = `${useNativeUpsert ? 'UPSERT' : 'INSERT'} INTO ${this.escapeId(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${placeholders.join(', ')})`;
3201
+ // Handle upsert (ON CONFLICT), unless the native UPSERT shorthand above
3202
+ // already provides replace-on-conflict semantics.
3203
+ if (options?.upsert && !useNativeUpsert) {
3204
+ const updateColumns = columns
3205
+ .map((c) => `${this.escapeId(c)} = excluded.${this.escapeId(c)}`)
3206
+ .join(', ');
3207
+ const conflictFields = options.conflictFields || columns;
3208
+ sql += ` ON CONFLICT (${conflictFields.map((c) => this.escapeId(c)).join(', ')}) DO UPDATE SET ${updateColumns}`;
3209
+ }
3210
+ // Handle RETURNING clause
3211
+ if (options?.returning) {
3212
+ if (options.returning === true) {
3213
+ sql += ' RETURNING *';
3214
+ }
3215
+ else if (Array.isArray(options.returning)) {
3216
+ sql += ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
3217
+ }
3218
+ }
3219
+ return { sql, values: processedValues };
3220
+ }
3221
+ /**
3222
+ * Build an UPSERT query for CockroachDB
3223
+ * Uses `ON CONFLICT ... DO UPDATE SET` syntax by default, or CockroachDB's
3224
+ * native `UPSERT INTO ... VALUES (...)` shorthand when `options.nativeUpsert`
3225
+ * is set (which replaces the row matching the primary key wholesale and
3226
+ * needs no conflict target).
3227
+ * @see https://www.cockroachlabs.com/docs/stable/upsert
3228
+ */
3229
+ buildUpsertQuery(tableName, values, options) {
3230
+ const columns = Object.keys(values);
3231
+ const placeholders = columns.map((_, i) => `$${i + 1}`).join(', ');
3232
+ const queryValues = Object.values(values);
3233
+ const useNativeUpsert = !!options?.nativeUpsert;
3234
+ let sql = `${useNativeUpsert ? 'UPSERT' : 'INSERT'} INTO ${this.escapeId(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${placeholders})`;
3235
+ if (!useNativeUpsert) {
3236
+ // Determine which fields to update
3237
+ const updateFields = options?.updateOnDuplicate && options.updateOnDuplicate.length > 0
3238
+ ? options.updateOnDuplicate
3239
+ : columns;
3240
+ // Add ON CONFLICT clause
3241
+ const conflictFields = options?.conflictFields || columns;
3242
+ const updateClauses = updateFields
3243
+ .map((c) => `${this.escapeId(c)} = excluded.${this.escapeId(c)}`)
3244
+ .join(', ');
3245
+ sql += ` ON CONFLICT (${conflictFields.map((c) => this.escapeId(c)).join(', ')}) DO UPDATE SET ${updateClauses}`;
3246
+ }
3247
+ // Handle RETURNING clause
3248
+ if (options?.returning) {
3249
+ if (options.returning === true) {
3250
+ sql += ' RETURNING *';
3251
+ }
3252
+ else if (Array.isArray(options.returning)) {
3253
+ sql += ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
3254
+ }
3255
+ }
3256
+ return { sql, values: queryValues };
3257
+ }
3258
+ /**
3259
+ * Build an increment query
3260
+ * @param tableName - Table name
3261
+ * @param fields - Fields to increment
3262
+ * @param where - Where clause
3263
+ * @param options - Query options (by: number)
3264
+ */
3265
+ buildIncrementQuery(tableName, fields, where, options) {
3266
+ const by = options?.by ?? 1;
3267
+ const setClauses = [];
3268
+ const queryValues = [];
3269
+ let paramIndex = 1;
3270
+ // Handle different field formats
3271
+ if (typeof fields === 'string') {
3272
+ // Single field: increment('count')
3273
+ setClauses.push(`${this.escapeId(fields)} = ${this.escapeId(fields)} + $${paramIndex}`);
3274
+ queryValues.push(by);
3275
+ paramIndex++;
3276
+ }
3277
+ else if (Array.isArray(fields)) {
3278
+ // Array of fields: increment(['count', 'value'], { by: 5 })
3279
+ for (const field of fields) {
3280
+ setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + $${paramIndex}`);
3281
+ queryValues.push(by);
3282
+ paramIndex++;
3283
+ }
3284
+ }
3285
+ else {
3286
+ // Object: increment({ count: 1, value: 10 })
3287
+ for (const [field, value] of Object.entries(fields)) {
3288
+ setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + $${paramIndex}`);
3289
+ queryValues.push(value);
3290
+ paramIndex++;
3291
+ }
3292
+ }
3293
+ // Build WHERE clause
3294
+ const whereClause = this.buildWhereClause(where);
3295
+ // Adjust parameter indices for where clause
3296
+ const adjustedWhereClause = {
3297
+ sql: whereClause.sql.replace(/\$(\d+)/g, (_, num) => `$${paramIndex + parseInt(num, 10) - 1}`),
3298
+ values: whereClause.values,
3299
+ };
3300
+ queryValues.push(...adjustedWhereClause.values);
3301
+ const sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')} WHERE ${adjustedWhereClause.sql}`;
3302
+ return { sql, values: queryValues };
3303
+ }
3304
+ /**
3305
+ * Build an UPDATE query with RETURNING support
3306
+ */
3307
+ buildUpdateQuery(tableName, values, where, options) {
3308
+ const setClauses = [];
3309
+ const queryValues = [];
3310
+ let paramIndex = 1;
3311
+ for (const [key, value] of Object.entries(values)) {
3312
+ if (value instanceof prorm_1.Literal) {
3313
+ // For Literal values, inline the SQL directly
3314
+ setClauses.push(`${this.escapeId(key)} = ${value.val}`);
3315
+ }
3316
+ else {
3317
+ // For regular values, use parameterized placeholder
3318
+ setClauses.push(`${this.escapeId(key)} = $${paramIndex}`);
3319
+ queryValues.push(value);
3320
+ paramIndex++;
3321
+ }
3322
+ }
3323
+ const whereClause = this.buildWhereClause(where);
3324
+ // Adjust parameter indices for where clause
3325
+ const adjustedWhereClause = {
3326
+ sql: whereClause.sql,
3327
+ values: [...queryValues, ...whereClause.values],
3328
+ };
3329
+ let sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')}`;
3330
+ if (adjustedWhereClause.sql) {
3331
+ sql += ` WHERE ${adjustedWhereClause.sql}`;
3332
+ }
3333
+ if (options?.limit) {
3334
+ sql += ` LIMIT ${options.limit}`;
3335
+ }
3336
+ // Handle RETURNING clause
3337
+ if (options?.returning) {
3338
+ if (options.returning === true) {
3339
+ sql += ' RETURNING *';
3340
+ }
3341
+ else if (Array.isArray(options.returning)) {
3342
+ sql += ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
3343
+ }
3344
+ }
3345
+ return { sql, values: adjustedWhereClause.values };
3346
+ }
3347
+ /**
3348
+ * Build a DELETE query with RETURNING support
3349
+ */
3350
+ buildDeleteQuery(tableName, where, options) {
3351
+ const whereClause = this.buildWhereClause(where);
3352
+ let sql = `DELETE FROM ${this.escapeId(tableName)}`;
3353
+ if (whereClause.sql) {
3354
+ sql += ` WHERE ${whereClause.sql}`;
3355
+ }
3356
+ if (options?.limit) {
3357
+ sql += ` LIMIT ${options.limit}`;
3358
+ }
3359
+ // Handle RETURNING clause
3360
+ if (options?.returning) {
3361
+ if (options.returning === true) {
3362
+ sql += ' RETURNING *';
3363
+ }
3364
+ else if (Array.isArray(options.returning)) {
3365
+ sql += ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
3366
+ }
3367
+ }
3368
+ // Handle TRUNCATE
3369
+ if (options?.truncate) {
3370
+ const pgOptions = options;
3371
+ sql = `TRUNCATE ${this.escapeId(tableName)}`;
3372
+ if (pgOptions.restartIdentity) {
3373
+ sql += ' RESTART IDENTITY';
3374
+ }
3375
+ if (pgOptions.cascade) {
3376
+ sql += ' CASCADE';
3377
+ }
3378
+ }
3379
+ return { sql, values: whereClause.values };
3380
+ }
3381
+ /**
3382
+ * Check if a value is a function expression (fn('COUNT', ...))
3383
+ */
3384
+ isFnExpression(value) {
3385
+ return value !== null && typeof value === 'object' && value.__type === 'fn';
3386
+ }
3387
+ /**
3388
+ * Check if a value is a column expression (col('name'))
3389
+ */
3390
+ isColExpression(value) {
3391
+ return value !== null && typeof value === 'object' && value.__type === 'col';
3392
+ }
3393
+ /**
3394
+ * Check if a value is a literal expression (literal('sql'))
3395
+ */
3396
+ isLiteralExpression(value) {
3397
+ return value !== null && typeof value === 'object' && value.__type === 'literal';
3398
+ }
3399
+ /**
3400
+ * Compile a function expression to SQL
3401
+ */
3402
+ compileFnExpression(fnExpr) {
3403
+ const args = fnExpr.args.map((arg) => {
3404
+ if (this.isColExpression(arg)) {
3405
+ return this.escapeId(arg.col);
3406
+ }
3407
+ else if (typeof arg === 'string') {
3408
+ return this.escape(arg);
3409
+ }
3410
+ else if (this.isFnExpression(arg)) {
3411
+ return this.compileFnExpression(arg);
3412
+ }
3413
+ else {
3414
+ return String(arg);
3415
+ }
3416
+ });
3417
+ return `${fnExpr.fn}(${args.join(', ')})`;
3418
+ }
3419
+ /**
3420
+ * Build SELECT clause with support for function expressions and aliases
3421
+ * Handles: 'field', ['field'], ['field', 'alias'], [fn('COUNT', 'id'), 'count']
3422
+ */
3423
+ buildSelectClause(attributes) {
3424
+ if (!attributes)
3425
+ return '*';
3426
+ if (Array.isArray(attributes)) {
3427
+ const parts = attributes.map((attr) => {
3428
+ // Handle array format: [expression, alias] or [fn, alias]
3429
+ if (Array.isArray(attr)) {
3430
+ const [expr, alias] = attr;
3431
+ let sql;
3432
+ if (this.isFnExpression(expr)) {
3433
+ sql = this.compileFnExpression(expr);
3434
+ }
3435
+ else if (this.isColExpression(expr)) {
3436
+ sql = this.escapeId(expr.col);
3437
+ }
3438
+ else if (this.isLiteralExpression(expr)) {
3439
+ sql = expr.sql;
3440
+ }
3441
+ else if (typeof expr === 'string') {
3442
+ // Check if it's a column reference or literal SQL
3443
+ if (expr.includes('(') || expr.includes(' ')) {
3444
+ sql = expr; // Literal SQL
3445
+ }
3446
+ else {
3447
+ sql = this.escapeId(expr);
3448
+ }
3449
+ }
3450
+ else {
3451
+ sql = String(expr);
3452
+ }
3453
+ return `${sql} AS ${this.escapeId(alias)}`;
3454
+ }
3455
+ else if (this.isFnExpression(attr)) {
3456
+ // Function without alias - let DB assign a name
3457
+ return this.compileFnExpression(attr);
3458
+ }
3459
+ else if (this.isColExpression(attr)) {
3460
+ return this.escapeId(attr.col);
3461
+ }
3462
+ else if (this.isLiteralExpression(attr)) {
3463
+ return attr.sql;
3464
+ }
3465
+ else if (typeof attr === 'string') {
3466
+ // Plain column name
3467
+ return this.escapeId(attr);
3468
+ }
3469
+ return String(attr);
3470
+ });
3471
+ return parts.join(', ');
3472
+ }
3473
+ else if (attributes.include) {
3474
+ return attributes.include.map((a) => this.escapeId(a)).join(', ');
3475
+ }
3476
+ else if (attributes.exclude) {
3477
+ // CockroachDB supports EXCEPT
3478
+ return `* EXCEPT (${attributes.exclude.map((a) => this.escapeId(a)).join(', ')})`;
3479
+ }
3480
+ return '*';
3481
+ }
3482
+ /**
3483
+ * Build GROUP BY clause with support for function expressions and aliases
3484
+ * Handles: 'field', ['field1', 'field2'], [[fn('COUNT', 'id'), 'count']]
3485
+ */
3486
+ buildGroupByClause(group) {
3487
+ if (!group)
3488
+ return '';
3489
+ const groupArray = Array.isArray(group) ? group : [group];
3490
+ if (groupArray.length === 0)
3491
+ return '';
3492
+ const parts = groupArray.map((field) => {
3493
+ if (Array.isArray(field)) {
3494
+ // Array format: [expression, alias] or [fn, alias]
3495
+ const [expr, alias] = field;
3496
+ let sql;
3497
+ if (this.isFnExpression(expr)) {
3498
+ sql = this.compileFnExpression(expr);
3499
+ }
3500
+ else if (this.isColExpression(expr)) {
3501
+ sql = this.escapeId(expr.col);
3502
+ }
3503
+ else if (this.isLiteralExpression(expr)) {
3504
+ sql = expr.sql;
3505
+ }
3506
+ else if (typeof expr === 'string') {
3507
+ sql = this.escapeId(expr);
3508
+ }
3509
+ else {
3510
+ sql = String(expr);
3511
+ }
3512
+ // Alias is optional in GROUP BY
3513
+ if (alias) {
3514
+ return `${sql} AS ${this.escapeId(alias)}`;
3515
+ }
3516
+ return sql;
3517
+ }
3518
+ else if (this.isFnExpression(field)) {
3519
+ return this.compileFnExpression(field);
3520
+ }
3521
+ else if (this.isColExpression(field)) {
3522
+ return this.escapeId(field.col);
3523
+ }
3524
+ else if (this.isLiteralExpression(field)) {
3525
+ return field.sql;
3526
+ }
3527
+ else if (typeof field === 'string') {
3528
+ if (field.includes('.')) {
3529
+ return field
3530
+ .split('.')
3531
+ .map((part) => this.escapeId(part))
3532
+ .join('.');
3533
+ }
3534
+ return this.escapeId(field);
3535
+ }
3536
+ return String(field);
3537
+ });
3538
+ return `GROUP BY ${parts.join(', ')}`;
3539
+ }
3540
+ /**
3541
+ * Build a WHERE clause condition using standard operators (Op.gt, Op.eq, etc.)
3542
+ * CockroachDB uses $1, $2, etc. for parameter placeholders
3543
+ */
3544
+ buildCondition(condition, values, paramPrefix = '$') {
3545
+ if (!condition || (typeof condition === 'object' && Object.keys(condition).length === 0)) {
3546
+ return '';
3547
+ }
3548
+ // Handle logical operators
3549
+ if ('$and' in condition) {
3550
+ const andParts = condition.$and.map((c) => this.buildCondition(c, values, paramPrefix));
3551
+ return `(${andParts.join(' AND ')})`;
3552
+ }
3553
+ if ('$or' in condition) {
3554
+ const orParts = condition.$or.map((c) => this.buildCondition(c, values, paramPrefix));
3555
+ return `(${orParts.join(' OR ')})`;
3556
+ }
3557
+ if ('$not' in condition) {
3558
+ return `NOT (${this.buildCondition(condition.$not, values, paramPrefix)})`;
3559
+ }
3560
+ // Handle regular conditions
3561
+ const conditions = [];
3562
+ for (const [key, value] of Object.entries(condition)) {
3563
+ if (key.startsWith('$'))
3564
+ continue; // Skip logical operators
3565
+ let sql = '';
3566
+ const paramIndex = values.length + 1;
3567
+ // Check if value is an operator object
3568
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
3569
+ const opKeys = Object.keys(value);
3570
+ if (opKeys.length === 1) {
3571
+ const opKey = opKeys[0];
3572
+ const opValue = value[opKey];
3573
+ // Map Op symbol to string
3574
+ if (opKey === 'Symbol(eq)' || opKey === 'Symbol(operators.eq)') {
3575
+ sql = `${this.escapeId(key)} = ${paramPrefix}${paramIndex}`;
3576
+ values.push(opValue);
3577
+ }
3578
+ else if (opKey === 'Symbol(ne)' || opKey === 'Symbol(operators.ne)') {
3579
+ sql = `${this.escapeId(key)} != ${paramPrefix}${paramIndex}`;
3580
+ values.push(opValue);
3581
+ }
3582
+ else if (opKey === 'Symbol(gt)' || opKey === 'Symbol(operators.gt)') {
3583
+ sql = `${this.escapeId(key)} > ${paramPrefix}${paramIndex}`;
3584
+ values.push(opValue);
3585
+ }
3586
+ else if (opKey === 'Symbol(gte)' || opKey === 'Symbol(operators.gte)') {
3587
+ sql = `${this.escapeId(key)} >= ${paramPrefix}${paramIndex}`;
3588
+ values.push(opValue);
3589
+ }
3590
+ else if (opKey === 'Symbol(lt)' || opKey === 'Symbol(operators.lt)') {
3591
+ sql = `${this.escapeId(key)} < ${paramPrefix}${paramIndex}`;
3592
+ values.push(opValue);
3593
+ }
3594
+ else if (opKey === 'Symbol(lte)' || opKey === 'Symbol(operators.lte)') {
3595
+ sql = `${this.escapeId(key)} <= ${paramPrefix}${paramIndex}`;
3596
+ values.push(opValue);
3597
+ }
3598
+ else if (opKey === 'Symbol(in)' || opKey === 'Symbol(operators.in)') {
3599
+ const placeholders = opValue
3600
+ .map((_, i) => `${paramPrefix}${paramIndex + i}`)
3601
+ .join(', ');
3602
+ sql = `${this.escapeId(key)} IN (${placeholders})`;
3603
+ values.push(...opValue);
3604
+ }
3605
+ else if (opKey === 'Symbol(notIn)' || opKey === 'Symbol(operators.notIn)') {
3606
+ const placeholders = opValue
3607
+ .map((_, i) => `${paramPrefix}${paramIndex + i}`)
3608
+ .join(', ');
3609
+ sql = `${this.escapeId(key)} NOT IN (${placeholders})`;
3610
+ values.push(...opValue);
3611
+ }
3612
+ else if (opKey === 'Symbol(between)' || opKey === 'Symbol(operators.between)') {
3613
+ sql = `${this.escapeId(key)} BETWEEN ${paramPrefix}${paramIndex} AND ${paramPrefix}${paramIndex + 1}`;
3614
+ values.push(...opValue);
3615
+ }
3616
+ else if (opKey === 'Symbol(notBetween)' || opKey === 'Symbol(operators.notBetween)') {
3617
+ sql = `${this.escapeId(key)} NOT BETWEEN ${paramPrefix}${paramIndex} AND ${paramPrefix}${paramIndex + 1}`;
3618
+ values.push(...opValue);
3619
+ }
3620
+ else if (opKey === 'Symbol(isNull)' || opKey === 'Symbol(operators.isNull)') {
3621
+ sql = `${this.escapeId(key)} IS NULL`;
3622
+ }
3623
+ else if (opKey === 'Symbol(isNotNull)' || opKey === 'Symbol(operators.isNotNull)') {
3624
+ sql = `${this.escapeId(key)} IS NOT NULL`;
3625
+ }
3626
+ else if (opKey === 'Symbol(like)' || opKey === 'Symbol(operators.like)') {
3627
+ sql = `${this.escapeId(key)} LIKE ${paramPrefix}${paramIndex}`;
3628
+ values.push(opValue);
3629
+ }
3630
+ else if (opKey === 'Symbol(notLike)' || opKey === 'Symbol(operators.notLike)') {
3631
+ sql = `${this.escapeId(key)} NOT LIKE ${paramPrefix}${paramIndex}`;
3632
+ values.push(opValue);
3633
+ }
3634
+ else if (opKey === 'Symbol(iLike)' || opKey === 'Symbol(operators.iLike)') {
3635
+ sql = `${this.escapeId(key)} ILIKE ${paramPrefix}${paramIndex}`;
3636
+ values.push(opValue);
3637
+ }
3638
+ else if (opKey === 'Symbol(notILike)' || opKey === 'Symbol(operators.notILike)') {
3639
+ sql = `${this.escapeId(key)} NOT ILIKE ${paramPrefix}${paramIndex}`;
3640
+ values.push(opValue);
3641
+ }
3642
+ }
3643
+ if (!sql) {
3644
+ // Fallback: treat as equals
3645
+ sql = `${this.escapeId(key)} = ${paramPrefix}${paramIndex}`;
3646
+ values.push(value);
3647
+ }
3648
+ }
3649
+ else if (Array.isArray(value)) {
3650
+ // Handle IN arrays
3651
+ const placeholders = value.map((_, i) => `${paramPrefix}${paramIndex + i}`).join(', ');
3652
+ sql = `${this.escapeId(key)} IN (${placeholders})`;
3653
+ values.push(...value);
3654
+ }
3655
+ else {
3656
+ // Simple equality
3657
+ sql = `${this.escapeId(key)} = ${paramPrefix}${paramIndex}`;
3658
+ values.push(value);
3659
+ }
3660
+ if (sql) {
3661
+ conditions.push(sql);
3662
+ }
3663
+ }
3664
+ return conditions.join(' AND ');
3665
+ }
3666
+ /**
3667
+ * Build a HAVING clause with support for standard Op operators
3668
+ */
3669
+ buildHavingClause(having) {
3670
+ const values = [];
3671
+ if (!having || (typeof having === 'object' && Object.keys(having).length === 0)) {
3672
+ return { sql: '', values };
3673
+ }
3674
+ // Use the condition builder with Op operator support
3675
+ const sql = this.buildCondition(having, values);
3676
+ return { sql, values };
3677
+ }
3678
+ /**
3679
+ * Build the expression used after `AS OF SYSTEM TIME` for CockroachDB
3680
+ * time-travel reads.
3681
+ *
3682
+ * Accepts:
3683
+ * - A relative interval shorthand, e.g. `'-10s'`, `'-500ms'`, `'-1h'`.
3684
+ * - An absolute ISO-8601 timestamp, e.g. `'2024-01-01T00:00:00Z'`.
3685
+ * - The literal function call `follower_read_timestamp()` (unquoted).
3686
+ *
3687
+ * The value is validated against a strict allow-list pattern before being
3688
+ * embedded in the SQL string (rather than passed as a bound parameter,
3689
+ * which CockroachDB's `AS OF SYSTEM TIME` clause does not accept) to avoid
3690
+ * SQL injection.
3691
+ *
3692
+ * @see https://www.cockroachlabs.com/docs/stable/as-of-system-time
3693
+ */
3694
+ buildAsOfSystemTimeClause(value) {
3695
+ const trimmed = value.trim();
3696
+ // `follower_read_timestamp()` (and the bounded-staleness variant
3697
+ // `with_min_timestamp`/`with_max_staleness`) are function calls and must
3698
+ // not be quoted.
3699
+ if (/^follower_read_timestamp\(\s*\)$/i.test(trimmed)) {
3700
+ return trimmed;
3701
+ }
3702
+ // Relative interval shorthand: optional leading `-`, digits, optional
3703
+ // decimal portion, optional unit (ms|s|m|h|d).
3704
+ const isRelativeInterval = /^-?\d+(\.\d+)?\s*(ms|s|m|h|d)?$/i.test(trimmed);
3705
+ // Absolute timestamp (ISO-8601-ish: date, optional time, optional
3706
+ // fractional seconds, optional timezone offset/Z).
3707
+ const isAbsoluteTimestamp = /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/.test(trimmed);
3708
+ if (!isRelativeInterval && !isAbsoluteTimestamp) {
3709
+ throw new Error(`Invalid AS OF SYSTEM TIME value: "${value}". Expected a relative interval ` +
3710
+ `(e.g. "-10s"), an ISO-8601 timestamp, or "follower_read_timestamp()".`);
3711
+ }
3712
+ return `'${trimmed.replace(/'/g, "''")}'`;
3713
+ }
3714
+ /**
3715
+ * Run a SELECT query as of a historical timestamp using CockroachDB's
3716
+ * `AS OF SYSTEM TIME` time-travel read feature.
3717
+ *
3718
+ * @param options - Standard SelectOptions plus the historical timestamp.
3719
+ * @param asOfSystemTime - Relative interval (e.g. `'-10s'`), ISO timestamp,
3720
+ * or `'follower_read_timestamp()'`.
3721
+ */
3722
+ async queryAsOfSystemTime(options, asOfSystemTime) {
3723
+ const { sql, values } = this.buildSelectQuery({
3724
+ ...options,
3725
+ asOfSystemTime,
3726
+ });
3727
+ return this.query(sql, { replacements: values });
3728
+ }
3729
+ /**
3730
+ * Build a SELECT query with CockroachDB-specific features
3731
+ */
3732
+ buildSelectQuery(options) {
3733
+ const queryValues = [];
3734
+ // Build SELECT clause
3735
+ let selectSql = '*';
3736
+ // Handle DISTINCT ON and DISTINCT with column
3737
+ const distinctOn = options.distinctOn;
3738
+ let distinctClause = '';
3739
+ if (options.distinct) {
3740
+ if (distinctOn && Array.isArray(distinctOn)) {
3741
+ distinctClause = `DISTINCT ON (${distinctOn.map((c) => this.escapeId(c)).join(', ')}) `;
3742
+ }
3743
+ else if (options.col) {
3744
+ // DISTINCT with specific column (e.g., COUNT(DISTINCT col))
3745
+ distinctClause = `DISTINCT ${this.escapeId(options.col)} `;
3746
+ }
3747
+ else {
3748
+ distinctClause = 'DISTINCT ';
3749
+ }
3750
+ }
3751
+ if (options.attributes) {
3752
+ if (Array.isArray(options.attributes)) {
3753
+ selectSql = options.attributes.map((a) => this.escapeId(a)).join(', ');
3754
+ }
3755
+ else if (options.attributes.include) {
3756
+ selectSql = options.attributes.include.map((a) => this.escapeId(a)).join(', ');
3757
+ }
3758
+ else if (options.attributes.exclude) {
3759
+ // CockroachDB supports EXCEPT
3760
+ selectSql = `* EXCEPT (${options.attributes.exclude.map((a) => this.escapeId(a)).join(', ')})`;
3761
+ }
3762
+ }
3763
+ let sql = `SELECT ${distinctClause}${selectSql} FROM ${this.escapeId(options.tableName)}`;
3764
+ // AS OF SYSTEM TIME (CockroachDB time-travel reads). Must immediately
3765
+ // follow the table reference in the FROM clause, before any JOINs/WHERE.
3766
+ // See: https://www.cockroachlabs.com/docs/stable/as-of-system-time
3767
+ const asOfSystemTime = options.asOfSystemTime;
3768
+ if (asOfSystemTime) {
3769
+ sql += ` AS OF SYSTEM TIME ${this.buildAsOfSystemTimeClause(asOfSystemTime)}`;
3770
+ }
3771
+ // Build JOIN clause (for includes)
3772
+ if (options.include && options.include.length > 0) {
3773
+ for (const include of options.include) {
3774
+ // Determine join type - check joinType option first, then fall back to required
3775
+ let joinType;
3776
+ if (include.joinType) {
3777
+ switch (include.joinType) {
3778
+ case 'INNER':
3779
+ joinType = 'INNER JOIN';
3780
+ break;
3781
+ case 'LEFT':
3782
+ joinType = 'LEFT JOIN';
3783
+ break;
3784
+ case 'RIGHT':
3785
+ joinType = 'RIGHT JOIN';
3786
+ break;
3787
+ case 'FULL':
3788
+ joinType = 'FULL OUTER JOIN';
3789
+ break;
3790
+ case 'CROSS':
3791
+ joinType = 'CROSS JOIN';
3792
+ break;
3793
+ default:
3794
+ joinType = include.required ? 'INNER JOIN' : 'LEFT JOIN';
3795
+ }
3796
+ }
3797
+ else {
3798
+ joinType = include.required ? 'INNER JOIN' : 'LEFT JOIN';
3799
+ }
3800
+ const modelTableName = include.model.tableName;
3801
+ const modelName = modelTableName ? modelTableName : include.model.toString();
3802
+ const alias = include.as || modelName;
3803
+ // Handle LATERAL keyword (CockroachDB specific)
3804
+ const lateralKeyword = include.lateral ? 'LATERAL ' : '';
3805
+ sql += ` ${joinType}${lateralKeyword ? ' ' + lateralKeyword : ''}${this.escapeId(modelName)} ${this.escapeId(alias)}`;
3806
+ if (include.where) {
3807
+ const whereClause = this.buildWhereClause(include.where);
3808
+ sql += ` ON ${whereClause.sql}`;
3809
+ queryValues.push(...whereClause.values);
3810
+ }
3811
+ }
3812
+ }
3813
+ // Build WHERE clause
3814
+ if (options.where) {
3815
+ const whereClause = this.buildWhereClause(options.where);
3816
+ if (whereClause.sql) {
3817
+ sql += ` WHERE ${whereClause.sql}`;
3818
+ queryValues.push(...whereClause.values);
3819
+ }
3820
+ }
3821
+ // Build GROUP BY clause
3822
+ if (options.group) {
3823
+ const groupByClause = this.buildGroupByClause(options.group);
3824
+ if (groupByClause) {
3825
+ sql += ` ${groupByClause}`;
3826
+ }
3827
+ }
3828
+ // Build HAVING clause
3829
+ if (options.having) {
3830
+ const havingClause = this.buildHavingClause(options.having);
3831
+ if (havingClause.sql) {
3832
+ sql += ` HAVING ${havingClause.sql}`;
3833
+ queryValues.push(...havingClause.values);
3834
+ }
3835
+ }
3836
+ // Build ORDER BY clause
3837
+ if (options.order) {
3838
+ const orderClause = this.buildOrderClause(options.order);
3839
+ if (orderClause) {
3840
+ sql += ` ${orderClause}`;
3841
+ }
3842
+ }
3843
+ // Build LIMIT/OFFSET clause
3844
+ sql += this.buildLimitOffset(options.limit, options.offset);
3845
+ // Handle row-level locking (CockroachDB specific)
3846
+ if (options.lock) {
3847
+ let lockType;
3848
+ // lock: true is equivalent to 'UPDATE'
3849
+ if (options.lock === true) {
3850
+ lockType = 'UPDATE';
3851
+ }
3852
+ else if (typeof options.lock === 'string') {
3853
+ lockType = options.lock;
3854
+ }
3855
+ else if (typeof options.lock === 'object' && options.lock.of) {
3856
+ // lock: { of: Model } - defaults to FOR UPDATE for table-specific locking
3857
+ lockType = 'UPDATE';
3858
+ }
3859
+ if (lockType === 'UPDATE') {
3860
+ sql += ' FOR UPDATE';
3861
+ }
3862
+ else if (lockType === 'SHARE') {
3863
+ sql += ' FOR SHARE';
3864
+ }
3865
+ else if (lockType === 'KEY SHARE') {
3866
+ sql += ' FOR KEY SHARE';
3867
+ }
3868
+ // Handle lock on specific table (CockroachDB supports OF clause)
3869
+ if (typeof options.lock === 'object' && options.lock.of) {
3870
+ const model = options.lock.of;
3871
+ sql += ` OF ${this.escapeId(model.tableName || '')}`;
3872
+ }
3873
+ }
3874
+ // Build UNION clause if provided
3875
+ if (options.union && options.union.length > 0) {
3876
+ const unionType = options.unionType || 'UNION';
3877
+ for (const unionQuery of options.union) {
3878
+ const unionModel = unionQuery.model;
3879
+ const unionTableName = unionModel.tableName || unionModel.name || '';
3880
+ // Build the union query
3881
+ let unionSql = `SELECT * FROM ${this.escapeId(unionTableName)}`;
3882
+ // Add WHERE clause for union query
3883
+ if (unionQuery.where) {
3884
+ const whereClause = this.buildWhereClause(unionQuery.where);
3885
+ if (whereClause.sql) {
3886
+ unionSql += ` WHERE ${whereClause.sql}`;
3887
+ queryValues.push(...whereClause.values);
3888
+ }
3889
+ }
3890
+ // Add ORDER BY for union query
3891
+ if (unionQuery.order) {
3892
+ const orderClause = this.buildOrderClause(unionQuery.order);
3893
+ if (orderClause) {
3894
+ unionSql += ` ${orderClause}`;
3895
+ }
3896
+ }
3897
+ // Add LIMIT for union query
3898
+ if (unionQuery.limit) {
3899
+ unionSql += ` LIMIT ${unionQuery.limit}`;
3900
+ }
3901
+ // Add OFFSET for union query
3902
+ if (unionQuery.offset) {
3903
+ unionSql += ` OFFSET ${unionQuery.offset}`;
3904
+ }
3905
+ sql += ` ${unionType} ${unionSql}`;
3906
+ }
3907
+ }
3908
+ return { sql, values: queryValues };
3909
+ }
3910
+ /**
3911
+ * Escape a value for use in a CockroachDB array literal
3912
+ */
3913
+ escapeArray(values) {
3914
+ const escaped = values.map((v) => {
3915
+ if (v === null) {
3916
+ return 'NULL';
3917
+ }
3918
+ if (typeof v === 'string') {
3919
+ return `"${v.replace(/"/g, '""')}"`;
3920
+ }
3921
+ if (typeof v === 'number' || typeof v === 'boolean') {
3922
+ return String(v);
3923
+ }
3924
+ return `"${JSON.stringify(v).replace(/"/g, '""')}"`;
3925
+ });
3926
+ return `ARRAY[${escaped.join(', ')}]`;
3927
+ }
3928
+ /**
3929
+ * Build an array contains operator query (@>)
3930
+ * SELECT * FROM table WHERE column @> ARRAY['elem1', 'elem2']
3931
+ */
3932
+ buildArrayContains(column, values) {
3933
+ const arrayLiteral = this.escapeArray(values);
3934
+ return `${this.escapeId(column)} @> ${arrayLiteral}`;
3935
+ }
3936
+ /**
3937
+ * Build an array contained by operator query (<@)
3938
+ * SELECT * FROM table WHERE column <@ ARRAY['elem1', 'elem2']
3939
+ */
3940
+ buildArrayContainedBy(column, values) {
3941
+ const arrayLiteral = this.escapeArray(values);
3942
+ return `${this.escapeId(column)} <@ ${arrayLiteral}`;
3943
+ }
3944
+ /**
3945
+ * Build an array overlaps operator query (&&)
3946
+ * SELECT * FROM table WHERE column && ARRAY['elem1', 'elem2']
3947
+ */
3948
+ buildArrayOverlaps(column, values) {
3949
+ const arrayLiteral = this.escapeArray(values);
3950
+ return `${this.escapeId(column)} && ${arrayLiteral}`;
3951
+ }
3952
+ /**
3953
+ * Build an array ANY operator query
3954
+ * SELECT * FROM table WHERE 'value' = ANY(column)
3955
+ */
3956
+ buildArrayAny(column, value) {
3957
+ const escaped = this.escape(value);
3958
+ return `${escaped} = ANY(${this.escapeId(column)})`;
3959
+ }
3960
+ /**
3961
+ * Build an array ALL operator query
3962
+ * SELECT * FROM table WHERE 'value' = ALL(column)
3963
+ * or with comparison: SELECT * FROM table WHERE value > ALL(column)
3964
+ */
3965
+ buildArrayAll(column, value, operator) {
3966
+ if (operator) {
3967
+ const escaped = this.escape(value);
3968
+ return `${escaped} ${operator} ALL(${this.escapeId(column)})`;
3969
+ }
3970
+ const escaped = this.escape(value);
3971
+ return `${escaped} = ALL(${this.escapeId(column)})`;
3972
+ }
3973
+ /**
3974
+ * Build a full-text search query
3975
+ */
3976
+ buildFullTextSearchQuery(tableName, searchColumns, searchTerm, options) {
3977
+ const columnList = searchColumns.map((c) => this.escapeId(c)).join(" || ' ' || ");
3978
+ const language = options?.language || 'english';
3979
+ const ranking = options?.ranking || 'ts_rank';
3980
+ const queryValues = [searchTerm];
3981
+ let sql = `SELECT *, ${ranking}(to_tsvector('${language}', ${columnList}), plainto_tsquery('${language}', $1)) as _rank`;
3982
+ sql += ` FROM ${this.escapeId(tableName)}`;
3983
+ sql += ` WHERE to_tsvector('${language}', ${columnList}) @@ plainto_tsquery('${language}', $1)`;
3984
+ if (options?.orderBy) {
3985
+ const orderClause = this.buildOrderClause(options.orderBy);
3986
+ if (orderClause) {
3987
+ sql += ` ${orderClause}`;
3988
+ }
3989
+ }
3990
+ return { sql, values: queryValues };
3991
+ }
3992
+ /**
3993
+ * Build a JSON/JSONB path query
3994
+ */
3995
+ buildJsonPathQuery(column, path, value, operator) {
3996
+ const values = [value];
3997
+ const columnRef = this.escapeId(column);
3998
+ // JSONB operators
3999
+ const jsonOperators = {
4000
+ '@>': 'jsonb_path_query_first',
4001
+ '<@': 'jsonb_path_query_first',
4002
+ '?': 'jsonb_exists',
4003
+ '?|': 'jsonb_exists',
4004
+ '~': '~',
4005
+ '~*': '~*',
4006
+ };
4007
+ let sql;
4008
+ if (jsonOperators[operator]) {
4009
+ sql = `${columnRef} ${operator} $1`;
4010
+ }
4011
+ else if (path) {
4012
+ sql = `${columnRef} #> $1 ${operator} $2`;
4013
+ values.unshift(path);
4014
+ }
4015
+ else {
4016
+ sql = `${columnRef} ${operator} $1`;
4017
+ }
4018
+ return { sql, values };
4019
+ }
4020
+ /**
4021
+ * Get CockroachDB specific lock options
4022
+ */
4023
+ getLockOptions(lock) {
4024
+ const params = [];
4025
+ let sql = '';
4026
+ const lockType = typeof lock === 'string' ? lock : lock?.level;
4027
+ switch (lockType) {
4028
+ case 'UPDATE':
4029
+ sql = 'FOR UPDATE';
4030
+ break;
4031
+ case 'SHARE':
4032
+ sql = 'FOR SHARE';
4033
+ break;
4034
+ case 'KEY SHARE':
4035
+ sql = 'FOR KEY SHARE';
4036
+ break;
4037
+ default:
4038
+ return { sql: '', params };
4039
+ }
4040
+ if (typeof lock === 'object' && lock?.of) {
4041
+ const model = lock.of;
4042
+ sql += ` OF ${this.escapeId(model.tableName || '')}`;
4043
+ }
4044
+ if (typeof lock === 'object' && lock?.nowait) {
4045
+ sql += ' NOWAIT';
4046
+ }
4047
+ else if (typeof lock === 'object' && lock?.skipLocked) {
4048
+ sql += ' SKIP LOCKED';
4049
+ }
4050
+ return { sql, params };
4051
+ }
4052
+ // ---------------------------------------------------------------------------
4053
+ // User / Privilege management — CockroachDB dialect
4054
+ // ---------------------------------------------------------------------------
4055
+ /**
4056
+ * Escape a single-quoted string value (replace ' with '').
4057
+ */
4058
+ escapeStringValue(value) {
4059
+ return value.replace(/'/g, "''");
4060
+ }
4061
+ /**
4062
+ * Build the shared WITH-clause attribute list used by both CREATE USER and
4063
+ * ALTER USER. Returns a (possibly empty) array of attribute tokens.
4064
+ */
4065
+ buildUserAttributes(options) {
4066
+ const attrs = [];
4067
+ if (options.password !== undefined) {
4068
+ attrs.push(`WITH PASSWORD '${this.escapeStringValue(String(options.password))}'`);
4069
+ }
4070
+ if (options.superuser === true) {
4071
+ attrs.push('SUPERUSER');
4072
+ }
4073
+ else if (options.superuser === false) {
4074
+ attrs.push('NOSUPERUSER');
4075
+ }
4076
+ if (options.createdb === true) {
4077
+ attrs.push('CREATEDB');
4078
+ }
4079
+ else if (options.createdb === false) {
4080
+ attrs.push('NOCREATEDB');
4081
+ }
4082
+ if (options.createrole === true) {
4083
+ attrs.push('CREATEROLE');
4084
+ }
4085
+ else if (options.createrole === false) {
4086
+ attrs.push('NOCREATEROLE');
4087
+ }
4088
+ // accountLocked takes precedence over login
4089
+ if (options.accountLocked === true) {
4090
+ attrs.push('NOLOGIN');
4091
+ }
4092
+ else if (options.login === false) {
4093
+ attrs.push('NOLOGIN');
4094
+ }
4095
+ else {
4096
+ // default: LOGIN
4097
+ attrs.push('LOGIN');
4098
+ }
4099
+ if (options.replication === true) {
4100
+ attrs.push('REPLICATION');
4101
+ }
4102
+ else if (options.replication === false) {
4103
+ attrs.push('NOREPLICATION');
4104
+ }
4105
+ if (options.bypassrls === true) {
4106
+ attrs.push('BYPASSRLS');
4107
+ }
4108
+ else if (options.bypassrls === false) {
4109
+ attrs.push('NOBYPASSRLS');
4110
+ }
4111
+ if (options.maxConnections !== undefined) {
4112
+ attrs.push(`CONNECTION LIMIT ${Number(options.maxConnections)}`);
4113
+ }
4114
+ if (options.expirePassword !== undefined) {
4115
+ if (options.expirePassword === true) {
4116
+ attrs.push(`VALID UNTIL 'epoch'`);
4117
+ }
4118
+ else if (options.expirePassword instanceof Date) {
4119
+ attrs.push(`VALID UNTIL '${this.escapeStringValue(options.expirePassword.toISOString())}'`);
4120
+ }
4121
+ else if (typeof options.expirePassword === 'string') {
4122
+ attrs.push(`VALID UNTIL '${this.escapeStringValue(options.expirePassword)}'`);
4123
+ }
4124
+ }
4125
+ return attrs;
4126
+ }
4127
+ /**
4128
+ * Build a CREATE USER statement.
4129
+ *
4130
+ * CockroachDB treats users as roles with LOGIN. Supported options:
4131
+ * ifNotExists, password, superuser, createdb, createrole, login,
4132
+ * replication, bypassrls, maxConnections, accountLocked, expirePassword,
4133
+ * defaultRole, requireSSL (noted but skipped — not a standard PG attribute).
4134
+ */
4135
+ buildCreateUserQuery(username, options) {
4136
+ const opts = options || {};
4137
+ let sql = 'CREATE USER';
4138
+ if (opts.ifNotExists) {
4139
+ sql += ' IF NOT EXISTS';
4140
+ }
4141
+ sql += ` "${this.escapeStringValue(username)}"`;
4142
+ // requireSSL is not a standard CockroachDB role attribute; emit a comment.
4143
+ if (opts.requireSSL) {
4144
+ sql += ' /* requireSSL: use pg_hba.conf hostssl rules instead */';
4145
+ }
4146
+ const attrs = this.buildUserAttributes(opts);
4147
+ if (attrs.length > 0) {
4148
+ sql += ` ${attrs.join(' ')}`;
4149
+ }
4150
+ if (opts.defaultRole !== undefined) {
4151
+ const roles = Array.isArray(opts.defaultRole) ? opts.defaultRole : [opts.defaultRole];
4152
+ sql += ` IN ROLE ${roles.map((r) => `"${this.escapeStringValue(r)}"`).join(', ')}`;
4153
+ }
4154
+ return sql;
4155
+ }
4156
+ /**
4157
+ * Build an ALTER USER statement.
4158
+ *
4159
+ * Supports the same attribute options as buildCreateUserQuery plus
4160
+ * `options.renameTo` to rename the role.
4161
+ */
4162
+ buildAlterUserQuery(username, options) {
4163
+ const opts = options || {};
4164
+ let sql = `ALTER USER "${this.escapeStringValue(username)}"`;
4165
+ if (opts.renameTo !== undefined) {
4166
+ sql += ` RENAME TO "${this.escapeStringValue(String(opts.renameTo))}"`;
4167
+ // RENAME TO cannot be combined with other clauses in the same statement.
4168
+ return sql;
4169
+ }
4170
+ const attrs = this.buildUserAttributes(opts);
4171
+ if (attrs.length > 0) {
4172
+ sql += ` ${attrs.join(' ')}`;
4173
+ }
4174
+ if (opts.defaultRole !== undefined) {
4175
+ const roles = Array.isArray(opts.defaultRole) ? opts.defaultRole : [opts.defaultRole];
4176
+ sql += ` IN ROLE ${roles.map((r) => `"${this.escapeStringValue(r)}"`).join(', ')}`;
4177
+ }
4178
+ return sql;
4179
+ }
4180
+ /**
4181
+ * Build a DROP USER statement.
4182
+ *
4183
+ * Options: ifExists, cascade, restrict.
4184
+ */
4185
+ buildDropUserQuery(username, options) {
4186
+ const opts = options || {};
4187
+ let sql = 'DROP USER';
4188
+ if (opts.ifExists) {
4189
+ sql += ' IF EXISTS';
4190
+ }
4191
+ sql += ` "${this.escapeStringValue(username)}"`;
4192
+ if (opts.cascade) {
4193
+ sql += ' CASCADE';
4194
+ }
4195
+ else if (opts.restrict) {
4196
+ sql += ' RESTRICT';
4197
+ }
4198
+ return sql;
4199
+ }
4200
+ /**
4201
+ * Return a query that lists all CockroachDB users (roles with LOGIN).
4202
+ */
4203
+ getUsersQuery() {
4204
+ return [
4205
+ 'SELECT usename AS "user", \'\' AS "host",',
4206
+ ' rolcanlogin AS "login", rolsuper AS "superuser",',
4207
+ ' rolpassword IS NOT NULL AS "hasPassword"',
4208
+ 'FROM pg_user',
4209
+ 'JOIN pg_authid ON pg_user.usesysid = pg_authid.oid',
4210
+ 'ORDER BY usename;',
4211
+ ].join('\n');
4212
+ }
4213
+ /**
4214
+ * Create a database user
4215
+ */
4216
+ async createUser(username, options) {
4217
+ const sql = this.buildCreateUserQuery(username, options);
4218
+ await this.query(sql);
4219
+ }
4220
+ /**
4221
+ * Map a generic privilege name to a CockroachDB privilege name.
4222
+ * Returns null when the privilege has no CockroachDB equivalent and
4223
+ * should be silently skipped.
4224
+ */
4225
+ mapPrivilege(priv) {
4226
+ const upper = priv.toUpperCase();
4227
+ if (upper === 'ALL')
4228
+ return 'ALL PRIVILEGES';
4229
+ // MySQL-specific — not valid in CockroachDB
4230
+ if (upper === 'INDEX')
4231
+ return null;
4232
+ return priv;
4233
+ }
4234
+ /**
4235
+ * Build the ON <scope> clause for GRANT / REVOKE.
4236
+ *
4237
+ * Scope shapes:
4238
+ * { level: 'global' }
4239
+ * { level: 'database', database: 'dbname' }
4240
+ * { level: 'schema', schema: 'schemaname' }
4241
+ * { level: 'table', table: 'tablename' }
4242
+ * { level: 'column', table: 'tablename', columns: ['col1', 'col2'] }
4243
+ * { level: 'sequence', schema: 'schemaname' }
4244
+ */
4245
+ buildGrantScope(options) {
4246
+ const level = (options.level || 'global').toLowerCase();
4247
+ switch (level) {
4248
+ case 'database':
4249
+ return `ON DATABASE "${this.escapeStringValue(options.database || '')}"`;
4250
+ case 'schema':
4251
+ return `ON SCHEMA "${this.escapeStringValue(options.schema || '')}"`;
4252
+ case 'table':
4253
+ return `ON TABLE "${this.escapeStringValue(options.table || '')}"`;
4254
+ case 'sequence':
4255
+ return `ON ALL SEQUENCES IN SCHEMA "${this.escapeStringValue(options.schema || 'public')}"`;
4256
+ case 'column':
4257
+ // Handled separately — the caller inserts column names into the
4258
+ // privilege list, not here.
4259
+ return `ON "${this.escapeStringValue(options.table || '')}"`;
4260
+ case 'global':
4261
+ default:
4262
+ return `ON ALL TABLES IN SCHEMA "${this.escapeStringValue(options.schema || 'public')}"`;
4263
+ }
4264
+ }
4265
+ /**
4266
+ * Build a GRANT statement.
4267
+ *
4268
+ * options.privileges — string | string[] (e.g. ['SELECT', 'INSERT'])
4269
+ * options.on — scope object (see buildGrantScope)
4270
+ * options.to — string | string[] (grantees)
4271
+ * options.withGrantOption — boolean
4272
+ * options.asUser — string (GRANTED BY, PG 14+)
4273
+ *
4274
+ * For column-level grants (options.on.level === 'column') the columns are
4275
+ * embedded in each privilege token: SELECT (col1, col2).
4276
+ */
4277
+ buildGrantQuery(options) {
4278
+ const rawPrivileges = Array.isArray(options.privileges)
4279
+ ? options.privileges
4280
+ : [options.privileges || 'ALL'];
4281
+ const isColumnLevel = (options.on || {}).level === 'column';
4282
+ const columnList = isColumnLevel
4283
+ ? (options.on.columns || []).map((c) => `"${this.escapeStringValue(c)}"`)
4284
+ : [];
4285
+ const mappedPrivileges = rawPrivileges
4286
+ .map((p) => this.mapPrivilege(p))
4287
+ .filter((p) => p !== null)
4288
+ .map((p) => isColumnLevel && columnList.length > 0 ? `${p} (${columnList.join(', ')})` : p);
4289
+ if (mappedPrivileges.length === 0) {
4290
+ return '-- No applicable privileges to grant';
4291
+ }
4292
+ const scope = this.buildGrantScope(options.on || {});
4293
+ const grantees = Array.isArray(options.to) ? options.to : [options.to || ''];
4294
+ let sql = `GRANT ${mappedPrivileges.join(', ')} ${scope} TO ${grantees.join(', ')}`;
4295
+ if (options.withGrantOption) {
4296
+ sql += ' WITH GRANT OPTION';
4297
+ }
4298
+ if (options.asUser !== undefined) {
4299
+ sql += ` GRANTED BY "${this.escapeStringValue(String(options.asUser))}"`;
4300
+ }
4301
+ return sql;
4302
+ }
4303
+ /**
4304
+ * Build a REVOKE statement.
4305
+ *
4306
+ * options.privileges — string | string[]
4307
+ * options.on — scope object (same as buildGrantQuery)
4308
+ * options.from — string | string[] (revokees)
4309
+ * options.grantOptionFor — boolean (revoke only the GRANT OPTION)
4310
+ * options.cascade — boolean
4311
+ * options.restrict — boolean
4312
+ */
4313
+ buildRevokeQuery(options) {
4314
+ const rawPrivileges = Array.isArray(options.privileges)
4315
+ ? options.privileges
4316
+ : [options.privileges || 'ALL'];
4317
+ const isColumnLevel = (options.on || {}).level === 'column';
4318
+ const columnList = isColumnLevel
4319
+ ? (options.on.columns || []).map((c) => `"${this.escapeStringValue(c)}"`)
4320
+ : [];
4321
+ const mappedPrivileges = rawPrivileges
4322
+ .map((p) => this.mapPrivilege(p))
4323
+ .filter((p) => p !== null)
4324
+ .map((p) => isColumnLevel && columnList.length > 0 ? `${p} (${columnList.join(', ')})` : p);
4325
+ if (mappedPrivileges.length === 0) {
4326
+ return '-- No applicable privileges to revoke';
4327
+ }
4328
+ let sql = 'REVOKE';
4329
+ if (options.grantOptionFor) {
4330
+ sql += ' GRANT OPTION FOR';
4331
+ }
4332
+ const scope = this.buildGrantScope(options.on || {});
4333
+ const revokees = Array.isArray(options.from) ? options.from : [options.from || ''];
4334
+ sql += ` ${mappedPrivileges.join(', ')} ${scope} FROM ${revokees.join(', ')}`;
4335
+ if (options.cascade) {
4336
+ sql += ' CASCADE';
4337
+ }
4338
+ else if (options.restrict) {
4339
+ sql += ' RESTRICT';
4340
+ }
4341
+ return sql;
4342
+ }
4343
+ /**
4344
+ * Return a query that shows the privileges granted to a specific user/role.
4345
+ * The optional `host` parameter is ignored — CockroachDB has no host concept.
4346
+ */
4347
+ buildShowGrantsQuery(username, _host) {
4348
+ const escaped = this.escapeStringValue(username);
4349
+ return [
4350
+ `SELECT grantee, privilege_type, table_schema, table_name, column_name, is_grantable`,
4351
+ `FROM information_schema.role_column_grants`,
4352
+ `WHERE grantee = '${escaped}'`,
4353
+ `UNION ALL`,
4354
+ `SELECT grantee, privilege_type, table_schema, table_name, NULL AS column_name, is_grantable`,
4355
+ `FROM information_schema.role_table_grants`,
4356
+ `WHERE grantee = '${escaped}'`,
4357
+ `ORDER BY table_schema, table_name, privilege_type;`,
4358
+ ].join('\n');
4359
+ }
4360
+ /**
4361
+ * CockroachDB applies privilege changes automatically — FLUSH PRIVILEGES is
4362
+ * a MySQL concept. Return a no-op with an explanatory comment.
4363
+ */
4364
+ buildFlushPrivilegesQuery() {
4365
+ return ('-- CockroachDB automatically applies privilege changes; FLUSH PRIVILEGES is not required\n' +
4366
+ 'SELECT 1;');
4367
+ }
4368
+ /**
4369
+ * Build a CREATE ROLE statement.
4370
+ *
4371
+ * Unlike CREATE USER, NOLOGIN is the default for roles. Pass
4372
+ * `options.login = true` to enable login.
4373
+ *
4374
+ * Supported options: ifNotExists, login, password, superuser, createdb,
4375
+ * createrole, inherit, replication, bypassrls, maxConnections,
4376
+ * expirePassword, accountLocked.
4377
+ */
4378
+ buildCreateRoleQuery(roleName, options) {
4379
+ const opts = options || {};
4380
+ let sql = 'CREATE ROLE';
4381
+ if (opts.ifNotExists) {
4382
+ sql += ' IF NOT EXISTS';
4383
+ }
4384
+ sql += ` "${this.escapeStringValue(roleName)}"`;
4385
+ const attrs = [];
4386
+ if (opts.login === true) {
4387
+ attrs.push('LOGIN');
4388
+ }
4389
+ else if (opts.login === false || opts.accountLocked === true) {
4390
+ attrs.push('NOLOGIN');
4391
+ }
4392
+ // No default LOGIN for roles (PG default is already NOLOGIN).
4393
+ if (opts.password !== undefined) {
4394
+ attrs.push(`WITH PASSWORD '${this.escapeStringValue(String(opts.password))}'`);
4395
+ }
4396
+ if (opts.superuser === true) {
4397
+ attrs.push('SUPERUSER');
4398
+ }
4399
+ else if (opts.superuser === false) {
4400
+ attrs.push('NOSUPERUSER');
4401
+ }
4402
+ if (opts.createdb === true) {
4403
+ attrs.push('CREATEDB');
4404
+ }
4405
+ else if (opts.createdb === false) {
4406
+ attrs.push('NOCREATEDB');
4407
+ }
4408
+ if (opts.createrole === true) {
4409
+ attrs.push('CREATEROLE');
4410
+ }
4411
+ else if (opts.createrole === false) {
4412
+ attrs.push('NOCREATEROLE');
4413
+ }
4414
+ if (opts.inherit === false) {
4415
+ attrs.push('NOINHERIT');
4416
+ }
4417
+ else if (opts.inherit === true) {
4418
+ attrs.push('INHERIT');
4419
+ }
4420
+ if (opts.replication === true) {
4421
+ attrs.push('REPLICATION');
4422
+ }
4423
+ else if (opts.replication === false) {
4424
+ attrs.push('NOREPLICATION');
4425
+ }
4426
+ if (opts.bypassrls === true) {
4427
+ attrs.push('BYPASSRLS');
4428
+ }
4429
+ else if (opts.bypassrls === false) {
4430
+ attrs.push('NOBYPASSRLS');
4431
+ }
4432
+ if (opts.maxConnections !== undefined) {
4433
+ attrs.push(`CONNECTION LIMIT ${Number(opts.maxConnections)}`);
4434
+ }
4435
+ if (opts.expirePassword !== undefined) {
4436
+ if (opts.expirePassword === true) {
4437
+ attrs.push(`VALID UNTIL 'epoch'`);
4438
+ }
4439
+ else if (opts.expirePassword instanceof Date) {
4440
+ attrs.push(`VALID UNTIL '${this.escapeStringValue(opts.expirePassword.toISOString())}'`);
4441
+ }
4442
+ else if (typeof opts.expirePassword === 'string') {
4443
+ attrs.push(`VALID UNTIL '${this.escapeStringValue(opts.expirePassword)}'`);
4444
+ }
4445
+ }
4446
+ if (attrs.length > 0) {
4447
+ sql += ` ${attrs.join(' ')}`;
4448
+ }
4449
+ return sql;
4450
+ }
4451
+ /**
4452
+ * Build a DROP ROLE statement.
4453
+ *
4454
+ * Options: ifExists, cascade.
4455
+ */
4456
+ buildDropRoleQuery(roleName, options) {
4457
+ const opts = options || {};
4458
+ let sql = 'DROP ROLE';
4459
+ if (opts.ifExists) {
4460
+ sql += ' IF EXISTS';
4461
+ }
4462
+ sql += ` "${this.escapeStringValue(roleName)}"`;
4463
+ if (opts.cascade) {
4464
+ sql += ' CASCADE';
4465
+ }
4466
+ return sql;
4467
+ }
4468
+ /**
4469
+ * Build a GRANT <role> TO <members> statement.
4470
+ *
4471
+ * Options: withAdminOption.
4472
+ */
4473
+ buildGrantRoleQuery(role, to, options) {
4474
+ const opts = options || {};
4475
+ const members = Array.isArray(to) ? to : [to];
4476
+ let sql = `GRANT "${this.escapeStringValue(role)}" TO ${members.join(', ')}`;
4477
+ if (opts.withAdminOption) {
4478
+ sql += ' WITH ADMIN OPTION';
4479
+ }
4480
+ return sql;
4481
+ }
4482
+ /**
4483
+ * Build a REVOKE <role> FROM <members> statement.
4484
+ *
4485
+ * Options: adminOptionFor, cascade.
4486
+ */
4487
+ buildRevokeRoleQuery(role, from, options) {
4488
+ const opts = options || {};
4489
+ let sql = 'REVOKE';
4490
+ if (opts.adminOptionFor) {
4491
+ sql += ' ADMIN OPTION FOR';
4492
+ }
4493
+ const members = Array.isArray(from) ? from : [from];
4494
+ sql += ` "${this.escapeStringValue(role)}" FROM ${members.join(', ')}`;
4495
+ if (opts.cascade) {
4496
+ sql += ' CASCADE';
4497
+ }
4498
+ return sql;
4499
+ }
4500
+ /**
4501
+ * Return a query that lists all CockroachDB roles that cannot log in
4502
+ * (i.e. "group roles" as opposed to user roles).
4503
+ */
4504
+ getRolesQuery() {
4505
+ return `SELECT rolname AS "rolname" FROM pg_roles WHERE rolcanlogin = false ORDER BY rolname;`;
4506
+ }
4507
+ /**
4508
+ * Generate SQL for creating a CockroachDB database
4509
+ */
4510
+ createDatabaseSQL(options) {
4511
+ const parts = [];
4512
+ parts.push(`CREATE DATABASE ${this.quoteIdentifier(options.name)}`);
4513
+ if (options.encoding) {
4514
+ parts.push(`ENCODING = '${options.encoding}'`);
4515
+ }
4516
+ if (options.lcCollate) {
4517
+ parts.push(`LC_COLLATE = '${options.lcCollate}'`);
4518
+ }
4519
+ if (options.lcCtype) {
4520
+ parts.push(`LC_CTYPE = '${options.lcCtype}'`);
4521
+ }
4522
+ if (options.template) {
4523
+ parts.push(`TEMPLATE = ${this.quoteIdentifier(options.template)}`);
4524
+ }
4525
+ if (options.tablespace) {
4526
+ parts.push(`TABLESPACE = ${this.quoteIdentifier(options.tablespace)}`);
4527
+ }
4528
+ if (options.isTemplate !== undefined) {
4529
+ parts.push(options.isTemplate ? 'IS_TEMPLATE = true' : 'IS_TEMPLATE = false');
4530
+ }
4531
+ return parts.join(' ');
4532
+ }
4533
+ /**
4534
+ * Generate SQL for dropping a CockroachDB database
4535
+ */
4536
+ dropDatabaseSQL(name) {
4537
+ return `DROP DATABASE IF EXISTS ${this.quoteIdentifier(name)}`;
4538
+ }
4539
+ /**
4540
+ * Generate SQL for creating a savepoint
4541
+ */
4542
+ createSavepointSQL(name) {
4543
+ const savepointName = name || `sp_${Date.now()}`;
4544
+ return `SAVEPOINT ${savepointName}`;
4545
+ }
4546
+ /**
4547
+ * Generate SQL for releasing a savepoint
4548
+ */
4549
+ releaseSavepointSQL(name) {
4550
+ return `RELEASE SAVEPOINT ${name}`;
4551
+ }
4552
+ /**
4553
+ * Generate SQL for rolling back to a savepoint
4554
+ */
4555
+ rollbackToSavepointSQL(name) {
4556
+ return `ROLLBACK TO SAVEPOINT ${name}`;
4557
+ }
4558
+ /**
4559
+ * Build a JSON_TABLE-equivalent expression to shred a JSON array into relational rows.
4560
+ * Not supported: CockroachDB implements a subset of Postgres's JSONB function surface
4561
+ * and has neither a native `JSON_TABLE` (PG17+) nor `json_to_recordset`/
4562
+ * `jsonb_to_recordset` (verified absent from CockroachDB's function reference as of
4563
+ * v23.x/v24.x). There is no row-shredding equivalent to fall back to.
4564
+ */
4565
+ buildJsonTable(_jsonExpression, _rowPath, _columns, _alias) {
4566
+ throw new Error('CockroachDB does not support JSON_TABLE, OPENJSON, or json_to_recordset/jsonb_to_recordset; ' +
4567
+ 'there is no supported way to shred a JSON array into relational rows');
4568
+ }
4569
+ }
4570
+ exports.CockroachDBDialect = CockroachDBDialect;
4571
+ /**
4572
+ * CockroachDB has no Foreign Data Wrapper (FDW) machinery: no
4573
+ * `CREATE FOREIGN DATA WRAPPER`/`CREATE SERVER`/`CREATE FOREIGN TABLE`/
4574
+ * `CREATE USER MAPPING`/`IMPORT FOREIGN SCHEMA` statements, and no
4575
+ * `pg_foreign_data_wrapper`/`pg_foreign_server` catalogs. These methods
4576
+ * exist only to satisfy the shared `Dialect` interface (which the
4577
+ * Postgres dialect implements for real); rather than silently emitting
4578
+ * Postgres-only DDL that would fail against a real CockroachDB cluster,
4579
+ * they throw a clear "not supported" error. External-data access on
4580
+ * CockroachDB instead goes through `IMPORT`/`EXPORT` or
4581
+ * `CREATE EXTERNAL CONNECTION`, which are out of scope for this ORM's
4582
+ * FDW-shaped API.
4583
+ * @see https://www.cockroachlabs.com/docs/stable/create-external-connection
4584
+ */
4585
+ CockroachDBDialect.FDW_NOT_SUPPORTED_MESSAGE = 'Foreign Data Wrappers are not supported in CockroachDB (no CREATE FOREIGN DATA WRAPPER/CREATE SERVER/CREATE FOREIGN TABLE support). ' +
4586
+ 'Use IMPORT/EXPORT or CREATE EXTERNAL CONNECTION for external data access instead.';
4587
+ /**
4588
+ * CockroachDB does NOT implement PostgreSQL's extension mechanism the way
4589
+ * Postgres does. `CREATE EXTENSION`/`DROP EXTENSION` are accepted by CRDB
4590
+ * purely as no-op compatibility shims for a small, fixed allowlist of
4591
+ * extension names (kept in sync with CockroachDB's documented "Supported
4592
+ * extensions" list) so that ORMs/tools which unconditionally issue
4593
+ * `CREATE EXTENSION IF NOT EXISTS <name>` don't fail outright. No actual
4594
+ * extension code is loaded and most Postgres extensions (arbitrary compiled
4595
+ * extensions, most contrib modules, etc.) are NOT usable at all. Callers
4596
+ * asking for anything outside this allowlist get a clear error instead of
4597
+ * a misleading "success" that doesn't provide any real functionality.
4598
+ */
4599
+ CockroachDBDialect.SUPPORTED_EXTENSION_SHIMS = new Set([
4600
+ 'citext',
4601
+ 'plpgsql',
4602
+ 'pg_trgm',
4603
+ 'fuzzystrmatch',
4604
+ 'hstore',
4605
+ 'pgcrypto',
4606
+ 'uuid-ossp',
4607
+ 'btree_gin',
4608
+ 'btree_gist',
4609
+ 'cube',
4610
+ 'intarray',
4611
+ 'ltree',
4612
+ 'pg_stat_statements',
4613
+ 'vector',
4614
+ ]);
4615
+ /**
4616
+ * CockroachDB-specific transaction class with isolation level support and savepoints
4617
+ */
4618
+ class CockroachDBTransaction {
4619
+ constructor(client, depth, options) {
4620
+ this.finished = false;
4621
+ this.parent = null;
4622
+ this.savepoints = [];
4623
+ this.client = null;
4624
+ this.savepointCount = 0;
4625
+ this.id = `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
4626
+ this.options = options || {};
4627
+ this.client = client;
4628
+ }
4629
+ /**
4630
+ * Create a savepoint
4631
+ */
4632
+ async createSavepoint(name) {
4633
+ const savepointName = name || `sp_${this.savepointCount++}`;
4634
+ if (this.client) {
4635
+ await this.client.query(`SAVEPOINT ${savepointName}`);
4636
+ this.savepoints.push(savepointName);
4637
+ }
4638
+ return savepointName;
4639
+ }
4640
+ /**
4641
+ * Rollback to a savepoint
4642
+ */
4643
+ async rollbackToSavepoint(name) {
4644
+ if (this.client) {
4645
+ await this.client.query(`ROLLBACK TO SAVEPOINT ${name}`);
4646
+ }
4647
+ }
4648
+ /**
4649
+ * Release a savepoint
4650
+ */
4651
+ async releaseSavepoint(name) {
4652
+ if (this.client) {
4653
+ await this.client.query(`RELEASE SAVEPOINT ${name}`);
4654
+ this.savepoints = this.savepoints.filter((sp) => sp !== name);
4655
+ }
4656
+ }
4657
+ async commit() {
4658
+ if (this.client) {
4659
+ await this.client.query('COMMIT');
4660
+ }
4661
+ this.finished = true;
4662
+ }
4663
+ async rollback() {
4664
+ if (this.client) {
4665
+ await this.client.query('ROLLBACK');
4666
+ }
4667
+ this.finished = true;
4668
+ }
4669
+ }
4670
+ exports.CockroachDBTransaction = CockroachDBTransaction;
4671
+ /**
4672
+ * Create a new CockroachDB dialect instance
4673
+ */
4674
+ function createCockroachDBDialect(options) {
4675
+ return new CockroachDBDialect(options || {});
4676
+ }
4677
+ exports.default = CockroachDBDialect;