ts-prorm-orm 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1111 -0
- package/LICENSE +21 -0
- package/README.md +573 -0
- package/dist/audit/history-query.js +180 -0
- package/dist/audit/index.js +23 -0
- package/dist/audit/logger.js +236 -0
- package/dist/cache/cache-manager.js +84 -0
- package/dist/cache/index.js +16 -0
- package/dist/cache/redis-cluster-cache.js +557 -0
- package/dist/cli.js +2200 -0
- package/dist/compliance/audit-trail.js +68 -0
- package/dist/compliance/backup-verification.js +685 -0
- package/dist/compliance/breach-detector.js +505 -0
- package/dist/compliance/consent-record.js +227 -0
- package/dist/compliance/consent-versioning.js +331 -0
- package/dist/compliance/cross-border-log.js +530 -0
- package/dist/compliance/data-classifier.js +258 -0
- package/dist/compliance/data-lineage.js +565 -0
- package/dist/compliance/data-masker.js +303 -0
- package/dist/compliance/data-portability.js +265 -0
- package/dist/compliance/data-retention.js +195 -0
- package/dist/compliance/dsar-workflow.js +342 -0
- package/dist/compliance/field-encryption.js +223 -0
- package/dist/compliance/immutable-record.js +148 -0
- package/dist/compliance/index.js +260 -0
- package/dist/compliance/privacy-impact-assessment.js +364 -0
- package/dist/compliance/pseudonymization.js +269 -0
- package/dist/compliance/query-firewall.js +1182 -0
- package/dist/compliance/rate-limiter.js +580 -0
- package/dist/compliance/right-to-erasure.js +117 -0
- package/dist/compliance/row-level-security.js +246 -0
- package/dist/compliance/security-decorator.js +574 -0
- package/dist/compliance/security-monitor.js +525 -0
- package/dist/compliance/sensitive-data-discovery.js +476 -0
- package/dist/compliance/session-isolation.js +472 -0
- package/dist/compliance/tls-enforcer.js +108 -0
- package/dist/compliance/worm-storage.js +712 -0
- package/dist/connection-manager.js +198 -0
- package/dist/connection-pool.js +519 -0
- package/dist/decorators/audit.js +136 -0
- package/dist/decorators/belongs-to-many.js +115 -0
- package/dist/decorators/belongs-to.js +115 -0
- package/dist/decorators/check.js +435 -0
- package/dist/decorators/collate.js +329 -0
- package/dist/decorators/comment.js +205 -0
- package/dist/decorators/database-settings.js +236 -0
- package/dist/decorators/default.js +244 -0
- package/dist/decorators/encryption.js +235 -0
- package/dist/decorators/engine.js +97 -0
- package/dist/decorators/fk-constraints.js +594 -0
- package/dist/decorators/foreign-table.js +136 -0
- package/dist/decorators/generated.js +274 -0
- package/dist/decorators/has-many.js +127 -0
- package/dist/decorators/has-one.js +116 -0
- package/dist/decorators/hstore.js +129 -0
- package/dist/decorators/index.js +355 -0
- package/dist/decorators/json-column.js +83 -0
- package/dist/decorators/jsonb.js +101 -0
- package/dist/decorators/orm-decorators.js +425 -0
- package/dist/decorators/permissions.js +294 -0
- package/dist/decorators/procedure.js +210 -0
- package/dist/decorators/query-options.js +556 -0
- package/dist/decorators/range.js +167 -0
- package/dist/decorators/set-column.js +83 -0
- package/dist/decorators/spatial.js +114 -0
- package/dist/decorators/storage.js +580 -0
- package/dist/decorators/timezone.js +512 -0
- package/dist/decorators/trigger.js +90 -0
- package/dist/decorators/uuid.js +135 -0
- package/dist/decorators/view.js +258 -0
- package/dist/diagrams/chen-diagram.js +354 -0
- package/dist/diagrams/class-diagram.js +384 -0
- package/dist/diagrams/dependency-diagram.js +432 -0
- package/dist/diagrams/er-diagram.js +605 -0
- package/dist/diagrams/flow-diagram.js +394 -0
- package/dist/diagrams/gantt-diagram.js +411 -0
- package/dist/diagrams/index-diagram.js +353 -0
- package/dist/diagrams/index.js +184 -0
- package/dist/diagrams/migration-diagram.js +316 -0
- package/dist/diagrams/model-diagram.js +616 -0
- package/dist/diagrams/package-diagram.js +376 -0
- package/dist/diagrams/palette.js +85 -0
- package/dist/diagrams/relational-diagram.js +455 -0
- package/dist/diagrams/schemadoc-diagram.js +309 -0
- package/dist/diagrams/sequence-diagram.js +307 -0
- package/dist/diagrams/state-diagram.js +344 -0
- package/dist/diagrams/svg-dom.js +111 -0
- package/dist/diagrams/tree-diagram.js +250 -0
- package/dist/dialects/clickhouse/index.js +1742 -0
- package/dist/dialects/cockroachdb/index.js +4677 -0
- package/dist/dialects/cratedb/index.js +441 -0
- package/dist/dialects/databricks/index.js +517 -0
- package/dist/dialects/db2/index.js +2225 -0
- package/dist/dialects/dialect.js +720 -0
- package/dist/dialects/duckdb/index.js +2014 -0
- package/dist/dialects/exasol/index.js +357 -0
- package/dist/dialects/firebird/index.js +495 -0
- package/dist/dialects/greenplum/index.js +457 -0
- package/dist/dialects/hana/index.js +1741 -0
- package/dist/dialects/mariadb/index.js +3590 -0
- package/dist/dialects/mssql/index.js +2296 -0
- package/dist/dialects/mysql/index.js +4058 -0
- package/dist/dialects/oracle/index.js +2999 -0
- package/dist/dialects/postgres/index.js +5319 -0
- package/dist/dialects/query-stream-helper.js +152 -0
- package/dist/dialects/questdb/index.js +433 -0
- package/dist/dialects/redshift/index.js +2277 -0
- package/dist/dialects/singlestore/index.js +354 -0
- package/dist/dialects/snowflake/index.js +2082 -0
- package/dist/dialects/spanner/index.js +1768 -0
- package/dist/dialects/sqlite/index.js +3382 -0
- package/dist/dialects/tidb/index.js +377 -0
- package/dist/dialects/timescaledb/index.js +164 -0
- package/dist/dialects/trino/index.js +420 -0
- package/dist/dialects/turso/index.js +227 -0
- package/dist/dialects/vertica/index.js +317 -0
- package/dist/dialects/yugabytedb/index.js +424 -0
- package/dist/errors/index.js +472 -0
- package/dist/errors/utils.js +340 -0
- package/dist/errors.js +21 -0
- package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
- package/dist/extensions/catalog/mssql-features.js +147 -0
- package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
- package/dist/extensions/catalog/oracle-db2-features.js +196 -0
- package/dist/extensions/catalog/postgres-extensions.js +516 -0
- package/dist/extensions/index.js +71 -0
- package/dist/extensions/types.js +13 -0
- package/dist/foreign-data.js +173 -0
- package/dist/hooks/hooks-manager.js +350 -0
- package/dist/hooks/index.js +37 -0
- package/dist/index.js +463 -0
- package/dist/logging.js +335 -0
- package/dist/migrations/index.js +40 -0
- package/dist/migrations/migration.js +196 -0
- package/dist/migrations/migrator.js +411 -0
- package/dist/migrations/prormmigration.js +275 -0
- package/dist/migrations/query-interface.js +435 -0
- package/dist/migrations/seeder.js +353 -0
- package/dist/models/associations.js +852 -0
- package/dist/models/constraints.js +288 -0
- package/dist/models/data-types.js +2518 -0
- package/dist/models/decorators.js +445 -0
- package/dist/models/index.js +33 -0
- package/dist/models/indexes.js +531 -0
- package/dist/models/methods.js +382 -0
- package/dist/models/model-manager.js +103 -0
- package/dist/models/model.js +5349 -0
- package/dist/models/operators.js +67 -0
- package/dist/models/scopes.js +189 -0
- package/dist/models/typescript-types.js +26 -0
- package/dist/nosql/aerospike/index.js +205 -0
- package/dist/nosql/allegrograph/index.js +169 -0
- package/dist/nosql/arangodb/index.js +364 -0
- package/dist/nosql/azure-blob/index.js +206 -0
- package/dist/nosql/beanstalkd/index.js +231 -0
- package/dist/nosql/beequeue/index.js +210 -0
- package/dist/nosql/bigchaindb/index.js +195 -0
- package/dist/nosql/bigtable/index.js +224 -0
- package/dist/nosql/blazegraph/index.js +173 -0
- package/dist/nosql/bullmq/index.js +191 -0
- package/dist/nosql/cassandra/index.js +174 -0
- package/dist/nosql/chroma/index.js +190 -0
- package/dist/nosql/cloudflare-kv/index.js +220 -0
- package/dist/nosql/coherence/index.js +200 -0
- package/dist/nosql/cosmosdb/index.js +157 -0
- package/dist/nosql/couchbase/index.js +213 -0
- package/dist/nosql/dax/index.js +212 -0
- package/dist/nosql/deno-kv/index.js +206 -0
- package/dist/nosql/dgraph/index.js +171 -0
- package/dist/nosql/doris/index.js +169 -0
- package/dist/nosql/druid/index.js +162 -0
- package/dist/nosql/dynamodb/index.js +937 -0
- package/dist/nosql/elasticsearch/index.js +377 -0
- package/dist/nosql/etcd/index.js +502 -0
- package/dist/nosql/eventhubs/index.js +213 -0
- package/dist/nosql/eventstore/index.js +254 -0
- package/dist/nosql/faunadb/index.js +188 -0
- package/dist/nosql/firestore/index.js +177 -0
- package/dist/nosql/fluree/index.js +148 -0
- package/dist/nosql/fuseki/index.js +170 -0
- package/dist/nosql/gcs/index.js +172 -0
- package/dist/nosql/gearman/index.js +160 -0
- package/dist/nosql/geode/index.js +196 -0
- package/dist/nosql/graphdb/index.js +169 -0
- package/dist/nosql/graylog/index.js +188 -0
- package/dist/nosql/gridgain/index.js +171 -0
- package/dist/nosql/hazelcast/index.js +162 -0
- package/dist/nosql/hbase/index.js +230 -0
- package/dist/nosql/ignite/index.js +173 -0
- package/dist/nosql/immudb/index.js +184 -0
- package/dist/nosql/index.js +232 -0
- package/dist/nosql/infinispan/index.js +200 -0
- package/dist/nosql/influxdb/index.js +0 -0
- package/dist/nosql/kafka/index.js +234 -0
- package/dist/nosql/keyspaces/index.js +189 -0
- package/dist/nosql/kinesis/index.js +253 -0
- package/dist/nosql/leveldb/index.js +153 -0
- package/dist/nosql/lmdb/index.js +160 -0
- package/dist/nosql/loki/index.js +201 -0
- package/dist/nosql/marklogic/index.js +204 -0
- package/dist/nosql/materialize/index.js +145 -0
- package/dist/nosql/meilisearch/index.js +154 -0
- package/dist/nosql/memcached/index.js +223 -0
- package/dist/nosql/milvus/index.js +410 -0
- package/dist/nosql/minio/index.js +265 -0
- package/dist/nosql/momento/index.js +179 -0
- package/dist/nosql/mongodb/index.js +461 -0
- package/dist/nosql/nats/index.js +247 -0
- package/dist/nosql/nedb/index.js +164 -0
- package/dist/nosql/neo4j/index.js +450 -0
- package/dist/nosql/neptune/index.js +470 -0
- package/dist/nosql/nsq/index.js +200 -0
- package/dist/nosql/opensearch/index.js +186 -0
- package/dist/nosql/orientdb/index.js +175 -0
- package/dist/nosql/papertrail/index.js +200 -0
- package/dist/nosql/pinecone/index.js +0 -0
- package/dist/nosql/pinot/index.js +133 -0
- package/dist/nosql/pouchdb/index.js +172 -0
- package/dist/nosql/prometheus/index.js +174 -0
- package/dist/nosql/provendb/index.js +147 -0
- package/dist/nosql/pubsub/index.js +187 -0
- package/dist/nosql/pulsar/index.js +232 -0
- package/dist/nosql/qdrant/index.js +295 -0
- package/dist/nosql/qldb/index.js +192 -0
- package/dist/nosql/r2/index.js +281 -0
- package/dist/nosql/rabbitmq/index.js +237 -0
- package/dist/nosql/ravendb/index.js +175 -0
- package/dist/nosql/redis/index.js +607 -0
- package/dist/nosql/redpanda/index.js +237 -0
- package/dist/nosql/resque/index.js +203 -0
- package/dist/nosql/rethinkdb/index.js +232 -0
- package/dist/nosql/rocksdb/index.js +152 -0
- package/dist/nosql/rockset/index.js +126 -0
- package/dist/nosql/s3/index.js +298 -0
- package/dist/nosql/scylladb/index.js +178 -0
- package/dist/nosql/signoz/index.js +227 -0
- package/dist/nosql/sns/index.js +201 -0
- package/dist/nosql/solr/index.js +231 -0
- package/dist/nosql/splunk/index.js +227 -0
- package/dist/nosql/sqs/index.js +246 -0
- package/dist/nosql/stardog/index.js +169 -0
- package/dist/nosql/starrocks/index.js +170 -0
- package/dist/nosql/store.js +2 -0
- package/dist/nosql/sumologic/index.js +213 -0
- package/dist/nosql/surrealdb/index.js +179 -0
- package/dist/nosql/terminusdb/index.js +151 -0
- package/dist/nosql/tigergraph/index.js +469 -0
- package/dist/nosql/typesense/index.js +148 -0
- package/dist/nosql/unqlite/index.js +157 -0
- package/dist/nosql/upstash/index.js +167 -0
- package/dist/nosql/vercel-kv/index.js +166 -0
- package/dist/nosql/victoriametrics/index.js +213 -0
- package/dist/nosql/virtuoso/index.js +169 -0
- package/dist/nosql/weaviate/index.js +276 -0
- package/dist/operators/index.js +108 -0
- package/dist/operators.js +2690 -0
- package/dist/prisma-migrate/index.js +38 -0
- package/dist/prisma-migrate/migration-generator.js +125 -0
- package/dist/prisma-migrate/model-generator.js +172 -0
- package/dist/prisma-migrate/relations.js +100 -0
- package/dist/prisma-migrate/schema-parser.js +167 -0
- package/dist/prisma-migrate/type-mapper.js +40 -0
- package/dist/prorm.js +6832 -0
- package/dist/query-builders/cte-builder.js +80 -0
- package/dist/query-builders/functions/aggregate.js +390 -0
- package/dist/query-builders/functions/conditional.js +503 -0
- package/dist/query-builders/functions/datetime.js +695 -0
- package/dist/query-builders/functions/fulltext.js +439 -0
- package/dist/query-builders/functions/index.js +93 -0
- package/dist/query-builders/functions/json.js +427 -0
- package/dist/query-builders/functions/math.js +399 -0
- package/dist/query-builders/functions/string.js +518 -0
- package/dist/query-builders/functions/window.js +328 -0
- package/dist/query-builders/include-builder.js +323 -0
- package/dist/query-builders/index-expression-builder.js +242 -0
- package/dist/query-builders/index.js +161 -0
- package/dist/query-builders/insert-builder.js +164 -0
- package/dist/query-builders/model-helpers.js +38 -0
- package/dist/query-builders/order-limit-builder.js +239 -0
- package/dist/query-builders/sql-compiler.js +2040 -0
- package/dist/query-builders/subquery-builder.js +152 -0
- package/dist/query-builders/update-builder.js +182 -0
- package/dist/query-builders/view-builder.js +218 -0
- package/dist/query-builders/where-builder.js +1532 -0
- package/dist/query-interface.js +622 -0
- package/dist/query-optimizers/batch-optimizer.js +426 -0
- package/dist/query-optimizers/explain-plans.js +415 -0
- package/dist/query-optimizers/index.js +51 -0
- package/dist/query-optimizers/prepared-statement-cache.js +423 -0
- package/dist/query-optimizers/query-hints.js +438 -0
- package/dist/query-optimizers/query-optimizer.js +278 -0
- package/dist/query-optimizers/slow-query-logger.js +271 -0
- package/dist/replica-manager.js +507 -0
- package/dist/schema/index.js +15 -0
- package/dist/schema/migration-generator.js +374 -0
- package/dist/schema/schema-differ.js +549 -0
- package/dist/schema/types.js +6 -0
- package/dist/sql-constants.js +300 -0
- package/dist/sqlite-advanced.js +1047 -0
- package/dist/streams/index.js +12 -0
- package/dist/streams/transforms.js +178 -0
- package/dist/transaction.js +456 -0
- package/dist/types/index.js +180 -0
- package/dist/user-management.js +127 -0
- package/dist/utils/date.js +300 -0
- package/dist/utils/index.js +1079 -0
- package/dist/utils/string.js +161 -0
- package/dist/validators/index.js +19 -0
- package/dist/validators/validator.js +911 -0
- package/package.json +190 -0
|
@@ -0,0 +1,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;
|