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,2277 @@
1
+ "use strict";
2
+ /**
3
+ * Amazon Redshift dialect implementation for the TypeScript ORM
4
+ *
5
+ * Redshift speaks the PostgreSQL wire protocol (it is a fork of Postgres 8.0.2),
6
+ * so this dialect reuses the `pg` driver exactly like the Postgres dialect and
7
+ * mirrors much of its query-building behavior. However, Redshift diverges from
8
+ * Postgres in ways that matter a lot in practice — two gotchas in particular:
9
+ *
10
+ * 1. FOREIGN KEY / UNIQUE / PRIMARY KEY constraints are accepted syntactically
11
+ * (and stored in the catalog for the query planner / documentation
12
+ * purposes) but are **NEVER enforced** at write time. `INSERT`/`UPDATE`
13
+ * statements that violate a declared FK or UNIQUE constraint will succeed
14
+ * silently. Do not rely on Redshift to guarantee referential integrity or
15
+ * uniqueness — enforce that in application code. See the inline comments
16
+ * on `getColumnDefinitionSql`, `buildConstraintSql`, `addForeignKey`,
17
+ * `createConstraint` below for where this shows up.
18
+ *
19
+ * 2. There are no traditional B-tree secondary indexes. Instead, performance
20
+ * is governed by table-level `DISTSTYLE`/`DISTKEY` (how rows are
21
+ * distributed across compute node slices) and `SORTKEY` (how rows are
22
+ * physically ordered on disk, either `COMPOUND` or `INTERLEAVED`). These
23
+ * are specified via `RedshiftTableOptions` on `createTable` and emitted
24
+ * directly in the `CREATE TABLE` DDL. Methods that would create a
25
+ * traditional index (`addIndex`, `createIndex`, `createPartialIndex`, ...)
26
+ * throw a clear error pointing callers at DISTKEY/SORTKEY instead.
27
+ *
28
+ * Other notable differences from the Postgres dialect:
29
+ * - Default port is 5439 (not 5432).
30
+ * - No native JSON/JSONB type before Redshift's semi-structured `SUPER` type;
31
+ * `getDataTypeSql` maps both `JSON` and `JSONB` DataTypes to `SUPER`.
32
+ * - Auto-increment columns use `IDENTITY(seed, step)` rather than
33
+ * `SERIAL`/`BIGSERIAL`.
34
+ * - `MERGE` is only available in Redshift starting ~2023 and `ON CONFLICT`
35
+ * has never been supported, so `buildUpsertQuery` does NOT generate
36
+ * `INSERT ... ON CONFLICT DO UPDATE` (that would simply fail against a real
37
+ * cluster prior to MERGE support, and is not portable). Instead it
38
+ * generates AWS's own documented staging-table + `DELETE` + `INSERT`
39
+ * pattern. See the comment on `buildUpsertQuery` for the full rationale.
40
+ * - Bulk loads are normally done via `COPY FROM S3` rather than row-by-row
41
+ * `INSERT`. `copyFromS3()` is added as an extra (non-`Dialect`-interface)
42
+ * method for that; ordinary `bulkInsert`/`buildInsertQuery` still work via
43
+ * plain multi-row `INSERT` for compatibility with the rest of the ORM.
44
+ * - No triggers, no sequences (`CREATE SEQUENCE`/`nextval`), no row-level
45
+ * security policies, and no foreign-data-wrapper support (Redshift uses
46
+ * Redshift Spectrum's `CREATE EXTERNAL SCHEMA` for federated/external data
47
+ * instead). All of these throw a clear, explanatory `Error`.
48
+ * - Redshift *does* support views, materialized views (with optional
49
+ * `AUTO REFRESH`), and a limited form of stored procedures
50
+ * (`CREATE OR REPLACE PROCEDURE ... LANGUAGE plpgsql`), so those are
51
+ * implemented much like Postgres.
52
+ */
53
+ var __importDefault = (this && this.__importDefault) || function (mod) {
54
+ return (mod && mod.__esModule) ? mod : { "default": mod };
55
+ };
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.RedshiftTransaction = exports.RedshiftDialect = void 0;
58
+ exports.createRedshiftDialect = createRedshiftDialect;
59
+ const pg_1 = __importDefault(require("pg"));
60
+ const query_stream_helper_1 = require("../query-stream-helper");
61
+ const prorm_1 = require("../../prorm");
62
+ const index_1 = require("../../errors/index");
63
+ const index_2 = require("../../utils/index");
64
+ const operators_1 = require("../../operators");
65
+ const DEFAULT_RETRY_OPTIONS = {
66
+ max: 3,
67
+ timeout: 1000,
68
+ match: [
69
+ 'ECONNREFUSED',
70
+ 'ENOTFOUND',
71
+ 'ETIMEDOUT',
72
+ 'connection timeout',
73
+ 'connect timeout',
74
+ 'too many connections',
75
+ ],
76
+ backoff: false,
77
+ backoffMultiplier: 2,
78
+ backoffMax: 10000,
79
+ };
80
+ const DEFAULT_POOL_OPTIONS = {
81
+ max: 10,
82
+ min: 0,
83
+ acquire: 60000,
84
+ idle: 30000,
85
+ evict: 1000,
86
+ handleDisconnects: false,
87
+ onConnect: undefined,
88
+ onDisconnect: undefined,
89
+ enableEvents: false,
90
+ logPoolOperations: false,
91
+ };
92
+ /**
93
+ * Amazon Redshift dialect class implementing the Dialect interface.
94
+ *
95
+ * See the module-level doc comment above for the two biggest Postgres ->
96
+ * Redshift surprises: unenforced FK/UNIQUE constraints, and DISTKEY/SORTKEY
97
+ * replacing B-tree indexes.
98
+ */
99
+ class RedshiftDialect {
100
+ constructor(config) {
101
+ this.name = 'redshift';
102
+ this.library = 'pg';
103
+ this.pool = null;
104
+ this._isConnected = false;
105
+ this.transactionDepth = 0;
106
+ this.config = {
107
+ host: 'localhost',
108
+ port: 5439,
109
+ max: 10,
110
+ idleTimeoutMillis: 30000,
111
+ connectionTimeoutMillis: 2000,
112
+ ...config,
113
+ };
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // Connection management
117
+ // ---------------------------------------------------------------------------
118
+ async connect() {
119
+ try {
120
+ const { Pool: PgPool } = pg_1.default;
121
+ this.pool = new PgPool({
122
+ host: this.config.host,
123
+ port: this.config.port,
124
+ user: this.config.username,
125
+ password: this.config.password,
126
+ database: this.config.database,
127
+ max: this.config.max,
128
+ idleTimeoutMillis: this.config.idleTimeoutMillis,
129
+ connectionTimeoutMillis: this.config.connectionTimeoutMillis,
130
+ ssl: this.config.ssl,
131
+ statement_timeout: this.config.statementTimeout,
132
+ query_timeout: this.config.queryTimeout,
133
+ });
134
+ const client = await this.pool.connect();
135
+ client.release();
136
+ this._isConnected = true;
137
+ }
138
+ catch (error) {
139
+ throw new Error(`Failed to connect to Redshift database: ${error}`);
140
+ }
141
+ }
142
+ async disconnect() {
143
+ if (this.pool) {
144
+ await this.pool.end();
145
+ this.pool = null;
146
+ this._isConnected = false;
147
+ }
148
+ }
149
+ getConnection() {
150
+ return this.pool;
151
+ }
152
+ isConnected() {
153
+ return this._isConnected && this.pool !== null;
154
+ }
155
+ /**
156
+ * Stream query results by paging through `sql` via repeated
157
+ * dialect-appropriate LIMIT/OFFSET queries (see
158
+ * `createPaginatedQueryStream()` in `src/dialects/query-stream-helper.ts`)
159
+ * instead of loading the whole result set into memory at once.
160
+ * @param sql - The SELECT statement to stream
161
+ * @param options - Streaming options (batch size, backpressure watermark, model mapping)
162
+ */
163
+ queryStream(sql, options) {
164
+ return (0, query_stream_helper_1.createPaginatedQueryStream)(this, sql, options);
165
+ }
166
+ async query(sql, options) {
167
+ const executeQuery = async () => {
168
+ if (!this.pool) {
169
+ throw new Error('Not connected to database');
170
+ }
171
+ const isSelect = sql.trim().toUpperCase().startsWith('SELECT') ||
172
+ sql.trim().toUpperCase().startsWith('WITH') ||
173
+ sql.trim().toUpperCase().startsWith('SHOW');
174
+ try {
175
+ const replacements = options?.replacements ?? options?.bindings;
176
+ const finalSql = replacements !== undefined ? this.replaceReplacements(sql, replacements) : sql;
177
+ const result = (await this.pool.query(finalSql));
178
+ const fieldInfo = result.fields
179
+ ? result.fields.map((field) => ({
180
+ name: field.name,
181
+ type: field.dataTypeID.toString(),
182
+ length: field.dataTypeModifier || 0,
183
+ tableID: 0,
184
+ columnID: 0,
185
+ nullable: true,
186
+ isEnum: false,
187
+ isPrimaryKey: false,
188
+ }))
189
+ : [];
190
+ if (isSelect) {
191
+ return { rows: result.rows, rowCount: result.rowCount || result.rows.length, fields: fieldInfo };
192
+ }
193
+ const rowCount = result.rowCount || 0;
194
+ if (result.rows && result.rows.length > 0) {
195
+ return { rows: result.rows, rowCount: result.rows.length, fields: fieldInfo };
196
+ }
197
+ return { rows: [], rowCount, fields: fieldInfo };
198
+ }
199
+ catch (error) {
200
+ throw new Error(`Redshift Query error: ${error}`);
201
+ }
202
+ };
203
+ const retryOptions = options?.retry;
204
+ if (retryOptions) {
205
+ const effectiveRetry = { ...DEFAULT_RETRY_OPTIONS, ...retryOptions, match: retryOptions.match || DEFAULT_RETRY_OPTIONS.match };
206
+ return this.executeWithRetry(executeQuery, effectiveRetry);
207
+ }
208
+ return executeQuery();
209
+ }
210
+ async executeWithRetry(fn, retryOptions) {
211
+ let lastError;
212
+ for (let attempt = 0; attempt <= retryOptions.max; attempt++) {
213
+ try {
214
+ return await fn();
215
+ }
216
+ catch (error) {
217
+ lastError = error;
218
+ const shouldRetry = retryOptions.match.some((p) => lastError.message.toLowerCase().includes(p.toLowerCase()));
219
+ if (!shouldRetry || attempt === retryOptions.max)
220
+ throw lastError;
221
+ await new Promise((resolve) => setTimeout(resolve, retryOptions.timeout));
222
+ }
223
+ }
224
+ throw lastError;
225
+ }
226
+ // ---------------------------------------------------------------------------
227
+ // Escaping / identifiers
228
+ // ---------------------------------------------------------------------------
229
+ escape(value) {
230
+ if (value instanceof prorm_1.Literal)
231
+ return value.val;
232
+ if (value === null || value === undefined)
233
+ return 'NULL';
234
+ if (typeof value === 'string')
235
+ return `'${this.escapeString(value)}'`;
236
+ if (typeof value === 'number')
237
+ return String(value);
238
+ if (typeof value === 'boolean')
239
+ return value ? 'TRUE' : 'FALSE';
240
+ if (value instanceof Date)
241
+ return `'${this.formatDate(value)}'`;
242
+ if (Buffer.isBuffer(value))
243
+ return `'${value.toString('hex')}'`;
244
+ if (Array.isArray(value)) {
245
+ // Redshift's SUPER type accepts JSON literals directly.
246
+ return `JSON_PARSE('${this.escapeString(JSON.stringify(value))}')`;
247
+ }
248
+ return `'${this.escapeString(JSON.stringify(value))}'`;
249
+ }
250
+ escapeString(str) {
251
+ return str.replace(/'/g, "''").replace(/\\/g, '\\\\');
252
+ }
253
+ formatDate(date) {
254
+ const year = date.getFullYear();
255
+ const month = String(date.getMonth() + 1).padStart(2, '0');
256
+ const day = String(date.getDate()).padStart(2, '0');
257
+ const hours = String(date.getHours()).padStart(2, '0');
258
+ const minutes = String(date.getMinutes()).padStart(2, '0');
259
+ const seconds = String(date.getSeconds()).padStart(2, '0');
260
+ const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
261
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
262
+ }
263
+ escapeId(identifier) {
264
+ const id = String(identifier ?? '');
265
+ return `"${id.replace(/"/g, '""')}"`;
266
+ }
267
+ quoteIdentifier(identifier) {
268
+ return this.escapeId(identifier);
269
+ }
270
+ quoteTable(tableName, schema) {
271
+ if (schema && schema !== 'public') {
272
+ return `${this.escapeId(schema)}.${this.escapeId(tableName)}`;
273
+ }
274
+ return this.escapeId(tableName);
275
+ }
276
+ async getDatabaseVersion() {
277
+ const result = await this.query('SELECT version() as version');
278
+ return result.rows[0]?.version || 'Unknown';
279
+ }
280
+ // ---------------------------------------------------------------------------
281
+ // Schemas / database DDL
282
+ // ---------------------------------------------------------------------------
283
+ async createSchema(schema) {
284
+ await this.query(`CREATE SCHEMA ${this.escapeId(schema)}`);
285
+ }
286
+ async dropSchema(schema, options) {
287
+ let sql = 'DROP SCHEMA';
288
+ if (options?.ifExists)
289
+ sql += ' IF EXISTS';
290
+ sql += ` ${this.escapeId(schema)}`;
291
+ if (options?.cascade)
292
+ sql += ' CASCADE';
293
+ await this.query(sql);
294
+ }
295
+ async showAllSchemas() {
296
+ 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`);
297
+ return result.rows?.map((row) => row.schema_name) || [];
298
+ }
299
+ async listSchemas() {
300
+ return this.showAllSchemas();
301
+ }
302
+ createDatabaseSQL(options) {
303
+ return `CREATE DATABASE ${this.quoteIdentifier(options.name)}`;
304
+ }
305
+ dropDatabaseSQL(name) {
306
+ return `DROP DATABASE ${this.quoteIdentifier(name)}`;
307
+ }
308
+ createSavepointSQL(name) {
309
+ return `SAVEPOINT ${name || `sp_${Date.now()}`}`;
310
+ }
311
+ releaseSavepointSQL(name) {
312
+ return `RELEASE SAVEPOINT ${name}`;
313
+ }
314
+ rollbackToSavepointSQL(name) {
315
+ return `ROLLBACK TO SAVEPOINT ${name}`;
316
+ }
317
+ // ---------------------------------------------------------------------------
318
+ // Extensions - Redshift has no CREATE EXTENSION mechanism.
319
+ // ---------------------------------------------------------------------------
320
+ async createExtension(_extensionName, _options) {
321
+ throw new index_1.DatabaseError('Extensions are not supported by Redshift. This is a PostgreSQL-specific feature.');
322
+ }
323
+ async dropExtension(_extensionName, _options) {
324
+ throw new index_1.DatabaseError('Extensions are not supported by Redshift. This is a PostgreSQL-specific feature.');
325
+ }
326
+ async getExtensions() {
327
+ throw new index_1.DatabaseError('Extensions are not supported by Redshift. This is a PostgreSQL-specific feature.');
328
+ }
329
+ async hasExtension(_extensionName) {
330
+ throw new index_1.DatabaseError('Extensions are not supported by Redshift. This is a PostgreSQL-specific feature.');
331
+ }
332
+ // ---------------------------------------------------------------------------
333
+ // Tables
334
+ // ---------------------------------------------------------------------------
335
+ /**
336
+ * Create a table.
337
+ *
338
+ * Emits Redshift's DISTSTYLE/DISTKEY/SORTKEY table-level clauses (the
339
+ * replacement for traditional B-tree indexes) when present on `options`.
340
+ *
341
+ * FOREIGN KEY / UNIQUE / PRIMARY KEY constraints given via `options.constraints`
342
+ * are still emitted as DDL — Redshift's parser accepts the syntax and stores
343
+ * it in the catalog for the query planner and for documentation/ERD tooling
344
+ * — but NONE of them are enforced at write time. Do not rely on this DDL for
345
+ * actual data integrity guarantees.
346
+ */
347
+ async createTable(tableName, columns, options) {
348
+ const columnDefs = [];
349
+ for (const [columnName, definition] of Object.entries(columns)) {
350
+ columnDefs.push(this.getColumnDefinitionSql(columnName, definition));
351
+ }
352
+ // Constraint DDL is accepted but NOT enforced by Redshift (see class doc comment).
353
+ if (options?.constraints) {
354
+ for (const constraint of options.constraints) {
355
+ const constraintSql = this.buildConstraintSql(constraint);
356
+ if (constraintSql)
357
+ columnDefs.push(constraintSql);
358
+ }
359
+ }
360
+ let sql = 'CREATE TABLE';
361
+ if (options?.ifNotExists)
362
+ sql += ' IF NOT EXISTS';
363
+ sql += ` ${this.escapeId(tableName)} (${columnDefs.join(', ')})`;
364
+ // ---- DISTSTYLE / DISTKEY / SORTKEY: Redshift's replacement for indexes ----
365
+ if (options?.diststyle) {
366
+ sql += ` DISTSTYLE ${options.diststyle}`;
367
+ }
368
+ if (options?.distKey) {
369
+ sql += ` DISTKEY (${this.escapeId(options.distKey)})`;
370
+ }
371
+ if (options?.sortKey) {
372
+ const sortKeyType = options.sortkeyType || 'COMPOUND';
373
+ const sortCols = Array.isArray(options.sortKey) ? options.sortKey : [options.sortKey];
374
+ sql += ` ${sortKeyType} SORTKEY (${sortCols.map((c) => this.escapeId(c)).join(', ')})`;
375
+ }
376
+ if (options?.engine) {
377
+ console.warn(`Redshift: engine option '${options.engine}' is not supported and will be ignored`);
378
+ }
379
+ if (options?.charset) {
380
+ console.warn(`Redshift: charset option '${options.charset}' is not supported and will be ignored`);
381
+ }
382
+ if (options?.rowFormat) {
383
+ console.warn(`Redshift: rowFormat option '${options.rowFormat}' is not supported and will be ignored`);
384
+ }
385
+ await this.query(sql);
386
+ if (options?.uniqueKeys) {
387
+ // Also unenforced in Redshift - see class doc comment.
388
+ if (Array.isArray(options.uniqueKeys)) {
389
+ for (const uk of options.uniqueKeys) {
390
+ const constraintName = uk.name || `${tableName}_${uk.fields.join('_')}_key`;
391
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} UNIQUE (${uk.fields.map((f) => this.escapeId(f)).join(', ')})`);
392
+ }
393
+ }
394
+ else {
395
+ for (const [keyName, fields] of Object.entries(options.uniqueKeys)) {
396
+ const constraintName = keyName || `${tableName}_${fields.join('_')}_key`;
397
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} UNIQUE (${fields.map((f) => this.escapeId(f)).join(', ')})`);
398
+ }
399
+ }
400
+ }
401
+ if (options?.comment) {
402
+ await this.commentTable(tableName, options.comment);
403
+ }
404
+ if (options?.indexes && options.indexes.length > 0) {
405
+ // Redshift has no CREATE INDEX at all; fail loudly instead of silently
406
+ // ignoring a request the caller probably expects to matter.
407
+ throw new index_1.DatabaseError('Redshift does not support secondary indexes. Use the distKey/sortKey/diststyle table options instead of options.indexes.');
408
+ }
409
+ }
410
+ /**
411
+ * Build constraint SQL for table creation.
412
+ *
413
+ * NOTE: Redshift accepts PRIMARY KEY / UNIQUE / FOREIGN KEY DDL but never
414
+ * enforces any of it. CHECK constraints are not supported at all by
415
+ * Redshift and will throw.
416
+ */
417
+ buildConstraintSql(constraint) {
418
+ const name = constraint.name ? this.escapeId(constraint.name) : '';
419
+ const fields = constraint.fields?.map((f) => this.escapeId(f)).join(', ') || '';
420
+ switch (constraint.type) {
421
+ case 'PRIMARY KEY':
422
+ return `${name ? name + ' ' : ''}PRIMARY KEY (${fields})`;
423
+ case 'UNIQUE':
424
+ return `${name ? name + ' ' : ''}UNIQUE (${fields})`;
425
+ case 'FOREIGN KEY': {
426
+ if (!constraint.references)
427
+ return null;
428
+ const refField = constraint.references.field;
429
+ const refFieldSql = Array.isArray(refField)
430
+ ? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
431
+ : `(${this.escapeId(refField)})`;
432
+ // Accepted-but-unenforced: Redshift stores this for the query planner only.
433
+ let fkSql = `${name ? name + ' ' : ''}FOREIGN KEY (${fields}) `;
434
+ fkSql += `REFERENCES ${this.escapeId(constraint.references.table)}${refFieldSql}`;
435
+ return fkSql;
436
+ }
437
+ case 'CHECK':
438
+ throw new index_1.DatabaseError('Redshift does not support CHECK constraints');
439
+ default:
440
+ return null;
441
+ }
442
+ }
443
+ /**
444
+ * Generate column definition SQL.
445
+ *
446
+ * Auto-increment columns use Redshift's `IDENTITY(seed, step)` clause
447
+ * instead of Postgres's `SERIAL`/`BIGSERIAL` (Redshift has no SERIAL type
448
+ * and no sequence objects backing it).
449
+ *
450
+ * Also emits Redshift's column-level `ENCODE <type>` compression clause
451
+ * (e.g. `ENCODE AZ64`, `ENCODE ZSTD`, `ENCODE BYTEDICT`, ...) immediately
452
+ * after the data type when `definition.encode` (see `RedshiftColumnDefinition`)
453
+ * is set. This is a perf-critical, Redshift-specific storage option with no
454
+ * Postgres equivalent.
455
+ */
456
+ getColumnDefinitionSql(columnName, definition) {
457
+ const dt = definition.type;
458
+ if (definition.autoIncrement) {
459
+ // Redshift's IDENTITY clause can be attached to SMALLINT/INT2, INTEGER/INT4,
460
+ // and BIGINT/INT8 columns; each keeps its own storage width and value range,
461
+ // so a SMALLINT autoIncrement column must stay SMALLINT IDENTITY rather than
462
+ // silently widening to INTEGER.
463
+ const identityBaseTypes = {
464
+ SMALLINT: 'SMALLINT',
465
+ INT2: 'SMALLINT',
466
+ INTEGER: 'INTEGER',
467
+ INT: 'INTEGER',
468
+ INT4: 'INTEGER',
469
+ BIGINT: 'BIGINT',
470
+ INT8: 'BIGINT',
471
+ };
472
+ const baseType = identityBaseTypes[String(dt?.key)] ?? 'INTEGER';
473
+ const identityOpts = definition.identity;
474
+ const seed = identityOpts?.seed ?? 1;
475
+ const step = identityOpts?.step ?? 1;
476
+ let sql = `${this.escapeId(columnName)} ${baseType} IDENTITY(${seed}, ${step})`;
477
+ if (definition.encode)
478
+ sql += ` ENCODE ${definition.encode}`;
479
+ if (definition.primaryKey)
480
+ sql += ' PRIMARY KEY';
481
+ if (definition.allowNull === false)
482
+ sql += ' NOT NULL';
483
+ if (definition.unique)
484
+ sql += ' UNIQUE'; // unenforced - see class doc comment
485
+ if (definition.references) {
486
+ const refField = definition.references.field;
487
+ const refFieldSql = Array.isArray(refField)
488
+ ? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
489
+ : this.escapeId(refField);
490
+ // Accepted-but-unenforced FK reference.
491
+ sql += ` REFERENCES ${this.escapeId(definition.references.table)}(${refFieldSql})`;
492
+ }
493
+ return sql;
494
+ }
495
+ let sql = `${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type)}`;
496
+ if (definition.encode)
497
+ sql += ` ENCODE ${definition.encode}`;
498
+ if (definition.allowNull === false)
499
+ sql += ' NOT NULL';
500
+ if (definition.defaultValue !== undefined)
501
+ sql += ` DEFAULT ${this.getDefaultValue(definition.defaultValue)}`;
502
+ if (definition.primaryKey)
503
+ sql += ' PRIMARY KEY'; // unenforced by Redshift
504
+ if (definition.unique) {
505
+ // unenforced by Redshift - accepted for documentation/planner purposes only
506
+ sql += typeof definition.unique === 'string' ? ` UNIQUE (${this.escapeId(definition.unique)})` : ' UNIQUE';
507
+ }
508
+ if (definition.references) {
509
+ const refField = definition.references.field;
510
+ const refFieldSql = Array.isArray(refField)
511
+ ? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
512
+ : this.escapeId(refField);
513
+ // Accepted-but-unenforced FK reference (see class doc comment).
514
+ sql += ` REFERENCES ${this.escapeId(definition.references.table)}(${refFieldSql})`;
515
+ }
516
+ return sql;
517
+ }
518
+ getDefaultValue(value) {
519
+ if (value === null)
520
+ return 'NULL';
521
+ if (typeof value === 'string') {
522
+ if (value.toUpperCase().includes('CURRENT_') || value.toUpperCase() === 'NULL')
523
+ return value;
524
+ return `'${this.escapeString(value)}'`;
525
+ }
526
+ if (typeof value === 'number' || typeof value === 'boolean')
527
+ return String(value);
528
+ if (value instanceof Date)
529
+ return `'${this.formatDate(value)}'`;
530
+ if (typeof value === 'object' && value !== null && typeof value.toDefaultValue === 'function') {
531
+ return value.toDefaultValue();
532
+ }
533
+ return `'${this.escapeString(String(value))}'`;
534
+ }
535
+ async dropTable(tableName, options) {
536
+ let sql = 'DROP TABLE';
537
+ if (options?.ifExists)
538
+ sql += ' IF EXISTS';
539
+ sql += ` ${this.escapeId(tableName)}`;
540
+ if (options?.cascade)
541
+ sql += ' CASCADE';
542
+ await this.query(sql);
543
+ }
544
+ // ---------------------------------------------------------------------------
545
+ // Native table partitioning - Redshift has no declarative partitioning.
546
+ // Use DISTKEY/SORTKEY for the equivalent performance tuning instead.
547
+ // ---------------------------------------------------------------------------
548
+ async createPartitionedTable() {
549
+ throw new index_1.DatabaseError('Redshift does not support declarative table partitioning. Use the distKey/sortKey/diststyle table options instead.');
550
+ }
551
+ async createPartition(_options) {
552
+ throw new index_1.DatabaseError('Redshift does not support table partitions.');
553
+ }
554
+ async attachPartition(_options) {
555
+ throw new index_1.DatabaseError('Redshift does not support table partitions.');
556
+ }
557
+ async detachPartition(_options) {
558
+ throw new index_1.DatabaseError('Redshift does not support table partitions.');
559
+ }
560
+ async dropPartition(_partitionName, _options) {
561
+ throw new index_1.DatabaseError('Redshift does not support table partitions.');
562
+ }
563
+ // ---------------------------------------------------------------------------
564
+ // Views
565
+ // ---------------------------------------------------------------------------
566
+ async createView(viewName, query, options) {
567
+ const viewNameWithSchema = options?.schema ? `${this.escapeId(options.schema)}.${this.escapeId(viewName)}` : this.escapeId(viewName);
568
+ let sql = options?.replace ? 'CREATE OR REPLACE VIEW' : 'CREATE VIEW';
569
+ sql += ` ${viewNameWithSchema} AS ${query}`;
570
+ await this.query(sql);
571
+ if (options?.comment) {
572
+ await this.query(`COMMENT ON VIEW ${viewNameWithSchema} IS ${this.escape(options.comment)}`);
573
+ }
574
+ }
575
+ async dropView(viewName, options) {
576
+ let sql = 'DROP VIEW';
577
+ if (options?.ifExists)
578
+ sql += ' IF EXISTS';
579
+ sql += ` ${this.escapeId(viewName)}`;
580
+ if (options?.cascade)
581
+ sql += ' CASCADE';
582
+ await this.query(sql);
583
+ }
584
+ async showViews() {
585
+ const result = await this.query(`SELECT table_name FROM information_schema.views WHERE table_schema = 'public' ORDER BY table_name`);
586
+ return result.rows?.map((row) => row.table_name) || [];
587
+ }
588
+ // ---------------------------------------------------------------------------
589
+ // Materialized views - supported natively by Redshift (with optional AUTO
590
+ // REFRESH, BACKUP NO, and DISTKEY/SORTKEY on the view's result table).
591
+ // ---------------------------------------------------------------------------
592
+ async createMaterializedView(options) {
593
+ const viewNameWithSchema = options.schema
594
+ ? `${this.escapeId(options.schema)}.${this.escapeId(options.name)}`
595
+ : this.escapeId(options.name);
596
+ let sql = 'CREATE MATERIALIZED VIEW';
597
+ if (options.ifNotExists)
598
+ sql = 'CREATE MATERIALIZED VIEW IF NOT EXISTS';
599
+ sql += ` ${viewNameWithSchema}`;
600
+ if (options.backup === false) {
601
+ sql += ' BACKUP NO';
602
+ }
603
+ if (options.diststyle) {
604
+ sql += ` DISTSTYLE ${options.diststyle}`;
605
+ }
606
+ if (options.distKey) {
607
+ sql += ` DISTKEY (${this.escapeId(options.distKey)})`;
608
+ }
609
+ if (options.sortKey) {
610
+ const sortKeyType = options.sortkeyType || 'COMPOUND';
611
+ const sortCols = Array.isArray(options.sortKey) ? options.sortKey : [options.sortKey];
612
+ sql += ` ${sortKeyType} SORTKEY (${sortCols.map((c) => this.escapeId(c)).join(', ')})`;
613
+ }
614
+ if (options.autoRefresh) {
615
+ sql += ' AUTO REFRESH YES';
616
+ }
617
+ sql += ` AS ${options.query}`;
618
+ await this.query(sql);
619
+ }
620
+ async refreshMaterializedView(viewName, _options) {
621
+ await this.query(`REFRESH MATERIALIZED VIEW ${this.escapeId(viewName)}`);
622
+ }
623
+ /**
624
+ * Toggle Redshift's `AUTO REFRESH` mode on an existing materialized view
625
+ * after creation, via `ALTER MATERIALIZED VIEW ... AUTO REFRESH YES|NO`.
626
+ * This is distinct from `refreshMaterializedView`, which performs a single
627
+ * manual blocking refresh; this method instead changes whether Redshift
628
+ * automatically re-refreshes the view in the background going forward.
629
+ */
630
+ async setMaterializedViewAutoRefresh(viewName, autoRefresh) {
631
+ await this.query(`ALTER MATERIALIZED VIEW ${this.escapeId(viewName)} AUTO REFRESH ${autoRefresh ? 'YES' : 'NO'}`);
632
+ }
633
+ async dropMaterializedView(viewName, options) {
634
+ let sql = 'DROP MATERIALIZED VIEW';
635
+ if (options?.ifExists)
636
+ sql += ' IF EXISTS';
637
+ sql += ` ${this.escapeId(viewName)}`;
638
+ if (options?.cascade)
639
+ sql += ' CASCADE';
640
+ await this.query(sql);
641
+ }
642
+ async hasMaterializedView(viewName) {
643
+ const result = await this.query(`SELECT 1 FROM pg_matviews WHERE matviewname = ${this.escape(viewName)}`);
644
+ return (result.rows?.length ?? 0) > 0;
645
+ }
646
+ async showMaterializedViews() {
647
+ const result = await this.query(`SELECT matviewname FROM pg_matviews WHERE schemaname = 'public' ORDER BY matviewname`);
648
+ return result.rows?.map((row) => row.matviewname) || [];
649
+ }
650
+ // ---------------------------------------------------------------------------
651
+ // Columns
652
+ // ---------------------------------------------------------------------------
653
+ async addColumn(tableName, columnName, definition) {
654
+ const sql = `ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${this.getColumnDefinitionSql(columnName, definition)}`;
655
+ await this.query(sql);
656
+ }
657
+ async removeColumn(tableName, columnName) {
658
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} DROP COLUMN ${this.escapeId(columnName)}`);
659
+ }
660
+ /**
661
+ * Change a column's type. Note: Redshift's ALTER COLUMN TYPE support is far
662
+ * more limited than Postgres's — it generally only allows widening VARCHAR
663
+ * lengths and a handful of numeric-type widenings, not arbitrary type
664
+ * changes. The SQL is still emitted here; Redshift itself will reject
665
+ * unsupported conversions.
666
+ */
667
+ async changeColumn(tableName, columnName, definition) {
668
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} TYPE ${this.getDataTypeSql(definition.type)}`);
669
+ if (definition.allowNull === false) {
670
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} SET NOT NULL`);
671
+ }
672
+ else if (definition.allowNull === true) {
673
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} DROP NOT NULL`);
674
+ }
675
+ }
676
+ async renameColumn(tableName, oldColumnName, newColumnName) {
677
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} RENAME COLUMN ${this.escapeId(oldColumnName)} TO ${this.escapeId(newColumnName)}`);
678
+ }
679
+ /**
680
+ * Add a foreign key to a table.
681
+ *
682
+ * IMPORTANT: Redshift accepts this DDL and records it in the catalog, but
683
+ * NEVER enforces it. Inserts/updates that violate the relationship will
684
+ * succeed. See class doc comment.
685
+ */
686
+ async addForeignKey(tableName, columnName, referencedTableName, referencedColumnName, options) {
687
+ const constraintName = options?.name || `${tableName}_${columnName}_fkey`;
688
+ const sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} FOREIGN KEY (${this.escapeId(columnName)}) REFERENCES ${this.quoteTable(referencedTableName)}(${this.escapeId(referencedColumnName)})`;
689
+ await this.query(sql);
690
+ }
691
+ // ---------------------------------------------------------------------------
692
+ // Bulk insert - plain multi-row INSERT for ORM/CRUD compatibility.
693
+ // For real bulk loads, use copyFromS3() instead (see class doc comment).
694
+ // ---------------------------------------------------------------------------
695
+ async bulkInsert(tableName, records, _options) {
696
+ if (records.length === 0)
697
+ return { rows: [], rowCount: 0, fields: [] };
698
+ const values = [];
699
+ const placeholders = [];
700
+ const columns = Object.keys(records[0]);
701
+ for (const record of records) {
702
+ const rowPlaceholders = [];
703
+ for (let i = 0; i < columns.length; i++) {
704
+ rowPlaceholders.push(`$${values.length + i + 1}`);
705
+ values.push(record[columns[i]]);
706
+ }
707
+ placeholders.push(`(${rowPlaceholders.join(', ')})`);
708
+ }
709
+ const sql = `INSERT INTO ${this.quoteTable(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES ${placeholders.join(', ')}`;
710
+ return this.query(sql, { replacements: values });
711
+ }
712
+ /**
713
+ * Bulk-load data into a table from Amazon S3 using Redshift's `COPY`
714
+ * command. This is NOT part of the `Dialect` interface (it's Redshift-
715
+ * specific) — it's an addition on top of the standard CRUD surface, which
716
+ * still works via plain `INSERT` through `bulkInsert`/`buildInsertQuery`.
717
+ *
718
+ * Authentication is via either an IAM role ARN (recommended) or static
719
+ * credentials — exactly one of `options.iamRole` / `options.credentials`
720
+ * must be supplied.
721
+ */
722
+ async copyFromS3(tableName, s3Path, options) {
723
+ if (!options?.iamRole && !options?.credentials) {
724
+ throw new index_1.DatabaseError('copyFromS3 requires either options.iamRole or options.credentials');
725
+ }
726
+ if (options.iamRole && options.credentials) {
727
+ throw new index_1.DatabaseError('copyFromS3 accepts either options.iamRole or options.credentials, not both');
728
+ }
729
+ let sql = `COPY ${this.quoteTable(tableName)} FROM '${this.escapeString(s3Path)}'`;
730
+ if (options.iamRole) {
731
+ sql += ` IAM_ROLE '${this.escapeString(options.iamRole)}'`;
732
+ }
733
+ else if (options.credentials) {
734
+ const { accessKeyId, secretAccessKey, sessionToken } = options.credentials;
735
+ let creds = `aws_access_key_id=${accessKeyId};aws_secret_access_key=${secretAccessKey}`;
736
+ if (sessionToken)
737
+ creds += `;token=${sessionToken}`;
738
+ sql += ` CREDENTIALS '${this.escapeString(creds)}'`;
739
+ }
740
+ const format = options.format || 'CSV';
741
+ switch (format) {
742
+ case 'CSV':
743
+ sql += ' FORMAT AS CSV';
744
+ if (options.delimiter)
745
+ sql += ` DELIMITER '${options.delimiter}'`;
746
+ if (options.ignoreHeader)
747
+ sql += ` IGNOREHEADER ${options.ignoreHeader}`;
748
+ break;
749
+ case 'JSON':
750
+ sql += ` FORMAT AS JSON '${options.jsonOption || 'auto'}'`;
751
+ break;
752
+ case 'PARQUET':
753
+ sql += ' FORMAT AS PARQUET';
754
+ break;
755
+ case 'AVRO':
756
+ sql += ` FORMAT AS AVRO '${options.jsonOption || 'auto'}'`;
757
+ break;
758
+ case 'ORC':
759
+ sql += ' FORMAT AS ORC';
760
+ break;
761
+ }
762
+ if (options.compression)
763
+ sql += ` ${options.compression}`;
764
+ if (options.region)
765
+ sql += ` REGION '${options.region}'`;
766
+ if (options.maxError !== undefined)
767
+ sql += ` MAXERROR ${options.maxError}`;
768
+ if (options.extra)
769
+ sql += ` ${options.extra}`;
770
+ return this.query(sql);
771
+ }
772
+ /**
773
+ * Bulk-export the result of a query to Amazon S3 using Redshift's `UNLOAD`
774
+ * command — the write-side counterpart to `copyFromS3`. Like `copyFromS3`,
775
+ * this is NOT part of the `Dialect` interface (it's Redshift-specific).
776
+ *
777
+ * Authentication is via either an IAM role ARN (recommended) or static
778
+ * credentials — exactly one of `options.iamRole` / `options.credentials`
779
+ * must be supplied.
780
+ *
781
+ * Builds: `UNLOAD ('<query>') TO 's3://...' IAM_ROLE '...' [FORMAT AS
782
+ * PARQUET|CSV|JSON] [DELIMITER ...] [PARALLEL ON|OFF] [MANIFEST] ...`
783
+ */
784
+ async unloadToS3(query, s3Path, options) {
785
+ if (!options?.iamRole && !options?.credentials) {
786
+ throw new index_1.DatabaseError('unloadToS3 requires either options.iamRole or options.credentials');
787
+ }
788
+ if (options.iamRole && options.credentials) {
789
+ throw new index_1.DatabaseError('unloadToS3 accepts either options.iamRole or options.credentials, not both');
790
+ }
791
+ let sql = `UNLOAD ('${this.escapeString(query)}') TO '${this.escapeString(s3Path)}'`;
792
+ if (options.iamRole) {
793
+ sql += ` IAM_ROLE '${this.escapeString(options.iamRole)}'`;
794
+ }
795
+ else if (options.credentials) {
796
+ const { accessKeyId, secretAccessKey, sessionToken } = options.credentials;
797
+ let creds = `aws_access_key_id=${accessKeyId};aws_secret_access_key=${secretAccessKey}`;
798
+ if (sessionToken)
799
+ creds += `;token=${sessionToken}`;
800
+ sql += ` CREDENTIALS '${this.escapeString(creds)}'`;
801
+ }
802
+ if (options.format === 'PARQUET') {
803
+ sql += ' FORMAT AS PARQUET';
804
+ }
805
+ else if (options.format === 'JSON') {
806
+ sql += ' FORMAT AS JSON';
807
+ }
808
+ else if (options.format === 'CSV') {
809
+ sql += ' FORMAT AS CSV';
810
+ }
811
+ if (options.delimiter)
812
+ sql += ` DELIMITER '${options.delimiter}'`;
813
+ if (options.header)
814
+ sql += ' HEADER';
815
+ if (options.compression)
816
+ sql += ` ${options.compression}`;
817
+ if (options.manifest)
818
+ sql += ' MANIFEST';
819
+ if (options.allowOverwrite)
820
+ sql += ' ALLOWOVERWRITE';
821
+ if (options.parallel !== undefined)
822
+ sql += ` PARALLEL ${options.parallel ? 'ON' : 'OFF'}`;
823
+ if (options.maxFileSize)
824
+ sql += ` MAXFILESIZE ${options.maxFileSize}`;
825
+ if (options.region)
826
+ sql += ` REGION '${options.region}'`;
827
+ if (options.extra)
828
+ sql += ` ${options.extra}`;
829
+ return this.query(sql);
830
+ }
831
+ // ---------------------------------------------------------------------------
832
+ // Datashares (AWS Data Sharing) - not part of the `Dialect` interface, these
833
+ // are Redshift-specific escape hatches like `copyFromS3`/`unloadToS3` above.
834
+ //
835
+ // See https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_DATASHARE.html
836
+ // and related CREATE/ALTER/DROP DATASHARE, GRANT, and CREATE DATABASE
837
+ // reference pages. Unlike most Redshift DDL, CREATE/DROP DATASHARE do NOT
838
+ // support `IF NOT EXISTS`/`IF EXISTS` clauses - that's a deliberate
839
+ // omission here, not an oversight.
840
+ // ---------------------------------------------------------------------------
841
+ /**
842
+ * `CREATE DATASHARE datashare_name [SET PUBLICACCESSIBLE TRUE|FALSE]`
843
+ *
844
+ * Creates a new datashare in the current database, owned by the issuing
845
+ * user. Adding objects to it is a separate step - see
846
+ * `alterDatashareAddSchema`/`alterDatashareAddTable`.
847
+ */
848
+ async createDatashare(name, options) {
849
+ let sql = `CREATE DATASHARE ${this.escapeId(name)}`;
850
+ if (options?.publiclyAccessible !== undefined) {
851
+ sql += ` SET PUBLICACCESSIBLE ${options.publiclyAccessible ? 'TRUE' : 'FALSE'}`;
852
+ }
853
+ return this.query(sql);
854
+ }
855
+ /**
856
+ * `DROP DATASHARE datashare_name`
857
+ *
858
+ * Irreversible. Only a superuser or the datashare owner can run this.
859
+ */
860
+ async dropDatashare(name) {
861
+ return this.query(`DROP DATASHARE ${this.escapeId(name)}`);
862
+ }
863
+ /**
864
+ * `ALTER DATASHARE datashare_name ADD SCHEMA schema`
865
+ *
866
+ * Adding a schema does NOT add the objects inside it - each table still
867
+ * needs to be added explicitly via `alterDatashareAddTable`.
868
+ */
869
+ async alterDatashareAddSchema(datashareName, schemaName) {
870
+ return this.query(`ALTER DATASHARE ${this.escapeId(datashareName)} ADD SCHEMA ${this.escapeId(schemaName)}`);
871
+ }
872
+ /**
873
+ * `ALTER DATASHARE datashare_name ADD TABLE schema.table`
874
+ *
875
+ * The schema must already have been added to the datashare (via
876
+ * `alterDatashareAddSchema`) before tables within it can be added.
877
+ */
878
+ async alterDatashareAddTable(datashareName, schemaName, tableName) {
879
+ return this.query(`ALTER DATASHARE ${this.escapeId(datashareName)} ADD TABLE ${this.escapeId(schemaName)}.${this.escapeId(tableName)}`);
880
+ }
881
+ /**
882
+ * `GRANT USAGE ON DATASHARE datashare_name TO NAMESPACE 'guid' | ACCOUNT 'accountnumber' [VIA DATA CATALOG]`
883
+ *
884
+ * Grants a consumer (another namespace in the same account, or an entire
885
+ * other AWS account) read-only access to the datashare. The caller must
886
+ * already have been granted the `SHARE` permission on the datashare (via
887
+ * `GRANT SHARE ON DATASHARE ... TO ...`) unless they're the owner/superuser.
888
+ */
889
+ async grantDatashareUsage(datashareName, target) {
890
+ let sql = `GRANT USAGE ON DATASHARE ${this.escapeId(datashareName)} TO `;
891
+ if ('namespace' in target) {
892
+ sql += `NAMESPACE '${this.escapeString(target.namespace)}'`;
893
+ }
894
+ else {
895
+ sql += `ACCOUNT '${this.escapeString(target.account)}'`;
896
+ if (target.viaDataCatalog)
897
+ sql += ' VIA DATA CATALOG';
898
+ }
899
+ return this.query(sql);
900
+ }
901
+ /**
902
+ * Consumer-side: `CREATE DATABASE db_name [WITH PERMISSIONS] FROM DATASHARE
903
+ * datashare_name OF [ACCOUNT account_id] NAMESPACE namespace_guid`
904
+ *
905
+ * Creates a local database backed by a datashare a producer has granted
906
+ * this cluster/namespace `USAGE` on. Only one consumer database can be
907
+ * created per datashare on a given cluster.
908
+ */
909
+ async createDatabaseFromDatashare(dbName, datashareName, fromNamespace, options) {
910
+ let sql = `CREATE DATABASE ${this.escapeId(dbName)}`;
911
+ if (options?.withPermissions)
912
+ sql += ' WITH PERMISSIONS';
913
+ sql += ` FROM DATASHARE ${this.escapeId(datashareName)} OF`;
914
+ if (options?.account)
915
+ sql += ` ACCOUNT '${this.escapeString(options.account)}'`;
916
+ sql += ` NAMESPACE '${this.escapeString(fromNamespace)}'`;
917
+ return this.query(sql);
918
+ }
919
+ // ---------------------------------------------------------------------------
920
+ // Federated query to Amazon RDS/Aurora (PostgreSQL/MySQL) - distinct from
921
+ // Redshift Spectrum's S3/data-catalog `CREATE EXTERNAL SCHEMA FROM DATA
922
+ // CATALOG|HIVE METASTORE`. Federated query connects live to an OLTP
923
+ // database rather than reading files, and uses its own `FROM POSTGRES` /
924
+ // `FROM MYSQL` syntax. Not part of the `Dialect` interface.
925
+ //
926
+ // See https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_EXTERNAL_SCHEMA.html
927
+ // ---------------------------------------------------------------------------
928
+ /**
929
+ * `CREATE EXTERNAL SCHEMA [IF NOT EXISTS] schema_name FROM POSTGRES|MYSQL
930
+ * DATABASE 'db' [SCHEMA 'schema'] URI 'host' [PORT n] IAM_ROLE 'role'
931
+ * SECRET_ARN 'arn'`
932
+ *
933
+ * `options.schema` is only valid (and only emitted) for `POSTGRES` -
934
+ * Redshift federated queries to MySQL don't support a separate schema
935
+ * namespace.
936
+ */
937
+ async createExternalSchemaFederated(schemaName, options) {
938
+ if (options.engine === 'MYSQL' && options.schema) {
939
+ throw new index_1.DatabaseError('createExternalSchemaFederated: options.schema is not supported for federated queries to MySQL ' +
940
+ '(SCHEMA is only valid for FROM POSTGRES). Omit options.schema for MySQL.');
941
+ }
942
+ let sql = `CREATE EXTERNAL SCHEMA${options.ifNotExists ? ' IF NOT EXISTS' : ''} ${this.escapeId(schemaName)}`;
943
+ sql += ` FROM ${options.engine}`;
944
+ sql += ` DATABASE '${this.escapeString(options.database)}'`;
945
+ if (options.engine === 'POSTGRES' && options.schema) {
946
+ sql += ` SCHEMA '${this.escapeString(options.schema)}'`;
947
+ }
948
+ sql += ` URI '${this.escapeString(options.uri)}'`;
949
+ if (options.port !== undefined)
950
+ sql += ` PORT ${options.port}`;
951
+ sql += ` IAM_ROLE '${this.escapeString(options.iamRole)}'`;
952
+ sql += ` SECRET_ARN '${this.escapeString(options.secretArn)}'`;
953
+ return this.query(sql);
954
+ }
955
+ // ---------------------------------------------------------------------------
956
+ // Introspection
957
+ // ---------------------------------------------------------------------------
958
+ async showTables() {
959
+ const result = await this.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'`);
960
+ return result.rows.map((row) => row.table_name);
961
+ }
962
+ /**
963
+ * Show constraints for a table. Remember: any FK/UNIQUE/PK rows returned
964
+ * here describe DECLARED, not ENFORCED, constraints in Redshift.
965
+ */
966
+ async showConstraints(tableName) {
967
+ const sql = `
968
+ SELECT
969
+ tc.constraint_name,
970
+ tc.table_name,
971
+ kcu.column_name,
972
+ tc.constraint_type
973
+ FROM information_schema.table_constraints AS tc
974
+ LEFT JOIN information_schema.key_column_usage AS kcu
975
+ ON tc.constraint_name = kcu.constraint_name
976
+ WHERE tc.table_name = $1
977
+ `;
978
+ const result = await this.query(sql, { replacements: [tableName] });
979
+ return result.rows;
980
+ }
981
+ /**
982
+ * Add a constraint. NOTE: Redshift accepts PRIMARY KEY/UNIQUE/FOREIGN KEY
983
+ * here but never enforces them (see class doc comment). CHECK constraints
984
+ * are rejected outright since Redshift has no CHECK constraint support.
985
+ */
986
+ async addConstraint(tableName, options) {
987
+ if (options.type === 'CHECK') {
988
+ throw new index_1.DatabaseError('Redshift does not support CHECK constraints');
989
+ }
990
+ const constraintName = options.name || `${tableName}_${options.fields.join('_')}_${options.type.toLowerCase().replace(/\s+/g, '_')}`;
991
+ const fieldsSql = options.fields.map((f) => this.escapeId(f)).join(', ');
992
+ let sql = `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} `;
993
+ switch (options.type) {
994
+ case 'UNIQUE':
995
+ sql += `UNIQUE (${fieldsSql})`;
996
+ break;
997
+ case 'PRIMARY KEY':
998
+ sql += `PRIMARY KEY (${fieldsSql})`;
999
+ break;
1000
+ case 'FOREIGN KEY':
1001
+ sql += `FOREIGN KEY (${fieldsSql}) REFERENCES ${this.escapeId(options.references.table)} (${options.references.fields.map((f) => this.escapeId(f)).join(', ')})`;
1002
+ break;
1003
+ }
1004
+ await this.query(sql);
1005
+ }
1006
+ async removeConstraint(tableName, constraintName) {
1007
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} DROP CONSTRAINT ${this.escapeId(constraintName)}`);
1008
+ }
1009
+ async createConstraint(tableName, constraintDef) {
1010
+ if (constraintDef.type === 'CHECK') {
1011
+ throw new index_1.DatabaseError('Redshift does not support CHECK constraints');
1012
+ }
1013
+ await this.addConstraint(tableName, {
1014
+ type: constraintDef.type,
1015
+ fields: constraintDef.fields || [],
1016
+ name: constraintDef.name,
1017
+ references: constraintDef.references
1018
+ ? { table: constraintDef.references.table, fields: [constraintDef.references.field] }
1019
+ : undefined,
1020
+ });
1021
+ }
1022
+ async dropConstraint(tableName, constraintName, options) {
1023
+ let sql = `ALTER TABLE ${this.escapeId(tableName)} DROP CONSTRAINT`;
1024
+ if (options?.ifExists)
1025
+ sql += ' IF EXISTS';
1026
+ sql += ` ${this.escapeId(constraintName)}`;
1027
+ if (options?.cascade)
1028
+ sql += ' CASCADE';
1029
+ await this.query(sql);
1030
+ }
1031
+ async describeTable(tableName) {
1032
+ const sql = `
1033
+ SELECT column_name, data_type, is_nullable, column_default, character_maximum_length
1034
+ FROM information_schema.columns
1035
+ WHERE table_name = $1 AND table_schema = 'public'
1036
+ `;
1037
+ const result = await this.query(sql, { replacements: [tableName] });
1038
+ const description = {};
1039
+ for (const row of result.rows) {
1040
+ const rowData = row;
1041
+ const columnName = rowData.column_name;
1042
+ let type = rowData.data_type;
1043
+ if (rowData.character_maximum_length)
1044
+ type += `(${rowData.character_maximum_length})`;
1045
+ const isIdentity = rowData.column_default?.toString().toLowerCase().includes('identity');
1046
+ description[columnName] = {
1047
+ type,
1048
+ allowNull: rowData.is_nullable === 'YES',
1049
+ defaultValue: rowData.column_default,
1050
+ primaryKey: false,
1051
+ autoIncrement: !!isIdentity,
1052
+ };
1053
+ }
1054
+ return description;
1055
+ }
1056
+ async renameTable(oldName, newName) {
1057
+ await this.query(`ALTER TABLE ${this.escapeId(oldName)} RENAME TO ${this.escapeId(newName)}`);
1058
+ }
1059
+ async changeOwner(newOwner, tableName) {
1060
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} OWNER TO ${this.escapeId(newOwner)}`);
1061
+ }
1062
+ // ---------------------------------------------------------------------------
1063
+ // Indexes - Redshift has no traditional B-tree secondary indexes at all.
1064
+ // Use DISTKEY/SORTKEY table options (createTable) instead.
1065
+ // ---------------------------------------------------------------------------
1066
+ noIndexSupport() {
1067
+ throw new index_1.DatabaseError('Redshift has no traditional secondary indexes. Use DISTKEY/SORTKEY/DISTSTYLE table options (see RedshiftTableOptions on createTable) instead of CREATE INDEX.');
1068
+ }
1069
+ async showIndexes(_tableName) {
1070
+ this.noIndexSupport();
1071
+ }
1072
+ async addIndex(_tableName, _indexName, _fields, _options) {
1073
+ this.noIndexSupport();
1074
+ }
1075
+ async removeIndex(_tableName, _indexName) {
1076
+ this.noIndexSupport();
1077
+ }
1078
+ async createIndex(_tableName, _indexDef) {
1079
+ this.noIndexSupport();
1080
+ }
1081
+ async dropIndex(_tableName, _indexName, _options) {
1082
+ this.noIndexSupport();
1083
+ }
1084
+ async createFulltextIndex() {
1085
+ this.noIndexSupport();
1086
+ }
1087
+ async createSpatialIndex() {
1088
+ this.noIndexSupport();
1089
+ }
1090
+ async createPartialIndex() {
1091
+ this.noIndexSupport();
1092
+ }
1093
+ async createExpressionIndex() {
1094
+ this.noIndexSupport();
1095
+ }
1096
+ // ---------------------------------------------------------------------------
1097
+ // Foreign Data Wrappers - Redshift uses Redshift Spectrum external schemas
1098
+ // (CREATE EXTERNAL SCHEMA ... FROM DATA CATALOG / HIVE METASTORE) instead of
1099
+ // Postgres-style FDWs, so all of these throw with guidance.
1100
+ // ---------------------------------------------------------------------------
1101
+ noFdwSupport() {
1102
+ throw new index_1.DatabaseError('Foreign Data Wrappers are not supported by Redshift. This is a PostgreSQL-specific feature. ' +
1103
+ 'Use Redshift Spectrum external schemas (CREATE EXTERNAL SCHEMA) for federated/external data access instead.');
1104
+ }
1105
+ async createForeignDataWrapper() {
1106
+ this.noFdwSupport();
1107
+ }
1108
+ async dropForeignDataWrapper() {
1109
+ this.noFdwSupport();
1110
+ }
1111
+ async createForeignServer() {
1112
+ this.noFdwSupport();
1113
+ }
1114
+ async dropForeignServer() {
1115
+ this.noFdwSupport();
1116
+ }
1117
+ async createForeignTable() {
1118
+ this.noFdwSupport();
1119
+ }
1120
+ buildCreateServerQuery(_name, _opts) {
1121
+ this.noFdwSupport();
1122
+ }
1123
+ buildAlterServerQuery(_name, _opts) {
1124
+ this.noFdwSupport();
1125
+ }
1126
+ buildDropServerQuery(_name, _opts) {
1127
+ this.noFdwSupport();
1128
+ }
1129
+ buildCreateUserMappingQuery(_opts) {
1130
+ this.noFdwSupport();
1131
+ }
1132
+ buildAlterUserMappingQuery(_opts) {
1133
+ this.noFdwSupport();
1134
+ }
1135
+ buildDropUserMappingQuery(_serverName, _user, _opts) {
1136
+ this.noFdwSupport();
1137
+ }
1138
+ async createUserMapping() {
1139
+ this.noFdwSupport();
1140
+ }
1141
+ async dropUserMapping() {
1142
+ this.noFdwSupport();
1143
+ }
1144
+ buildCreateForeignTableQuery(_tableName, _opts) {
1145
+ this.noFdwSupport();
1146
+ }
1147
+ buildDropForeignTableQuery(_tableName, _opts) {
1148
+ this.noFdwSupport();
1149
+ }
1150
+ buildImportForeignSchemaQuery(_remoteSchema, _serverName, _opts) {
1151
+ this.noFdwSupport();
1152
+ }
1153
+ getServersQuery() {
1154
+ this.noFdwSupport();
1155
+ }
1156
+ // ---------------------------------------------------------------------------
1157
+ // Row-level security - not supported by Redshift.
1158
+ // ---------------------------------------------------------------------------
1159
+ async createSecurityPolicy() {
1160
+ throw new index_1.DatabaseError('Row-level security policies are not supported by Redshift');
1161
+ }
1162
+ async dropSecurityPolicy() {
1163
+ throw new index_1.DatabaseError('Row-level security policies are not supported by Redshift');
1164
+ }
1165
+ async createPolicy(_options) {
1166
+ throw new index_1.DatabaseError('Row-level security policies are not supported by Redshift');
1167
+ }
1168
+ async dropPolicy(_policyName, _tableName, _options) {
1169
+ throw new index_1.DatabaseError('Row-level security policies are not supported by Redshift');
1170
+ }
1171
+ async enableRLS() {
1172
+ throw new index_1.DatabaseError('Row-level security is not supported by Redshift');
1173
+ }
1174
+ async disableRLS() {
1175
+ throw new index_1.DatabaseError('Row-level security is not supported by Redshift');
1176
+ }
1177
+ async hasPolicy() {
1178
+ throw new index_1.DatabaseError('Row-level security policies are not supported by Redshift');
1179
+ }
1180
+ // ---------------------------------------------------------------------------
1181
+ // Sequences - Redshift has no CREATE SEQUENCE/nextval. Use IDENTITY columns
1182
+ // (see getColumnDefinitionSql) for auto-increment behavior instead.
1183
+ // ---------------------------------------------------------------------------
1184
+ noSequenceSupport() {
1185
+ throw new index_1.DatabaseError('Redshift does not support sequences (no CREATE SEQUENCE/nextval). Use an IDENTITY(seed, step) column instead (set autoIncrement: true on the column definition).');
1186
+ }
1187
+ async createSequence(_options) {
1188
+ this.noSequenceSupport();
1189
+ }
1190
+ async dropSequence(_sequenceName, _options) {
1191
+ this.noSequenceSupport();
1192
+ }
1193
+ async nextSequenceValue(_sequenceName) {
1194
+ this.noSequenceSupport();
1195
+ }
1196
+ async hasSequence(_sequenceName) {
1197
+ this.noSequenceSupport();
1198
+ }
1199
+ async listSequences() {
1200
+ this.noSequenceSupport();
1201
+ }
1202
+ // ---------------------------------------------------------------------------
1203
+ // Triggers - Redshift has no trigger support at all.
1204
+ // ---------------------------------------------------------------------------
1205
+ async createTrigger(_options) {
1206
+ throw new index_1.DatabaseError('Redshift does not support triggers');
1207
+ }
1208
+ async dropTrigger(_triggerName, _tableName, _options) {
1209
+ throw new index_1.DatabaseError('Redshift does not support triggers');
1210
+ }
1211
+ async hasTrigger() {
1212
+ throw new index_1.DatabaseError('Redshift does not support triggers');
1213
+ }
1214
+ // ---------------------------------------------------------------------------
1215
+ // Identity / computed columns
1216
+ // ---------------------------------------------------------------------------
1217
+ async createIdentityColumn() {
1218
+ throw new index_1.DatabaseError('Redshift IDENTITY columns can only be defined at CREATE TABLE time, not added later via ALTER TABLE. Set autoIncrement: true on the column definition when creating the table instead.');
1219
+ }
1220
+ async createComputedColumn() {
1221
+ throw new index_1.DatabaseError('Redshift does not support computed/generated columns');
1222
+ }
1223
+ // ---------------------------------------------------------------------------
1224
+ // Comments - Redshift supports COMMENT ON TABLE/COLUMN
1225
+ // ---------------------------------------------------------------------------
1226
+ async commentTable(tableName, comment) {
1227
+ await this.query(`COMMENT ON TABLE ${this.quoteTable(tableName)} IS ${this.escape(comment)}`);
1228
+ }
1229
+ async commentColumn(tableName, columnName, comment) {
1230
+ await this.query(`COMMENT ON COLUMN ${this.quoteTable(tableName)}.${this.quoteIdentifier(columnName)} IS ${this.escape(comment)}`);
1231
+ }
1232
+ // ---------------------------------------------------------------------------
1233
+ // Stored procedures - Redshift supports a limited plpgsql-like dialect.
1234
+ // ---------------------------------------------------------------------------
1235
+ async createStoredProcedure(options) {
1236
+ const schema = options.schema || 'public';
1237
+ const procName = `${this.escapeId(schema)}.${this.escapeId(options.name)}`;
1238
+ let sql = options.replace ? 'CREATE OR REPLACE PROCEDURE' : 'CREATE PROCEDURE';
1239
+ if (options.params && options.params.length > 0) {
1240
+ const params = options.params
1241
+ .map((p) => `${this.escapeId(p.name)} ${p.type}`)
1242
+ .join(', ');
1243
+ sql += ` ${procName} (${params})`;
1244
+ }
1245
+ else {
1246
+ sql += ` ${procName}()`;
1247
+ }
1248
+ sql += ` AS $$ ${options.body} $$ LANGUAGE plpgsql`;
1249
+ await this.query(sql);
1250
+ }
1251
+ async createProcedure(options) {
1252
+ return this.createStoredProcedure(options);
1253
+ }
1254
+ async dropStoredProcedure(procedureName, options) {
1255
+ const schema = options?.schema || 'public';
1256
+ let sql = 'DROP PROCEDURE';
1257
+ if (options?.ifExists)
1258
+ sql += ' IF EXISTS';
1259
+ sql += ` ${this.escapeId(schema)}.${this.escapeId(procedureName)}`;
1260
+ await this.query(sql);
1261
+ }
1262
+ async dropProcedure(procedureName, options) {
1263
+ return this.dropStoredProcedure(procedureName, options);
1264
+ }
1265
+ async executeStoredProcedure(options) {
1266
+ const schema = options.schema || 'public';
1267
+ const procName = `${this.escapeId(schema)}.${this.escapeId(options.procedureName)}`;
1268
+ let sql = `CALL ${procName}`;
1269
+ if (options.params && Object.keys(options.params).length > 0) {
1270
+ sql += `(${Object.values(options.params).map((v) => this.escape(v)).join(', ')})`;
1271
+ }
1272
+ else {
1273
+ sql += '()';
1274
+ }
1275
+ return this.query(sql);
1276
+ }
1277
+ async hasStoredProcedure(procedureName, schema) {
1278
+ const db = schema || 'public';
1279
+ const sql = `SELECT proname FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = $1 AND proname = $2`;
1280
+ const result = await this.query(sql, { replacements: [db, procedureName] });
1281
+ return (result.rows?.length ?? 0) > 0;
1282
+ }
1283
+ // ---------------------------------------------------------------------------
1284
+ // Maintenance - VACUUM / ANALYZE
1285
+ //
1286
+ // Unlike Postgres-family dialects where autovacuum transparently reclaims
1287
+ // dead tuples and refreshes planner stats, Redshift requires callers to run
1288
+ // these explicitly. This matters especially here because `buildUpsertQuery`
1289
+ // (see the staging-table + DELETE + INSERT upsert path) generates frequent
1290
+ // DELETE/INSERT churn, which both bloats unsorted/deleted-but-not-reclaimed
1291
+ // blocks and skews `ANALYZE` statistics over time if nothing ever runs
1292
+ // VACUUM/ANALYZE against the affected tables.
1293
+ // ---------------------------------------------------------------------------
1294
+ /**
1295
+ * Run Redshift's `VACUUM` maintenance command, which resorts rows within
1296
+ * sort key order and reclaims space from deleted rows. Neither of these
1297
+ * happen automatically the way Postgres's autovacuum does.
1298
+ *
1299
+ * Builds: `VACUUM [FULL|SORT ONLY|DELETE ONLY|REINDEX] [<table>] [TO <n> PERCENT] [BOOST]`
1300
+ */
1301
+ async vacuum(tableName, options) {
1302
+ let sql = 'VACUUM';
1303
+ const mode = options?.mode;
1304
+ if (mode && mode !== 'FULL') {
1305
+ sql += ` ${mode}`;
1306
+ }
1307
+ if (tableName) {
1308
+ sql += ` ${this.escapeId(tableName)}`;
1309
+ }
1310
+ if (options?.toPercent !== undefined) {
1311
+ if (options.toPercent < 1 || options.toPercent > 100) {
1312
+ throw new index_1.DatabaseError('vacuum() options.toPercent must be between 1 and 100');
1313
+ }
1314
+ sql += ` TO ${options.toPercent} PERCENT`;
1315
+ }
1316
+ if (options?.boost) {
1317
+ sql += ' BOOST';
1318
+ }
1319
+ return this.query(sql);
1320
+ }
1321
+ /**
1322
+ * Run Redshift's `ANALYZE` maintenance command to refresh planner
1323
+ * statistics (or `ANALYZE COMPRESSION` to get column encoding
1324
+ * recommendations). Redshift's query planner relies entirely on these
1325
+ * explicitly-collected stats; there is no background stats collector like
1326
+ * Postgres's autovacuum/autoanalyze.
1327
+ *
1328
+ * Builds: `ANALYZE [<table> [(<col>, ...)]] [PREDICATE COLUMNS]`
1329
+ */
1330
+ async analyze(tableName, options) {
1331
+ if (options?.columns?.length && options?.predicateColumns) {
1332
+ throw new index_1.DatabaseError('analyze() accepts either options.columns or options.predicateColumns, not both');
1333
+ }
1334
+ let sql = 'ANALYZE';
1335
+ if (tableName) {
1336
+ sql += ` ${this.escapeId(tableName)}`;
1337
+ if (options?.columns?.length) {
1338
+ sql += ` (${options.columns.map((c) => this.escapeId(c)).join(', ')})`;
1339
+ }
1340
+ }
1341
+ if (options?.predicateColumns) {
1342
+ sql += ' PREDICATE COLUMNS';
1343
+ }
1344
+ return this.query(sql);
1345
+ }
1346
+ /**
1347
+ * Run Redshift's `ANALYZE COMPRESSION` command, which samples a table (or
1348
+ * the whole database when `tableName` is omitted) and recommends column
1349
+ * encodings. This is a separate command from `ANALYZE` in Redshift - it
1350
+ * does not update planner statistics, it only reports encoding advice.
1351
+ */
1352
+ async analyzeCompression(tableName) {
1353
+ let sql = 'ANALYZE COMPRESSION';
1354
+ if (tableName)
1355
+ sql += ` ${this.escapeId(tableName)}`;
1356
+ return this.query(sql);
1357
+ }
1358
+ getIsolationLevelSql(isolationLevel) {
1359
+ const level = isolationLevel.toUpperCase().trim();
1360
+ if (!RedshiftDialect.SUPPORTED_ISOLATION_LEVELS.has(level)) {
1361
+ throw new index_1.DatabaseError(`Redshift does not support isolation level '${isolationLevel}'. Redshift only supports ` +
1362
+ `READ COMMITTED and SERIALIZABLE (the ANSI READ UNCOMMITTED/REPEATABLE READ levels have ` +
1363
+ `no equivalent in Redshift's concurrency model).`);
1364
+ }
1365
+ return level;
1366
+ }
1367
+ async startTransaction(options) {
1368
+ if (!this.pool)
1369
+ throw new Error('Not connected to database');
1370
+ const client = await this.pool.connect();
1371
+ let beginSql = 'BEGIN';
1372
+ if (options?.isolationLevel) {
1373
+ beginSql += ` ISOLATION LEVEL ${this.getIsolationLevelSql(options.isolationLevel)}`;
1374
+ }
1375
+ const readOnly = options?.readOnly;
1376
+ if (readOnly !== undefined) {
1377
+ beginSql += readOnly ? ' READ ONLY' : ' READ WRITE';
1378
+ }
1379
+ await client.query(beginSql);
1380
+ return new RedshiftTransaction(client, this.transactionDepth++, options);
1381
+ }
1382
+ async commitTransaction(transaction) {
1383
+ const tx = transaction;
1384
+ if (tx.client) {
1385
+ await tx.client.query('COMMIT');
1386
+ tx.client.release();
1387
+ tx.finished = true;
1388
+ }
1389
+ }
1390
+ async rollbackTransaction(transaction) {
1391
+ const tx = transaction;
1392
+ if (tx.client) {
1393
+ await tx.client.query('ROLLBACK');
1394
+ tx.client.release();
1395
+ tx.finished = true;
1396
+ }
1397
+ }
1398
+ // ---------------------------------------------------------------------------
1399
+ // Data types
1400
+ // ---------------------------------------------------------------------------
1401
+ /**
1402
+ * Get the SQL for a data type.
1403
+ *
1404
+ * Notably: Redshift has no native JSON/JSONB type (that only exists in
1405
+ * Postgres). Both `JSON` and `JSONB` DataTypes map to Redshift's
1406
+ * semi-structured `SUPER` type instead.
1407
+ */
1408
+ getDataTypeSql(dataType) {
1409
+ if (typeof dataType === 'string')
1410
+ return dataType;
1411
+ if (!dataType || typeof dataType !== 'object')
1412
+ return 'VARCHAR(255)';
1413
+ const dt = dataType;
1414
+ switch (dt.key) {
1415
+ case 'STRING':
1416
+ return `VARCHAR(${dt.length || 255})`;
1417
+ case 'CHAR':
1418
+ return `CHAR(${dt.length || 1})`;
1419
+ case 'TEXT':
1420
+ return dt.length ? `VARCHAR(${dt.length})` : 'VARCHAR(65535)';
1421
+ case 'INTEGER':
1422
+ // NOTE: auto-increment INTEGER columns are handled in
1423
+ // getColumnDefinitionSql via IDENTITY(seed, step), not here.
1424
+ return 'INTEGER';
1425
+ case 'BIGINT':
1426
+ return 'BIGINT';
1427
+ case 'FLOAT':
1428
+ return 'REAL';
1429
+ case 'DOUBLE':
1430
+ return 'DOUBLE PRECISION';
1431
+ case 'DECIMAL':
1432
+ return `DECIMAL(${dt.precision || 18},${dt.scale || 0})`;
1433
+ case 'BOOLEAN':
1434
+ return 'BOOLEAN';
1435
+ case 'DATE':
1436
+ return 'TIMESTAMP';
1437
+ case 'DATEONLY':
1438
+ return 'DATE';
1439
+ case 'TIME':
1440
+ return 'TIME';
1441
+ case 'BLOB':
1442
+ return 'VARBYTE';
1443
+ case 'ENUM': {
1444
+ // Redshift has no native ENUM type; fall back to VARCHAR with the
1445
+ // longest declared value as a rough width guide.
1446
+ const values = dt.values || [];
1447
+ const maxLen = values.reduce((m, v) => Math.max(m, v.length), 1);
1448
+ return `VARCHAR(${Math.max(maxLen, 255)})`;
1449
+ }
1450
+ case 'JSON':
1451
+ case 'JSONB':
1452
+ // Redshift has no JSON/JSONB type before SUPER - map both to SUPER.
1453
+ return 'SUPER';
1454
+ case 'UUID':
1455
+ return 'VARCHAR(36)';
1456
+ case 'ARRAY':
1457
+ // Redshift represents nested/array data via SUPER, not native array types.
1458
+ return 'SUPER';
1459
+ case 'VIRTUAL':
1460
+ return '';
1461
+ default:
1462
+ return 'VARCHAR(255)';
1463
+ }
1464
+ }
1465
+ // ---------------------------------------------------------------------------
1466
+ // Query builders
1467
+ // ---------------------------------------------------------------------------
1468
+ buildWhereClause(where, _options) {
1469
+ const values = [];
1470
+ if (!where || Object.keys(where).length === 0)
1471
+ return { sql: '', values };
1472
+ const buildCondition = (condition) => {
1473
+ if (!condition || typeof condition !== 'object')
1474
+ return '';
1475
+ const cond = condition;
1476
+ if (cond.$and || cond.$or || cond.$not) {
1477
+ const parts = [];
1478
+ if (cond.$and)
1479
+ parts.push(`(${cond.$and.map((c) => buildCondition(c)).join(' AND ')})`);
1480
+ if (cond.$or)
1481
+ parts.push(`(${cond.$or.map((c) => buildCondition(c)).join(' OR ')})`);
1482
+ if (cond.$not)
1483
+ parts.push(`NOT (${buildCondition(cond.$not)})`);
1484
+ return parts.join(' AND ');
1485
+ }
1486
+ const fieldConditions = [];
1487
+ for (const [key, value] of Object.entries(cond)) {
1488
+ if (key.startsWith('$'))
1489
+ continue;
1490
+ if (value && typeof value === 'object' && !(value instanceof Date)) {
1491
+ const valueObj = value;
1492
+ if (valueObj.$eq !== undefined) {
1493
+ values.push(valueObj.$eq);
1494
+ fieldConditions.push(`${this.escapeId(key)} = $${values.length}`);
1495
+ }
1496
+ else if (valueObj.$ne !== undefined) {
1497
+ values.push(valueObj.$ne);
1498
+ fieldConditions.push(`${this.escapeId(key)} != $${values.length}`);
1499
+ }
1500
+ else if (valueObj.$gt !== undefined) {
1501
+ values.push(valueObj.$gt);
1502
+ fieldConditions.push(`${this.escapeId(key)} > $${values.length}`);
1503
+ }
1504
+ else if (valueObj.$gte !== undefined) {
1505
+ values.push(valueObj.$gte);
1506
+ fieldConditions.push(`${this.escapeId(key)} >= $${values.length}`);
1507
+ }
1508
+ else if (valueObj.$lt !== undefined) {
1509
+ values.push(valueObj.$lt);
1510
+ fieldConditions.push(`${this.escapeId(key)} < $${values.length}`);
1511
+ }
1512
+ else if (valueObj.$lte !== undefined) {
1513
+ values.push(valueObj.$lte);
1514
+ fieldConditions.push(`${this.escapeId(key)} <= $${values.length}`);
1515
+ }
1516
+ else if (valueObj.$like !== undefined) {
1517
+ values.push(valueObj.$like);
1518
+ fieldConditions.push(`${this.escapeId(key)} LIKE $${values.length}`);
1519
+ }
1520
+ else if (valueObj.$in) {
1521
+ const inValues = valueObj.$in;
1522
+ values.push(...inValues);
1523
+ const placeholders = inValues.map((_, i) => `$${values.length - inValues.length + i + 1}`).join(',');
1524
+ fieldConditions.push(`${this.escapeId(key)} IN (${placeholders})`);
1525
+ }
1526
+ else if (valueObj.$notIn) {
1527
+ const notInValues = valueObj.$notIn;
1528
+ values.push(...notInValues);
1529
+ const placeholders = notInValues.map((_, i) => `$${values.length - notInValues.length + i + 1}`).join(',');
1530
+ fieldConditions.push(`${this.escapeId(key)} NOT IN (${placeholders})`);
1531
+ }
1532
+ else if (valueObj.$between) {
1533
+ const between = valueObj.$between;
1534
+ values.push(between[0], between[1]);
1535
+ fieldConditions.push(`${this.escapeId(key)} BETWEEN $${values.length - 1} AND $${values.length}`);
1536
+ }
1537
+ else if (valueObj.$isNull !== undefined) {
1538
+ fieldConditions.push(`${this.escapeId(key)} IS ${valueObj.$isNull ? '' : 'NOT '}NULL`);
1539
+ }
1540
+ else {
1541
+ values.push(value);
1542
+ fieldConditions.push(`${this.escapeId(key)} = $${values.length}`);
1543
+ }
1544
+ }
1545
+ else {
1546
+ values.push(value);
1547
+ fieldConditions.push(`${this.escapeId(key)} = $${values.length}`);
1548
+ }
1549
+ }
1550
+ return fieldConditions.join(' AND ');
1551
+ };
1552
+ return { sql: buildCondition(where), values };
1553
+ }
1554
+ buildOrderClause(order, _options) {
1555
+ const orderArray = order;
1556
+ if (!orderArray || !Array.isArray(orderArray) || orderArray.length === 0)
1557
+ return '';
1558
+ const orderParts = [];
1559
+ for (const item of orderArray) {
1560
+ if (Array.isArray(item)) {
1561
+ const field = typeof item[0] === 'string' ? this.escapeId(item[0]) : item[0];
1562
+ const direction = item[1] ? ` ${item[1]}` : '';
1563
+ orderParts.push(`${field}${direction}`);
1564
+ }
1565
+ else if (typeof item === 'string') {
1566
+ orderParts.push(this.escapeId(item));
1567
+ }
1568
+ }
1569
+ return orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
1570
+ }
1571
+ buildLimitOffset(limit, offset) {
1572
+ let sql = '';
1573
+ if (limit !== undefined) {
1574
+ sql += ` LIMIT ${Number(limit)}`;
1575
+ if (offset !== undefined)
1576
+ sql += ` OFFSET ${Number(offset)}`;
1577
+ }
1578
+ return sql;
1579
+ }
1580
+ buildInsertQuery(tableName, values, options) {
1581
+ if (options?.upsert) {
1582
+ throw new index_1.DatabaseError('Redshift does not support ON CONFLICT / upsert-via-INSERT. Use buildUpsertQuery() instead, which returns the staging-table + DELETE + INSERT statements Redshift documents for upserts.');
1583
+ }
1584
+ const columns = Object.keys(values);
1585
+ const processedValues = [];
1586
+ const placeholders = [];
1587
+ let paramIndex = 1;
1588
+ for (const value of Object.values(values)) {
1589
+ if (value instanceof prorm_1.Literal) {
1590
+ placeholders.push(value.val);
1591
+ }
1592
+ else {
1593
+ placeholders.push(`$${paramIndex}`);
1594
+ processedValues.push(value);
1595
+ paramIndex++;
1596
+ }
1597
+ }
1598
+ let sql = `INSERT INTO ${this.escapeId(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${placeholders.join(', ')})`;
1599
+ if (options?.returning) {
1600
+ sql += options.returning === true ? ' RETURNING *' : ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
1601
+ }
1602
+ return { sql, values: processedValues };
1603
+ }
1604
+ /**
1605
+ * Generate a collision-resistant temp/staging table name.
1606
+ *
1607
+ * Previously this used `${tableName}_staging_${Date.now()}` — millisecond
1608
+ * wall-clock time only, with no randomness. Two concurrent upserts against
1609
+ * the same table within the same millisecond (realistic under load, or in
1610
+ * a tight parallel batch) would produce an identical staging table name.
1611
+ * A random component (via `generateShortUUID()`) makes collisions
1612
+ * practically impossible even when many upserts race within the same
1613
+ * millisecond, on the same connection, or across processes/hosts.
1614
+ *
1615
+ * Also guards against exceeding Redshift's 127-byte identifier limit by
1616
+ * truncating the base table name portion if necessary.
1617
+ */
1618
+ generateStagingTableName(tableName) {
1619
+ const suffix = `_staging_${Date.now()}_${(0, index_2.generateShortUUID)()}`;
1620
+ const maxBaseLen = Math.max(1, 127 - suffix.length);
1621
+ const base = tableName.length > maxBaseLen ? tableName.slice(0, maxBaseLen) : tableName;
1622
+ return `${base}${suffix}`;
1623
+ }
1624
+ /**
1625
+ * Build an UPSERT "query" for Redshift.
1626
+ *
1627
+ * Why not `ON CONFLICT ... DO UPDATE` (what the Postgres dialect uses)?
1628
+ * Redshift has never supported `ON CONFLICT` — it is a Postgres 9.5+
1629
+ * feature, and Redshift's SQL layer, forked from Postgres 8.0, does not
1630
+ * have it. Attempting to send `ON CONFLICT` SQL to a real Redshift cluster
1631
+ * fails with a syntax error.
1632
+ *
1633
+ * Two strategies are available:
1634
+ *
1635
+ * 1. (default) AWS's documented "merge by replacing existing rows"
1636
+ * staging-table pattern, which works on every Redshift version:
1637
+ * a. Load the new/changed row(s) into a staging table (temp table
1638
+ * with the same shape as the target).
1639
+ * b. `DELETE FROM target USING staging WHERE target.<key> = staging.<key>`
1640
+ * to remove rows that are about to be replaced.
1641
+ * c. `INSERT INTO target SELECT * FROM staging` to insert the new
1642
+ * versions (and any brand new rows).
1643
+ *
1644
+ * 2. (opt-in via `options.useMerge`) A native `MERGE INTO ... USING ...
1645
+ * WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT ...`
1646
+ * statement. Redshift only added `MERGE` support in 2023, so this is
1647
+ * NOT the default (it would fail with a syntax error on older/still-
1648
+ * common cluster versions) — set `options.useMerge: true` only when you
1649
+ * know the target cluster supports it. It has the advantage of being a
1650
+ * single atomic statement with no staging table required at all.
1651
+ *
1652
+ * IMPORTANT — atomicity: the staging-table strategy is inherently multiple
1653
+ * statements (not a single parameterized `INSERT`), so the returned `sql`
1654
+ * is a semicolon-separated multi-statement string. Running these
1655
+ * statements one at a time through a connection pool (rather than a single
1656
+ * pinned connection wrapped in `BEGIN`/`COMMIT`) risks the temp table
1657
+ * created by statement 1 not being visible to later statements (temp
1658
+ * tables are session-scoped), and leaves no rollback path if a
1659
+ * mid-sequence statement fails. Use `upsert()` (below), which pins a
1660
+ * single connection and wraps the whole sequence in an explicit
1661
+ * transaction, rather than sending this string through the pooled
1662
+ * `query()` method directly.
1663
+ */
1664
+ buildUpsertQuery(tableName, values, options) {
1665
+ const columns = Object.keys(values);
1666
+ const conflictFields = options?.conflictFields && options.conflictFields.length > 0 ? options.conflictFields : columns;
1667
+ const queryValues = Object.values(values);
1668
+ const quotedTarget = this.escapeId(tableName);
1669
+ const quotedColumns = columns.map((c) => this.escapeId(c)).join(', ');
1670
+ if (options?.useMerge) {
1671
+ const sql = this.buildMergeStatement(tableName, [columns], conflictFields, options);
1672
+ return { sql, values: queryValues };
1673
+ }
1674
+ const stagingTable = this.generateStagingTableName(tableName);
1675
+ const placeholders = columns.map((_, i) => `$${i + 1}`).join(', ');
1676
+ const quotedStaging = this.escapeId(stagingTable);
1677
+ const joinCondition = conflictFields
1678
+ .map((f) => `${quotedTarget}.${this.escapeId(f)} = ${quotedStaging}.${this.escapeId(f)}`)
1679
+ .join(' AND ');
1680
+ const statements = [
1681
+ // 1. Staging table shaped like the target, holding just the incoming row(s).
1682
+ `CREATE TEMP TABLE ${quotedStaging} (LIKE ${quotedTarget})`,
1683
+ `INSERT INTO ${quotedStaging} (${quotedColumns}) VALUES (${placeholders})`,
1684
+ // 2. Remove rows in the target that this upsert is about to replace.
1685
+ `DELETE FROM ${quotedTarget} USING ${quotedStaging} WHERE ${joinCondition}`,
1686
+ // 3. Insert the new/updated rows (and brand-new ones) from staging.
1687
+ `INSERT INTO ${quotedTarget} (${quotedColumns}) SELECT ${quotedColumns} FROM ${quotedStaging}`,
1688
+ `DROP TABLE ${quotedStaging}`,
1689
+ ];
1690
+ return { sql: statements.join(';\n'), values: queryValues };
1691
+ }
1692
+ /**
1693
+ * Bulk variant of `buildUpsertQuery`: accepts an array of rows and builds
1694
+ * ONE staging table populated with a single multi-row `INSERT`, rather
1695
+ * than requiring one `buildUpsertQuery`/`upsert` call per row.
1696
+ *
1697
+ * This matches how the staging-table pattern is actually used in
1698
+ * production Redshift pipelines — AWS documents it primarily as a *bulk*
1699
+ * upsert technique (commonly fed by a `COPY` into the staging table for
1700
+ * very large batches), so a single-row-only implementation undermines the
1701
+ * main reason to prefer it over row-by-row `INSERT`/`UPDATE`.
1702
+ *
1703
+ * Not part of the `Dialect` interface (whose `buildUpsertQuery` takes a
1704
+ * single `Record`) — this is a Redshift-specific addition, similar to
1705
+ * `copyFromS3`.
1706
+ */
1707
+ buildBulkUpsertQuery(tableName, rows, options) {
1708
+ if (rows.length === 0) {
1709
+ return { sql: '', values: [] };
1710
+ }
1711
+ const columns = Object.keys(rows[0]);
1712
+ const conflictFields = options?.conflictFields && options.conflictFields.length > 0 ? options.conflictFields : columns;
1713
+ if (options?.useMerge) {
1714
+ const sql = this.buildMergeStatement(tableName, rows.map(() => columns), conflictFields, options);
1715
+ const values = rows.flatMap((row) => columns.map((c) => row[c]));
1716
+ return { sql, values };
1717
+ }
1718
+ const stagingTable = this.generateStagingTableName(tableName);
1719
+ const quotedTarget = this.escapeId(tableName);
1720
+ const quotedStaging = this.escapeId(stagingTable);
1721
+ const quotedColumns = columns.map((c) => this.escapeId(c)).join(', ');
1722
+ const queryValues = [];
1723
+ const rowPlaceholders = [];
1724
+ let paramIndex = 1;
1725
+ for (const row of rows) {
1726
+ const placeholders = columns.map((c) => {
1727
+ queryValues.push(row[c]);
1728
+ return `$${paramIndex++}`;
1729
+ });
1730
+ rowPlaceholders.push(`(${placeholders.join(', ')})`);
1731
+ }
1732
+ const joinCondition = conflictFields
1733
+ .map((f) => `${quotedTarget}.${this.escapeId(f)} = ${quotedStaging}.${this.escapeId(f)}`)
1734
+ .join(' AND ');
1735
+ const statements = [
1736
+ `CREATE TEMP TABLE ${quotedStaging} (LIKE ${quotedTarget})`,
1737
+ `INSERT INTO ${quotedStaging} (${quotedColumns}) VALUES ${rowPlaceholders.join(', ')}`,
1738
+ `DELETE FROM ${quotedTarget} USING ${quotedStaging} WHERE ${joinCondition}`,
1739
+ `INSERT INTO ${quotedTarget} (${quotedColumns}) SELECT ${quotedColumns} FROM ${quotedStaging}`,
1740
+ `DROP TABLE ${quotedStaging}`,
1741
+ ];
1742
+ return { sql: statements.join(';\n'), values: queryValues };
1743
+ }
1744
+ /**
1745
+ * Build a native `MERGE INTO` statement (Redshift 2023+ only). Shared by
1746
+ * the single-row and bulk upsert paths — `columnsPerRow` is one entry per
1747
+ * incoming row (all rows are expected to share the same column set).
1748
+ */
1749
+ buildMergeStatement(tableName, columnsPerRow, conflictFields, options) {
1750
+ const columns = columnsPerRow[0];
1751
+ const quotedTarget = this.escapeId(tableName);
1752
+ const quotedColumns = columns.map((c) => this.escapeId(c));
1753
+ let paramIndex = 1;
1754
+ const valueRows = columnsPerRow.map((cols) => `(${cols.map(() => `$${paramIndex++}`).join(', ')})`);
1755
+ const usingSql = `(VALUES ${valueRows.join(', ')}) AS src (${quotedColumns.join(', ')})`;
1756
+ const onCondition = conflictFields.map((f) => `${quotedTarget}.${this.escapeId(f)} = src.${this.escapeId(f)}`).join(' AND ');
1757
+ const updateColumns = options?.updateColumns && options.updateColumns.length > 0
1758
+ ? options.updateColumns
1759
+ : columns.filter((c) => !conflictFields.includes(c));
1760
+ const updateSet = updateColumns.length > 0
1761
+ ? updateColumns.map((c) => `${this.escapeId(c)} = src.${this.escapeId(c)}`).join(', ')
1762
+ : null;
1763
+ let sql = `MERGE INTO ${quotedTarget} USING ${usingSql} ON ${onCondition}`;
1764
+ if (updateSet) {
1765
+ sql += ` WHEN MATCHED THEN UPDATE SET ${updateSet}`;
1766
+ }
1767
+ sql += ` WHEN NOT MATCHED THEN INSERT (${quotedColumns.join(', ')}) VALUES (${quotedColumns.map((c) => `src.${c}`).join(', ')})`;
1768
+ return sql;
1769
+ }
1770
+ /**
1771
+ * Execute a single-row upsert against Redshift, pinning ONE connection for
1772
+ * the whole multi-statement sequence and wrapping it in an explicit
1773
+ * `BEGIN`/`COMMIT` (`ROLLBACK` on any failure). This is the fix for the
1774
+ * correctness bug in the raw `buildUpsertQuery()` string: sending that
1775
+ * string's statements through the pooled `query()` method one at a time
1776
+ * can land different statements on different physical connections (temp
1777
+ * tables are session-scoped in Redshift, so the staging table created in
1778
+ * statement 1 may not exist by the time statement 3 runs), and gives no
1779
+ * atomicity/rollback if a statement mid-sequence fails.
1780
+ *
1781
+ * When `options.useMerge` is set, this still pins a connection and wraps
1782
+ * the (single) `MERGE` statement in `BEGIN`/`COMMIT` for a consistent
1783
+ * calling convention, even though a single statement is already atomic on
1784
+ * its own.
1785
+ */
1786
+ async upsert(tableName, values, options) {
1787
+ const { sql, values: params } = this.buildUpsertQuery(tableName, values, options);
1788
+ return this.runUpsertStatements(sql, params);
1789
+ }
1790
+ /**
1791
+ * Bulk counterpart to `upsert()`: builds one staging-table (or `MERGE`)
1792
+ * statement sequence for the whole `rows` array via `buildBulkUpsertQuery`,
1793
+ * then runs it on a single pinned connection inside an explicit
1794
+ * transaction, exactly like `upsert()`.
1795
+ */
1796
+ async bulkUpsert(tableName, rows, options) {
1797
+ if (rows.length === 0)
1798
+ return { rows: [], rowCount: 0, fields: [] };
1799
+ const { sql, values: params } = this.buildBulkUpsertQuery(tableName, rows, options);
1800
+ return this.runUpsertStatements(sql, params);
1801
+ }
1802
+ /**
1803
+ * Shared execution path for `upsert()`/`bulkUpsert()`: pins a single
1804
+ * client from the pool, runs `BEGIN`, then each `;`-separated statement in
1805
+ * `sql` in order (substituting `params` into only the first statement that
1806
+ * needs them, i.e. the staging-table INSERT or the MERGE's USING clause),
1807
+ * then `COMMIT`. Any failure triggers `ROLLBACK` before re-throwing, and
1808
+ * the client is always released back to the pool.
1809
+ */
1810
+ async runUpsertStatements(sql, params) {
1811
+ if (!this.pool)
1812
+ throw new Error('Not connected to database');
1813
+ if (!sql)
1814
+ return { rows: [], rowCount: 0, fields: [] };
1815
+ const client = await this.pool.connect();
1816
+ let lastResult;
1817
+ try {
1818
+ await client.query('BEGIN');
1819
+ const statements = sql.split(';\n').map((s) => s.trim()).filter((s) => s.length > 0);
1820
+ let paramsConsumed = false;
1821
+ for (const statement of statements) {
1822
+ // Only the statement(s) containing positional placeholders need the
1823
+ // parameter values; substitute them in once via replaceReplacements
1824
+ // (matching how the pooled `query()` method handles replacements).
1825
+ const needsParams = !paramsConsumed && /\$1(?!\d)/.test(statement);
1826
+ const finalSql = needsParams ? this.replaceReplacements(statement, params) : statement;
1827
+ if (needsParams)
1828
+ paramsConsumed = true;
1829
+ lastResult = (await client.query(finalSql));
1830
+ }
1831
+ await client.query('COMMIT');
1832
+ }
1833
+ catch (error) {
1834
+ try {
1835
+ await client.query('ROLLBACK');
1836
+ }
1837
+ catch {
1838
+ // Ignore rollback failures - the original error is more useful.
1839
+ }
1840
+ throw new Error(`Redshift upsert error: ${error}`);
1841
+ }
1842
+ finally {
1843
+ client.release();
1844
+ }
1845
+ const rows = lastResult?.rows ?? [];
1846
+ return { rows, rowCount: lastResult?.rowCount ?? rows.length, fields: [] };
1847
+ }
1848
+ buildIncrementQuery(tableName, fields, where, options) {
1849
+ const by = options?.by ?? 1;
1850
+ const setClauses = [];
1851
+ const queryValues = [];
1852
+ let paramIndex = 1;
1853
+ if (typeof fields === 'string') {
1854
+ setClauses.push(`${this.escapeId(fields)} = ${this.escapeId(fields)} + $${paramIndex}`);
1855
+ queryValues.push(by);
1856
+ paramIndex++;
1857
+ }
1858
+ else if (Array.isArray(fields)) {
1859
+ for (const field of fields) {
1860
+ setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + $${paramIndex}`);
1861
+ queryValues.push(by);
1862
+ paramIndex++;
1863
+ }
1864
+ }
1865
+ else {
1866
+ for (const [field, value] of Object.entries(fields)) {
1867
+ setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + $${paramIndex}`);
1868
+ queryValues.push(value);
1869
+ paramIndex++;
1870
+ }
1871
+ }
1872
+ const whereClause = this.buildWhereClause(where);
1873
+ const adjustedWhere = whereClause.sql.replace(/\$(\d+)/g, (_, num) => `$${paramIndex + parseInt(num, 10) - 1}`);
1874
+ queryValues.push(...whereClause.values);
1875
+ const sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')} WHERE ${adjustedWhere}`;
1876
+ return { sql, values: queryValues };
1877
+ }
1878
+ buildUpdateQuery(tableName, values, where, options) {
1879
+ const setClauses = [];
1880
+ const queryValues = [];
1881
+ let paramIndex = 1;
1882
+ for (const [key, value] of Object.entries(values)) {
1883
+ if (value instanceof prorm_1.Literal) {
1884
+ setClauses.push(`${this.escapeId(key)} = ${value.val}`);
1885
+ }
1886
+ else {
1887
+ setClauses.push(`${this.escapeId(key)} = $${paramIndex}`);
1888
+ queryValues.push(value);
1889
+ paramIndex++;
1890
+ }
1891
+ }
1892
+ const whereClause = this.buildWhereClause(where);
1893
+ const allValues = [...queryValues, ...whereClause.values];
1894
+ let sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')}`;
1895
+ if (whereClause.sql)
1896
+ sql += ` WHERE ${whereClause.sql}`;
1897
+ if (options?.returning) {
1898
+ sql += options.returning === true ? ' RETURNING *' : ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
1899
+ }
1900
+ return { sql, values: allValues };
1901
+ }
1902
+ buildDeleteQuery(tableName, where, options) {
1903
+ const whereClause = this.buildWhereClause(where);
1904
+ let sql = `DELETE FROM ${this.escapeId(tableName)}`;
1905
+ if (whereClause.sql)
1906
+ sql += ` WHERE ${whereClause.sql}`;
1907
+ if (options?.returning) {
1908
+ sql += options.returning === true ? ' RETURNING *' : ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
1909
+ }
1910
+ if (options?.truncate) {
1911
+ sql = `TRUNCATE ${this.escapeId(tableName)}`;
1912
+ }
1913
+ return { sql, values: whereClause.values };
1914
+ }
1915
+ /**
1916
+ * Format a single SELECT-list attribute into SQL.
1917
+ *
1918
+ * `options.attributes` isn't limited to plain column-name strings: the
1919
+ * typed window-function builder library (`src/query-builders/functions/
1920
+ * window.ts`, e.g. `rowNumber().partitionBy(...).as('rn')`) and the
1921
+ * `fn()`/`col()`/`literal()`/`cast()`/`extract()` helpers in
1922
+ * `src/operators.ts` all resolve to either a raw `LiteralExpression`
1923
+ * (`{ __type: 'literal', sql }`) or a `[expression, alias]` tuple.
1924
+ *
1925
+ * Blindly calling `escapeId()` on those (as this method used to do) is
1926
+ * broken: `escapeId` stringifies whatever it's given and wraps it in
1927
+ * double quotes, so a tuple like `[literalExpr('ROW_NUMBER() OVER (...)'),
1928
+ * 'rn']` became the identifier `"[object Object],rn"` instead of
1929
+ * `ROW_NUMBER() OVER (...) AS "rn"`. This mirrors the attribute-resolution
1930
+ * logic in `SqlCompiler.attributeToSqlWithAlias`/`processAttribute`
1931
+ * (`src/query-builders/sql-compiler.ts`), but is implemented locally so it
1932
+ * always renders identifiers via this dialect's own `escapeId`.
1933
+ */
1934
+ formatSelectAttribute(attr) {
1935
+ // [expression, alias] tuple - render the expression, then ` AS "alias"`.
1936
+ if (Array.isArray(attr)) {
1937
+ const [expr, alias] = attr;
1938
+ const exprSql = this.formatSelectAttribute(expr);
1939
+ return alias ? `${exprSql} AS ${this.escapeId(alias)}` : exprSql;
1940
+ }
1941
+ // FnExpression: { __type: 'fn', fn: 'COUNT', args: [...] }
1942
+ if ((0, operators_1.isFnExpression)(attr)) {
1943
+ const args = attr.args.map((a) => this.formatSelectAttribute(a)).join(', ');
1944
+ return `${attr.fn}(${args})`;
1945
+ }
1946
+ // ColExpression: { __type: 'col', col: 'table.field' | 'field' }
1947
+ if ((0, operators_1.isColExpression)(attr)) {
1948
+ const col = attr.col;
1949
+ return col.includes('.')
1950
+ ? col.split('.').map((part) => this.escapeId(part)).join('.')
1951
+ : this.escapeId(col);
1952
+ }
1953
+ // LiteralExpression: { __type: 'literal', sql } or { $literal: sql }.
1954
+ // This is the shape produced by window.ts's `.toLiteral()`/`.as()` and
1955
+ // by `literal()` in operators.ts - already raw, dialect-agnostic SQL, so
1956
+ // it's passed through verbatim (this is exactly what was previously
1957
+ // being mis-handled by escapeId).
1958
+ //
1959
+ // Note: `isLiteralExpression` probes for a `$literal` key via the `in`
1960
+ // operator, which throws on non-object values (e.g. plain strings) -
1961
+ // guard with a typeof check first rather than calling it unconditionally.
1962
+ if (attr !== null && typeof attr === 'object' && (0, operators_1.isLiteralExpression)(attr)) {
1963
+ return attr.sql ?? attr.$literal ?? '';
1964
+ }
1965
+ // CastExpression: { __type: 'cast', value, type }
1966
+ if ((0, operators_1.isCastExpression)(attr)) {
1967
+ const value = this.formatSelectAttribute(attr.value);
1968
+ return `CAST(${value} AS ${attr.type || 'TEXT'})`;
1969
+ }
1970
+ // ExtractExpression: { __type: 'extract', field, part }
1971
+ if ((0, operators_1.isExtractExpression)(attr)) {
1972
+ const field = this.formatSelectAttribute(attr.field);
1973
+ const part = (attr.part || 'YEAR').toUpperCase();
1974
+ return `EXTRACT(${part} FROM ${field})`;
1975
+ }
1976
+ // Plain string: either a bare column name or a raw SQL expression
1977
+ // (heuristically detected via parens/whitespace, matching the
1978
+ // permissive convention SqlCompiler and the other dialects use).
1979
+ if (typeof attr === 'string') {
1980
+ if (attr.includes('(') || attr.includes(' ')) {
1981
+ return attr;
1982
+ }
1983
+ return this.escapeId(attr);
1984
+ }
1985
+ return this.escapeId(attr);
1986
+ }
1987
+ /**
1988
+ * Build a `WITH [RECURSIVE] name [(cols)] AS (query), ...` prefix from
1989
+ * `options.cte`.
1990
+ *
1991
+ * Each entry's `query` may be a raw SQL string, or a nested `SelectOptions`
1992
+ * object (compiled recursively via `buildSelectQuery`); nested `SelectOptions`
1993
+ * CTE bodies never carry positional parameter values of their own in this
1994
+ * dialect - callers needing bound values in a CTE body should pass a raw
1995
+ * SQL string with the values already inlined/escaped, or use `Literal`.
1996
+ *
1997
+ * If any entry sets `recursive: true`, the WHOLE clause is emitted as
1998
+ * `WITH RECURSIVE` - Redshift, like Postgres and most other engines,
1999
+ * applies `RECURSIVE` to the entire `WITH` clause rather than per-CTE.
2000
+ *
2001
+ * NOTE on version support: Amazon Redshift only added `WITH RECURSIVE`
2002
+ * support relatively recently (RA3 node types on a sufficiently current
2003
+ * maintenance track); clusters on older node types/tracks will reject
2004
+ * `WITH RECURSIVE` with a syntax error even though plain (non-recursive)
2005
+ * `WITH` CTEs have always been supported. This method does not attempt to
2006
+ * detect the cluster's capabilities - it emits `WITH RECURSIVE` whenever
2007
+ * asked and leaves it to the caller to know their cluster supports it.
2008
+ */
2009
+ buildCteClause(cte) {
2010
+ if (!cte || cte.length === 0)
2011
+ return '';
2012
+ const recursive = cte.some((c) => c.recursive);
2013
+ const body = cte
2014
+ .map((c) => {
2015
+ const columns = c.columns && c.columns.length > 0 ? ` (${c.columns.map((col) => this.escapeId(col)).join(', ')})` : '';
2016
+ const inner = typeof c.query === 'string' ? c.query : this.buildSelectQuery(c.query).sql;
2017
+ return `${this.escapeId(c.name)}${columns} AS (${inner})`;
2018
+ })
2019
+ .join(', ');
2020
+ return `WITH ${recursive ? 'RECURSIVE ' : ''}${body} `;
2021
+ }
2022
+ buildSelectQuery(options) {
2023
+ const queryValues = [];
2024
+ let selectSql = '*';
2025
+ let distinctClause = '';
2026
+ if (options.distinct) {
2027
+ distinctClause = options.col ? `DISTINCT ${this.escapeId(options.col)} ` : 'DISTINCT ';
2028
+ // Note: Redshift does not support Postgres's DISTINCT ON (...) syntax.
2029
+ }
2030
+ if (options.attributes) {
2031
+ if (Array.isArray(options.attributes)) {
2032
+ selectSql = options.attributes.map((a) => this.formatSelectAttribute(a)).join(', ');
2033
+ }
2034
+ else if (options.attributes.include) {
2035
+ selectSql = options.attributes.include.map((a) => this.escapeId(a)).join(', ');
2036
+ }
2037
+ }
2038
+ const cteClause = this.buildCteClause(options.cte);
2039
+ let sql = `${cteClause}SELECT ${distinctClause}${selectSql} FROM ${this.escapeId(options.tableName)}`;
2040
+ if (options.include && options.include.length > 0) {
2041
+ for (const include of options.include) {
2042
+ const joinType = include.required ? 'INNER JOIN' : 'LEFT JOIN';
2043
+ const modelTableName = include.model?.tableName;
2044
+ const modelName = modelTableName || String(include.model);
2045
+ const alias = include.as || modelName;
2046
+ sql += ` ${joinType} ${this.escapeId(modelName)} ${this.escapeId(alias)}`;
2047
+ if (include.where) {
2048
+ const whereClause = this.buildWhereClause(include.where);
2049
+ sql += ` ON ${whereClause.sql}`;
2050
+ queryValues.push(...whereClause.values);
2051
+ }
2052
+ }
2053
+ }
2054
+ if (options.where) {
2055
+ const whereClause = this.buildWhereClause(options.where);
2056
+ if (whereClause.sql) {
2057
+ sql += ` WHERE ${whereClause.sql}`;
2058
+ queryValues.push(...whereClause.values);
2059
+ }
2060
+ }
2061
+ if (options.group) {
2062
+ const groupArray = Array.isArray(options.group) ? options.group : [options.group];
2063
+ if (groupArray.length > 0) {
2064
+ sql += ` GROUP BY ${groupArray.map((f) => this.escapeId(f)).join(', ')}`;
2065
+ }
2066
+ }
2067
+ if (options.having) {
2068
+ const havingClause = this.buildWhereClause(options.having);
2069
+ if (havingClause.sql) {
2070
+ sql += ` HAVING ${havingClause.sql}`;
2071
+ queryValues.push(...havingClause.values);
2072
+ }
2073
+ }
2074
+ if (options.order) {
2075
+ const orderClause = this.buildOrderClause(options.order);
2076
+ if (orderClause)
2077
+ sql += ` ${orderClause}`;
2078
+ }
2079
+ sql += this.buildLimitOffset(options.limit, options.offset);
2080
+ if (options.union && options.union.length > 0) {
2081
+ const unionType = options.unionType || 'UNION';
2082
+ for (const unionQuery of options.union) {
2083
+ const unionModel = unionQuery.model;
2084
+ const unionTableName = unionModel.tableName || unionModel.name || '';
2085
+ let unionSql = `SELECT * FROM ${this.escapeId(unionTableName)}`;
2086
+ if (unionQuery.where) {
2087
+ const whereClause = this.buildWhereClause(unionQuery.where);
2088
+ if (whereClause.sql) {
2089
+ unionSql += ` WHERE ${whereClause.sql}`;
2090
+ queryValues.push(...whereClause.values);
2091
+ }
2092
+ }
2093
+ sql += ` ${unionType} ${unionSql}`;
2094
+ }
2095
+ }
2096
+ return { sql, values: queryValues };
2097
+ }
2098
+ replaceReplacements(sql, replacements) {
2099
+ if (!replacements)
2100
+ return sql;
2101
+ if (Array.isArray(replacements)) {
2102
+ let result = sql;
2103
+ replacements.forEach((value, index) => {
2104
+ const placeholder = `$${index + 1}`;
2105
+ const re = new RegExp(placeholder.replace('$', '\\$'), 'g');
2106
+ if (value === null)
2107
+ result = result.replace(re, 'NULL');
2108
+ else if (typeof value === 'string')
2109
+ result = result.replace(re, `'${value.replace(/'/g, "''")}'`);
2110
+ else if (value instanceof Date)
2111
+ result = result.replace(re, `'${value.toISOString()}'`);
2112
+ else
2113
+ result = result.replace(re, String(value));
2114
+ });
2115
+ return result;
2116
+ }
2117
+ let result = sql;
2118
+ for (const [key, value] of Object.entries(replacements)) {
2119
+ const re = new RegExp(`:${key}\\b`.replace(/:/g, '\\:'), 'g');
2120
+ if (value === null)
2121
+ result = result.replace(re, 'NULL');
2122
+ else if (typeof value === 'string')
2123
+ result = result.replace(re, `'${value.replace(/'/g, "''")}'`);
2124
+ else if (value instanceof Date)
2125
+ result = result.replace(re, `'${value.toISOString()}'`);
2126
+ else
2127
+ result = result.replace(re, String(value));
2128
+ }
2129
+ return result;
2130
+ }
2131
+ // ---------------------------------------------------------------------------
2132
+ // User / privilege management - Redshift supports its own CREATE/ALTER/DROP
2133
+ // USER syntax (a subset of Postgres's).
2134
+ // ---------------------------------------------------------------------------
2135
+ buildCreateUserQuery(username, options) {
2136
+ const opts = options || {};
2137
+ let sql = `CREATE USER ${this.escapeId(username)}`;
2138
+ if (opts.password) {
2139
+ sql += ` PASSWORD '${this.escapeString(String(opts.password))}'`;
2140
+ }
2141
+ else {
2142
+ sql += ' PASSWORD DISABLE';
2143
+ }
2144
+ if (opts.createdb === true)
2145
+ sql += ' CREATEDB';
2146
+ if (opts.superuser === true)
2147
+ sql += ' CREATEUSER';
2148
+ return sql;
2149
+ }
2150
+ buildAlterUserQuery(username, options) {
2151
+ const opts = options || {};
2152
+ let sql = `ALTER USER ${this.escapeId(username)}`;
2153
+ if (opts.renameTo)
2154
+ return `${sql} RENAME TO ${this.escapeId(opts.renameTo)}`;
2155
+ if (opts.password)
2156
+ sql += ` PASSWORD '${this.escapeString(String(opts.password))}'`;
2157
+ return sql;
2158
+ }
2159
+ buildDropUserQuery(username, options) {
2160
+ let sql = 'DROP USER';
2161
+ if (options?.ifExists)
2162
+ sql += ' IF EXISTS';
2163
+ sql += ` ${this.escapeId(username)}`;
2164
+ return sql;
2165
+ }
2166
+ getUsersQuery() {
2167
+ return `SELECT usename AS "user" FROM pg_user ORDER BY usename`;
2168
+ }
2169
+ async createUser(username, options) {
2170
+ await this.query(this.buildCreateUserQuery(username, options));
2171
+ }
2172
+ buildGrantQuery(_options) {
2173
+ throw new index_1.DatabaseError('Fine-grained GRANT query building is not implemented for Redshift');
2174
+ }
2175
+ buildRevokeQuery(_options) {
2176
+ throw new index_1.DatabaseError('Fine-grained REVOKE query building is not implemented for Redshift');
2177
+ }
2178
+ buildShowGrantsQuery(_username, _host) {
2179
+ throw new index_1.DatabaseError('SHOW GRANTS is not supported by Redshift');
2180
+ }
2181
+ buildFlushPrivilegesQuery() {
2182
+ throw new index_1.DatabaseError('FLUSH PRIVILEGES is not applicable to Redshift');
2183
+ }
2184
+ buildCreateRoleQuery(_roleName, _options) {
2185
+ throw new index_1.DatabaseError('Roles are not supported by Redshift in the Postgres sense; use users/groups instead');
2186
+ }
2187
+ buildDropRoleQuery(_roleName, _options) {
2188
+ throw new index_1.DatabaseError('Roles are not supported by Redshift in the Postgres sense; use users/groups instead');
2189
+ }
2190
+ buildGrantRoleQuery(_role, _to, _options) {
2191
+ throw new index_1.DatabaseError('Roles are not supported by Redshift in the Postgres sense; use users/groups instead');
2192
+ }
2193
+ buildRevokeRoleQuery(_role, _from, _options) {
2194
+ throw new index_1.DatabaseError('Roles are not supported by Redshift in the Postgres sense; use users/groups instead');
2195
+ }
2196
+ getRolesQuery() {
2197
+ throw new index_1.DatabaseError('Roles are not supported by Redshift in the Postgres sense; use users/groups instead');
2198
+ }
2199
+ /**
2200
+ * Build a JSON_TABLE-equivalent expression to shred a JSON array into relational rows.
2201
+ * Not supported: Redshift has no `JSON_TABLE`, no `OPENJSON`, and no
2202
+ * `json_to_recordset`/`jsonb_to_recordset` (it has no JSONB type at all — JSON is
2203
+ * mapped to the `SUPER` type, see `getDataTypeSql`). Redshift's real answer to
2204
+ * shredding nested/array `SUPER` data is `UNNEST`/implicit lateral unnest over a
2205
+ * `SUPER` column already stored in a table, not a general `(json, path) => rows`
2206
+ * function that can operate on an arbitrary JSON expression, so it cannot be exposed
2207
+ * through this API shape.
2208
+ */
2209
+ buildJsonTable(_jsonExpression, _rowPath, _columns, _alias) {
2210
+ throw new index_1.DatabaseError('Redshift does not support JSON_TABLE, OPENJSON, or json_to_recordset/jsonb_to_recordset; ' +
2211
+ 'shredding SUPER arrays requires UNNEST over a stored SUPER column, not an arbitrary JSON expression');
2212
+ }
2213
+ }
2214
+ exports.RedshiftDialect = RedshiftDialect;
2215
+ // ---------------------------------------------------------------------------
2216
+ // Transactions
2217
+ // ---------------------------------------------------------------------------
2218
+ /**
2219
+ * Redshift only supports two isolation levels: READ COMMITTED (the
2220
+ * default) and SERIALIZABLE. Unlike Postgres, it has no MVCC snapshot
2221
+ * mode equivalent to REPEATABLE READ, and no dirty-read mode (READ
2222
+ * UNCOMMITTED). Requesting either of those would silently give the caller
2223
+ * a *different, unrequested* isolation guarantee if we just ignored the
2224
+ * option, so both are rejected outright instead.
2225
+ */
2226
+ RedshiftDialect.SUPPORTED_ISOLATION_LEVELS = new Set(['READ COMMITTED', 'SERIALIZABLE']);
2227
+ /**
2228
+ * Transaction handle for Redshift, backed by a single pooled `pg` client.
2229
+ */
2230
+ class RedshiftTransaction {
2231
+ constructor(client, _depth, options) {
2232
+ this.finished = false;
2233
+ this.parent = null;
2234
+ this.savepoints = [];
2235
+ this.client = null;
2236
+ this.savepointCount = 0;
2237
+ this.id = `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2238
+ this.options = options || {};
2239
+ this.client = client;
2240
+ }
2241
+ async createSavepoint(name) {
2242
+ const savepointName = name || `sp_${this.savepointCount++}`;
2243
+ if (this.client) {
2244
+ await this.client.query(`SAVEPOINT ${savepointName}`);
2245
+ this.savepoints.push(savepointName);
2246
+ }
2247
+ return savepointName;
2248
+ }
2249
+ async rollbackToSavepoint(name) {
2250
+ if (this.client)
2251
+ await this.client.query(`ROLLBACK TO SAVEPOINT ${name}`);
2252
+ }
2253
+ async releaseSavepoint(name) {
2254
+ if (this.client) {
2255
+ await this.client.query(`RELEASE SAVEPOINT ${name}`);
2256
+ this.savepoints = this.savepoints.filter((sp) => sp !== name);
2257
+ }
2258
+ }
2259
+ async commit() {
2260
+ if (this.client)
2261
+ await this.client.query('COMMIT');
2262
+ this.finished = true;
2263
+ }
2264
+ async rollback() {
2265
+ if (this.client)
2266
+ await this.client.query('ROLLBACK');
2267
+ this.finished = true;
2268
+ }
2269
+ }
2270
+ exports.RedshiftTransaction = RedshiftTransaction;
2271
+ /**
2272
+ * Create a new Redshift dialect instance.
2273
+ */
2274
+ function createRedshiftDialect(options) {
2275
+ return new RedshiftDialect(options || {});
2276
+ }
2277
+ exports.default = RedshiftDialect;