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,2225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* IBM Db2 (Db2 for LUW - Linux/Unix/Windows) dialect implementation for the TypeScript ORM
|
|
4
|
+
* Uses the `ibm_db` driver (ODBC-based CLI driver) for database connectivity.
|
|
5
|
+
*
|
|
6
|
+
* Notable Db2 LUW SQL characteristics implemented here:
|
|
7
|
+
* - Double-quoted identifiers (like Oracle/ANSI SQL)
|
|
8
|
+
* - `OFFSET n ROWS FETCH FIRST m ROWS ONLY` for pagination
|
|
9
|
+
* - `MERGE INTO ... WHEN MATCHED / WHEN NOT MATCHED` for upsert
|
|
10
|
+
* - `GENERATED ALWAYS AS IDENTITY` for auto-increment columns
|
|
11
|
+
* - `CREATE SEQUENCE` / `NEXT VALUE FOR` for sequences
|
|
12
|
+
* - `CREATE SCHEMA` for first-class schema support
|
|
13
|
+
* - `?` (ODBC-style) positional bind markers, matching the ibm_db driver
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.DB2Dialect = void 0;
|
|
17
|
+
const ibm_db_1 = require("ibm_db");
|
|
18
|
+
const query_stream_helper_1 = require("../query-stream-helper");
|
|
19
|
+
const prorm_1 = require("../../prorm");
|
|
20
|
+
/**
|
|
21
|
+
* Db2-specific transaction class.
|
|
22
|
+
* Db2 transactions (via ibm_db) are tied to a specific connection - the
|
|
23
|
+
* connection used to BEGIN the transaction must be the one used to COMMIT
|
|
24
|
+
* or ROLLBACK it.
|
|
25
|
+
*/
|
|
26
|
+
class DB2Transaction {
|
|
27
|
+
constructor(connection, options) {
|
|
28
|
+
this.finished = false;
|
|
29
|
+
this.parent = null;
|
|
30
|
+
this.savepoints = [];
|
|
31
|
+
this.connection = null;
|
|
32
|
+
this.id = `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
33
|
+
this.options = options || {};
|
|
34
|
+
this.connection = connection;
|
|
35
|
+
}
|
|
36
|
+
async commit() {
|
|
37
|
+
this.finished = true;
|
|
38
|
+
}
|
|
39
|
+
async rollback() {
|
|
40
|
+
this.finished = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const DEFAULT_POOL_OPTIONS = {
|
|
44
|
+
max: 10,
|
|
45
|
+
min: 0,
|
|
46
|
+
};
|
|
47
|
+
const DEFAULT_RETRY_OPTIONS = {
|
|
48
|
+
max: 3,
|
|
49
|
+
timeout: 1000,
|
|
50
|
+
match: [
|
|
51
|
+
'ECONNREFUSED',
|
|
52
|
+
'ENOTFOUND',
|
|
53
|
+
'ETIMEDOUT',
|
|
54
|
+
'SQL30081N',
|
|
55
|
+
'SQL30061N',
|
|
56
|
+
'SQL1224N',
|
|
57
|
+
'Connection timed out',
|
|
58
|
+
],
|
|
59
|
+
backoff: false,
|
|
60
|
+
backoffMultiplier: 2,
|
|
61
|
+
backoffMax: 10000,
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Db2 (LUW) dialect class implementing the shared Dialect interface.
|
|
65
|
+
*/
|
|
66
|
+
class DB2Dialect {
|
|
67
|
+
constructor(config) {
|
|
68
|
+
this.name = 'db2';
|
|
69
|
+
this.library = 'ibm_db';
|
|
70
|
+
this.pool = null;
|
|
71
|
+
this._isConnected = false;
|
|
72
|
+
this.poolOptions = config.pool || {};
|
|
73
|
+
this.retryOptions = config.retry || {};
|
|
74
|
+
this.config = {
|
|
75
|
+
host: 'localhost',
|
|
76
|
+
port: 50000,
|
|
77
|
+
database: 'testdb',
|
|
78
|
+
username: 'db2inst1',
|
|
79
|
+
password: '',
|
|
80
|
+
...config,
|
|
81
|
+
};
|
|
82
|
+
this.connectionString = this.buildConnectionString(this.config);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build the Db2 keyword=value connection string from structured options,
|
|
86
|
+
* unless a raw `connectionString` override was provided.
|
|
87
|
+
*/
|
|
88
|
+
buildConnectionString(config) {
|
|
89
|
+
if (config.connectionString) {
|
|
90
|
+
return config.connectionString;
|
|
91
|
+
}
|
|
92
|
+
const parts = [
|
|
93
|
+
`DATABASE=${config.database}`,
|
|
94
|
+
`HOSTNAME=${config.host}`,
|
|
95
|
+
`PORT=${config.port}`,
|
|
96
|
+
'PROTOCOL=TCPIP',
|
|
97
|
+
`UID=${config.username}`,
|
|
98
|
+
`PWD=${config.password}`,
|
|
99
|
+
];
|
|
100
|
+
if (config.ssl) {
|
|
101
|
+
parts.push('SECURITY=SSL');
|
|
102
|
+
}
|
|
103
|
+
if (config.extraConnectionParams) {
|
|
104
|
+
for (const [key, value] of Object.entries(config.extraConnectionParams)) {
|
|
105
|
+
parts.push(`${key}=${value}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return `${parts.join(';')};`;
|
|
109
|
+
}
|
|
110
|
+
async withRetry(fn) {
|
|
111
|
+
const retryConfig = { ...DEFAULT_RETRY_OPTIONS, ...this.retryOptions };
|
|
112
|
+
let lastError;
|
|
113
|
+
const max = retryConfig.max ?? 3;
|
|
114
|
+
const match = retryConfig.match ?? [];
|
|
115
|
+
const timeout = retryConfig.timeout ?? 1000;
|
|
116
|
+
for (let attempt = 0; attempt <= max; attempt++) {
|
|
117
|
+
try {
|
|
118
|
+
return await fn();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
lastError = error;
|
|
122
|
+
const errorMessage = lastError.message;
|
|
123
|
+
const shouldRetry = match.some((pattern) => errorMessage.toLowerCase().includes(pattern.toLowerCase()));
|
|
124
|
+
if (!shouldRetry || attempt === max) {
|
|
125
|
+
throw lastError;
|
|
126
|
+
}
|
|
127
|
+
let delay = timeout;
|
|
128
|
+
if (retryConfig.backoff) {
|
|
129
|
+
delay = Math.min(timeout * Math.pow(retryConfig.backoffMultiplier || 2, attempt), retryConfig.backoffMax || 10000);
|
|
130
|
+
}
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw lastError;
|
|
135
|
+
}
|
|
136
|
+
async connect() {
|
|
137
|
+
await this.withRetry(async () => {
|
|
138
|
+
const pool = new ibm_db_1.Pool();
|
|
139
|
+
pool.setMaxPoolSize(this.poolOptions.max || DEFAULT_POOL_OPTIONS.max);
|
|
140
|
+
// Verify connectivity by opening and immediately releasing a connection.
|
|
141
|
+
const conn = await pool.open(this.connectionString);
|
|
142
|
+
await conn.close();
|
|
143
|
+
this.pool = pool;
|
|
144
|
+
this._isConnected = true;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async disconnect() {
|
|
148
|
+
if (this.pool) {
|
|
149
|
+
await this.pool.close();
|
|
150
|
+
this.pool = null;
|
|
151
|
+
this._isConnected = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get the current connection pool
|
|
156
|
+
*/
|
|
157
|
+
getConnection() {
|
|
158
|
+
return this.pool;
|
|
159
|
+
}
|
|
160
|
+
isConnected() {
|
|
161
|
+
return this._isConnected && this.pool !== null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Execute `sql` against a specific, already-open connection (either a
|
|
165
|
+
* transaction's pinned connection or a freshly-opened pooled one) and
|
|
166
|
+
* normalize the result shape. This is the single execution path shared by
|
|
167
|
+
* `query()` for both transactional and non-transactional calls, so there is
|
|
168
|
+
* no divergence between how a plain `query()` call and a transactional one
|
|
169
|
+
* interpret results.
|
|
170
|
+
*
|
|
171
|
+
* Uses `queryResult`/`ODBCResult` (rather than the plain `query` helper) so
|
|
172
|
+
* affected-row counts for DML/DDL statements are available via
|
|
173
|
+
* `getAffectedRowsSync()`, matching how ibm_db surfaces this information for
|
|
174
|
+
* non-SELECT statements.
|
|
175
|
+
*/
|
|
176
|
+
async executeOnConnection(conn, sql, params) {
|
|
177
|
+
// Cast to bypass the driver's overloaded (sql, params, cb) / (sql, cb)
|
|
178
|
+
// signatures - at runtime, calling with (sql, params) resolves to the
|
|
179
|
+
// Promise-returning overload.
|
|
180
|
+
const [result, outParams] = await conn.queryResult(sql, params);
|
|
181
|
+
const isSelect = /^\s*(SELECT|WITH|VALUES)\b/i.test(sql.trim());
|
|
182
|
+
if (result) {
|
|
183
|
+
const rows = isSelect ? (await result.fetchAll()) : [];
|
|
184
|
+
const rowCount = isSelect ? rows.length : result.getAffectedRowsSync();
|
|
185
|
+
await result.close();
|
|
186
|
+
return { rows, rowCount, fields: [] };
|
|
187
|
+
}
|
|
188
|
+
const rows = Array.isArray(outParams) ? outParams : [];
|
|
189
|
+
return { rows: isSelect ? rows : [], rowCount: rows.length, fields: [] };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Stream query results by paging through `sql` via repeated
|
|
193
|
+
* dialect-appropriate LIMIT/OFFSET queries (see
|
|
194
|
+
* `createPaginatedQueryStream()` in `src/dialects/query-stream-helper.ts`)
|
|
195
|
+
* instead of loading the whole result set into memory at once.
|
|
196
|
+
* @param sql - The SELECT statement to stream
|
|
197
|
+
* @param options - Streaming options (batch size, backpressure watermark, model mapping)
|
|
198
|
+
*/
|
|
199
|
+
queryStream(sql, options) {
|
|
200
|
+
return (0, query_stream_helper_1.createPaginatedQueryStream)(this, sql, options);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Execute a query.
|
|
204
|
+
*
|
|
205
|
+
* If `options.transaction` carries a `DB2Transaction` (as returned by
|
|
206
|
+
* `startTransaction()`), the statement is executed on that transaction's
|
|
207
|
+
* *pinned* connection instead of a fresh connection from the pool. This is
|
|
208
|
+
* required by `ibm_db`: transactions are tied to the specific connection
|
|
209
|
+
* that issued `beginTransaction()` (see DRIVER_NOTES.md section 6), so
|
|
210
|
+
* running statements on a different, autocommit-by-default pooled
|
|
211
|
+
* connection would make them commit independently of the transaction and
|
|
212
|
+
* never actually participate in it.
|
|
213
|
+
*/
|
|
214
|
+
async query(sql, options) {
|
|
215
|
+
const bindValues = options?.replacements ?? options?.bindings ?? options?.bind ?? [];
|
|
216
|
+
const params = Array.isArray(bindValues) ? bindValues : Object.values(bindValues);
|
|
217
|
+
const tx = options?.transaction;
|
|
218
|
+
if (tx) {
|
|
219
|
+
if (tx.finished) {
|
|
220
|
+
throw new Error('Cannot execute query: the transaction has already been committed or rolled back.');
|
|
221
|
+
}
|
|
222
|
+
if (!tx.connection) {
|
|
223
|
+
throw new Error('Cannot execute query: the transaction has no associated connection.');
|
|
224
|
+
}
|
|
225
|
+
// Run on the transaction's pinned connection - do NOT close it here;
|
|
226
|
+
// it is closed by commitTransaction()/rollbackTransaction().
|
|
227
|
+
return this.executeOnConnection(tx.connection, sql, params);
|
|
228
|
+
}
|
|
229
|
+
if (!this.pool) {
|
|
230
|
+
throw new Error('Not connected to database. Call connect() first.');
|
|
231
|
+
}
|
|
232
|
+
const conn = await this.pool.open(this.connectionString);
|
|
233
|
+
try {
|
|
234
|
+
return await this.executeOnConnection(conn, sql, params);
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
await conn.close();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
escape(value) {
|
|
241
|
+
if (value instanceof prorm_1.Literal) {
|
|
242
|
+
return value.val;
|
|
243
|
+
}
|
|
244
|
+
if (value === null || value === undefined)
|
|
245
|
+
return 'NULL';
|
|
246
|
+
if (typeof value === 'boolean')
|
|
247
|
+
return value ? '1' : '0';
|
|
248
|
+
if (typeof value === 'number')
|
|
249
|
+
return String(value);
|
|
250
|
+
if (value instanceof Date)
|
|
251
|
+
return this.formatDate(value);
|
|
252
|
+
if (Buffer.isBuffer(value))
|
|
253
|
+
return `BLOB(X'${value.toString('hex')}')`;
|
|
254
|
+
if (Array.isArray(value) || typeof value === 'object') {
|
|
255
|
+
return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
|
|
256
|
+
}
|
|
257
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
258
|
+
}
|
|
259
|
+
formatDate(date) {
|
|
260
|
+
const year = date.getFullYear();
|
|
261
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
262
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
263
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
264
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
265
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
266
|
+
const microseconds = String(date.getMilliseconds()).padStart(3, '0') + '000';
|
|
267
|
+
return `TIMESTAMP '${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${microseconds}'`;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Db2 uses double-quoted delimited identifiers (like Oracle/ANSI SQL).
|
|
271
|
+
* Unquoted identifiers are folded to uppercase by Db2, so identifiers
|
|
272
|
+
* created via quoteIdentifier()/escapeId() must always be quoted the
|
|
273
|
+
* same way on every reference.
|
|
274
|
+
*/
|
|
275
|
+
escapeId(identifier) {
|
|
276
|
+
const id = String(identifier ?? '');
|
|
277
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
278
|
+
}
|
|
279
|
+
quoteIdentifier(identifier) {
|
|
280
|
+
return this.escapeId(identifier);
|
|
281
|
+
}
|
|
282
|
+
quoteTable(tableName, schema) {
|
|
283
|
+
const schemaName = schema || this.config.schema;
|
|
284
|
+
if (schemaName) {
|
|
285
|
+
return `${this.escapeId(schemaName)}.${this.escapeId(tableName)}`;
|
|
286
|
+
}
|
|
287
|
+
return this.escapeId(tableName);
|
|
288
|
+
}
|
|
289
|
+
async getDatabaseVersion() {
|
|
290
|
+
const result = await this.query('SELECT SERVICE_LEVEL AS VERSION FROM SYSIBMADM.ENV_INST_INFO');
|
|
291
|
+
return result.rows[0]?.VERSION || result.rows[0]?.version || 'unknown';
|
|
292
|
+
}
|
|
293
|
+
// ==================== Schema Operations ====================
|
|
294
|
+
async createSchema(schema) {
|
|
295
|
+
await this.query(`CREATE SCHEMA ${this.escapeId(schema)}`);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Drop a schema. Db2 only supports `DROP SCHEMA ... RESTRICT` - the
|
|
299
|
+
* schema must already be empty, there is no CASCADE equivalent.
|
|
300
|
+
*/
|
|
301
|
+
async dropSchema(schema, options) {
|
|
302
|
+
let sql = 'DROP SCHEMA';
|
|
303
|
+
if (options?.ifExists) {
|
|
304
|
+
const exists = await this.query(`SELECT SCHEMANAME FROM SYSCAT.SCHEMATA WHERE SCHEMANAME = '${schema}'`);
|
|
305
|
+
if (!exists.rows || exists.rows.length === 0) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
sql += ` ${this.escapeId(schema)} RESTRICT`;
|
|
310
|
+
await this.query(sql);
|
|
311
|
+
}
|
|
312
|
+
async showAllSchemas() {
|
|
313
|
+
const result = await this.query('SELECT SCHEMANAME FROM SYSCAT.SCHEMATA ORDER BY SCHEMANAME');
|
|
314
|
+
return result.rows.map((row) => row.SCHEMANAME || row.schemaname);
|
|
315
|
+
}
|
|
316
|
+
async listSchemas() {
|
|
317
|
+
return this.showAllSchemas();
|
|
318
|
+
}
|
|
319
|
+
createDatabaseSQL(options) {
|
|
320
|
+
// Db2 databases are usually created via the `db2 CREATE DATABASE` CLP
|
|
321
|
+
// command rather than plain SQL, but Db2 also accepts this form via CLI.
|
|
322
|
+
let sql = `CREATE DATABASE ${options.name}`;
|
|
323
|
+
if (options.encoding) {
|
|
324
|
+
sql += ` USING CODESET ${options.encoding}`;
|
|
325
|
+
}
|
|
326
|
+
return sql;
|
|
327
|
+
}
|
|
328
|
+
dropDatabaseSQL(name) {
|
|
329
|
+
return `DROP DATABASE ${name}`;
|
|
330
|
+
}
|
|
331
|
+
createSavepointSQL(name) {
|
|
332
|
+
const savepointName = name || `sp_${Date.now()}`;
|
|
333
|
+
return `SAVEPOINT ${savepointName} ON ROLLBACK RETAIN CURSORS`;
|
|
334
|
+
}
|
|
335
|
+
releaseSavepointSQL(name) {
|
|
336
|
+
return `RELEASE SAVEPOINT ${name}`;
|
|
337
|
+
}
|
|
338
|
+
rollbackToSavepointSQL(name) {
|
|
339
|
+
return `ROLLBACK TO SAVEPOINT ${name}`;
|
|
340
|
+
}
|
|
341
|
+
// ==================== Extensions (Not Supported) ====================
|
|
342
|
+
async createExtension() {
|
|
343
|
+
throw new Error('Extensions are not supported by Db2. This is a PostgreSQL-specific feature. Use Db2 modules/features instead.');
|
|
344
|
+
}
|
|
345
|
+
async dropExtension() {
|
|
346
|
+
throw new Error('Extensions are not supported by Db2. This is a PostgreSQL-specific feature. Use Db2 modules/features instead.');
|
|
347
|
+
}
|
|
348
|
+
async getExtensions() {
|
|
349
|
+
throw new Error('Extensions are not supported by Db2. This is a PostgreSQL-specific feature. Use Db2 modules/features instead.');
|
|
350
|
+
}
|
|
351
|
+
async hasExtension() {
|
|
352
|
+
throw new Error('Extensions are not supported by Db2. This is a PostgreSQL-specific feature. Use Db2 modules/features instead.');
|
|
353
|
+
}
|
|
354
|
+
// ==================== Table Operations ====================
|
|
355
|
+
async createTable(tableName, columns, options) {
|
|
356
|
+
const columnDefs = [];
|
|
357
|
+
for (const [columnName, definition] of Object.entries(columns)) {
|
|
358
|
+
columnDefs.push(this.buildColumnDefinition(columnName, definition));
|
|
359
|
+
}
|
|
360
|
+
if (options?.constraints) {
|
|
361
|
+
for (const constraint of options.constraints) {
|
|
362
|
+
const constraintSql = this.buildConstraintSql(constraint);
|
|
363
|
+
if (constraintSql)
|
|
364
|
+
columnDefs.push(constraintSql);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
let sql = 'CREATE TABLE';
|
|
368
|
+
if (options?.ifNotExists) {
|
|
369
|
+
sql += ' IF NOT EXISTS';
|
|
370
|
+
}
|
|
371
|
+
sql += ` ${this.quoteTable(tableName)} (${columnDefs.join(', ')})`;
|
|
372
|
+
if (options?.tablespace) {
|
|
373
|
+
sql += ` IN ${options.tablespace}`;
|
|
374
|
+
}
|
|
375
|
+
sql += this.buildCompressClause(options?.compress);
|
|
376
|
+
await this.query(sql);
|
|
377
|
+
if (options?.indexes) {
|
|
378
|
+
for (const index of options.indexes) {
|
|
379
|
+
await this.createIndex(tableName, {
|
|
380
|
+
name: index.name || `${tableName}_${index.fields.join('_')}_idx`,
|
|
381
|
+
fields: index.fields,
|
|
382
|
+
unique: index.unique,
|
|
383
|
+
type: index.type,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
buildColumnDefinition(columnName, definition) {
|
|
389
|
+
let sql = `${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type)}`;
|
|
390
|
+
if (definition.autoIncrement) {
|
|
391
|
+
sql += ' GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1)';
|
|
392
|
+
}
|
|
393
|
+
if (definition.allowNull === false) {
|
|
394
|
+
sql += ' NOT NULL';
|
|
395
|
+
}
|
|
396
|
+
if (definition.defaultValue !== undefined && !definition.autoIncrement) {
|
|
397
|
+
if (definition.defaultValue === null) {
|
|
398
|
+
sql += ' DEFAULT NULL';
|
|
399
|
+
}
|
|
400
|
+
else if (definition.defaultValue instanceof prorm_1.Literal) {
|
|
401
|
+
sql += ` DEFAULT ${definition.defaultValue.val}`;
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
sql += ` DEFAULT ${this.escape(definition.defaultValue)}`;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (definition.primaryKey) {
|
|
408
|
+
sql += ' PRIMARY KEY';
|
|
409
|
+
}
|
|
410
|
+
if (definition.unique) {
|
|
411
|
+
if (typeof definition.unique === 'string') {
|
|
412
|
+
sql += ` UNIQUE`;
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
sql += ' UNIQUE';
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (definition.references) {
|
|
419
|
+
const refField = definition.references.field;
|
|
420
|
+
const refFieldSql = Array.isArray(refField)
|
|
421
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
422
|
+
: `(${this.escapeId(refField)})`;
|
|
423
|
+
sql += ` REFERENCES ${this.quoteTable(definition.references.table)}${refFieldSql}`;
|
|
424
|
+
if (definition.references.onDelete) {
|
|
425
|
+
sql += ` ON DELETE ${definition.references.onDelete.toUpperCase()}`;
|
|
426
|
+
}
|
|
427
|
+
if (definition.references.onUpdate) {
|
|
428
|
+
sql += ` ON UPDATE ${definition.references.onUpdate.toUpperCase()}`;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return sql;
|
|
432
|
+
}
|
|
433
|
+
buildConstraintSql(constraint) {
|
|
434
|
+
const name = constraint.name ? `CONSTRAINT ${this.escapeId(constraint.name)} ` : '';
|
|
435
|
+
const fields = constraint.fields?.map((f) => this.escapeId(f)).join(', ') || '';
|
|
436
|
+
switch (constraint.type) {
|
|
437
|
+
case 'PRIMARY KEY':
|
|
438
|
+
return `${name}PRIMARY KEY (${fields})`;
|
|
439
|
+
case 'UNIQUE':
|
|
440
|
+
return `${name}UNIQUE (${fields})`;
|
|
441
|
+
case 'CHECK':
|
|
442
|
+
return `${name}CHECK (${constraint.check})`;
|
|
443
|
+
case 'FOREIGN KEY': {
|
|
444
|
+
if (!constraint.references)
|
|
445
|
+
return null;
|
|
446
|
+
const refField = constraint.references.field;
|
|
447
|
+
const refFieldSql = Array.isArray(refField)
|
|
448
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
449
|
+
: `(${this.escapeId(refField)})`;
|
|
450
|
+
let fkSql = `${name}FOREIGN KEY (${fields}) REFERENCES ${this.quoteTable(constraint.references.table)}${refFieldSql}`;
|
|
451
|
+
if (constraint.references.onDelete) {
|
|
452
|
+
fkSql += ` ON DELETE ${constraint.references.onDelete.toUpperCase()}`;
|
|
453
|
+
}
|
|
454
|
+
if (constraint.references.onUpdate) {
|
|
455
|
+
fkSql += ` ON UPDATE ${constraint.references.onUpdate.toUpperCase()}`;
|
|
456
|
+
}
|
|
457
|
+
return fkSql;
|
|
458
|
+
}
|
|
459
|
+
default:
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Builds the `COMPRESS ...` clause shared by `CREATE TABLE` and
|
|
465
|
+
* `ALTER TABLE` statements. Returns an empty string when `mode` is
|
|
466
|
+
* `undefined` (compression left unspecified), ` COMPRESS YES ADAPTIVE`
|
|
467
|
+
* / ` COMPRESS YES STATIC` for the respective modes, or ` COMPRESS NO`
|
|
468
|
+
* when explicitly disabled via `false`.
|
|
469
|
+
*/
|
|
470
|
+
buildCompressClause(mode) {
|
|
471
|
+
if (mode === undefined)
|
|
472
|
+
return '';
|
|
473
|
+
if (mode === false)
|
|
474
|
+
return ' COMPRESS NO';
|
|
475
|
+
return ` COMPRESS YES ${mode}`;
|
|
476
|
+
}
|
|
477
|
+
async dropTable(tableName, options) {
|
|
478
|
+
let sql = 'DROP TABLE';
|
|
479
|
+
if (options?.ifExists) {
|
|
480
|
+
const exists = await this.query(`SELECT TABNAME FROM SYSCAT.TABLES WHERE TABNAME = '${tableName}'`);
|
|
481
|
+
if (!exists.rows || exists.rows.length === 0) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
sql += ` ${this.quoteTable(tableName)}`;
|
|
486
|
+
await this.query(sql);
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Bulk insert records into a table. Db2's `ibm_db` driver does not offer a
|
|
490
|
+
* multi-row VALUES shortcut that's meaningfully different from repeated
|
|
491
|
+
* single-row inserts sent through the ODBC layer, so each record is
|
|
492
|
+
* inserted individually within a single statement using multiple VALUES
|
|
493
|
+
* clauses (all Db2 LUW versions targeted here support multi-row INSERT).
|
|
494
|
+
*/
|
|
495
|
+
async bulkInsert(tableName, records, options) {
|
|
496
|
+
if (records.length === 0) {
|
|
497
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
498
|
+
}
|
|
499
|
+
const columns = Object.keys(records[0]);
|
|
500
|
+
const values = [];
|
|
501
|
+
const rowPlaceholders = [];
|
|
502
|
+
for (const record of records) {
|
|
503
|
+
const placeholders = [];
|
|
504
|
+
for (const column of columns) {
|
|
505
|
+
placeholders.push('?');
|
|
506
|
+
values.push(record[column]);
|
|
507
|
+
}
|
|
508
|
+
rowPlaceholders.push(`(${placeholders.join(', ')})`);
|
|
509
|
+
}
|
|
510
|
+
const sql = `INSERT INTO ${this.quoteTable(tableName, options?.schema)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES ${rowPlaceholders.join(', ')}`;
|
|
511
|
+
return this.query(sql, { replacements: values });
|
|
512
|
+
}
|
|
513
|
+
// Db2 LUW range partitioning is expressed via `PARTITION BY RANGE(...)` on
|
|
514
|
+
// the base CREATE TABLE statement (there's no separate list/hash
|
|
515
|
+
// partitioning syntax as with PostgreSQL/Oracle).
|
|
516
|
+
async createPartitionedTable(tableName, columns, options) {
|
|
517
|
+
if (options?.partitionBy?.type !== 'range') {
|
|
518
|
+
throw new Error('Db2 only supports RANGE partitioning for CREATE TABLE ... PARTITION BY');
|
|
519
|
+
}
|
|
520
|
+
const columnDefs = Object.entries(columns).map(([columnName, definition]) => this.buildColumnDefinition(columnName, definition));
|
|
521
|
+
const partitionColumn = Array.isArray(options.partitionBy.column)
|
|
522
|
+
? options.partitionBy.column.join(', ')
|
|
523
|
+
: options.partitionBy.column;
|
|
524
|
+
let sql = `CREATE TABLE ${this.quoteTable(tableName)} (${columnDefs.join(', ')})`;
|
|
525
|
+
if (options.tablespace) {
|
|
526
|
+
sql += ` IN ${options.tablespace}`;
|
|
527
|
+
}
|
|
528
|
+
sql += this.buildCompressClause(options.compress);
|
|
529
|
+
sql += ` PARTITION BY RANGE (${this.escapeId(partitionColumn)}) (`;
|
|
530
|
+
const partitionDefs = (options.partitions || []).map((part) => {
|
|
531
|
+
let def = `PARTITION ${this.escapeId(part.name)} STARTING FROM (${this.formatPartitionBound(part.bound?.from)})`;
|
|
532
|
+
if (part.bound?.to !== undefined) {
|
|
533
|
+
def += ` ENDING AT (${this.formatPartitionBound(part.bound.to)})`;
|
|
534
|
+
}
|
|
535
|
+
if (part.tablespace) {
|
|
536
|
+
def += ` IN ${part.tablespace}`;
|
|
537
|
+
}
|
|
538
|
+
return def;
|
|
539
|
+
});
|
|
540
|
+
sql += partitionDefs.join(', ');
|
|
541
|
+
sql += ')';
|
|
542
|
+
await this.query(sql);
|
|
543
|
+
}
|
|
544
|
+
formatPartitionBound(value) {
|
|
545
|
+
if (value === undefined)
|
|
546
|
+
return 'MINVALUE';
|
|
547
|
+
if (value instanceof Date)
|
|
548
|
+
return this.escape(value);
|
|
549
|
+
if (typeof value === 'number')
|
|
550
|
+
return String(value);
|
|
551
|
+
return this.escape(value);
|
|
552
|
+
}
|
|
553
|
+
async createPartition(options) {
|
|
554
|
+
let sql = `ALTER TABLE ${this.quoteTable(options.parentTable)} ADD PARTITION ${this.escapeId(options.name)}`;
|
|
555
|
+
if (options.bound && 'from' in options.bound) {
|
|
556
|
+
sql += ` STARTING FROM (${this.formatPartitionBound(options.bound.from)})`;
|
|
557
|
+
if (options.bound.to !== undefined) {
|
|
558
|
+
sql += ` ENDING AT (${this.formatPartitionBound(options.bound.to)})`;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (options.tablespace) {
|
|
562
|
+
sql += ` IN ${options.tablespace}`;
|
|
563
|
+
}
|
|
564
|
+
await this.query(sql);
|
|
565
|
+
}
|
|
566
|
+
async attachPartition(options) {
|
|
567
|
+
// Db2 uses ALTER TABLE ... ATTACH PARTITION ... FROM TABLE ...
|
|
568
|
+
await this.query(`ALTER TABLE ${this.quoteTable(options.parentTable)} ATTACH PARTITION ${this.escapeId(options.partitionName)} FROM TABLE ${this.quoteTable(options.partitionName)}`);
|
|
569
|
+
}
|
|
570
|
+
async detachPartition(options) {
|
|
571
|
+
await this.query(`ALTER TABLE ${this.quoteTable(options.partitionName)} DETACH PARTITION ${this.escapeId(options.partitionName)}`);
|
|
572
|
+
}
|
|
573
|
+
async dropPartition(partitionName, options) {
|
|
574
|
+
let sql = `ALTER TABLE ${this.quoteTable(partitionName)} DROP PARTITION ${this.escapeId(partitionName)}`;
|
|
575
|
+
await this.query(sql);
|
|
576
|
+
}
|
|
577
|
+
// ==================== Temporal Tables ====================
|
|
578
|
+
// Db2 for LUW's bitemporal support: system-period temporal tables track
|
|
579
|
+
// *every* change to a row automatically (row-begin/row-end timestamps
|
|
580
|
+
// maintained by the engine, with prior versions moved to a linked history
|
|
581
|
+
// table), while application-period ("business-time") temporal tables let
|
|
582
|
+
// the application itself declare the period during which a row is
|
|
583
|
+
// considered valid/effective. The two can be combined on the same table
|
|
584
|
+
// for full bitemporal support, but are modeled here as separate entry
|
|
585
|
+
// points since most callers only need one or the other.
|
|
586
|
+
/**
|
|
587
|
+
* Create a system-period temporal table.
|
|
588
|
+
*
|
|
589
|
+
* Emits:
|
|
590
|
+
* CREATE TABLE tableName (
|
|
591
|
+
* ...columns...,
|
|
592
|
+
* rowBeginColumn TIMESTAMP(12) GENERATED ALWAYS AS ROW BEGIN NOT NULL,
|
|
593
|
+
* rowEndColumn TIMESTAMP(12) GENERATED ALWAYS AS ROW END NOT NULL,
|
|
594
|
+
* [transactionIdColumn TIMESTAMP(12) GENERATED ALWAYS AS TRANSACTION START ID IMPLICITLY HIDDEN NOT NULL,]
|
|
595
|
+
* PERIOD SYSTEM_TIME (rowBeginColumn, rowEndColumn)
|
|
596
|
+
* );
|
|
597
|
+
* CREATE TABLE historyTableName LIKE tableName;
|
|
598
|
+
* ALTER TABLE tableName ADD VERSIONING USE HISTORY TABLE historyTableName;
|
|
599
|
+
*
|
|
600
|
+
* This is real Db2 LUW 10.5+ syntax - the hidden row-begin/row-end pair is
|
|
601
|
+
* declared via `GENERATED ALWAYS AS ROW BEGIN/END`, grouped into a
|
|
602
|
+
* `PERIOD SYSTEM_TIME`, and then wired to a (structurally identical)
|
|
603
|
+
* history table via `ADD VERSIONING`, after which the engine automatically
|
|
604
|
+
* moves the pre-update/pre-delete image of every row into the history
|
|
605
|
+
* table on every UPDATE/DELETE.
|
|
606
|
+
*/
|
|
607
|
+
async createSystemTemporalTable(tableName, columns, options) {
|
|
608
|
+
const rowBeginColumn = options?.rowBeginColumn || 'SYS_START';
|
|
609
|
+
const rowEndColumn = options?.rowEndColumn || 'SYS_END';
|
|
610
|
+
const historyTableName = options?.historyTableName || `${tableName}_HISTORY`;
|
|
611
|
+
const columnDefs = Object.entries(columns).map(([columnName, definition]) => this.buildColumnDefinition(columnName, definition));
|
|
612
|
+
if (options?.constraints) {
|
|
613
|
+
for (const constraint of options.constraints) {
|
|
614
|
+
const constraintSql = this.buildConstraintSql(constraint);
|
|
615
|
+
if (constraintSql)
|
|
616
|
+
columnDefs.push(constraintSql);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
columnDefs.push(`${this.escapeId(rowBeginColumn)} TIMESTAMP(12) NOT NULL GENERATED ALWAYS AS ROW BEGIN`);
|
|
620
|
+
columnDefs.push(`${this.escapeId(rowEndColumn)} TIMESTAMP(12) NOT NULL GENERATED ALWAYS AS ROW END`);
|
|
621
|
+
if (options?.transactionIdColumn) {
|
|
622
|
+
columnDefs.push(`${this.escapeId(options.transactionIdColumn)} TIMESTAMP(12) GENERATED ALWAYS AS TRANSACTION START ID IMPLICITLY HIDDEN NOT NULL`);
|
|
623
|
+
}
|
|
624
|
+
columnDefs.push(`PERIOD SYSTEM_TIME (${this.escapeId(rowBeginColumn)}, ${this.escapeId(rowEndColumn)})`);
|
|
625
|
+
let sql = 'CREATE TABLE';
|
|
626
|
+
if (options?.ifNotExists)
|
|
627
|
+
sql += ' IF NOT EXISTS';
|
|
628
|
+
sql += ` ${this.quoteTable(tableName)} (${columnDefs.join(', ')})`;
|
|
629
|
+
await this.query(sql);
|
|
630
|
+
await this.query(`CREATE TABLE ${this.quoteTable(historyTableName)} LIKE ${this.quoteTable(tableName)}`);
|
|
631
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD VERSIONING USE HISTORY TABLE ${this.quoteTable(historyTableName)}`);
|
|
632
|
+
if (options?.indexes) {
|
|
633
|
+
for (const index of options.indexes) {
|
|
634
|
+
await this.createIndex(tableName, {
|
|
635
|
+
name: index.name || `${tableName}_${index.fields.join('_')}_idx`,
|
|
636
|
+
fields: index.fields,
|
|
637
|
+
unique: index.unique,
|
|
638
|
+
type: index.type,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Disable system-period versioning and drop both the base table and its
|
|
645
|
+
* history table (mirrors `createSystemTemporalTable`).
|
|
646
|
+
*/
|
|
647
|
+
async dropSystemTemporalTable(tableName, options) {
|
|
648
|
+
const historyTableName = options?.historyTableName || `${tableName}_HISTORY`;
|
|
649
|
+
if (options?.ifExists) {
|
|
650
|
+
const exists = await this.query(`SELECT TABNAME FROM SYSCAT.TABLES WHERE TABNAME = '${tableName}'`);
|
|
651
|
+
if (!exists.rows || exists.rows.length === 0)
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} DROP VERSIONING`);
|
|
655
|
+
await this.query(`DROP TABLE ${this.quoteTable(tableName)}`);
|
|
656
|
+
if (options?.dropHistoryTable !== false) {
|
|
657
|
+
await this.query(`DROP TABLE ${this.quoteTable(historyTableName)}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Create an application-period ("business-time") temporal table.
|
|
662
|
+
*
|
|
663
|
+
* Emits:
|
|
664
|
+
* CREATE TABLE tableName (
|
|
665
|
+
* ...columns...,
|
|
666
|
+
* businessStartColumn DATE NOT NULL,
|
|
667
|
+
* businessEndColumn DATE NOT NULL,
|
|
668
|
+
* PERIOD BUSINESS_TIME (businessStartColumn, businessEndColumn)
|
|
669
|
+
* );
|
|
670
|
+
*
|
|
671
|
+
* Unlike system-period tables, application-period tables are not
|
|
672
|
+
* automatically versioned by the engine - the application is responsible
|
|
673
|
+
* for setting/maintaining the business-time period on each row; Db2 only
|
|
674
|
+
* enforces `FOR PORTION OF BUSINESS_TIME` UPDATE/DELETE semantics and
|
|
675
|
+
* `WITHOUT OVERLAPS` constraints (optionally) on top of the declared
|
|
676
|
+
* period.
|
|
677
|
+
*/
|
|
678
|
+
async createApplicationTemporalTable(tableName, columns, options) {
|
|
679
|
+
const { businessStartColumn, businessEndColumn, periodDataType } = options;
|
|
680
|
+
const periodType = periodDataType || 'DATE';
|
|
681
|
+
const columnDefs = Object.entries(columns).map(([columnName, definition]) => this.buildColumnDefinition(columnName, definition));
|
|
682
|
+
if (options.constraints) {
|
|
683
|
+
for (const constraint of options.constraints) {
|
|
684
|
+
const constraintSql = this.buildConstraintSql(constraint);
|
|
685
|
+
if (constraintSql)
|
|
686
|
+
columnDefs.push(constraintSql);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
columnDefs.push(`${this.escapeId(businessStartColumn)} ${periodType} NOT NULL`);
|
|
690
|
+
columnDefs.push(`${this.escapeId(businessEndColumn)} ${periodType} NOT NULL`);
|
|
691
|
+
columnDefs.push(`PERIOD BUSINESS_TIME (${this.escapeId(businessStartColumn)}, ${this.escapeId(businessEndColumn)})`);
|
|
692
|
+
if (options.withoutOverlapsKey && options.withoutOverlapsKey.length > 0) {
|
|
693
|
+
const keyFields = options.withoutOverlapsKey.map((f) => this.escapeId(f)).join(', ');
|
|
694
|
+
columnDefs.push(`PRIMARY KEY (${keyFields}, BUSINESS_TIME WITHOUT OVERLAPS)`);
|
|
695
|
+
}
|
|
696
|
+
let sql = 'CREATE TABLE';
|
|
697
|
+
if (options.ifNotExists)
|
|
698
|
+
sql += ' IF NOT EXISTS';
|
|
699
|
+
sql += ` ${this.quoteTable(tableName)} (${columnDefs.join(', ')})`;
|
|
700
|
+
await this.query(sql);
|
|
701
|
+
if (options.indexes) {
|
|
702
|
+
for (const index of options.indexes) {
|
|
703
|
+
await this.createIndex(tableName, {
|
|
704
|
+
name: index.name || `${tableName}_${index.fields.join('_')}_idx`,
|
|
705
|
+
fields: index.fields,
|
|
706
|
+
unique: index.unique,
|
|
707
|
+
type: index.type,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Format a point-in-time value for use in a `FOR SYSTEM_TIME`/
|
|
714
|
+
* `FOR BUSINESS_TIME` clause. Accepts a `Date`, or a raw SQL expression
|
|
715
|
+
* string (e.g. `'CURRENT TIMESTAMP'`, or an already-quoted literal) which
|
|
716
|
+
* is passed through unescaped so callers can supply expressions.
|
|
717
|
+
*/
|
|
718
|
+
formatTemporalTimestamp(value) {
|
|
719
|
+
if (value instanceof Date) {
|
|
720
|
+
const iso = value.toISOString(); // e.g. 2024-01-01T00:00:00.000Z
|
|
721
|
+
const datePart = iso.slice(0, 10); // 2024-01-01
|
|
722
|
+
const timePart = iso.slice(11, 23).replace(/:/g, '.'); // 00.00.00.000
|
|
723
|
+
const db2Ts = `${datePart}-${timePart}000`; // pad ms -> microseconds
|
|
724
|
+
return `TIMESTAMP('${db2Ts}')`;
|
|
725
|
+
}
|
|
726
|
+
const trimmed = value.trim();
|
|
727
|
+
if (/^current[\s_]timestamp$/i.test(trimmed))
|
|
728
|
+
return 'CURRENT TIMESTAMP';
|
|
729
|
+
// Already a function call / expression (contains parens or is a bind
|
|
730
|
+
// placeholder) - pass through verbatim.
|
|
731
|
+
if (/[(?]/.test(trimmed))
|
|
732
|
+
return trimmed;
|
|
733
|
+
return `TIMESTAMP('${trimmed.replace(/'/g, "''")}')`;
|
|
734
|
+
}
|
|
735
|
+
formatTemporalDate(value) {
|
|
736
|
+
if (value instanceof Date) {
|
|
737
|
+
return `DATE('${value.toISOString().slice(0, 10)}')`;
|
|
738
|
+
}
|
|
739
|
+
const trimmed = value.trim();
|
|
740
|
+
if (/^current[\s_]date$/i.test(trimmed))
|
|
741
|
+
return 'CURRENT DATE';
|
|
742
|
+
if (/[(?]/.test(trimmed))
|
|
743
|
+
return trimmed;
|
|
744
|
+
return `DATE('${trimmed.replace(/'/g, "''")}')`;
|
|
745
|
+
}
|
|
746
|
+
/** Build a `FOR SYSTEM_TIME AS OF ...` clause. */
|
|
747
|
+
buildSystemTimeAsOfClause(asOf) {
|
|
748
|
+
return `FOR SYSTEM_TIME AS OF ${this.formatTemporalTimestamp(asOf)}`;
|
|
749
|
+
}
|
|
750
|
+
/** Build a `FOR SYSTEM_TIME BETWEEN ... AND ...` clause (inclusive of both endpoints). */
|
|
751
|
+
buildSystemTimeBetweenClause(start, end) {
|
|
752
|
+
return `FOR SYSTEM_TIME BETWEEN ${this.formatTemporalTimestamp(start)} AND ${this.formatTemporalTimestamp(end)}`;
|
|
753
|
+
}
|
|
754
|
+
/** Build a `FOR SYSTEM_TIME FROM ... TO ...` clause (end-exclusive). */
|
|
755
|
+
buildSystemTimeFromToClause(start, end) {
|
|
756
|
+
return `FOR SYSTEM_TIME FROM ${this.formatTemporalTimestamp(start)} TO ${this.formatTemporalTimestamp(end)}`;
|
|
757
|
+
}
|
|
758
|
+
/** Build a `FOR BUSINESS_TIME AS OF ...` clause. */
|
|
759
|
+
buildBusinessTimeAsOfClause(asOf) {
|
|
760
|
+
return `FOR BUSINESS_TIME AS OF ${this.formatTemporalDate(asOf)}`;
|
|
761
|
+
}
|
|
762
|
+
/** Build a `FOR BUSINESS_TIME BETWEEN ... AND ...` clause (inclusive). */
|
|
763
|
+
buildBusinessTimeBetweenClause(start, end) {
|
|
764
|
+
return `FOR BUSINESS_TIME BETWEEN ${this.formatTemporalDate(start)} AND ${this.formatTemporalDate(end)}`;
|
|
765
|
+
}
|
|
766
|
+
/** Build a `FOR BUSINESS_TIME FROM ... TO ...` clause (end-exclusive). */
|
|
767
|
+
buildBusinessTimeFromToClause(start, end) {
|
|
768
|
+
return `FOR BUSINESS_TIME FROM ${this.formatTemporalDate(start)} TO ${this.formatTemporalDate(end)}`;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Compose whichever temporal clause(s) are present on an (extended)
|
|
772
|
+
* SelectOptions object into a single string suitable for insertion right
|
|
773
|
+
* after the table reference in a `FROM` clause. Not part of the shared
|
|
774
|
+
* `Dialect` interface's `SelectOptions` type - callers pass the temporal
|
|
775
|
+
* options as extra (untyped) properties, e.g.:
|
|
776
|
+
*
|
|
777
|
+
* dialect.buildSelectQuery({
|
|
778
|
+
* tableName: 'Employees',
|
|
779
|
+
* systemTimeAsOf: new Date('2024-01-01T00:00:00Z'),
|
|
780
|
+
* } as any)
|
|
781
|
+
*/
|
|
782
|
+
buildTemporalClause(options) {
|
|
783
|
+
const clauses = [];
|
|
784
|
+
if (options.systemTimeAsOf !== undefined) {
|
|
785
|
+
clauses.push(this.buildSystemTimeAsOfClause(options.systemTimeAsOf));
|
|
786
|
+
}
|
|
787
|
+
else if (options.systemTimeBetween !== undefined) {
|
|
788
|
+
const [start, end] = options.systemTimeBetween;
|
|
789
|
+
clauses.push(this.buildSystemTimeBetweenClause(start, end));
|
|
790
|
+
}
|
|
791
|
+
else if (options.systemTimeFromTo !== undefined) {
|
|
792
|
+
const [start, end] = options.systemTimeFromTo;
|
|
793
|
+
clauses.push(this.buildSystemTimeFromToClause(start, end));
|
|
794
|
+
}
|
|
795
|
+
if (options.businessTimeAsOf !== undefined) {
|
|
796
|
+
clauses.push(this.buildBusinessTimeAsOfClause(options.businessTimeAsOf));
|
|
797
|
+
}
|
|
798
|
+
else if (options.businessTimeBetween !== undefined) {
|
|
799
|
+
const [start, end] = options.businessTimeBetween;
|
|
800
|
+
clauses.push(this.buildBusinessTimeBetweenClause(start, end));
|
|
801
|
+
}
|
|
802
|
+
else if (options.businessTimeFromTo !== undefined) {
|
|
803
|
+
const [start, end] = options.businessTimeFromTo;
|
|
804
|
+
clauses.push(this.buildSystemTimeFromToClause(start, end));
|
|
805
|
+
}
|
|
806
|
+
return clauses.length > 0 ? ` ${clauses.join(' ')}` : '';
|
|
807
|
+
}
|
|
808
|
+
// ==================== View Operations ====================
|
|
809
|
+
async createView(viewName, query, options) {
|
|
810
|
+
if (options?.replace) {
|
|
811
|
+
await this.query(`DROP VIEW ${this.escapeId(viewName)}`).catch(() => undefined);
|
|
812
|
+
}
|
|
813
|
+
const sql = `CREATE VIEW ${this.escapeId(viewName)} AS ${query}`;
|
|
814
|
+
await this.query(sql);
|
|
815
|
+
}
|
|
816
|
+
async dropView(viewName, options) {
|
|
817
|
+
if (options?.ifExists) {
|
|
818
|
+
const exists = await this.query(`SELECT VIEWNAME FROM SYSCAT.VIEWS WHERE VIEWNAME = '${viewName}'`);
|
|
819
|
+
if (!exists.rows || exists.rows.length === 0) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
await this.query(`DROP VIEW ${this.escapeId(viewName)}`);
|
|
824
|
+
}
|
|
825
|
+
async showViews() {
|
|
826
|
+
const result = await this.query('SELECT VIEWNAME FROM SYSCAT.VIEWS ORDER BY VIEWNAME');
|
|
827
|
+
return result.rows.map((row) => row.VIEWNAME || row.viewname);
|
|
828
|
+
}
|
|
829
|
+
// ==================== Materialized Query Tables (MQTs) ====================
|
|
830
|
+
// Db2's equivalent of materialized views are Materialized Query Tables.
|
|
831
|
+
async createMaterializedView(options) {
|
|
832
|
+
const viewName = options.schema
|
|
833
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.name)}`
|
|
834
|
+
: this.escapeId(options.name);
|
|
835
|
+
if (options.replace) {
|
|
836
|
+
await this.dropMaterializedView(options.name, { ifExists: true, schema: options.schema });
|
|
837
|
+
}
|
|
838
|
+
// Db2 only supports `DATA INITIALLY DEFERRED` at creation time (there is
|
|
839
|
+
// no `DATA INITIALLY IMMEDIATE` for MQTs) - initial population is always
|
|
840
|
+
// done via an explicit `REFRESH TABLE` afterwards, controlled by
|
|
841
|
+
// `withData`. `REFRESH {DEFERRED|IMMEDIATE}` is a completely separate
|
|
842
|
+
// concept: it controls Db2's *ongoing* maintenance mode (whether the MQT
|
|
843
|
+
// is kept in sync automatically on every base-table write), controlled
|
|
844
|
+
// by `refreshMode`.
|
|
845
|
+
const refreshClause = options.refreshMode === 'IMMEDIATE' ? 'IMMEDIATE' : 'DEFERRED';
|
|
846
|
+
const sql = `CREATE TABLE ${viewName} AS (${options.query}) DATA INITIALLY DEFERRED REFRESH ${refreshClause}`;
|
|
847
|
+
await this.query(sql);
|
|
848
|
+
if (options.withData !== false) {
|
|
849
|
+
await this.query(`REFRESH TABLE ${viewName}`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async refreshMaterializedView(viewName, options) {
|
|
853
|
+
const viewNameWithSchema = options?.schema
|
|
854
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(viewName)}`
|
|
855
|
+
: this.escapeId(viewName);
|
|
856
|
+
await this.query(`REFRESH TABLE ${viewNameWithSchema}`);
|
|
857
|
+
}
|
|
858
|
+
async dropMaterializedView(viewName, options) {
|
|
859
|
+
const viewNameWithSchema = options?.schema
|
|
860
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(viewName)}`
|
|
861
|
+
: this.escapeId(viewName);
|
|
862
|
+
if (options?.ifExists) {
|
|
863
|
+
const exists = await this.query(`SELECT TABNAME FROM SYSCAT.TABLES WHERE TABNAME = '${viewName}' AND TYPE = 'S'`);
|
|
864
|
+
if (!exists.rows || exists.rows.length === 0) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
await this.query(`DROP TABLE ${viewNameWithSchema}`);
|
|
869
|
+
}
|
|
870
|
+
async showMaterializedViews() {
|
|
871
|
+
// Db2 MQTs (summary tables) have TYPE = 'S' in SYSCAT.TABLES
|
|
872
|
+
const result = await this.query("SELECT TABNAME FROM SYSCAT.TABLES WHERE TYPE = 'S' ORDER BY TABNAME");
|
|
873
|
+
return result.rows.map((row) => row.TABNAME || row.tabname);
|
|
874
|
+
}
|
|
875
|
+
async hasMaterializedView(viewName, schema) {
|
|
876
|
+
let sql = `SELECT TABNAME FROM SYSCAT.TABLES WHERE TABNAME = '${viewName}' AND TYPE = 'S'`;
|
|
877
|
+
if (schema) {
|
|
878
|
+
sql += ` AND TABSCHEMA = '${schema}'`;
|
|
879
|
+
}
|
|
880
|
+
const result = await this.query(sql);
|
|
881
|
+
return result.rows.length > 0;
|
|
882
|
+
}
|
|
883
|
+
// ==================== Column Operations ====================
|
|
884
|
+
async addColumn(tableName, columnName, definition) {
|
|
885
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD COLUMN ${this.buildColumnDefinition(columnName, definition)}`);
|
|
886
|
+
}
|
|
887
|
+
async removeColumn(tableName, columnName) {
|
|
888
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} DROP COLUMN ${this.escapeId(columnName)}`);
|
|
889
|
+
}
|
|
890
|
+
async changeColumn(tableName, columnName, definition) {
|
|
891
|
+
// Db2 requires separate ALTER COLUMN clauses per attribute changed.
|
|
892
|
+
const clauses = [];
|
|
893
|
+
clauses.push(`ALTER COLUMN ${this.escapeId(columnName)} SET DATA TYPE ${this.getDataTypeSql(definition.type)}`);
|
|
894
|
+
if (definition.allowNull === false) {
|
|
895
|
+
clauses.push(`ALTER COLUMN ${this.escapeId(columnName)} SET NOT NULL`);
|
|
896
|
+
}
|
|
897
|
+
else if (definition.allowNull === true) {
|
|
898
|
+
clauses.push(`ALTER COLUMN ${this.escapeId(columnName)} DROP NOT NULL`);
|
|
899
|
+
}
|
|
900
|
+
if (definition.defaultValue !== undefined) {
|
|
901
|
+
clauses.push(`ALTER COLUMN ${this.escapeId(columnName)} SET DEFAULT ${this.escape(definition.defaultValue)}`);
|
|
902
|
+
}
|
|
903
|
+
for (const clause of clauses) {
|
|
904
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ${clause}`);
|
|
905
|
+
}
|
|
906
|
+
// Db2 requires re-organizing the table after altering column types.
|
|
907
|
+
await this.query(`CALL SYSPROC.ADMIN_CMD('REORG TABLE ${this.quoteTable(tableName)}')`).catch(() => undefined);
|
|
908
|
+
}
|
|
909
|
+
async renameColumn(tableName, oldColumnName, newColumnName) {
|
|
910
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} RENAME COLUMN ${this.escapeId(oldColumnName)} TO ${this.escapeId(newColumnName)}`);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Changes row compression on an existing table via
|
|
914
|
+
* `ALTER TABLE ... COMPRESS YES ADAPTIVE|STATIC|COMPRESS NO`.
|
|
915
|
+
*
|
|
916
|
+
* Per Db2 LUW, altering the COMPRESS attribute only affects rows written
|
|
917
|
+
* after the change; existing rows are only rewritten once the table is
|
|
918
|
+
* reorganized, so a `REORG TABLE` is issued afterwards to apply the new
|
|
919
|
+
* compression setting to existing data (failures are swallowed, mirroring
|
|
920
|
+
* `changeColumn`'s best-effort REORG, since REORG requires elevated
|
|
921
|
+
* privileges that may not be granted in every environment).
|
|
922
|
+
*/
|
|
923
|
+
async alterTableCompression(tableName, mode) {
|
|
924
|
+
const sql = `ALTER TABLE ${this.quoteTable(tableName)}${this.buildCompressClause(mode)}`;
|
|
925
|
+
await this.query(sql);
|
|
926
|
+
await this.query(`CALL SYSPROC.ADMIN_CMD('REORG TABLE ${this.quoteTable(tableName)}')`).catch(() => undefined);
|
|
927
|
+
}
|
|
928
|
+
async addForeignKey(tableName, columnName, referencedTableName, referencedColumnName, options) {
|
|
929
|
+
const constraintName = options?.name
|
|
930
|
+
? this.escapeId(options.name)
|
|
931
|
+
: this.escapeId(`fk_${tableName}_${columnName}`);
|
|
932
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD CONSTRAINT ${constraintName} FOREIGN KEY (${this.escapeId(columnName)}) REFERENCES ${this.quoteTable(referencedTableName)} (${this.escapeId(referencedColumnName)})`;
|
|
933
|
+
if (options?.onDelete) {
|
|
934
|
+
sql += ` ON DELETE ${options.onDelete.toUpperCase()}`;
|
|
935
|
+
}
|
|
936
|
+
if (options?.onUpdate) {
|
|
937
|
+
sql += ` ON UPDATE ${options.onUpdate.toUpperCase()}`;
|
|
938
|
+
}
|
|
939
|
+
await this.query(sql);
|
|
940
|
+
}
|
|
941
|
+
async describeTable(tableName) {
|
|
942
|
+
const result = await this.query(`SELECT COLNAME, TYPENAME, NULLS, "DEFAULT", KEYSEQ, IDENTITY
|
|
943
|
+
FROM SYSCAT.COLUMNS
|
|
944
|
+
WHERE TABNAME = '${tableName}'
|
|
945
|
+
ORDER BY COLNO`);
|
|
946
|
+
const description = {};
|
|
947
|
+
for (const row of result.rows) {
|
|
948
|
+
const colName = (row.COLNAME || row.colname || '').toLowerCase();
|
|
949
|
+
description[colName] = {
|
|
950
|
+
type: row.TYPENAME || row.typename,
|
|
951
|
+
allowNull: (row.NULLS ?? row.nulls) === 'Y',
|
|
952
|
+
defaultValue: row.DEFAULT ?? row.default,
|
|
953
|
+
primaryKey: (row.KEYSEQ ?? row.keyseq) != null && (row.KEYSEQ ?? row.keyseq) > 0,
|
|
954
|
+
autoIncrement: (row.IDENTITY ?? row.identity) === 'Y',
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
return description;
|
|
958
|
+
}
|
|
959
|
+
async renameTable(oldName, newName) {
|
|
960
|
+
await this.query(`RENAME TABLE ${this.quoteTable(oldName)} TO ${this.escapeId(newName)}`);
|
|
961
|
+
}
|
|
962
|
+
async showTables() {
|
|
963
|
+
const result = await this.query("SELECT TABNAME FROM SYSCAT.TABLES WHERE TYPE = 'T' ORDER BY TABNAME");
|
|
964
|
+
return result.rows.map((row) => row.TABNAME || row.tabname);
|
|
965
|
+
}
|
|
966
|
+
async showConstraints(tableName) {
|
|
967
|
+
const sql = `
|
|
968
|
+
SELECT
|
|
969
|
+
CONSTNAME AS name,
|
|
970
|
+
TABNAME AS tableName,
|
|
971
|
+
TYPE AS type
|
|
972
|
+
FROM SYSCAT.TABCONST
|
|
973
|
+
WHERE TABNAME = '${tableName}'
|
|
974
|
+
ORDER BY CONSTNAME
|
|
975
|
+
`;
|
|
976
|
+
const result = await this.query(sql);
|
|
977
|
+
return result.rows;
|
|
978
|
+
}
|
|
979
|
+
async addConstraint(tableName, options) {
|
|
980
|
+
await this.createConstraint(tableName, {
|
|
981
|
+
name: options.name || `${tableName}_${options.type.toLowerCase().replace(/\s+/g, '_')}`,
|
|
982
|
+
type: options.type,
|
|
983
|
+
fields: options.fields,
|
|
984
|
+
references: options.references
|
|
985
|
+
? { table: options.references.table, field: options.references.fields }
|
|
986
|
+
: undefined,
|
|
987
|
+
check: options.check,
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
async removeConstraint(tableName, constraintName) {
|
|
991
|
+
await this.dropConstraint(tableName, constraintName);
|
|
992
|
+
}
|
|
993
|
+
async showIndexes(tableName) {
|
|
994
|
+
const sql = `
|
|
995
|
+
SELECT
|
|
996
|
+
INDNAME AS name,
|
|
997
|
+
TABNAME AS tableName,
|
|
998
|
+
UNIQUERULE AS uniqueRule,
|
|
999
|
+
COLNAMES AS columnNames
|
|
1000
|
+
FROM SYSCAT.INDEXES
|
|
1001
|
+
WHERE TABNAME = '${tableName}'
|
|
1002
|
+
ORDER BY INDNAME
|
|
1003
|
+
`;
|
|
1004
|
+
const result = await this.query(sql);
|
|
1005
|
+
return result.rows;
|
|
1006
|
+
}
|
|
1007
|
+
// ==================== Index Operations ====================
|
|
1008
|
+
buildCreateIndexSql(tableName, indexName, fields, options) {
|
|
1009
|
+
const fieldsSql = (fields || []).map((f) => this.escapeId(f)).join(', ');
|
|
1010
|
+
let sql = 'CREATE';
|
|
1011
|
+
if (options?.unique) {
|
|
1012
|
+
sql += ' UNIQUE';
|
|
1013
|
+
}
|
|
1014
|
+
sql += ` INDEX ${this.escapeId(indexName)} ON ${this.quoteTable(tableName)} (${fieldsSql})`;
|
|
1015
|
+
if (options?.include && options.include.length > 0) {
|
|
1016
|
+
sql += ` INCLUDE (${options.include.map((f) => this.escapeId(f)).join(', ')})`;
|
|
1017
|
+
}
|
|
1018
|
+
return sql;
|
|
1019
|
+
}
|
|
1020
|
+
async addIndex(tableName, indexName, fields, options) {
|
|
1021
|
+
await this.query(this.buildCreateIndexSql(tableName, indexName, fields, options));
|
|
1022
|
+
}
|
|
1023
|
+
async removeIndex(tableName, indexName) {
|
|
1024
|
+
await this.query(`DROP INDEX ${this.escapeId(indexName)}`);
|
|
1025
|
+
}
|
|
1026
|
+
async createIndex(tableName, indexDef) {
|
|
1027
|
+
await this.addIndex(tableName, indexDef.name, indexDef.fields, {
|
|
1028
|
+
unique: indexDef.unique,
|
|
1029
|
+
type: indexDef.type,
|
|
1030
|
+
using: indexDef.using,
|
|
1031
|
+
where: indexDef.where,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
async dropIndex(tableName, indexName, _options) {
|
|
1035
|
+
await this.removeIndex(tableName, indexName);
|
|
1036
|
+
}
|
|
1037
|
+
async createFulltextIndex() {
|
|
1038
|
+
throw new Error('Fulltext indexes are not supported in Db2 LUW (requires Net Search Extender)');
|
|
1039
|
+
}
|
|
1040
|
+
async createSpatialIndex() {
|
|
1041
|
+
throw new Error('Spatial indexes are not supported in Db2 LUW (requires Db2 Spatial Extender)');
|
|
1042
|
+
}
|
|
1043
|
+
// ==================== Constraint Operations ====================
|
|
1044
|
+
async createConstraint(tableName, constraintDef) {
|
|
1045
|
+
const constraintSql = this.buildConstraintSql({
|
|
1046
|
+
name: constraintDef.name,
|
|
1047
|
+
type: constraintDef.type,
|
|
1048
|
+
fields: constraintDef.fields,
|
|
1049
|
+
references: constraintDef.references
|
|
1050
|
+
? {
|
|
1051
|
+
table: constraintDef.references.table,
|
|
1052
|
+
field: constraintDef.references.field,
|
|
1053
|
+
onDelete: constraintDef.references.onDelete,
|
|
1054
|
+
onUpdate: constraintDef.references.onUpdate,
|
|
1055
|
+
}
|
|
1056
|
+
: undefined,
|
|
1057
|
+
check: constraintDef.check,
|
|
1058
|
+
});
|
|
1059
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD ${constraintSql}`);
|
|
1060
|
+
}
|
|
1061
|
+
async dropConstraint(tableName, constraintName, _options) {
|
|
1062
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.escapeId(constraintName)}`);
|
|
1063
|
+
}
|
|
1064
|
+
async changeOwner() {
|
|
1065
|
+
throw new Error('changeOwner is not supported in Db2 (use TRANSFER OWNERSHIP instead)');
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Transfer ownership of a table (Db2's equivalent of changeOwner)
|
|
1069
|
+
*/
|
|
1070
|
+
async transferOwnership(tableName, newOwner) {
|
|
1071
|
+
await this.query(`TRANSFER OWNERSHIP OF TABLE ${this.quoteTable(tableName)} TO USER ${this.escapeId(newOwner)} PRESERVE PRIVILEGES`);
|
|
1072
|
+
}
|
|
1073
|
+
// ==================== Federation (Db2's Foreign Data Wrapper equivalent) ====================
|
|
1074
|
+
// Db2 for LUW genuinely supports federated access to remote/external data
|
|
1075
|
+
// sources - it is *not* correct to say Db2 has no FDW-equivalent (that was
|
|
1076
|
+
// only true of the generic Postgres-shaped API surface, not the
|
|
1077
|
+
// underlying capability). Db2's federation feature is built from four
|
|
1078
|
+
// real DDL statements:
|
|
1079
|
+
// - `CREATE WRAPPER` - registers a data-source driver/library
|
|
1080
|
+
// (Db2's analogue of a Postgres FDW handler).
|
|
1081
|
+
// - `CREATE SERVER` - registers a specific remote data source
|
|
1082
|
+
// instance that uses a given wrapper.
|
|
1083
|
+
// - `CREATE USER MAPPING` - maps a local authorization ID to remote
|
|
1084
|
+
// credentials for a given server.
|
|
1085
|
+
// - `CREATE NICKNAME` - Db2's analogue of a "foreign table": a
|
|
1086
|
+
// local object that transparently maps onto
|
|
1087
|
+
// a remote table/view reachable through a
|
|
1088
|
+
// server.
|
|
1089
|
+
//
|
|
1090
|
+
// The generic FDW-shaped interface (designed around Postgres) is mapped
|
|
1091
|
+
// onto this real Db2 DDL below. A few notes on where the mapping isn't
|
|
1092
|
+
// 1:1:
|
|
1093
|
+
// - Db2's `CREATE SERVER`/`CREATE WRAPPER`/`CREATE NICKNAME` have no
|
|
1094
|
+
// `IF NOT EXISTS` clause (Db2 doesn't support it there), so
|
|
1095
|
+
// `ifNotExists`/`ifExists` options are honored by pre-checking the
|
|
1096
|
+
// relevant `SYSCAT` catalog view and short-circuiting, matching the
|
|
1097
|
+
// pattern already used elsewhere in this file (see `dropSequence`,
|
|
1098
|
+
// `dropView`, etc.) rather than by emitting nonexistent syntax.
|
|
1099
|
+
// - `opts.options` on `createForeignServer`/`buildCreateServerQuery` may
|
|
1100
|
+
// additionally carry the reserved keys `TYPE`/`type` and
|
|
1101
|
+
// `AUTHORIZATION`/`authorization` + `PASSWORD`/`password`, which are
|
|
1102
|
+
// pulled out into their own clauses (`TYPE ...`, `AUTHORIZATION ...
|
|
1103
|
+
// PASSWORD ...`) since Db2's `CREATE SERVER` models them as first-class
|
|
1104
|
+
// syntax rather than opaque options.
|
|
1105
|
+
// - `FdwForeignTableOptions.columns` (local column name -> type/options)
|
|
1106
|
+
// doesn't map onto `CREATE NICKNAME` directly, since Db2 nicknames
|
|
1107
|
+
// auto-discover their columns from the remote object. Instead, each
|
|
1108
|
+
// supplied column is applied as a real `ALTER NICKNAME ... ALTER
|
|
1109
|
+
// COLUMN ... LOCAL TYPE ...` statement after creation, which is Db2's
|
|
1110
|
+
// genuine mechanism for overriding a nickname column's local type.
|
|
1111
|
+
// - Db2 has no bulk `IMPORT FOREIGN SCHEMA` equivalent - nicknames are
|
|
1112
|
+
// always created one at a time - so `buildImportForeignSchemaQuery`
|
|
1113
|
+
// throws with a specific, accurate explanation (as opposed to a
|
|
1114
|
+
// blanket "not supported" for federation as a whole).
|
|
1115
|
+
async createForeignDataWrapper(fdwName, options) {
|
|
1116
|
+
let sql = `CREATE WRAPPER ${this.escapeId(fdwName)}`;
|
|
1117
|
+
if (options?.handler) {
|
|
1118
|
+
sql += ` LIBRARY '${options.handler.replace(/'/g, "''")}'`;
|
|
1119
|
+
}
|
|
1120
|
+
await this.query(sql);
|
|
1121
|
+
}
|
|
1122
|
+
async dropForeignDataWrapper(fdwName, options) {
|
|
1123
|
+
if (options?.ifExists) {
|
|
1124
|
+
const exists = await this.query(`SELECT WRAPNAME FROM SYSCAT.WRAPPERS WHERE WRAPNAME = '${fdwName}'`);
|
|
1125
|
+
if (!exists.rows || exists.rows.length === 0)
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
await this.query(`DROP WRAPPER ${this.escapeId(fdwName)}`);
|
|
1129
|
+
}
|
|
1130
|
+
async createForeignServer(serverName, fdwName, options) {
|
|
1131
|
+
if (options?.ifNotExists) {
|
|
1132
|
+
const exists = await this.query(`SELECT SERVERNAME FROM SYSCAT.SERVERS WHERE SERVERNAME = '${serverName}'`);
|
|
1133
|
+
if (exists.rows && exists.rows.length > 0)
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const sql = this.buildCreateServerQuery(serverName, { fdw: fdwName, options: options?.options });
|
|
1137
|
+
await this.query(sql);
|
|
1138
|
+
}
|
|
1139
|
+
async dropForeignServer(serverName, options) {
|
|
1140
|
+
if (options?.ifExists) {
|
|
1141
|
+
const exists = await this.query(`SELECT SERVERNAME FROM SYSCAT.SERVERS WHERE SERVERNAME = '${serverName}'`);
|
|
1142
|
+
if (!exists.rows || exists.rows.length === 0)
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
await this.query(this.buildDropServerQuery(serverName));
|
|
1146
|
+
}
|
|
1147
|
+
async createForeignTable(tableName, columns, options) {
|
|
1148
|
+
if (options?.ifNotExists) {
|
|
1149
|
+
const exists = await this.query(`SELECT TABNAME FROM SYSCAT.NICKNAMES WHERE TABNAME = '${tableName}'`);
|
|
1150
|
+
if (exists.rows && exists.rows.length > 0)
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const serverName = options?.serverName || 'DEFAULT_SERVER';
|
|
1154
|
+
const fdwColumns = {};
|
|
1155
|
+
for (const [colName, colDef] of Object.entries(columns)) {
|
|
1156
|
+
fdwColumns[colName] = { type: colDef.type };
|
|
1157
|
+
}
|
|
1158
|
+
const sql = this.buildCreateForeignTableQuery(tableName, {
|
|
1159
|
+
serverName,
|
|
1160
|
+
columns: fdwColumns,
|
|
1161
|
+
});
|
|
1162
|
+
await this.query(sql);
|
|
1163
|
+
// Db2 nicknames auto-discover their remote columns; apply each supplied
|
|
1164
|
+
// column's type as a real `LOCAL TYPE` override via `ALTER NICKNAME`.
|
|
1165
|
+
for (const [colName, colDef] of Object.entries(columns)) {
|
|
1166
|
+
await this.query(`ALTER NICKNAME ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(colName)} LOCAL TYPE ${colDef.type}`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
async createUserMapping(userName, serverName, options) {
|
|
1170
|
+
const mappingOptions = { ...(options?.options || {}) };
|
|
1171
|
+
if (options?.username)
|
|
1172
|
+
mappingOptions.REMOTE_AUTHID = options.username;
|
|
1173
|
+
if (options?.password)
|
|
1174
|
+
mappingOptions.REMOTE_PASSWORD = options.password;
|
|
1175
|
+
const sql = this.buildCreateUserMappingQuery({
|
|
1176
|
+
serverName,
|
|
1177
|
+
user: userName,
|
|
1178
|
+
options: mappingOptions,
|
|
1179
|
+
});
|
|
1180
|
+
await this.query(sql);
|
|
1181
|
+
}
|
|
1182
|
+
async dropUserMapping(userName, serverName, options) {
|
|
1183
|
+
if (options?.ifExists) {
|
|
1184
|
+
const exists = await this.query(`SELECT AUTHID FROM SYSCAT.USEROPTIONS WHERE SERVERNAME = '${serverName}' AND AUTHID = '${userName}'`);
|
|
1185
|
+
if (!exists.rows || exists.rows.length === 0)
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
await this.query(this.buildDropUserMappingQuery(serverName, userName));
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Build `CREATE SERVER` SQL.
|
|
1192
|
+
*
|
|
1193
|
+
* Db2 federation syntax:
|
|
1194
|
+
* CREATE SERVER server-name
|
|
1195
|
+
* [TYPE server-type] [VERSION 'server-version']
|
|
1196
|
+
* WRAPPER wrapper-name
|
|
1197
|
+
* [AUTHORIZATION auth-id PASSWORD password]
|
|
1198
|
+
* [OPTIONS (option-name 'value', ...)]
|
|
1199
|
+
*
|
|
1200
|
+
* `opts.options` may carry the reserved keys `TYPE`/`AUTHORIZATION`/
|
|
1201
|
+
* `PASSWORD` (case-insensitive) which are extracted into their dedicated
|
|
1202
|
+
* clauses rather than passed through the generic `OPTIONS(...)` list -
|
|
1203
|
+
* see the section header comment above for why.
|
|
1204
|
+
*/
|
|
1205
|
+
buildCreateServerQuery(name, opts) {
|
|
1206
|
+
const rest = { ...(opts.options || {}) };
|
|
1207
|
+
const takeReserved = (...keys) => {
|
|
1208
|
+
for (const key of keys) {
|
|
1209
|
+
if (rest[key] !== undefined) {
|
|
1210
|
+
const value = rest[key];
|
|
1211
|
+
delete rest[key];
|
|
1212
|
+
return value;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return undefined;
|
|
1216
|
+
};
|
|
1217
|
+
const type = takeReserved('TYPE', 'type');
|
|
1218
|
+
const authorization = takeReserved('AUTHORIZATION', 'authorization');
|
|
1219
|
+
const password = takeReserved('PASSWORD', 'password');
|
|
1220
|
+
let sql = `CREATE SERVER ${this.escapeId(name)}`;
|
|
1221
|
+
if (type)
|
|
1222
|
+
sql += ` TYPE ${type}`;
|
|
1223
|
+
if (opts.version)
|
|
1224
|
+
sql += ` VERSION '${opts.version.replace(/'/g, "''")}'`;
|
|
1225
|
+
sql += ` WRAPPER ${this.escapeId(opts.fdw)}`;
|
|
1226
|
+
if (authorization) {
|
|
1227
|
+
sql += ` AUTHORIZATION ${this.escapeId(authorization)}`;
|
|
1228
|
+
if (password)
|
|
1229
|
+
sql += ` PASSWORD ${this.escapeId(password)}`;
|
|
1230
|
+
}
|
|
1231
|
+
if (Object.keys(rest).length > 0) {
|
|
1232
|
+
sql += ` OPTIONS (${Object.entries(rest)
|
|
1233
|
+
.map(([k, v]) => `${k} '${String(v).replace(/'/g, "''")}'`)
|
|
1234
|
+
.join(', ')})`;
|
|
1235
|
+
}
|
|
1236
|
+
return sql;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Build `ALTER SERVER` SQL.
|
|
1240
|
+
*
|
|
1241
|
+
* ALTER SERVER server-name
|
|
1242
|
+
* [VERSION 'server-version']
|
|
1243
|
+
* [OPTIONS ({ADD|SET|DROP} option-name ['value'], ...)]
|
|
1244
|
+
*/
|
|
1245
|
+
buildAlterServerQuery(name, opts) {
|
|
1246
|
+
const parts = [];
|
|
1247
|
+
if (opts.version) {
|
|
1248
|
+
parts.push(`VERSION '${opts.version.replace(/'/g, "''")}'`);
|
|
1249
|
+
}
|
|
1250
|
+
const optParts = [];
|
|
1251
|
+
if (opts.addOptions) {
|
|
1252
|
+
for (const [k, v] of Object.entries(opts.addOptions)) {
|
|
1253
|
+
optParts.push(`ADD ${k} '${v.replace(/'/g, "''")}'`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
if (opts.setOptions) {
|
|
1257
|
+
for (const [k, v] of Object.entries(opts.setOptions)) {
|
|
1258
|
+
optParts.push(`SET ${k} '${v.replace(/'/g, "''")}'`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (opts.dropOptions) {
|
|
1262
|
+
for (const k of opts.dropOptions) {
|
|
1263
|
+
optParts.push(`DROP ${k}`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
if (opts.options) {
|
|
1267
|
+
for (const [k, v] of Object.entries(opts.options)) {
|
|
1268
|
+
optParts.push(`SET ${k} '${v.replace(/'/g, "''")}'`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (optParts.length > 0) {
|
|
1272
|
+
parts.push(`OPTIONS (${optParts.join(', ')})`);
|
|
1273
|
+
}
|
|
1274
|
+
if (parts.length === 0) {
|
|
1275
|
+
throw new Error(`ALTER SERVER requires at least one change for server "${name}"`);
|
|
1276
|
+
}
|
|
1277
|
+
return `ALTER SERVER ${this.escapeId(name)} ${parts.join(' ')}`;
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Build `DROP SERVER` SQL. Db2's `DROP SERVER` has no `CASCADE`/`RESTRICT`
|
|
1281
|
+
* clause - any nicknames referencing the server must be dropped first -
|
|
1282
|
+
* so the `cascade` option (present only for interface compatibility with
|
|
1283
|
+
* other dialects) has no SQL-level effect here.
|
|
1284
|
+
*/
|
|
1285
|
+
buildDropServerQuery(name, _opts) {
|
|
1286
|
+
return `DROP SERVER ${this.escapeId(name)}`;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Build `CREATE USER MAPPING` SQL.
|
|
1290
|
+
*
|
|
1291
|
+
* CREATE USER MAPPING FOR { auth-id | USER | PUBLIC }
|
|
1292
|
+
* SERVER server-name
|
|
1293
|
+
* [OPTIONS (option-name 'value', ...)]
|
|
1294
|
+
*/
|
|
1295
|
+
buildCreateUserMappingQuery(opts) {
|
|
1296
|
+
const userToken = this.formatMappingUserToken(opts.user);
|
|
1297
|
+
const optClause = opts.options && Object.keys(opts.options).length > 0
|
|
1298
|
+
? ` OPTIONS (${Object.entries(opts.options)
|
|
1299
|
+
.map(([k, v]) => `${k} '${String(v).replace(/'/g, "''")}'`)
|
|
1300
|
+
.join(', ')})`
|
|
1301
|
+
: '';
|
|
1302
|
+
return `CREATE USER MAPPING FOR ${userToken} SERVER ${this.escapeId(opts.serverName)}${optClause}`;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Build `ALTER USER MAPPING` SQL.
|
|
1306
|
+
*
|
|
1307
|
+
* ALTER USER MAPPING FOR { auth-id | USER | PUBLIC }
|
|
1308
|
+
* SERVER server-name
|
|
1309
|
+
* OPTIONS ({ADD|SET|DROP} option-name ['value'], ...)
|
|
1310
|
+
*/
|
|
1311
|
+
buildAlterUserMappingQuery(opts) {
|
|
1312
|
+
const userToken = this.formatMappingUserToken(opts.user);
|
|
1313
|
+
const optClause = opts.options && Object.keys(opts.options).length > 0
|
|
1314
|
+
? ` OPTIONS (${Object.entries(opts.options)
|
|
1315
|
+
.map(([k, v]) => `SET ${k} '${String(v).replace(/'/g, "''")}'`)
|
|
1316
|
+
.join(', ')})`
|
|
1317
|
+
: '';
|
|
1318
|
+
return `ALTER USER MAPPING FOR ${userToken} SERVER ${this.escapeId(opts.serverName)}${optClause}`;
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Build `DROP USER MAPPING` SQL.
|
|
1322
|
+
*
|
|
1323
|
+
* DROP USER MAPPING FOR { auth-id | USER | PUBLIC } SERVER server-name
|
|
1324
|
+
*/
|
|
1325
|
+
buildDropUserMappingQuery(serverName, user, _opts) {
|
|
1326
|
+
const userToken = this.formatMappingUserToken(user);
|
|
1327
|
+
return `DROP USER MAPPING FOR ${userToken} SERVER ${this.escapeId(serverName)}`;
|
|
1328
|
+
}
|
|
1329
|
+
formatMappingUserToken(user) {
|
|
1330
|
+
const upper = user.toUpperCase();
|
|
1331
|
+
if (upper === 'PUBLIC')
|
|
1332
|
+
return 'PUBLIC';
|
|
1333
|
+
// Db2's federation grammar uses the bare keyword `USER` to mean "the
|
|
1334
|
+
// current authorization ID" (Postgres' `CURRENT_USER` is accepted here
|
|
1335
|
+
// too, as a convenience alias for callers porting from Postgres).
|
|
1336
|
+
if (upper === 'USER' || upper === 'CURRENT_USER')
|
|
1337
|
+
return 'USER';
|
|
1338
|
+
return this.escapeId(user);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Build `CREATE NICKNAME` SQL - Db2's analogue of `CREATE FOREIGN TABLE`.
|
|
1342
|
+
*
|
|
1343
|
+
* CREATE NICKNAME [schema.]nickname
|
|
1344
|
+
* FOR server-name.[remote-schema.]remote-table
|
|
1345
|
+
* [OPTIONS (option-name 'value', ...)]
|
|
1346
|
+
*
|
|
1347
|
+
* Db2 nicknames auto-discover their columns from the remote object rather
|
|
1348
|
+
* than declaring them inline, so `opts.columns` isn't rendered here -
|
|
1349
|
+
* `createForeignTable()` applies each supplied column as a follow-up
|
|
1350
|
+
* `ALTER NICKNAME ... ALTER COLUMN ... LOCAL TYPE ...` statement instead.
|
|
1351
|
+
* `opts.options` may carry the reserved keys `remoteSchema`/`remoteTable`
|
|
1352
|
+
* (case-insensitive) to identify the remote object; if omitted, the local
|
|
1353
|
+
* nickname name is used as the remote table name with no remote schema.
|
|
1354
|
+
*/
|
|
1355
|
+
buildCreateForeignTableQuery(tableName, opts) {
|
|
1356
|
+
const localName = opts.schema
|
|
1357
|
+
? `${this.escapeId(opts.schema)}.${this.escapeId(tableName)}`
|
|
1358
|
+
: this.escapeId(tableName);
|
|
1359
|
+
const rest = { ...(opts.options || {}) };
|
|
1360
|
+
const takeReserved = (...keys) => {
|
|
1361
|
+
for (const key of keys) {
|
|
1362
|
+
if (rest[key] !== undefined) {
|
|
1363
|
+
const value = rest[key];
|
|
1364
|
+
delete rest[key];
|
|
1365
|
+
return value;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return undefined;
|
|
1369
|
+
};
|
|
1370
|
+
const remoteSchema = takeReserved('remoteSchema', 'REMOTE_SCHEMA');
|
|
1371
|
+
const remoteTable = takeReserved('remoteTable', 'REMOTE_TABLE') || tableName;
|
|
1372
|
+
const remoteObject = remoteSchema
|
|
1373
|
+
? `${this.escapeId(remoteSchema)}.${this.escapeId(remoteTable)}`
|
|
1374
|
+
: this.escapeId(remoteTable);
|
|
1375
|
+
let sql = `CREATE NICKNAME ${localName} FOR ${this.escapeId(opts.serverName)}.${remoteObject}`;
|
|
1376
|
+
if (Object.keys(rest).length > 0) {
|
|
1377
|
+
sql += ` OPTIONS (${Object.entries(rest)
|
|
1378
|
+
.map(([k, v]) => `${k} '${String(v).replace(/'/g, "''")}'`)
|
|
1379
|
+
.join(', ')})`;
|
|
1380
|
+
}
|
|
1381
|
+
return sql;
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Build `DROP NICKNAME` SQL. Db2's `DROP NICKNAME` has no
|
|
1385
|
+
* `CASCADE`/`RESTRICT` clause, so the `cascade` option (present only for
|
|
1386
|
+
* interface compatibility) has no SQL-level effect here.
|
|
1387
|
+
*/
|
|
1388
|
+
buildDropForeignTableQuery(tableName, _opts) {
|
|
1389
|
+
return `DROP NICKNAME ${this.escapeId(tableName)}`;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Db2 has no bulk "discover and create nicknames for every table in a
|
|
1393
|
+
* remote schema" statement analogous to Postgres' `IMPORT FOREIGN
|
|
1394
|
+
* SCHEMA` - nicknames must be created one at a time via `CREATE NICKNAME`
|
|
1395
|
+
* (see `createForeignTable`/`buildCreateForeignTableQuery`). This throws
|
|
1396
|
+
* with that specific explanation rather than a blanket "federation isn't
|
|
1397
|
+
* supported" message.
|
|
1398
|
+
*/
|
|
1399
|
+
buildImportForeignSchemaQuery(_remoteSchema, _serverName, _opts) {
|
|
1400
|
+
throw new Error('Db2 has no bulk IMPORT FOREIGN SCHEMA equivalent - create nicknames individually via createForeignTable()/buildCreateForeignTableQuery() instead.');
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Return SQL to list all registered federation servers from the Db2
|
|
1404
|
+
* catalog.
|
|
1405
|
+
*/
|
|
1406
|
+
getServersQuery() {
|
|
1407
|
+
return 'SELECT SERVERNAME, SERVERTYPE, SERVERVERSION, WRAPNAME FROM SYSCAT.SERVERS ORDER BY SERVERNAME';
|
|
1408
|
+
}
|
|
1409
|
+
// ==================== Row/Column Access Control ====================
|
|
1410
|
+
// Db2's equivalent of MSSQL security policies is Row and Column Access
|
|
1411
|
+
// Control (RCAC) - a table-level PERMISSION with an enable/disable step.
|
|
1412
|
+
async createSecurityPolicy(policyName, tableName, options) {
|
|
1413
|
+
const predicate = options?.predicate || '1=1';
|
|
1414
|
+
await this.query(`CREATE PERMISSION ${this.escapeId(policyName)} ON ${this.quoteTable(tableName)} FOR ROWS WHERE (${predicate}) ENFORCED FOR ALL ACCESS ENABLE`);
|
|
1415
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ACTIVATE ROW ACCESS CONTROL`);
|
|
1416
|
+
}
|
|
1417
|
+
async dropSecurityPolicy(policyName, _tableName) {
|
|
1418
|
+
await this.query(`DROP PERMISSION ${this.escapeId(policyName)}`);
|
|
1419
|
+
}
|
|
1420
|
+
// ==================== Transaction Operations ====================
|
|
1421
|
+
async startTransaction(options) {
|
|
1422
|
+
if (!this.pool) {
|
|
1423
|
+
throw new Error('Not connected to database. Call connect() first.');
|
|
1424
|
+
}
|
|
1425
|
+
const conn = await this.pool.open(this.connectionString);
|
|
1426
|
+
if (options?.isolationLevel) {
|
|
1427
|
+
// Accept both ANSI isolation level names and Db2's native
|
|
1428
|
+
// abbreviations (UR/CS/RS/RR), which map roughly onto the ODBC
|
|
1429
|
+
// SQL_TXN_* bitmask values used by `setIsolationLevel`.
|
|
1430
|
+
const levelMap = {
|
|
1431
|
+
'READ UNCOMMITTED': 1,
|
|
1432
|
+
UR: 1,
|
|
1433
|
+
'READ COMMITTED': 2,
|
|
1434
|
+
CS: 2,
|
|
1435
|
+
'REPEATABLE READ': 4,
|
|
1436
|
+
RS: 4,
|
|
1437
|
+
SERIALIZABLE: 8,
|
|
1438
|
+
RR: 8,
|
|
1439
|
+
};
|
|
1440
|
+
const key = options.isolationLevel.toUpperCase();
|
|
1441
|
+
const level = levelMap[key];
|
|
1442
|
+
if (level === undefined) {
|
|
1443
|
+
throw new Error(`Unsupported isolation level "${options.isolationLevel}". Expected one of: ` +
|
|
1444
|
+
"'READ UNCOMMITTED'/'UR', 'READ COMMITTED'/'CS', 'REPEATABLE READ'/'RS', 'SERIALIZABLE'/'RR'.");
|
|
1445
|
+
}
|
|
1446
|
+
conn.setIsolationLevel?.(level);
|
|
1447
|
+
}
|
|
1448
|
+
await conn.beginTransaction();
|
|
1449
|
+
return new DB2Transaction(conn, options);
|
|
1450
|
+
}
|
|
1451
|
+
async commitTransaction(transaction) {
|
|
1452
|
+
const tx = transaction;
|
|
1453
|
+
if (tx.connection) {
|
|
1454
|
+
await tx.connection.commitTransaction();
|
|
1455
|
+
await tx.connection.close();
|
|
1456
|
+
tx.finished = true;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
async rollbackTransaction(transaction) {
|
|
1460
|
+
const tx = transaction;
|
|
1461
|
+
if (tx.connection) {
|
|
1462
|
+
await tx.connection.rollbackTransaction();
|
|
1463
|
+
await tx.connection.close();
|
|
1464
|
+
tx.finished = true;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
// ==================== Data Type Mapping ====================
|
|
1468
|
+
getDataTypeSql(dataType) {
|
|
1469
|
+
if (typeof dataType === 'string') {
|
|
1470
|
+
return this.mapDataType(dataType, undefined);
|
|
1471
|
+
}
|
|
1472
|
+
const dt = dataType;
|
|
1473
|
+
return this.mapDataType(String(dt.key ?? dataType), dt);
|
|
1474
|
+
}
|
|
1475
|
+
mapDataType(typeKey, dt) {
|
|
1476
|
+
switch (typeKey.toUpperCase()) {
|
|
1477
|
+
case 'STRING':
|
|
1478
|
+
if (dt?.length === 'max')
|
|
1479
|
+
return 'CLOB';
|
|
1480
|
+
return `VARCHAR(${String(dt?.length || 255)})`;
|
|
1481
|
+
case 'CHAR':
|
|
1482
|
+
return `CHAR(${String(dt?.length || 1)})`;
|
|
1483
|
+
case 'TEXT':
|
|
1484
|
+
return 'CLOB';
|
|
1485
|
+
case 'TINYTEXT':
|
|
1486
|
+
return 'VARCHAR(255)';
|
|
1487
|
+
case 'MEDIUMTEXT':
|
|
1488
|
+
case 'LONGTEXT':
|
|
1489
|
+
return 'CLOB';
|
|
1490
|
+
case 'INTEGER': {
|
|
1491
|
+
if (dt?.length === 1)
|
|
1492
|
+
return 'SMALLINT';
|
|
1493
|
+
if (dt?.length === 2)
|
|
1494
|
+
return 'SMALLINT';
|
|
1495
|
+
if (dt?.length === 8)
|
|
1496
|
+
return 'BIGINT';
|
|
1497
|
+
return 'INTEGER';
|
|
1498
|
+
}
|
|
1499
|
+
case 'BIGINT':
|
|
1500
|
+
return 'BIGINT';
|
|
1501
|
+
case 'FLOAT':
|
|
1502
|
+
return 'REAL';
|
|
1503
|
+
case 'DOUBLE':
|
|
1504
|
+
return 'DOUBLE';
|
|
1505
|
+
case 'DECIMAL':
|
|
1506
|
+
case 'NUMERIC':
|
|
1507
|
+
return `DECIMAL(${String(dt?.precision || 10)},${String(dt?.scale || 0)})`;
|
|
1508
|
+
case 'BOOLEAN':
|
|
1509
|
+
return 'BOOLEAN';
|
|
1510
|
+
case 'DATE':
|
|
1511
|
+
case 'DATETIME':
|
|
1512
|
+
case 'TIMESTAMP':
|
|
1513
|
+
return dt?.precision ? `TIMESTAMP(${String(dt.precision)})` : 'TIMESTAMP';
|
|
1514
|
+
case 'DATEONLY':
|
|
1515
|
+
return 'DATE';
|
|
1516
|
+
case 'TIME':
|
|
1517
|
+
return 'TIME';
|
|
1518
|
+
case 'BLOB':
|
|
1519
|
+
return 'BLOB';
|
|
1520
|
+
case 'BINARY':
|
|
1521
|
+
return `BINARY(${String(dt?.length || 255)})`;
|
|
1522
|
+
case 'VARBINARY':
|
|
1523
|
+
return `VARBINARY(${String(dt?.length || 2000)})`;
|
|
1524
|
+
case 'BIT':
|
|
1525
|
+
return 'VARBINARY(1)';
|
|
1526
|
+
case 'JSON':
|
|
1527
|
+
return 'CLOB';
|
|
1528
|
+
case 'JSONB':
|
|
1529
|
+
return 'BLOB';
|
|
1530
|
+
case 'UUID':
|
|
1531
|
+
return 'CHAR(36)';
|
|
1532
|
+
case 'GEOMETRY':
|
|
1533
|
+
return 'DB2GSE.ST_GEOMETRY';
|
|
1534
|
+
case 'ENUM':
|
|
1535
|
+
return 'VARCHAR(255)';
|
|
1536
|
+
case 'VIRTUAL':
|
|
1537
|
+
return '';
|
|
1538
|
+
default:
|
|
1539
|
+
return 'VARCHAR(255)';
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
// ==================== Query Builders ====================
|
|
1543
|
+
buildWhereClause(where, _options) {
|
|
1544
|
+
const values = [];
|
|
1545
|
+
if (!where || Object.keys(where).length === 0) {
|
|
1546
|
+
return { sql: '', values };
|
|
1547
|
+
}
|
|
1548
|
+
const buildCondition = (condition) => {
|
|
1549
|
+
if (!condition || typeof condition !== 'object') {
|
|
1550
|
+
values.push(condition);
|
|
1551
|
+
return '?';
|
|
1552
|
+
}
|
|
1553
|
+
const cond = condition;
|
|
1554
|
+
if (cond.$and || cond.$or || cond.$not) {
|
|
1555
|
+
const conditions = [];
|
|
1556
|
+
if (cond.$and) {
|
|
1557
|
+
conditions.push(`(${cond.$and.map((c) => buildCondition(c)).join(' AND ')})`);
|
|
1558
|
+
}
|
|
1559
|
+
if (cond.$or) {
|
|
1560
|
+
conditions.push(`(${cond.$or.map((c) => buildCondition(c)).join(' OR ')})`);
|
|
1561
|
+
}
|
|
1562
|
+
if (cond.$not) {
|
|
1563
|
+
conditions.push(`NOT (${buildCondition(cond.$not)})`);
|
|
1564
|
+
}
|
|
1565
|
+
return conditions.join(' AND ');
|
|
1566
|
+
}
|
|
1567
|
+
const fieldConditions = [];
|
|
1568
|
+
for (const [key, value] of Object.entries(cond)) {
|
|
1569
|
+
if (key.startsWith('$'))
|
|
1570
|
+
continue;
|
|
1571
|
+
if (value && typeof value === 'object' && !(value instanceof Date) && !Array.isArray(value)) {
|
|
1572
|
+
const valueObj = value;
|
|
1573
|
+
if (valueObj.$eq !== undefined) {
|
|
1574
|
+
values.push(valueObj.$eq);
|
|
1575
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
1576
|
+
}
|
|
1577
|
+
else if (valueObj.$ne !== undefined) {
|
|
1578
|
+
values.push(valueObj.$ne);
|
|
1579
|
+
fieldConditions.push(`${this.escapeId(key)} != ?`);
|
|
1580
|
+
}
|
|
1581
|
+
else if (valueObj.$gt !== undefined) {
|
|
1582
|
+
values.push(valueObj.$gt);
|
|
1583
|
+
fieldConditions.push(`${this.escapeId(key)} > ?`);
|
|
1584
|
+
}
|
|
1585
|
+
else if (valueObj.$gte !== undefined) {
|
|
1586
|
+
values.push(valueObj.$gte);
|
|
1587
|
+
fieldConditions.push(`${this.escapeId(key)} >= ?`);
|
|
1588
|
+
}
|
|
1589
|
+
else if (valueObj.$lt !== undefined) {
|
|
1590
|
+
values.push(valueObj.$lt);
|
|
1591
|
+
fieldConditions.push(`${this.escapeId(key)} < ?`);
|
|
1592
|
+
}
|
|
1593
|
+
else if (valueObj.$lte !== undefined) {
|
|
1594
|
+
values.push(valueObj.$lte);
|
|
1595
|
+
fieldConditions.push(`${this.escapeId(key)} <= ?`);
|
|
1596
|
+
}
|
|
1597
|
+
else if (valueObj.$like !== undefined) {
|
|
1598
|
+
values.push(valueObj.$like);
|
|
1599
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
1600
|
+
}
|
|
1601
|
+
else if (valueObj.$notLike !== undefined) {
|
|
1602
|
+
values.push(valueObj.$notLike);
|
|
1603
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
1604
|
+
}
|
|
1605
|
+
else if (valueObj.$in) {
|
|
1606
|
+
const inValues = valueObj.$in;
|
|
1607
|
+
values.push(...inValues);
|
|
1608
|
+
fieldConditions.push(`${this.escapeId(key)} IN (${inValues.map(() => '?').join(', ')})`);
|
|
1609
|
+
}
|
|
1610
|
+
else if (valueObj.$notIn) {
|
|
1611
|
+
const notInValues = valueObj.$notIn;
|
|
1612
|
+
values.push(...notInValues);
|
|
1613
|
+
fieldConditions.push(`${this.escapeId(key)} NOT IN (${notInValues.map(() => '?').join(', ')})`);
|
|
1614
|
+
}
|
|
1615
|
+
else if (valueObj.$between) {
|
|
1616
|
+
const between = valueObj.$between;
|
|
1617
|
+
values.push(between[0], between[1]);
|
|
1618
|
+
fieldConditions.push(`${this.escapeId(key)} BETWEEN ? AND ?`);
|
|
1619
|
+
}
|
|
1620
|
+
else if (valueObj.$notBetween) {
|
|
1621
|
+
const notBetween = valueObj.$notBetween;
|
|
1622
|
+
values.push(notBetween[0], notBetween[1]);
|
|
1623
|
+
fieldConditions.push(`${this.escapeId(key)} NOT BETWEEN ? AND ?`);
|
|
1624
|
+
}
|
|
1625
|
+
else if (valueObj.$isNull !== undefined) {
|
|
1626
|
+
fieldConditions.push(valueObj.$isNull
|
|
1627
|
+
? `${this.escapeId(key)} IS NULL`
|
|
1628
|
+
: `${this.escapeId(key)} IS NOT NULL`);
|
|
1629
|
+
}
|
|
1630
|
+
else {
|
|
1631
|
+
values.push(value);
|
|
1632
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
else if (value === null) {
|
|
1636
|
+
fieldConditions.push(`${this.escapeId(key)} IS NULL`);
|
|
1637
|
+
}
|
|
1638
|
+
else if (Array.isArray(value)) {
|
|
1639
|
+
values.push(...value);
|
|
1640
|
+
fieldConditions.push(`${this.escapeId(key)} IN (${value.map(() => '?').join(', ')})`);
|
|
1641
|
+
}
|
|
1642
|
+
else {
|
|
1643
|
+
values.push(value);
|
|
1644
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return fieldConditions.join(' AND ');
|
|
1648
|
+
};
|
|
1649
|
+
const sql = buildCondition(where);
|
|
1650
|
+
return { sql, values };
|
|
1651
|
+
}
|
|
1652
|
+
buildOrderClause(order, _options) {
|
|
1653
|
+
const orderArray = order;
|
|
1654
|
+
if (!orderArray || !Array.isArray(orderArray) || orderArray.length === 0) {
|
|
1655
|
+
return '';
|
|
1656
|
+
}
|
|
1657
|
+
const orderParts = [];
|
|
1658
|
+
for (const item of orderArray) {
|
|
1659
|
+
if (Array.isArray(item)) {
|
|
1660
|
+
const field = typeof item[0] === 'string' ? this.escapeId(item[0]) : item[0];
|
|
1661
|
+
const direction = item[1] ? ` ${item[1]}` : '';
|
|
1662
|
+
orderParts.push(`${field}${direction}`);
|
|
1663
|
+
}
|
|
1664
|
+
else if (typeof item === 'string') {
|
|
1665
|
+
orderParts.push(this.escapeId(item));
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Db2 LUW (9.7+) pagination uses the SQL standard
|
|
1672
|
+
* `OFFSET n ROWS FETCH FIRST m ROWS ONLY` clause. OFFSET must precede
|
|
1673
|
+
* FETCH FIRST when both are supplied.
|
|
1674
|
+
*/
|
|
1675
|
+
buildLimitOffset(limit, offset) {
|
|
1676
|
+
let sql = '';
|
|
1677
|
+
if (offset !== undefined) {
|
|
1678
|
+
sql += ` OFFSET ${Number(offset)} ROWS`;
|
|
1679
|
+
}
|
|
1680
|
+
if (limit !== undefined) {
|
|
1681
|
+
sql += ` FETCH FIRST ${Number(limit)} ROWS ONLY`;
|
|
1682
|
+
}
|
|
1683
|
+
return sql;
|
|
1684
|
+
}
|
|
1685
|
+
buildInsertQuery(tableName, values, options) {
|
|
1686
|
+
const columns = Object.keys(values);
|
|
1687
|
+
const processedValues = [];
|
|
1688
|
+
const placeholders = [];
|
|
1689
|
+
for (const value of Object.values(values)) {
|
|
1690
|
+
if (value instanceof prorm_1.Literal) {
|
|
1691
|
+
placeholders.push(value.val);
|
|
1692
|
+
}
|
|
1693
|
+
else {
|
|
1694
|
+
placeholders.push('?');
|
|
1695
|
+
processedValues.push(value);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
let sql = `INSERT INTO ${this.quoteTable(tableName, options?.schema)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${placeholders.join(', ')})`;
|
|
1699
|
+
if (options?.returning) {
|
|
1700
|
+
const cols = options.returning === true
|
|
1701
|
+
? '*'
|
|
1702
|
+
: options.returning.map((c) => this.escapeId(c)).join(', ');
|
|
1703
|
+
// Db2's `SELECT ... FROM FINAL TABLE (...)` needs the INSERT wrapped.
|
|
1704
|
+
sql = `SELECT ${cols} FROM FINAL TABLE (${sql})`;
|
|
1705
|
+
}
|
|
1706
|
+
return { sql, values: processedValues };
|
|
1707
|
+
}
|
|
1708
|
+
buildUpdateQuery(tableName, values, where, options) {
|
|
1709
|
+
const processedValues = [];
|
|
1710
|
+
const setClauses = [];
|
|
1711
|
+
for (const [key, value] of Object.entries(values)) {
|
|
1712
|
+
if (value instanceof prorm_1.Literal) {
|
|
1713
|
+
setClauses.push(`${this.escapeId(key)} = ${value.val}`);
|
|
1714
|
+
}
|
|
1715
|
+
else {
|
|
1716
|
+
setClauses.push(`${this.escapeId(key)} = ?`);
|
|
1717
|
+
processedValues.push(value);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
let sql = `UPDATE ${this.quoteTable(tableName)} SET ${setClauses.join(', ')}`;
|
|
1721
|
+
const whereClause = this.buildWhereClause(where);
|
|
1722
|
+
if (whereClause.sql) {
|
|
1723
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1724
|
+
processedValues.push(...whereClause.values);
|
|
1725
|
+
}
|
|
1726
|
+
if (options?.returning) {
|
|
1727
|
+
const cols = options.returning === true
|
|
1728
|
+
? '*'
|
|
1729
|
+
: options.returning.map((c) => this.escapeId(c)).join(', ');
|
|
1730
|
+
sql = `SELECT ${cols} FROM FINAL TABLE (${sql})`;
|
|
1731
|
+
}
|
|
1732
|
+
return { sql, values: processedValues };
|
|
1733
|
+
}
|
|
1734
|
+
buildDeleteQuery(tableName, where, options) {
|
|
1735
|
+
if (options?.truncate) {
|
|
1736
|
+
return { sql: `TRUNCATE TABLE ${this.quoteTable(tableName)} IMMEDIATE`, values: [] };
|
|
1737
|
+
}
|
|
1738
|
+
let sql = `DELETE FROM ${this.quoteTable(tableName)}`;
|
|
1739
|
+
const processedValues = [];
|
|
1740
|
+
const whereClause = this.buildWhereClause(where);
|
|
1741
|
+
if (whereClause.sql) {
|
|
1742
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1743
|
+
processedValues.push(...whereClause.values);
|
|
1744
|
+
}
|
|
1745
|
+
if (options?.returning) {
|
|
1746
|
+
const cols = options.returning === true
|
|
1747
|
+
? '*'
|
|
1748
|
+
: options.returning.map((c) => this.escapeId(c)).join(', ');
|
|
1749
|
+
sql = `SELECT ${cols} FROM OLD TABLE (${sql})`;
|
|
1750
|
+
}
|
|
1751
|
+
return { sql, values: processedValues };
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Build a `WITH name(columns) AS (base UNION ALL recursive)` prefix for a
|
|
1755
|
+
* recursive common table expression. Db2 LUW has full standard recursive
|
|
1756
|
+
* CTE support (self-referencing `WITH`, required `UNION ALL` between the
|
|
1757
|
+
* seed and recursive branches).
|
|
1758
|
+
*/
|
|
1759
|
+
buildRecursiveCteClause(cte) {
|
|
1760
|
+
const columnList = cte.columns && cte.columns.length > 0
|
|
1761
|
+
? `(${cte.columns.map((c) => this.escapeId(c)).join(', ')})`
|
|
1762
|
+
: '';
|
|
1763
|
+
const unionKeyword = cte.unionAll === false ? 'UNION' : 'UNION ALL';
|
|
1764
|
+
return `WITH ${this.escapeId(cte.name)}${columnList} AS (${cte.baseQuery} ${unionKeyword} ${cte.recursiveQuery})`;
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Build a full recursive-CTE `SELECT ... FROM cte-name` query in one call,
|
|
1768
|
+
* combining `buildRecursiveCteClause` with `buildSelectQuery`.
|
|
1769
|
+
*/
|
|
1770
|
+
buildRecursiveCteQuery(cte, outer) {
|
|
1771
|
+
return this.buildSelectQuery({
|
|
1772
|
+
...outer,
|
|
1773
|
+
tableName: outer.tableName || cte.name,
|
|
1774
|
+
cte,
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Build an OLAP window/analytic function expression, e.g.:
|
|
1779
|
+
* ROW_NUMBER() OVER (PARTITION BY "dept" ORDER BY "salary" DESC)
|
|
1780
|
+
*
|
|
1781
|
+
* Db2 LUW has mature, standard window-function support (`ROW_NUMBER`,
|
|
1782
|
+
* `RANK`, `DENSE_RANK`, `LAG`/`LEAD`, aggregate-as-window-function forms,
|
|
1783
|
+
* frame clauses, etc.) since 9.x - this builds the `OVER (...)` clause
|
|
1784
|
+
* generically so it works for any of them.
|
|
1785
|
+
*/
|
|
1786
|
+
buildWindowFunction(options) {
|
|
1787
|
+
const args = (options.args || []).join(', ');
|
|
1788
|
+
const overParts = [];
|
|
1789
|
+
if (options.partitionBy) {
|
|
1790
|
+
const cols = Array.isArray(options.partitionBy) ? options.partitionBy : [options.partitionBy];
|
|
1791
|
+
overParts.push(`PARTITION BY ${cols.map((c) => this.escapeId(c)).join(', ')}`);
|
|
1792
|
+
}
|
|
1793
|
+
if (options.orderBy && options.orderBy.length > 0) {
|
|
1794
|
+
const orderParts = options.orderBy.map((o) => typeof o === 'string'
|
|
1795
|
+
? this.escapeId(o)
|
|
1796
|
+
: `${this.escapeId(o.column)}${o.direction ? ` ${o.direction}` : ''}`);
|
|
1797
|
+
overParts.push(`ORDER BY ${orderParts.join(', ')}`);
|
|
1798
|
+
}
|
|
1799
|
+
if (options.frame) {
|
|
1800
|
+
overParts.push(options.frame);
|
|
1801
|
+
}
|
|
1802
|
+
return `${options.fn.toUpperCase()}(${args}) OVER (${overParts.join(' ')})`;
|
|
1803
|
+
}
|
|
1804
|
+
buildSelectQuery(options) {
|
|
1805
|
+
const values = [];
|
|
1806
|
+
// Recursive CTE prefix (`WITH name(cols) AS (base UNION ALL recursive)`)
|
|
1807
|
+
// is Db2-specific and not part of the shared `SelectOptions` interface,
|
|
1808
|
+
// so - like the temporal clauses above - it's read as an extra (untyped)
|
|
1809
|
+
// property on the options object.
|
|
1810
|
+
const cte = options.cte;
|
|
1811
|
+
let sql = cte ? `${this.buildRecursiveCteClause(cte)} ` : '';
|
|
1812
|
+
sql += 'SELECT ';
|
|
1813
|
+
if (options.distinct) {
|
|
1814
|
+
sql += 'DISTINCT ';
|
|
1815
|
+
}
|
|
1816
|
+
if (options.attributes) {
|
|
1817
|
+
if (Array.isArray(options.attributes)) {
|
|
1818
|
+
sql += options.attributes
|
|
1819
|
+
.map((a) => {
|
|
1820
|
+
if (a === '*')
|
|
1821
|
+
return a;
|
|
1822
|
+
if (typeof a === 'object' && a !== null && 'window' in a) {
|
|
1823
|
+
const windowAttr = a;
|
|
1824
|
+
return `${this.buildWindowFunction(windowAttr.window)} AS ${this.escapeId(windowAttr.as)}`;
|
|
1825
|
+
}
|
|
1826
|
+
return this.escapeId(a);
|
|
1827
|
+
})
|
|
1828
|
+
.join(', ');
|
|
1829
|
+
}
|
|
1830
|
+
else {
|
|
1831
|
+
const include = options.attributes.include
|
|
1832
|
+
?.map((a) => this.escapeId(a))
|
|
1833
|
+
.join(', ');
|
|
1834
|
+
sql += include || '*';
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
else {
|
|
1838
|
+
sql += '*';
|
|
1839
|
+
}
|
|
1840
|
+
sql += ` FROM ${this.quoteTable(options.tableName, options.schema)}`;
|
|
1841
|
+
// Temporal query clauses (`FOR SYSTEM_TIME ...` / `FOR BUSINESS_TIME
|
|
1842
|
+
// ...`) are Db2-specific and not part of the shared `SelectOptions`
|
|
1843
|
+
// interface, so they're read as extra (untyped) properties on the
|
|
1844
|
+
// options object rather than requiring a shared-type change.
|
|
1845
|
+
sql += this.buildTemporalClause(options);
|
|
1846
|
+
if (options.include && options.include.length > 0) {
|
|
1847
|
+
for (const include of options.include) {
|
|
1848
|
+
const joinType = include.required ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1849
|
+
const joinModel = include.model;
|
|
1850
|
+
const joinTableName = joinModel.tableName || joinModel.name || '';
|
|
1851
|
+
const joinAlias = include.as || joinTableName;
|
|
1852
|
+
sql += ` ${joinType} ${this.quoteTable(joinTableName)} AS ${this.escapeId(joinAlias)}`;
|
|
1853
|
+
if (include.on) {
|
|
1854
|
+
const onClause = this.buildWhereClause(include.on);
|
|
1855
|
+
sql += ` ON ${onClause.sql}`;
|
|
1856
|
+
values.push(...onClause.values);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (options.where && Object.keys(options.where).length > 0) {
|
|
1861
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
1862
|
+
if (whereClause.sql) {
|
|
1863
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1864
|
+
values.push(...whereClause.values);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (options.group) {
|
|
1868
|
+
const groupBy = Array.isArray(options.group) ? options.group : [options.group];
|
|
1869
|
+
sql += ` GROUP BY ${groupBy.map((g) => this.escapeId(g)).join(', ')}`;
|
|
1870
|
+
if (options.having && Object.keys(options.having).length > 0) {
|
|
1871
|
+
const havingClause = this.buildWhereClause(options.having);
|
|
1872
|
+
sql += ` HAVING ${havingClause.sql}`;
|
|
1873
|
+
values.push(...havingClause.values);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
if (options.order) {
|
|
1877
|
+
const orderClause = this.buildOrderClause(options.order);
|
|
1878
|
+
if (orderClause)
|
|
1879
|
+
sql += ` ${orderClause}`;
|
|
1880
|
+
}
|
|
1881
|
+
if (options.limit !== undefined || options.offset !== undefined) {
|
|
1882
|
+
sql += this.buildLimitOffset(options.limit, options.offset);
|
|
1883
|
+
}
|
|
1884
|
+
if (options.lock) {
|
|
1885
|
+
sql += ' FOR UPDATE';
|
|
1886
|
+
if (options.lock === 'SHARE') {
|
|
1887
|
+
sql += ' WITH RS';
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return { sql, values };
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Build an UPSERT query using Db2's `MERGE INTO ... WHEN MATCHED / WHEN
|
|
1894
|
+
* NOT MATCHED` syntax against a single-row VALUES() source.
|
|
1895
|
+
*/
|
|
1896
|
+
buildUpsertQuery(tableName, values, options) {
|
|
1897
|
+
const columns = Object.keys(values);
|
|
1898
|
+
const queryValues = Object.values(values);
|
|
1899
|
+
const conflictFields = options?.conflictFields && options.conflictFields.length > 0
|
|
1900
|
+
? options.conflictFields
|
|
1901
|
+
: [columns[0]];
|
|
1902
|
+
const updateFields = options?.updateOnDuplicate && options.updateOnDuplicate.length > 0
|
|
1903
|
+
? options.updateOnDuplicate
|
|
1904
|
+
: columns.filter((c) => !conflictFields.includes(c));
|
|
1905
|
+
const sourceColumns = columns.map((c) => this.escapeId(c)).join(', ');
|
|
1906
|
+
const sourceValues = columns.map(() => '?').join(', ');
|
|
1907
|
+
const onClauses = conflictFields
|
|
1908
|
+
.map((c) => `target.${this.escapeId(c)} = source.${this.escapeId(c)}`)
|
|
1909
|
+
.join(' AND ');
|
|
1910
|
+
const updateSet = updateFields.length > 0
|
|
1911
|
+
? updateFields.map((c) => `${this.escapeId(c)} = source.${this.escapeId(c)}`).join(', ')
|
|
1912
|
+
: columns.map((c) => `${this.escapeId(c)} = source.${this.escapeId(c)}`).join(', ');
|
|
1913
|
+
let sql = `MERGE INTO ${this.quoteTable(tableName)} AS target ` +
|
|
1914
|
+
`USING (VALUES (${sourceValues})) AS source (${sourceColumns}) ` +
|
|
1915
|
+
`ON ${onClauses} ` +
|
|
1916
|
+
`WHEN MATCHED THEN UPDATE SET ${updateSet} ` +
|
|
1917
|
+
`WHEN NOT MATCHED THEN INSERT (${sourceColumns}) VALUES (${columns.map((c) => `source.${this.escapeId(c)}`).join(', ')})`;
|
|
1918
|
+
if (options?.returning) {
|
|
1919
|
+
// Db2 does not support RETURNING on MERGE directly; the caller should
|
|
1920
|
+
// re-select the row after the upsert.
|
|
1921
|
+
throw new Error('Db2 does not support a RETURNING clause on MERGE. Re-fetch the row after the upsert instead.');
|
|
1922
|
+
}
|
|
1923
|
+
return { sql, values: queryValues };
|
|
1924
|
+
}
|
|
1925
|
+
buildIncrementQuery(tableName, fields, where, options) {
|
|
1926
|
+
const by = options?.by ?? 1;
|
|
1927
|
+
const setClauses = [];
|
|
1928
|
+
const queryValues = [];
|
|
1929
|
+
if (typeof fields === 'string') {
|
|
1930
|
+
setClauses.push(`${this.escapeId(fields)} = ${this.escapeId(fields)} + ?`);
|
|
1931
|
+
queryValues.push(by);
|
|
1932
|
+
}
|
|
1933
|
+
else if (Array.isArray(fields)) {
|
|
1934
|
+
for (const field of fields) {
|
|
1935
|
+
setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + ?`);
|
|
1936
|
+
queryValues.push(by);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
else {
|
|
1940
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
1941
|
+
setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + ?`);
|
|
1942
|
+
queryValues.push(value);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
let sql = `UPDATE ${this.quoteTable(tableName)} SET ${setClauses.join(', ')}`;
|
|
1946
|
+
const whereClause = this.buildWhereClause(where);
|
|
1947
|
+
if (whereClause.sql) {
|
|
1948
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1949
|
+
queryValues.push(...whereClause.values);
|
|
1950
|
+
}
|
|
1951
|
+
return { sql, values: queryValues };
|
|
1952
|
+
}
|
|
1953
|
+
replaceReplacements(sql, replacements) {
|
|
1954
|
+
if (!replacements) {
|
|
1955
|
+
return sql;
|
|
1956
|
+
}
|
|
1957
|
+
if (Array.isArray(replacements)) {
|
|
1958
|
+
let result = sql;
|
|
1959
|
+
for (const value of replacements) {
|
|
1960
|
+
result = result.replace('?', this.escape(value));
|
|
1961
|
+
}
|
|
1962
|
+
return result;
|
|
1963
|
+
}
|
|
1964
|
+
let result = sql;
|
|
1965
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
1966
|
+
result = result.replace(new RegExp(`:${key}\\b`, 'g'), this.escape(value));
|
|
1967
|
+
}
|
|
1968
|
+
return result;
|
|
1969
|
+
}
|
|
1970
|
+
// ==================== User Management (limited) ====================
|
|
1971
|
+
// Db2 authenticates users via the OS/LDAP, not SQL CREATE USER - only
|
|
1972
|
+
// privilege GRANT/REVOKE management is meaningful at the SQL level.
|
|
1973
|
+
buildCreateUserQuery() {
|
|
1974
|
+
throw new Error('Db2 does not manage users via SQL (users are authenticated externally via OS/LDAP)');
|
|
1975
|
+
}
|
|
1976
|
+
buildAlterUserQuery() {
|
|
1977
|
+
throw new Error('Db2 does not manage users via SQL (users are authenticated externally via OS/LDAP)');
|
|
1978
|
+
}
|
|
1979
|
+
buildDropUserQuery() {
|
|
1980
|
+
throw new Error('Db2 does not manage users via SQL (users are authenticated externally via OS/LDAP)');
|
|
1981
|
+
}
|
|
1982
|
+
getUsersQuery() {
|
|
1983
|
+
return 'SELECT GRANTEE FROM SYSCAT.DBAUTH WHERE GRANTEETYPE = \'U\' GROUP BY GRANTEE';
|
|
1984
|
+
}
|
|
1985
|
+
buildGrantQuery(options) {
|
|
1986
|
+
const privileges = Array.isArray(options.privileges)
|
|
1987
|
+
? options.privileges.join(', ')
|
|
1988
|
+
: options.privileges;
|
|
1989
|
+
let sql = `GRANT ${privileges} ON ${options.on} TO ${options.to}`;
|
|
1990
|
+
if (options.withGrantOption) {
|
|
1991
|
+
sql += ' WITH GRANT OPTION';
|
|
1992
|
+
}
|
|
1993
|
+
return sql;
|
|
1994
|
+
}
|
|
1995
|
+
buildRevokeQuery(options) {
|
|
1996
|
+
const privileges = Array.isArray(options.privileges)
|
|
1997
|
+
? options.privileges.join(', ')
|
|
1998
|
+
: options.privileges;
|
|
1999
|
+
return `REVOKE ${privileges} ON ${options.on} FROM ${options.from}`;
|
|
2000
|
+
}
|
|
2001
|
+
buildShowGrantsQuery(username) {
|
|
2002
|
+
return `SELECT * FROM SYSCAT.DBAUTH WHERE GRANTEE = UPPER('${username}')`;
|
|
2003
|
+
}
|
|
2004
|
+
buildFlushPrivilegesQuery() {
|
|
2005
|
+
throw new Error('Db2 does not require FLUSH PRIVILEGES (grants take effect immediately)');
|
|
2006
|
+
}
|
|
2007
|
+
buildCreateRoleQuery(roleName) {
|
|
2008
|
+
return `CREATE ROLE ${this.escapeId(roleName)}`;
|
|
2009
|
+
}
|
|
2010
|
+
buildDropRoleQuery(roleName) {
|
|
2011
|
+
return `DROP ROLE ${this.escapeId(roleName)}`;
|
|
2012
|
+
}
|
|
2013
|
+
buildGrantRoleQuery(role, to) {
|
|
2014
|
+
return `GRANT ROLE ${this.escapeId(role)} TO ${to}`;
|
|
2015
|
+
}
|
|
2016
|
+
buildRevokeRoleQuery(role, from) {
|
|
2017
|
+
return `REVOKE ROLE ${this.escapeId(role)} FROM ${from}`;
|
|
2018
|
+
}
|
|
2019
|
+
getRolesQuery() {
|
|
2020
|
+
return 'SELECT ROLENAME FROM SYSCAT.ROLES ORDER BY ROLENAME';
|
|
2021
|
+
}
|
|
2022
|
+
// ==================== Sequences ====================
|
|
2023
|
+
async createSequence(options) {
|
|
2024
|
+
const seqName = options.schema
|
|
2025
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.name)}`
|
|
2026
|
+
: this.escapeId(options.name);
|
|
2027
|
+
if (options.ifNotExists) {
|
|
2028
|
+
const exists = await this.hasSequence(options.name, options.schema);
|
|
2029
|
+
if (exists)
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
let sql = `CREATE SEQUENCE ${seqName}`;
|
|
2033
|
+
if (options.startWith !== undefined)
|
|
2034
|
+
sql += ` START WITH ${options.startWith}`;
|
|
2035
|
+
if (options.incrementBy !== undefined)
|
|
2036
|
+
sql += ` INCREMENT BY ${options.incrementBy}`;
|
|
2037
|
+
if (options.minvalue !== undefined)
|
|
2038
|
+
sql += ` MINVALUE ${options.minvalue}`;
|
|
2039
|
+
if (options.maxvalue !== undefined)
|
|
2040
|
+
sql += ` MAXVALUE ${options.maxvalue}`;
|
|
2041
|
+
sql += options.cycle ? ' CYCLE' : ' NO CYCLE';
|
|
2042
|
+
if (options.cache !== undefined) {
|
|
2043
|
+
sql += options.cache > 0 ? ` CACHE ${options.cache}` : ' NO CACHE';
|
|
2044
|
+
}
|
|
2045
|
+
await this.query(sql);
|
|
2046
|
+
}
|
|
2047
|
+
async dropSequence(sequenceName, options) {
|
|
2048
|
+
const seqName = options?.schema
|
|
2049
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(sequenceName)}`
|
|
2050
|
+
: this.escapeId(sequenceName);
|
|
2051
|
+
if (options?.ifExists) {
|
|
2052
|
+
const exists = await this.hasSequence(sequenceName, options.schema);
|
|
2053
|
+
if (!exists)
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
await this.query(`DROP SEQUENCE ${seqName}`);
|
|
2057
|
+
}
|
|
2058
|
+
async nextSequenceValue(sequenceName) {
|
|
2059
|
+
const result = await this.query(`SELECT NEXT VALUE FOR ${this.escapeId(sequenceName)} AS NEXTVAL FROM SYSIBM.SYSDUMMY1`);
|
|
2060
|
+
const row = result.rows[0];
|
|
2061
|
+
return Number(row?.NEXTVAL ?? row?.nextval);
|
|
2062
|
+
}
|
|
2063
|
+
async currSequenceValue(sequenceName) {
|
|
2064
|
+
const result = await this.query(`SELECT PREVIOUS VALUE FOR ${this.escapeId(sequenceName)} AS CURRVAL FROM SYSIBM.SYSDUMMY1`);
|
|
2065
|
+
const row = result.rows[0];
|
|
2066
|
+
return Number(row?.CURRVAL ?? row?.currval);
|
|
2067
|
+
}
|
|
2068
|
+
async hasSequence(sequenceName, schema) {
|
|
2069
|
+
let sql = `SELECT SEQNAME FROM SYSCAT.SEQUENCES WHERE SEQNAME = '${sequenceName}'`;
|
|
2070
|
+
if (schema) {
|
|
2071
|
+
sql += ` AND SEQSCHEMA = '${schema}'`;
|
|
2072
|
+
}
|
|
2073
|
+
const result = await this.query(sql);
|
|
2074
|
+
return result.rows.length > 0;
|
|
2075
|
+
}
|
|
2076
|
+
async listSequences() {
|
|
2077
|
+
const result = await this.query('SELECT SEQSCHEMA, SEQNAME FROM SYSCAT.SEQUENCES ORDER BY SEQSCHEMA, SEQNAME');
|
|
2078
|
+
return result.rows.map((row) => `${row.SEQSCHEMA || row.seqschema}.${row.SEQNAME || row.seqname}`);
|
|
2079
|
+
}
|
|
2080
|
+
// ==================== Stored Procedures ====================
|
|
2081
|
+
async createStoredProcedure(options) {
|
|
2082
|
+
const procName = options.schema
|
|
2083
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.name)}`
|
|
2084
|
+
: this.escapeId(options.name);
|
|
2085
|
+
if (options.ifNotExists && !options.replace) {
|
|
2086
|
+
const exists = await this.hasStoredProcedure(options.name, options.schema);
|
|
2087
|
+
if (exists)
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
if (options.replace) {
|
|
2091
|
+
await this.query(`DROP PROCEDURE ${procName}`).catch(() => undefined);
|
|
2092
|
+
}
|
|
2093
|
+
const paramList = (options.params || [])
|
|
2094
|
+
.map((param) => {
|
|
2095
|
+
const dir = param.mode === 'OUT' ? 'OUT' : param.mode === 'INOUT' ? 'INOUT' : 'IN';
|
|
2096
|
+
return `${dir} ${this.escapeId(param.name)} ${param.type}`;
|
|
2097
|
+
})
|
|
2098
|
+
.join(', ');
|
|
2099
|
+
const sql = `CREATE PROCEDURE ${procName} (${paramList})\nLANGUAGE SQL\nBEGIN\n${options.body}\nEND`;
|
|
2100
|
+
await this.query(sql);
|
|
2101
|
+
}
|
|
2102
|
+
async dropStoredProcedure(procedureName, options) {
|
|
2103
|
+
const procName = options?.schema
|
|
2104
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(procedureName)}`
|
|
2105
|
+
: this.escapeId(procedureName);
|
|
2106
|
+
if (options?.ifExists) {
|
|
2107
|
+
const exists = await this.hasStoredProcedure(procedureName, options.schema);
|
|
2108
|
+
if (!exists)
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
await this.query(`DROP PROCEDURE ${procName}`);
|
|
2112
|
+
}
|
|
2113
|
+
async dropProcedure(procedureName, options) {
|
|
2114
|
+
return this.dropStoredProcedure(procedureName, options);
|
|
2115
|
+
}
|
|
2116
|
+
async createProcedure(options) {
|
|
2117
|
+
return this.createStoredProcedure(options);
|
|
2118
|
+
}
|
|
2119
|
+
async executeStoredProcedure(options) {
|
|
2120
|
+
const procName = options.schema
|
|
2121
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.procedureName)}`
|
|
2122
|
+
: this.escapeId(options.procedureName);
|
|
2123
|
+
const paramValues = options.params ? Object.values(options.params) : [];
|
|
2124
|
+
const placeholders = paramValues.map(() => '?').join(', ');
|
|
2125
|
+
const sql = `CALL ${procName}(${placeholders})`;
|
|
2126
|
+
return this.query(sql, { replacements: paramValues, timeout: options.timeout });
|
|
2127
|
+
}
|
|
2128
|
+
async hasStoredProcedure(procedureName, schema) {
|
|
2129
|
+
let sql = `SELECT PROCNAME FROM SYSCAT.PROCEDURES WHERE PROCNAME = '${procedureName}'`;
|
|
2130
|
+
if (schema) {
|
|
2131
|
+
sql += ` AND PROCSCHEMA = '${schema}'`;
|
|
2132
|
+
}
|
|
2133
|
+
const result = await this.query(sql);
|
|
2134
|
+
return result.rows.length > 0;
|
|
2135
|
+
}
|
|
2136
|
+
// ==================== Triggers ====================
|
|
2137
|
+
async createTrigger(options) {
|
|
2138
|
+
const triggerName = options.schema
|
|
2139
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.name)}`
|
|
2140
|
+
: this.escapeId(options.name);
|
|
2141
|
+
const tableName = options.schema
|
|
2142
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.tableName)}`
|
|
2143
|
+
: this.quoteTable(options.tableName);
|
|
2144
|
+
if (options.replace) {
|
|
2145
|
+
await this.query(`DROP TRIGGER ${triggerName}`).catch(() => undefined);
|
|
2146
|
+
}
|
|
2147
|
+
const timing = options.timing === 'BEFORE' ? 'NO CASCADE BEFORE' : options.timing;
|
|
2148
|
+
const events = (options.events || []).join(' OR ');
|
|
2149
|
+
let sql = `CREATE TRIGGER ${triggerName}\n${timing} ${events} ON ${tableName}\nREFERENCING NEW AS N OLD AS O\nFOR EACH ${options.level === 'STATEMENT' ? 'STATEMENT' : 'ROW'}\nMODE DB2SQL\nBEGIN ATOMIC\n${options.body}\nEND`;
|
|
2150
|
+
await this.query(sql);
|
|
2151
|
+
}
|
|
2152
|
+
async dropTrigger(triggerName, _tableName, options) {
|
|
2153
|
+
const name = options?.schema
|
|
2154
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(triggerName)}`
|
|
2155
|
+
: this.escapeId(triggerName);
|
|
2156
|
+
if (options?.ifExists) {
|
|
2157
|
+
const exists = await this.hasTrigger(triggerName, _tableName, options.schema);
|
|
2158
|
+
if (!exists)
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
await this.query(`DROP TRIGGER ${name}`);
|
|
2162
|
+
}
|
|
2163
|
+
async hasTrigger(triggerName, tableName, schema) {
|
|
2164
|
+
let sql = `SELECT TRIGNAME FROM SYSCAT.TRIGGERS WHERE TRIGNAME = '${triggerName}' AND TABNAME = '${tableName}'`;
|
|
2165
|
+
if (schema) {
|
|
2166
|
+
sql += ` AND TRIGSCHEMA = '${schema}'`;
|
|
2167
|
+
}
|
|
2168
|
+
const result = await this.query(sql);
|
|
2169
|
+
return result.rows.length > 0;
|
|
2170
|
+
}
|
|
2171
|
+
// ==================== Row/Column Access Control (RLS-like) ====================
|
|
2172
|
+
async createPolicy(options) {
|
|
2173
|
+
const tableName = options.schema
|
|
2174
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.tableName)}`
|
|
2175
|
+
: this.quoteTable(options.tableName);
|
|
2176
|
+
const predicate = options.using || options.withCheck || '1=1';
|
|
2177
|
+
await this.query(`CREATE PERMISSION ${this.escapeId(options.name)} ON ${tableName} FOR ROWS WHERE (${predicate}) ENFORCED FOR ALL ACCESS ENABLE`);
|
|
2178
|
+
}
|
|
2179
|
+
async dropPolicy(policyName, _tableName, options) {
|
|
2180
|
+
if (options?.ifExists) {
|
|
2181
|
+
const exists = await this.query(`SELECT PERMISSIONNAME FROM SYSCAT.CONTROLS WHERE PERMISSIONNAME = '${policyName}'`).catch(() => ({ rows: [], rowCount: 0, fields: [] }));
|
|
2182
|
+
if (!exists.rows || exists.rows.length === 0)
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
await this.query(`DROP PERMISSION ${this.escapeId(policyName)}`);
|
|
2186
|
+
}
|
|
2187
|
+
async enableRLS(tableName, schema) {
|
|
2188
|
+
const table = schema ? `${this.escapeId(schema)}.${this.escapeId(tableName)}` : this.quoteTable(tableName);
|
|
2189
|
+
await this.query(`ALTER TABLE ${table} ACTIVATE ROW ACCESS CONTROL`);
|
|
2190
|
+
}
|
|
2191
|
+
async disableRLS(tableName, schema) {
|
|
2192
|
+
const table = schema ? `${this.escapeId(schema)}.${this.escapeId(tableName)}` : this.quoteTable(tableName);
|
|
2193
|
+
await this.query(`ALTER TABLE ${table} DEACTIVATE ROW ACCESS CONTROL`);
|
|
2194
|
+
}
|
|
2195
|
+
async hasPolicy(policyName, _tableName) {
|
|
2196
|
+
const result = await this.query(`SELECT PERMISSIONNAME FROM SYSCAT.CONTROLS WHERE PERMISSIONNAME = '${policyName}'`).catch(() => ({ rows: [], rowCount: 0, fields: [] }));
|
|
2197
|
+
return result.rows.length > 0;
|
|
2198
|
+
}
|
|
2199
|
+
// ==================== Comments ====================
|
|
2200
|
+
async commentTable(tableName, comment) {
|
|
2201
|
+
await this.query(`COMMENT ON TABLE ${this.quoteTable(tableName)} IS '${comment.replace(/'/g, "''")}'`);
|
|
2202
|
+
}
|
|
2203
|
+
async commentColumn(tableName, columnName, comment) {
|
|
2204
|
+
await this.query(`COMMENT ON COLUMN ${this.quoteTable(tableName)}.${this.escapeId(columnName)} IS '${comment.replace(/'/g, "''")}'`);
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Build a JSON_TABLE expression to shred a JSON document/array into relational rows.
|
|
2208
|
+
* Db2 for LUW added the SQL/JSON standard `JSON_TABLE` function in 11.5.6+ (Mod Pack 6),
|
|
2209
|
+
* with syntax matching Oracle/MySQL: JSON_TABLE(expr, path COLUMNS(name type PATH path, ...)).
|
|
2210
|
+
* On earlier Db2 versions this function does not exist and callers must fall back to
|
|
2211
|
+
* `JSON_VAL`/`SYSTOOLS.JSON2BSON` based workarounds instead.
|
|
2212
|
+
*/
|
|
2213
|
+
buildJsonTable(jsonExpression, rowPath, columns, alias) {
|
|
2214
|
+
const columnDefs = columns
|
|
2215
|
+
.map((col) => {
|
|
2216
|
+
if (col.forOrdinality) {
|
|
2217
|
+
return `${this.escapeId(col.name)} FOR ORDINALITY`;
|
|
2218
|
+
}
|
|
2219
|
+
return `${this.escapeId(col.name)} ${col.type} PATH '${col.path}'`;
|
|
2220
|
+
})
|
|
2221
|
+
.join(', ');
|
|
2222
|
+
return `JSON_TABLE(${jsonExpression}, '${rowPath}' COLUMNS(${columnDefs})) AS ${this.escapeId(alias)}`;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
exports.DB2Dialect = DB2Dialect;
|