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,2999 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Oracle dialect implementation for the TypeScript ORM
|
|
4
|
+
* Uses the oracledb driver for database connectivity
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.OracleDialect = void 0;
|
|
11
|
+
exports.createOracleDialect = createOracleDialect;
|
|
12
|
+
const oracledb_1 = __importDefault(require("oracledb"));
|
|
13
|
+
const query_stream_helper_1 = require("../query-stream-helper");
|
|
14
|
+
const errors_1 = require("../../errors");
|
|
15
|
+
const crypto_1 = require("crypto");
|
|
16
|
+
/**
|
|
17
|
+
* Oracle-specific transaction class
|
|
18
|
+
*/
|
|
19
|
+
class OracleTransaction {
|
|
20
|
+
constructor(connection, options) {
|
|
21
|
+
this.finished = false;
|
|
22
|
+
this.parent = null;
|
|
23
|
+
this.savepoints = [];
|
|
24
|
+
this.connection = null;
|
|
25
|
+
this.id = `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
26
|
+
this.options = options || {};
|
|
27
|
+
this.parent = null;
|
|
28
|
+
this.savepoints = [];
|
|
29
|
+
this.connection = connection;
|
|
30
|
+
}
|
|
31
|
+
async commit() {
|
|
32
|
+
this.finished = true;
|
|
33
|
+
}
|
|
34
|
+
async rollback() {
|
|
35
|
+
this.finished = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Default pool options for Oracle
|
|
40
|
+
*/
|
|
41
|
+
const DEFAULT_POOL_OPTIONS = {
|
|
42
|
+
max: 10,
|
|
43
|
+
min: 2,
|
|
44
|
+
increment: 2,
|
|
45
|
+
timeout: 30,
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Default retry options for connection
|
|
49
|
+
*/
|
|
50
|
+
const DEFAULT_RETRY_OPTIONS = {
|
|
51
|
+
max: 3,
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
match: ['ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ORA-12514', 'ORA-12541', 'ORA-12154'],
|
|
54
|
+
backoff: false,
|
|
55
|
+
backoffMultiplier: 2,
|
|
56
|
+
backoffMax: 10000,
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Oracle dialect class that implements the Dialect interface
|
|
60
|
+
*/
|
|
61
|
+
class OracleDialect {
|
|
62
|
+
constructor(config) {
|
|
63
|
+
this.name = 'oracle';
|
|
64
|
+
this.library = 'oracledb';
|
|
65
|
+
this.pool = null;
|
|
66
|
+
this._isConnected = false;
|
|
67
|
+
this.retryOptions = config.retry || {};
|
|
68
|
+
this.config = {
|
|
69
|
+
host: 'localhost',
|
|
70
|
+
port: 1521,
|
|
71
|
+
...config,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Execute a function with retry logic
|
|
76
|
+
*/
|
|
77
|
+
async withRetry(fn) {
|
|
78
|
+
const retryConfig = {
|
|
79
|
+
...DEFAULT_RETRY_OPTIONS,
|
|
80
|
+
...this.retryOptions,
|
|
81
|
+
};
|
|
82
|
+
let lastError;
|
|
83
|
+
for (let attempt = 0; attempt <= retryConfig.max; attempt++) {
|
|
84
|
+
try {
|
|
85
|
+
return await fn();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
lastError = error;
|
|
89
|
+
const errorMessage = lastError.message;
|
|
90
|
+
// Check if this error should be retried
|
|
91
|
+
const shouldRetry = retryConfig.match.some((match) => errorMessage.includes(match));
|
|
92
|
+
if (!shouldRetry || attempt === retryConfig.max) {
|
|
93
|
+
throw lastError;
|
|
94
|
+
}
|
|
95
|
+
// Calculate backoff
|
|
96
|
+
let delay = retryConfig.timeout;
|
|
97
|
+
if (retryConfig.backoff) {
|
|
98
|
+
delay = Math.min(retryConfig.timeout * Math.pow(retryConfig.backoffMultiplier, attempt), retryConfig.backoffMax);
|
|
99
|
+
}
|
|
100
|
+
// Wait before retry
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
throw lastError;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Escape a string for SQL (replace ' with '')
|
|
108
|
+
*/
|
|
109
|
+
escapeString(str) {
|
|
110
|
+
return str.replace(/'/g, "''");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Connect to the Oracle database
|
|
114
|
+
*/
|
|
115
|
+
async connect() {
|
|
116
|
+
try {
|
|
117
|
+
// `initOracleClient()` forces thick mode, which requires an Instant
|
|
118
|
+
// Client install. When `config.thinMode` is requested, skip it
|
|
119
|
+
// entirely so the driver stays in its pure-JS thin mode (its default
|
|
120
|
+
// when the client isn't initialized) instead of unconditionally
|
|
121
|
+
// switching to thick mode.
|
|
122
|
+
if (!this.config.thinMode) {
|
|
123
|
+
await oracledb_1.default.initOracleClient();
|
|
124
|
+
}
|
|
125
|
+
const poolOptions = {
|
|
126
|
+
...DEFAULT_POOL_OPTIONS,
|
|
127
|
+
...this.config.pool,
|
|
128
|
+
user: this.config.username,
|
|
129
|
+
password: this.config.password,
|
|
130
|
+
connectString: `${this.config.host}:${this.config.port}/${this.config.database}`,
|
|
131
|
+
};
|
|
132
|
+
this.pool = await oracledb_1.default.createPool(poolOptions);
|
|
133
|
+
this._isConnected = true;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
// Surface the real underlying error (message, ORA code, stack, etc.)
|
|
137
|
+
// instead of collapsing it into a generic string, which previously
|
|
138
|
+
// hid the actual cause of connection failures.
|
|
139
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
140
|
+
throw new Error(`Failed to connect to Oracle database: ${message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Disconnect from the Oracle database
|
|
145
|
+
*/
|
|
146
|
+
async disconnect() {
|
|
147
|
+
if (this.pool) {
|
|
148
|
+
await this.pool.close(0);
|
|
149
|
+
this.pool = null;
|
|
150
|
+
this._isConnected = false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get the current connection from pool
|
|
155
|
+
*/
|
|
156
|
+
async getConnection() {
|
|
157
|
+
if (!this.pool) {
|
|
158
|
+
throw new Error('Not connected to database. Call connect() first.');
|
|
159
|
+
}
|
|
160
|
+
return await this.pool.getConnection();
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Check if connected
|
|
164
|
+
*/
|
|
165
|
+
isConnected() {
|
|
166
|
+
return this._isConnected && this.pool !== null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Stream query results using a correct ROWNUM-windowed pagination query
|
|
170
|
+
* (see `createOracleQueryStream()` in `src/dialects/query-stream-helper.ts`)
|
|
171
|
+
* rather than this dialect's own `buildLimitOffset()`, whose bare
|
|
172
|
+
* `WHERE ROWNUM <= n` doesn't actually skip the first `offset` rows.
|
|
173
|
+
* @param sql - The SELECT statement to stream
|
|
174
|
+
* @param options - Streaming options (batch size, backpressure watermark, model mapping)
|
|
175
|
+
*/
|
|
176
|
+
queryStream(sql, options) {
|
|
177
|
+
return (0, query_stream_helper_1.createOracleQueryStream)(this, sql, options);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Execute a query
|
|
181
|
+
*/
|
|
182
|
+
async query(sql, options) {
|
|
183
|
+
const connection = await this.getConnection();
|
|
184
|
+
try {
|
|
185
|
+
const queryOptions = {
|
|
186
|
+
autoCommit: true,
|
|
187
|
+
outFormat: oracledb_1.default.OUT_FORMAT_OBJECT,
|
|
188
|
+
};
|
|
189
|
+
// Callers pass bind values under different option keys (`replacements`,
|
|
190
|
+
// `bindings`, or `bind`); oracledb accepts either a positional array
|
|
191
|
+
// (for `:1`-style binds) or a named object (for `:name`-style binds).
|
|
192
|
+
const bindParams = options?.bind ??
|
|
193
|
+
options?.replacements ??
|
|
194
|
+
options?.bindings ??
|
|
195
|
+
[];
|
|
196
|
+
const result = await connection.execute(sql, bindParams, queryOptions);
|
|
197
|
+
const fields = (result.metaData || []).map((meta) => ({
|
|
198
|
+
name: meta.name,
|
|
199
|
+
// `meta.dbType` is an oracledb `DbType` object (e.g. DB_TYPE_VARCHAR),
|
|
200
|
+
// not a plain number, so it can't be used as a key into the
|
|
201
|
+
// `oracledb` module's exports (those are keyed by constant name, a
|
|
202
|
+
// name -> number/object map, not number -> name). The driver already
|
|
203
|
+
// computes the human-readable type name for us on `dbTypeName`
|
|
204
|
+
// (falling back to `dbType.name` for older driver versions), which is
|
|
205
|
+
// the correct reverse lookup to use here.
|
|
206
|
+
type: meta.dbTypeName || meta.dbType?.name || 'UNKNOWN',
|
|
207
|
+
length: meta.byteSize || 0,
|
|
208
|
+
tableID: 0,
|
|
209
|
+
columnID: 0,
|
|
210
|
+
nullable: true,
|
|
211
|
+
isEnum: false,
|
|
212
|
+
isPrimaryKey: false,
|
|
213
|
+
isAutoIncrement: false,
|
|
214
|
+
isJson: false,
|
|
215
|
+
isEnumLiteral: false,
|
|
216
|
+
}));
|
|
217
|
+
return {
|
|
218
|
+
rows: result.rows || [],
|
|
219
|
+
rowCount: result.rows?.length || 0,
|
|
220
|
+
fields,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
await connection.close();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Escape a value for use in a query
|
|
229
|
+
*/
|
|
230
|
+
escape(value) {
|
|
231
|
+
if (value === null || value === undefined) {
|
|
232
|
+
return 'NULL';
|
|
233
|
+
}
|
|
234
|
+
if (typeof value === 'number') {
|
|
235
|
+
return value.toString();
|
|
236
|
+
}
|
|
237
|
+
if (typeof value === 'boolean') {
|
|
238
|
+
return value ? '1' : '0';
|
|
239
|
+
}
|
|
240
|
+
if (value instanceof Date) {
|
|
241
|
+
// Oracle date format
|
|
242
|
+
const year = value.getFullYear();
|
|
243
|
+
const month = String(value.getMonth() + 1).padStart(2, '0');
|
|
244
|
+
const day = String(value.getDate()).padStart(2, '0');
|
|
245
|
+
const hours = String(value.getHours()).padStart(2, '0');
|
|
246
|
+
const minutes = String(value.getMinutes()).padStart(2, '0');
|
|
247
|
+
const seconds = String(value.getSeconds()).padStart(2, '0');
|
|
248
|
+
return `TO_DATE('${year}-${month}-${day} ${hours}:${minutes}:${seconds}', 'YYYY-MM-DD HH24:MI:SS')`;
|
|
249
|
+
}
|
|
250
|
+
if (Buffer.isBuffer(value)) {
|
|
251
|
+
// A bare quoted hex string (e.g. 'deadbeef') is a Oracle CHAR/VARCHAR2
|
|
252
|
+
// literal containing those literal ASCII characters, not the raw
|
|
253
|
+
// bytes. To insert the actual binary payload into a RAW/BLOB column,
|
|
254
|
+
// the hex text must be wrapped in HEXTORAW(...), which Oracle converts
|
|
255
|
+
// to the corresponding raw bytes at the server.
|
|
256
|
+
return `HEXTORAW('${value.toString('hex')}')`;
|
|
257
|
+
}
|
|
258
|
+
if (Array.isArray(value)) {
|
|
259
|
+
return `(${value.map((v) => this.escape(v)).join(', ')})`;
|
|
260
|
+
}
|
|
261
|
+
// String - escape single quotes by doubling them
|
|
262
|
+
const escaped = String(value).replace(/'/g, "''");
|
|
263
|
+
return `'${escaped}'`;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Escape an identifier (Oracle uses double quotes, same as quoteIdentifier).
|
|
267
|
+
* Without the surrounding quotes, Oracle folds unquoted identifiers to
|
|
268
|
+
* uppercase, which breaks lookups for mixed-case table/column names created
|
|
269
|
+
* elsewhere via quoteIdentifier()/quoteTable().
|
|
270
|
+
*/
|
|
271
|
+
escapeId(identifier) {
|
|
272
|
+
const id = String(identifier ?? '');
|
|
273
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Quote an identifier with double quotes (Oracle standard)
|
|
277
|
+
*/
|
|
278
|
+
quoteIdentifier(identifier) {
|
|
279
|
+
const id = String(identifier ?? '');
|
|
280
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Quote a table name with optional schema
|
|
284
|
+
*/
|
|
285
|
+
quoteTable(tableName, schema) {
|
|
286
|
+
if (schema) {
|
|
287
|
+
return `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
|
288
|
+
}
|
|
289
|
+
return this.quoteIdentifier(tableName);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Get database version
|
|
293
|
+
*/
|
|
294
|
+
async getDatabaseVersion() {
|
|
295
|
+
const result = await this.query('SELECT VERSION FROM V$INSTANCE');
|
|
296
|
+
return result.rows[0]?.VERSION || 'Unknown';
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* `IF [NOT] EXISTS` on `CREATE`/`DROP` DDL (tables, users, roles, etc.) was
|
|
300
|
+
* only added in Oracle 23c. Against the 19c/21c versions this dialect
|
|
301
|
+
* otherwise targets, that syntax raises `ORA-00922: missing or invalid
|
|
302
|
+
* option`. This helper provides a portable, version-independent
|
|
303
|
+
* equivalent by wrapping the plain (no `IF [NOT] EXISTS`) DDL statement in
|
|
304
|
+
* an anonymous PL/SQL block that executes it via `EXECUTE IMMEDIATE` and
|
|
305
|
+
* swallows only the specific "already exists" / "does not exist" ORA
|
|
306
|
+
* error code(s) supplied, letting any other failure propagate normally.
|
|
307
|
+
*
|
|
308
|
+
* @param sql - The plain DDL statement, without any `IF [NOT] EXISTS`.
|
|
309
|
+
* @param oraCodes - The ORA error number(s) (positive, e.g. `955` for
|
|
310
|
+
* ORA-00955) that should be silently ignored.
|
|
311
|
+
*/
|
|
312
|
+
wrapIgnoringOraErrors(sql, oraCodes) {
|
|
313
|
+
const escapedSql = sql.replace(/'/g, "''");
|
|
314
|
+
const codeList = oraCodes.map((code) => -Math.abs(code)).join(', ');
|
|
315
|
+
return (`BEGIN\n` +
|
|
316
|
+
` EXECUTE IMMEDIATE '${escapedSql}';\n` +
|
|
317
|
+
`EXCEPTION\n` +
|
|
318
|
+
` WHEN OTHERS THEN\n` +
|
|
319
|
+
` IF SQLCODE NOT IN (${codeList}) THEN\n` +
|
|
320
|
+
` RAISE;\n` +
|
|
321
|
+
` END IF;\n` +
|
|
322
|
+
`END;`);
|
|
323
|
+
}
|
|
324
|
+
// ==================== Schema Operations ====================
|
|
325
|
+
/**
|
|
326
|
+
* Create a schema.
|
|
327
|
+
*
|
|
328
|
+
* Oracle has a `CREATE SCHEMA` statement, but it only exists to bundle a
|
|
329
|
+
* batch of `CREATE TABLE`/`CREATE VIEW`/`GRANT` statements into one
|
|
330
|
+
* transaction (`CREATE SCHEMA AUTHORIZATION x <stmt> <stmt> ...`) - it does
|
|
331
|
+
* not, by itself, create a schema as a standalone object, and
|
|
332
|
+
* `CREATE SCHEMA AUTHORIZATION x` with no accompanying statements is not
|
|
333
|
+
* valid Oracle usage. In Oracle a schema *is* a user (every user owns
|
|
334
|
+
* exactly one schema, and objects are always created "in" a user's
|
|
335
|
+
* schema), so creating a schema really means creating a user. A random
|
|
336
|
+
* password is generated since `CREATE USER` requires one; callers that
|
|
337
|
+
* need a specific password (or other user attributes) should use the
|
|
338
|
+
* User Management API (`buildCreateUserQuery`/`UserManager.createUser`)
|
|
339
|
+
* directly instead.
|
|
340
|
+
*/
|
|
341
|
+
async createSchema(schema) {
|
|
342
|
+
const password = (0, crypto_1.randomBytes)(24).toString('base64').replace(/[^A-Za-z0-9]/g, '') + 'Aa1';
|
|
343
|
+
await this.query(`CREATE USER ${this.quoteIdentifier(schema)} IDENTIFIED BY "${password}"`);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Drop a schema.
|
|
347
|
+
*
|
|
348
|
+
* Since a schema in Oracle *is* a user, dropping a schema means dropping
|
|
349
|
+
* that user (`DROP SCHEMA`/`DROP SCHEMA ... CASCADE` are not Oracle
|
|
350
|
+
* statements at all). `CASCADE` is always applied so that the user's
|
|
351
|
+
* owned objects are dropped along with the user, matching the intent of
|
|
352
|
+
* "drop this schema and everything in it". When `ifExists` is requested,
|
|
353
|
+
* the statement is wrapped so that ORA-01918 ("user does not exist") is
|
|
354
|
+
* silently ignored (see {@link wrapIgnoringOraErrors}); Oracle's own
|
|
355
|
+
* `DROP USER ... IF EXISTS` syntax is 23c-only and would fail against the
|
|
356
|
+
* 19c/21c versions this dialect targets.
|
|
357
|
+
*/
|
|
358
|
+
async dropSchema(schema, options) {
|
|
359
|
+
const sql = `DROP USER ${this.quoteIdentifier(schema)} CASCADE`;
|
|
360
|
+
const finalSql = options?.ifExists
|
|
361
|
+
? this.wrapIgnoringOraErrors(sql, [OracleDialect.ORA_USER_DOES_NOT_EXIST])
|
|
362
|
+
: sql;
|
|
363
|
+
await this.query(finalSql);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Show all schemas
|
|
367
|
+
*/
|
|
368
|
+
async showAllSchemas() {
|
|
369
|
+
const result = await this.query('SELECT USERNAME FROM ALL_USERS ORDER BY USERNAME');
|
|
370
|
+
return result.rows.map((row) => row.USERNAME);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* List all schemas (Oracle users)
|
|
374
|
+
*/
|
|
375
|
+
async listSchemas() {
|
|
376
|
+
return this.showAllSchemas();
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Create database SQL (Oracle uses tablespaces)
|
|
380
|
+
*/
|
|
381
|
+
createDatabaseSQL(options) {
|
|
382
|
+
const parts = [];
|
|
383
|
+
parts.push(`CREATE DATABASE ${this.quoteIdentifier(options.name)}`);
|
|
384
|
+
if (options.tablespace) {
|
|
385
|
+
parts.push(`DEFAULT TABLESPACE ${options.tablespace}`);
|
|
386
|
+
}
|
|
387
|
+
return parts.join(' ');
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Drop database SQL
|
|
391
|
+
*/
|
|
392
|
+
dropDatabaseSQL(name) {
|
|
393
|
+
return `DROP DATABASE ${this.quoteIdentifier(name)}`;
|
|
394
|
+
}
|
|
395
|
+
// ==================== Table Operations ====================
|
|
396
|
+
/**
|
|
397
|
+
* Create a table
|
|
398
|
+
*/
|
|
399
|
+
async createTable(tableName, columns, options) {
|
|
400
|
+
const columnDefs = [];
|
|
401
|
+
for (const [columnName, definition] of Object.entries(columns)) {
|
|
402
|
+
columnDefs.push(this.buildColumnDefinition(columnName, definition));
|
|
403
|
+
}
|
|
404
|
+
if (options?.constraints) {
|
|
405
|
+
for (const constraint of options.constraints) {
|
|
406
|
+
columnDefs.push(this.buildConstraintDefinition(constraint));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const plainSql = `CREATE TABLE ${this.quoteTable(tableName)} (${columnDefs.join(', ')})`;
|
|
410
|
+
// Oracle's `CREATE TABLE IF NOT EXISTS` is 23c-only syntax and raises
|
|
411
|
+
// ORA-00922 against the 19c/21c versions this dialect targets. Use a
|
|
412
|
+
// portable existence check instead: run the plain DDL and silently
|
|
413
|
+
// ignore ORA-00955 ("name is already used by an existing object").
|
|
414
|
+
const sql = options?.ifNotExists
|
|
415
|
+
? this.wrapIgnoringOraErrors(plainSql, [OracleDialect.ORA_NAME_ALREADY_USED])
|
|
416
|
+
: plainSql;
|
|
417
|
+
await this.query(sql);
|
|
418
|
+
// Create indexes if specified
|
|
419
|
+
if (options?.indexes) {
|
|
420
|
+
for (const index of options.indexes) {
|
|
421
|
+
await this.createIndex(tableName, {
|
|
422
|
+
name: index.name || `${tableName}_${index.fields.join('_')}_idx`,
|
|
423
|
+
fields: index.fields,
|
|
424
|
+
unique: index.unique,
|
|
425
|
+
type: index.type,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Build column definition for Oracle
|
|
432
|
+
*/
|
|
433
|
+
buildColumnDefinition(columnName, definition) {
|
|
434
|
+
let sql = `${this.quoteIdentifier(columnName)} ${this.getDataTypeSql(definition.type)}`;
|
|
435
|
+
// Oracle's column_definition grammar requires `DEFAULT expr` to appear
|
|
436
|
+
// immediately after the datatype and *before* any inline constraints
|
|
437
|
+
// (NOT NULL, PRIMARY KEY, UNIQUE, REFERENCES, CHECK). Emitting it after
|
|
438
|
+
// those constraints (as e.g. `... NOT NULL DEFAULT 'x'`) raises
|
|
439
|
+
// ORA-03076 ("unexpected item DEFAULT in a column definition or inline
|
|
440
|
+
// constraint") because the parser has already committed to the
|
|
441
|
+
// constraint clause by the time it sees DEFAULT.
|
|
442
|
+
if (definition.defaultValue !== undefined) {
|
|
443
|
+
// A handful of well-known Oracle default expressions/keywords (e.g.
|
|
444
|
+
// CURRENT_TIMESTAMP, used for auto-managed createdAt/updatedAt
|
|
445
|
+
// columns) must be emitted unquoted - escaping them as a string
|
|
446
|
+
// literal would store the literal text "CURRENT_TIMESTAMP" instead of
|
|
447
|
+
// evaluating the expression.
|
|
448
|
+
const rawDefaultKeywords = new Set([
|
|
449
|
+
'CURRENT_TIMESTAMP',
|
|
450
|
+
'CURRENT_DATE',
|
|
451
|
+
'SYSDATE',
|
|
452
|
+
'SYSTIMESTAMP',
|
|
453
|
+
'LOCALTIMESTAMP',
|
|
454
|
+
'NULL',
|
|
455
|
+
]);
|
|
456
|
+
const defaultValue = definition.defaultValue;
|
|
457
|
+
const defaultSql = typeof defaultValue === 'string' && rawDefaultKeywords.has(defaultValue.toUpperCase())
|
|
458
|
+
? defaultValue.toUpperCase()
|
|
459
|
+
: this.escape(defaultValue);
|
|
460
|
+
sql += ` DEFAULT ${defaultSql}`;
|
|
461
|
+
}
|
|
462
|
+
if (definition.allowNull === false) {
|
|
463
|
+
sql += ' NOT NULL';
|
|
464
|
+
}
|
|
465
|
+
if (definition.primaryKey) {
|
|
466
|
+
sql += ' PRIMARY KEY';
|
|
467
|
+
}
|
|
468
|
+
if (definition.autoIncrement) {
|
|
469
|
+
// Oracle uses sequences for auto-increment
|
|
470
|
+
// This is handled separately via triggers
|
|
471
|
+
}
|
|
472
|
+
if (definition.unique) {
|
|
473
|
+
if (typeof definition.unique === 'string') {
|
|
474
|
+
sql += ` UNIQUE (${this.quoteIdentifier(definition.unique)})`;
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
sql += ' UNIQUE';
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (definition.references) {
|
|
481
|
+
const refTable = definition.references.table;
|
|
482
|
+
const refFields = Array.isArray(definition.references.field)
|
|
483
|
+
? definition.references.field.join(', ')
|
|
484
|
+
: definition.references.field;
|
|
485
|
+
sql += ` REFERENCES ${this.quoteTable(refTable)} (${this.quoteIdentifier(refFields)})`;
|
|
486
|
+
if (definition.references.onDelete) {
|
|
487
|
+
sql += ` ON DELETE ${definition.references.onDelete.toUpperCase()}`;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return sql;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Build constraint definition
|
|
494
|
+
*/
|
|
495
|
+
buildConstraintDefinition(constraint) {
|
|
496
|
+
const constraintName = constraint.name ? this.quoteIdentifier(constraint.name) : '';
|
|
497
|
+
const fields = constraint.fields
|
|
498
|
+
? constraint.fields.map((f) => this.quoteIdentifier(f)).join(', ')
|
|
499
|
+
: '';
|
|
500
|
+
switch (constraint.type) {
|
|
501
|
+
case 'PRIMARY KEY':
|
|
502
|
+
return `${constraintName} PRIMARY KEY (${fields})`;
|
|
503
|
+
case 'UNIQUE':
|
|
504
|
+
return `${constraintName} UNIQUE (${fields})`;
|
|
505
|
+
case 'FOREIGN KEY': {
|
|
506
|
+
const refTable = constraint.references?.table;
|
|
507
|
+
const refFields = constraint.references?.field;
|
|
508
|
+
return `${constraintName} FOREIGN KEY (${fields}) REFERENCES ${this.quoteTable(refTable)} (${this.quoteIdentifier(refFields)})`;
|
|
509
|
+
}
|
|
510
|
+
case 'CHECK':
|
|
511
|
+
return `${constraintName} CHECK (${constraint.check})`;
|
|
512
|
+
default:
|
|
513
|
+
return '';
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Drop a table
|
|
518
|
+
*/
|
|
519
|
+
async dropTable(tableName, options) {
|
|
520
|
+
const plainSql = `DROP TABLE ${this.quoteTable(tableName)} CASCADE CONSTRAINTS`;
|
|
521
|
+
// Oracle's `DROP TABLE ... IF EXISTS` is 23c-only syntax and raises
|
|
522
|
+
// ORA-00922 against the 19c/21c versions this dialect targets. Use a
|
|
523
|
+
// portable existence check instead: run the plain DDL and silently
|
|
524
|
+
// ignore ORA-00942 ("table or view does not exist").
|
|
525
|
+
const sql = options?.ifExists
|
|
526
|
+
? this.wrapIgnoringOraErrors(plainSql, [OracleDialect.ORA_TABLE_DOES_NOT_EXIST])
|
|
527
|
+
: plainSql;
|
|
528
|
+
await this.query(sql);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Create partitioned table (Oracle supports RANGE, LIST, HASH, and INTERVAL partitioning)
|
|
532
|
+
*/
|
|
533
|
+
async createPartitionedTable(tableName, columns, options) {
|
|
534
|
+
const columnDefs = [];
|
|
535
|
+
for (const [columnName, definition] of Object.entries(columns)) {
|
|
536
|
+
columnDefs.push(this.buildColumnDefinition(columnName, definition));
|
|
537
|
+
}
|
|
538
|
+
const partitionType = options?.partitionBy?.type || 'range';
|
|
539
|
+
let sql = `CREATE TABLE ${this.quoteTable(tableName)} (${columnDefs.join(', ')}) PARTITION BY ${partitionType.toUpperCase()}`;
|
|
540
|
+
if (partitionType === 'reference') {
|
|
541
|
+
// Reference partitioning has no partition key column of its own - the
|
|
542
|
+
// child table inherits its parent's partitioning scheme via an FK
|
|
543
|
+
// constraint: `PARTITION BY REFERENCE (constraint_name)`. The
|
|
544
|
+
// referenced constraint must already be defined on this table (e.g.
|
|
545
|
+
// via `options.constraints` / an inline column-level REFERENCES) and
|
|
546
|
+
// must point at a partitioned parent table.
|
|
547
|
+
if (!options?.partitionBy?.referenceConstraint) {
|
|
548
|
+
throw new Error("createPartitionedTable: partitionBy.type 'reference' requires " +
|
|
549
|
+
'partitionBy.referenceConstraint (the name of the FK constraint whose parent ' +
|
|
550
|
+
"table's partitioning scheme should be inherited).");
|
|
551
|
+
}
|
|
552
|
+
sql += ` (${this.quoteIdentifier(options.partitionBy.referenceConstraint)})`;
|
|
553
|
+
// Unlike RANGE/LIST/HASH, individual partitions (if named at all) take
|
|
554
|
+
// no VALUES/bound clause - they simply mirror the parent's partitions,
|
|
555
|
+
// optionally with their own name/tablespace.
|
|
556
|
+
if (options?.partitions && options.partitions.length > 0) {
|
|
557
|
+
const partitionDefs = options.partitions.map((part) => {
|
|
558
|
+
let partDef = `PARTITION ${this.quoteIdentifier(part.name)}`;
|
|
559
|
+
if (part.tablespace) {
|
|
560
|
+
partDef += ` TABLESPACE ${part.tablespace}`;
|
|
561
|
+
}
|
|
562
|
+
return partDef;
|
|
563
|
+
});
|
|
564
|
+
sql += ` (${partitionDefs.join(', ')})`;
|
|
565
|
+
}
|
|
566
|
+
await this.query(sql);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const partitionColumn = Array.isArray(options?.partitionBy?.column)
|
|
570
|
+
? options.partitionBy.column.join(', ')
|
|
571
|
+
: options?.partitionBy?.column;
|
|
572
|
+
// Handle INTERVAL partitioning
|
|
573
|
+
if (partitionType === 'interval' && options?.partitionBy?.interval) {
|
|
574
|
+
sql += ` INTERVAL (${options.partitionBy.interval})`;
|
|
575
|
+
}
|
|
576
|
+
sql += ` (${this.quoteIdentifier(partitionColumn)}) (`;
|
|
577
|
+
if (options?.partitions) {
|
|
578
|
+
const partitionDefs = options.partitions.map((part) => {
|
|
579
|
+
let partDef = `PARTITION ${this.quoteIdentifier(part.name)}`;
|
|
580
|
+
if (part.bound) {
|
|
581
|
+
if ('values' in part.bound && part.bound.values) {
|
|
582
|
+
// LIST partition
|
|
583
|
+
const values = part.bound.values.map((v) => (typeof v === 'string' ? `'${this.escapeString(String(v))}'` : v)).join(', ');
|
|
584
|
+
partDef += ` VALUES (${values})`;
|
|
585
|
+
}
|
|
586
|
+
else if ('modulus' in part.bound && part.bound.modulus !== undefined) {
|
|
587
|
+
// HASH partition
|
|
588
|
+
const hashBound = part.bound;
|
|
589
|
+
partDef += ` VALUES (MODULUS ${hashBound.modulus}, REMAINDER ${hashBound.remainder})`;
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
// RANGE partition
|
|
593
|
+
const rangeBound = part.bound;
|
|
594
|
+
partDef += ` VALUES LESS THAN (${rangeBound.from})`;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (part.tablespace) {
|
|
598
|
+
partDef += ` TABLESPACE ${part.tablespace}`;
|
|
599
|
+
}
|
|
600
|
+
if (part.compression) {
|
|
601
|
+
partDef += ` COMPRESS`;
|
|
602
|
+
}
|
|
603
|
+
if (part.storageParameters) {
|
|
604
|
+
const params = Object.entries(part.storageParameters)
|
|
605
|
+
.map(([key, value]) => `${key} = ${value}`)
|
|
606
|
+
.join(', ');
|
|
607
|
+
partDef += ` STORAGE (${params})`;
|
|
608
|
+
}
|
|
609
|
+
return partDef;
|
|
610
|
+
});
|
|
611
|
+
sql += partitionDefs.join(', ');
|
|
612
|
+
}
|
|
613
|
+
sql += ')';
|
|
614
|
+
// Handle subpartitions
|
|
615
|
+
if (options?.subpartitionBy) {
|
|
616
|
+
sql += ` SUBPARTITION BY ${options.subpartitionBy.type.toUpperCase()}`;
|
|
617
|
+
const subpartitionColumn = Array.isArray(options.subpartitionBy.column)
|
|
618
|
+
? options.subpartitionBy.column.join(', ')
|
|
619
|
+
: options.subpartitionBy.column;
|
|
620
|
+
sql += ` (${this.quoteIdentifier(subpartitionColumn)}) (`;
|
|
621
|
+
if (options?.subpartitions) {
|
|
622
|
+
const subpartitionDefs = options.subpartitions.map((part) => {
|
|
623
|
+
let partDef = `SUBPARTITION ${this.quoteIdentifier(part.name)}`;
|
|
624
|
+
if (part.bound) {
|
|
625
|
+
if ('values' in part.bound && part.bound.values) {
|
|
626
|
+
const values = part.bound.values.map((v) => (typeof v === 'string' ? `'${this.escapeString(String(v))}'` : v)).join(', ');
|
|
627
|
+
partDef += ` VALUES (${values})`;
|
|
628
|
+
}
|
|
629
|
+
else if ('modulus' in part.bound && part.bound.modulus !== undefined) {
|
|
630
|
+
const hashBound = part.bound;
|
|
631
|
+
partDef += ` VALUES (MODULUS ${hashBound.modulus}, REMAINDER ${hashBound.remainder})`;
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
const rangeBound = part.bound;
|
|
635
|
+
partDef += ` VALUES LESS THAN (${rangeBound.from})`;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (part.tablespace) {
|
|
639
|
+
partDef += ` TABLESPACE ${part.tablespace}`;
|
|
640
|
+
}
|
|
641
|
+
return partDef;
|
|
642
|
+
});
|
|
643
|
+
sql += subpartitionDefs.join(', ');
|
|
644
|
+
}
|
|
645
|
+
sql += ')';
|
|
646
|
+
}
|
|
647
|
+
await this.query(sql);
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Create partition
|
|
651
|
+
* Supports subpartitions in Oracle
|
|
652
|
+
*/
|
|
653
|
+
async createPartition(options) {
|
|
654
|
+
let sql = `ALTER TABLE ${this.quoteTable(options.parentTable)} ADD PARTITION ${this.quoteIdentifier(options.name)}`;
|
|
655
|
+
if (options.bound) {
|
|
656
|
+
if ('values' in options.bound && options.bound.values) {
|
|
657
|
+
const values = options.bound.values.map((v) => (typeof v === 'string' ? `'${this.escapeString(String(v))}'` : v)).join(', ');
|
|
658
|
+
sql += ` VALUES (${values})`;
|
|
659
|
+
}
|
|
660
|
+
else if ('modulus' in options.bound && options.bound.modulus !== undefined) {
|
|
661
|
+
const hashBound = options.bound;
|
|
662
|
+
sql += ` VALUES (MODULUS ${hashBound.modulus}, REMAINDER ${hashBound.remainder})`;
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
const rangeBound = options.bound;
|
|
666
|
+
sql += ` VALUES LESS THAN (${rangeBound.from})`;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (options.tablespace) {
|
|
670
|
+
sql += ` TABLESPACE ${options.tablespace}`;
|
|
671
|
+
}
|
|
672
|
+
if (options.compression) {
|
|
673
|
+
sql += ` COMPRESS`;
|
|
674
|
+
}
|
|
675
|
+
if (options.storageParameters) {
|
|
676
|
+
const params = Object.entries(options.storageParameters)
|
|
677
|
+
.map(([key, value]) => `${key} = ${value}`)
|
|
678
|
+
.join(', ');
|
|
679
|
+
sql += ` STORAGE (${params})`;
|
|
680
|
+
}
|
|
681
|
+
// Handle subpartitions
|
|
682
|
+
if (options?.subpartitionBy) {
|
|
683
|
+
sql += ` SUBPARTITION BY ${options.subpartitionBy.type.toUpperCase()}`;
|
|
684
|
+
const subpartitionColumn = Array.isArray(options.subpartitionBy.column)
|
|
685
|
+
? options.subpartitionBy.column.join(', ')
|
|
686
|
+
: options.subpartitionBy.column;
|
|
687
|
+
sql += ` (${this.quoteIdentifier(subpartitionColumn)}) (`;
|
|
688
|
+
if (options?.subpartitions) {
|
|
689
|
+
const subpartitionDefs = options.subpartitions.map((part) => {
|
|
690
|
+
let partDef = `SUBPARTITION ${this.quoteIdentifier(part.name)}`;
|
|
691
|
+
if (part.bound) {
|
|
692
|
+
if ('values' in part.bound && part.bound.values) {
|
|
693
|
+
const values = part.bound.values.map((v) => (typeof v === 'string' ? `'${this.escapeString(String(v))}'` : v)).join(', ');
|
|
694
|
+
partDef += ` VALUES (${values})`;
|
|
695
|
+
}
|
|
696
|
+
else if ('modulus' in part.bound && part.bound.modulus !== undefined) {
|
|
697
|
+
const hashBound = part.bound;
|
|
698
|
+
partDef += ` VALUES (MODULUS ${hashBound.modulus}, REMAINDER ${hashBound.remainder})`;
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
const rangeBound = part.bound;
|
|
702
|
+
partDef += ` VALUES LESS THAN (${rangeBound.from})`;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (part.tablespace) {
|
|
706
|
+
partDef += ` TABLESPACE ${part.tablespace}`;
|
|
707
|
+
}
|
|
708
|
+
return partDef;
|
|
709
|
+
});
|
|
710
|
+
sql += subpartitionDefs.join(', ');
|
|
711
|
+
}
|
|
712
|
+
sql += ')';
|
|
713
|
+
}
|
|
714
|
+
await this.query(sql);
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* "Attach" a partition (Oracle has no PostgreSQL-style `ATTACH PARTITION`
|
|
718
|
+
* statement). The real Oracle equivalent for bringing an existing
|
|
719
|
+
* standalone table's data into a partitioned table's partition is
|
|
720
|
+
* `ALTER TABLE ... EXCHANGE PARTITION ... WITH TABLE ...`, which swaps the
|
|
721
|
+
* data segment of the partition with that of the standalone table as a
|
|
722
|
+
* fast, DDL-only (no data movement) operation.
|
|
723
|
+
*
|
|
724
|
+
* `options.exchangeTable` names the standalone table whose data becomes
|
|
725
|
+
* the partition's data; it defaults to `options.partitionName` for
|
|
726
|
+
* backwards compatibility with callers that used the partition name and
|
|
727
|
+
* the exchange-table name interchangeably.
|
|
728
|
+
*/
|
|
729
|
+
async attachPartition(options) {
|
|
730
|
+
const exchangeTable = options.exchangeTable || options.partitionName;
|
|
731
|
+
let sql = `ALTER TABLE ${this.quoteTable(options.parentTable)} EXCHANGE PARTITION ${this.quoteIdentifier(options.partitionName)} WITH TABLE ${this.quoteTable(exchangeTable)}`;
|
|
732
|
+
if (options.includingIndexes) {
|
|
733
|
+
sql += ' INCLUDING INDEXES';
|
|
734
|
+
}
|
|
735
|
+
sql += ` ${options.validation || 'WITH VALIDATION'}`;
|
|
736
|
+
await this.query(sql);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* "Detach" a partition (Oracle has no PostgreSQL-style `DETACH PARTITION`
|
|
740
|
+
* statement). The real Oracle equivalent for extracting a partition's data
|
|
741
|
+
* out into a standalone table is the same `EXCHANGE PARTITION` statement
|
|
742
|
+
* used by {@link attachPartition} - it symmetrically swaps segments, so
|
|
743
|
+
* "detaching" is exchanging the partition with an (often empty) standalone
|
|
744
|
+
* table, leaving that table holding the former partition's data.
|
|
745
|
+
*/
|
|
746
|
+
async detachPartition(options) {
|
|
747
|
+
const exchangeTable = options.exchangeTable || options.partitionName;
|
|
748
|
+
let sql = `ALTER TABLE ${this.quoteTable(options.parentTable)} EXCHANGE PARTITION ${this.quoteIdentifier(options.partitionName)} WITH TABLE ${this.quoteTable(exchangeTable)}`;
|
|
749
|
+
if (options.includingIndexes) {
|
|
750
|
+
sql += ' INCLUDING INDEXES';
|
|
751
|
+
}
|
|
752
|
+
sql += options.validate === false ? ' WITHOUT VALIDATION' : ' WITH VALIDATION';
|
|
753
|
+
await this.query(sql);
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Drop partition
|
|
757
|
+
* Enhanced with cascade option for Oracle
|
|
758
|
+
*/
|
|
759
|
+
async dropPartition(partitionName, options) {
|
|
760
|
+
let sql = 'ALTER TABLE';
|
|
761
|
+
if (options?.ifExists) {
|
|
762
|
+
sql += ' IF EXISTS';
|
|
763
|
+
}
|
|
764
|
+
sql += ` ${this.quoteTable(partitionName)} DROP PARTITION`;
|
|
765
|
+
if (options?.cascade) {
|
|
766
|
+
sql += ' CASCADE';
|
|
767
|
+
}
|
|
768
|
+
if (options?.updateIndexes) {
|
|
769
|
+
sql += ' UPDATE INDEXES';
|
|
770
|
+
}
|
|
771
|
+
await this.query(sql);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Add a partition to an existing partitioned table (Oracle)
|
|
775
|
+
* @param tableName - Name of the partitioned table
|
|
776
|
+
* @param partitionName - Name for the new partition
|
|
777
|
+
* @param partitionSpec - Partition specification
|
|
778
|
+
*/
|
|
779
|
+
async addPartition(tableName, partitionName, partitionSpec) {
|
|
780
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD PARTITION ${this.escapeId(partitionName)}`;
|
|
781
|
+
if (partitionSpec.values) {
|
|
782
|
+
sql += ` VALUES ${partitionSpec.values}`;
|
|
783
|
+
}
|
|
784
|
+
else if (partitionSpec.forValues) {
|
|
785
|
+
sql += ` VALUES LESS THAN ${partitionSpec.forValues}`;
|
|
786
|
+
}
|
|
787
|
+
await this.query(sql);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* List partitions for a table with details
|
|
791
|
+
*/
|
|
792
|
+
async listPartitions(tableName) {
|
|
793
|
+
const sql = `
|
|
794
|
+
SELECT
|
|
795
|
+
PARTITION_NAME as partitionName,
|
|
796
|
+
PARTITION_POSITION as partitionPosition,
|
|
797
|
+
HIGH_VALUE as highValue,
|
|
798
|
+
PARTITION_TYPE as partitionType,
|
|
799
|
+
TABLESPACE_NAME as tablespaceName,
|
|
800
|
+
COMPRESSION as compression,
|
|
801
|
+
NUM_ROWS as numRows
|
|
802
|
+
FROM USER_TAB_PARTITIONS
|
|
803
|
+
WHERE TABLE_NAME = :tableName
|
|
804
|
+
ORDER BY PARTITION_POSITION
|
|
805
|
+
`;
|
|
806
|
+
const result = await this.query(sql, { replacements: { tableName: tableName.toUpperCase() }, raw: true });
|
|
807
|
+
return result.rows.map((row) => ({
|
|
808
|
+
partitionName: row.partitionName,
|
|
809
|
+
partitionPosition: row.partitionPosition,
|
|
810
|
+
highValue: row.highValue,
|
|
811
|
+
partitionType: row.partitionType,
|
|
812
|
+
tablespaceName: row.tablespaceName,
|
|
813
|
+
compression: row.compression,
|
|
814
|
+
numRows: row.numRows,
|
|
815
|
+
}));
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Modify partition (move, compress)
|
|
819
|
+
*/
|
|
820
|
+
async modifyPartition(tableName, partitionName, options) {
|
|
821
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} MODIFY PARTITION ${this.quoteIdentifier(partitionName)}`;
|
|
822
|
+
const actions = [];
|
|
823
|
+
if (options?.move && options?.tablespace) {
|
|
824
|
+
actions.push(`MOVE TABLESPACE ${options.tablespace}`);
|
|
825
|
+
}
|
|
826
|
+
if (options?.compress) {
|
|
827
|
+
actions.push('COMPRESS');
|
|
828
|
+
}
|
|
829
|
+
if (options?.storageParameters) {
|
|
830
|
+
const params = Object.entries(options.storageParameters)
|
|
831
|
+
.map(([key, value]) => `${key} = ${value}`)
|
|
832
|
+
.join(', ');
|
|
833
|
+
actions.push(`STORAGE (${params})`);
|
|
834
|
+
}
|
|
835
|
+
if (options?.updateIndexes) {
|
|
836
|
+
actions.push('UPDATE INDEXES');
|
|
837
|
+
}
|
|
838
|
+
if (actions.length > 0) {
|
|
839
|
+
sql += ' ' + actions.join(' ');
|
|
840
|
+
}
|
|
841
|
+
await this.query(sql);
|
|
842
|
+
}
|
|
843
|
+
// ==================== Foreign Data Wrapper Stubs ====================
|
|
844
|
+
// Oracle uses Data Guard for external data connections, not FDW
|
|
845
|
+
/**
|
|
846
|
+
* Create foreign data wrapper (stub)
|
|
847
|
+
* Oracle uses Data Guard instead of FDW
|
|
848
|
+
*/
|
|
849
|
+
async createForeignDataWrapper(_fdwName, _options) {
|
|
850
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Drop foreign data wrapper (stub)
|
|
854
|
+
* Oracle uses Data Guard instead of FDW
|
|
855
|
+
*/
|
|
856
|
+
async dropForeignDataWrapper(_fdwName, _options) {
|
|
857
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Create foreign server (stub)
|
|
861
|
+
* Oracle uses Data Guard instead of FDW
|
|
862
|
+
*/
|
|
863
|
+
async createForeignServer(_serverName, _fdwName, _options) {
|
|
864
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Drop foreign server (stub)
|
|
868
|
+
* Oracle uses Data Guard instead of FDW
|
|
869
|
+
*/
|
|
870
|
+
async dropForeignServer(_serverName, _options) {
|
|
871
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Create foreign table (stub)
|
|
875
|
+
* Oracle uses Data Guard instead of FDW
|
|
876
|
+
*/
|
|
877
|
+
async createForeignTable(_tableName, _columns, _options) {
|
|
878
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Create user mapping (stub)
|
|
882
|
+
* Oracle uses Data Guard instead of FDW
|
|
883
|
+
*/
|
|
884
|
+
async createUserMapping(_serverName, _userName, _options) {
|
|
885
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Drop user mapping (stub)
|
|
889
|
+
* Oracle uses Data Guard instead of FDW
|
|
890
|
+
*/
|
|
891
|
+
async dropUserMapping(_serverName, _userName, _options) {
|
|
892
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
893
|
+
}
|
|
894
|
+
async changeOwner(_newOwner, _tableName) {
|
|
895
|
+
throw new Error('changeOwner is not supported in Oracle');
|
|
896
|
+
}
|
|
897
|
+
async addConstraint(_tableName, _options) {
|
|
898
|
+
throw new Error('addConstraint is not supported in Oracle');
|
|
899
|
+
}
|
|
900
|
+
async removeConstraint(_tableName, _constraintName) {
|
|
901
|
+
throw new Error('removeConstraint is not supported in Oracle');
|
|
902
|
+
}
|
|
903
|
+
async createSecurityPolicy(_policyName, _tableName, _options) {
|
|
904
|
+
throw new Error('Security policies are not supported in Oracle');
|
|
905
|
+
}
|
|
906
|
+
async dropSecurityPolicy(_policyName, _tableName) {
|
|
907
|
+
throw new Error('Security policies are not supported in Oracle');
|
|
908
|
+
}
|
|
909
|
+
// ==================== View Operations ====================
|
|
910
|
+
/**
|
|
911
|
+
* Create a view
|
|
912
|
+
*/
|
|
913
|
+
async createView(viewName, query, options) {
|
|
914
|
+
const sql = options?.replace
|
|
915
|
+
? `CREATE OR REPLACE VIEW ${this.quoteIdentifier(viewName)} AS ${query}`
|
|
916
|
+
: `CREATE VIEW ${this.quoteIdentifier(viewName)} AS ${query}`;
|
|
917
|
+
await this.query(sql);
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Drop a view
|
|
921
|
+
*/
|
|
922
|
+
async dropView(viewName, options) {
|
|
923
|
+
const sql = options?.ifExists
|
|
924
|
+
? `DROP VIEW ${this.quoteIdentifier(viewName)}`
|
|
925
|
+
: `DROP VIEW ${this.quoteIdentifier(viewName)} CASCADE`;
|
|
926
|
+
await this.query(sql);
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Show all views
|
|
930
|
+
*/
|
|
931
|
+
async showViews() {
|
|
932
|
+
const result = await this.query(`SELECT VIEW_NAME FROM USER_VIEWS ORDER BY VIEW_NAME`);
|
|
933
|
+
return result.rows.map((row) => row.VIEW_NAME);
|
|
934
|
+
}
|
|
935
|
+
// ==================== Materialized View Operations ====================
|
|
936
|
+
/**
|
|
937
|
+
* Create a materialized view
|
|
938
|
+
* Oracle syntax:
|
|
939
|
+
* CREATE MATERIALIZED VIEW mv_name
|
|
940
|
+
* BUILD [IMMEDIATE | DEFERRED]
|
|
941
|
+
* REFRESH [FAST | COMPLETE | FORCE]
|
|
942
|
+
* ON [COMMIT | DEMAND]
|
|
943
|
+
* AS query
|
|
944
|
+
*/
|
|
945
|
+
async createMaterializedView(options) {
|
|
946
|
+
const viewNameWithSchema = options.schema
|
|
947
|
+
? `${this.quoteIdentifier(options.schema)}.${this.quoteIdentifier(options.name)}`
|
|
948
|
+
: this.quoteIdentifier(options.name);
|
|
949
|
+
let sql = 'CREATE MATERIALIZED VIEW';
|
|
950
|
+
// Note: Oracle doesn't support OR REPLACE or IF NOT EXISTS for materialized views
|
|
951
|
+
// We need to handle these differently
|
|
952
|
+
if (options.replace) {
|
|
953
|
+
// Check if exists and drop first
|
|
954
|
+
const exists = await this.hasMaterializedView(options.name, options.schema);
|
|
955
|
+
if (exists) {
|
|
956
|
+
await this.dropMaterializedView(options.name, { schema: options.schema });
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
sql += ` ${viewNameWithSchema}`;
|
|
960
|
+
// BUILD clause
|
|
961
|
+
if (options.build) {
|
|
962
|
+
sql += ` BUILD ${options.build}`;
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
sql += ' BUILD IMMEDIATE'; // Default for Oracle
|
|
966
|
+
}
|
|
967
|
+
sql += ` AS ${options.query}`;
|
|
968
|
+
// REFRESH clause
|
|
969
|
+
if (options.refresh) {
|
|
970
|
+
sql += ` REFRESH ${options.refresh}`;
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
sql += ' REFRESH FORCE'; // Default for Oracle
|
|
974
|
+
}
|
|
975
|
+
// ON clause
|
|
976
|
+
if (options.on) {
|
|
977
|
+
sql += ` ON ${options.on}`;
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
sql += ' ON DEMAND'; // Default for Oracle
|
|
981
|
+
}
|
|
982
|
+
await this.query(sql);
|
|
983
|
+
// Add comment if provided
|
|
984
|
+
if (options.comment) {
|
|
985
|
+
await this.commentMaterializedView(options.name, options.comment, options.schema);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Refresh a materialized view
|
|
990
|
+
* Oracle supports: FAST, COMPLETE, FORCE
|
|
991
|
+
* - FAST: Incremental refresh using materialized view logs
|
|
992
|
+
* - COMPLETE: Full refresh (recreates the entire view)
|
|
993
|
+
* - FORCE: Tries FAST, falls back to COMPLETE if not possible
|
|
994
|
+
*/
|
|
995
|
+
async refreshMaterializedView(viewName, options) {
|
|
996
|
+
// Oracle has no `REFRESH MATERIALIZED VIEW` DDL/DML statement (that
|
|
997
|
+
// syntax is Postgres's) - materialized views are refreshed by calling
|
|
998
|
+
// the DBMS_MVIEW.REFRESH PL/SQL package, which takes the (schema-
|
|
999
|
+
// qualified, unquoted) view name as a plain string argument, not a SQL
|
|
1000
|
+
// identifier.
|
|
1001
|
+
const schema = options?.schema;
|
|
1002
|
+
const viewNameWithSchema = schema ? `${schema}.${viewName}` : viewName;
|
|
1003
|
+
let refreshType = 'FORCE'; // Default
|
|
1004
|
+
// Handle both Oracle refresh method and PostgreSQL concurrently option
|
|
1005
|
+
if (options?.refresh) {
|
|
1006
|
+
refreshType = options.refresh;
|
|
1007
|
+
}
|
|
1008
|
+
else if (options?.concurrently) {
|
|
1009
|
+
// CONCURRENTLY not supported in Oracle, use FORCE instead
|
|
1010
|
+
refreshType = 'FORCE';
|
|
1011
|
+
}
|
|
1012
|
+
// DBMS_MVIEW.REFRESH's method parameter is a single-character code:
|
|
1013
|
+
// 'C' = complete, 'F' = fast, '?' = force (let Oracle decide).
|
|
1014
|
+
const methodCode = refreshType === 'COMPLETE' ? 'C' : refreshType === 'FAST' ? 'F' : '?';
|
|
1015
|
+
const sql = `BEGIN DBMS_MVIEW.REFRESH(:1, :2); END;`;
|
|
1016
|
+
await this.query(sql, { replacements: [viewNameWithSchema, methodCode] });
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Drop a materialized view
|
|
1020
|
+
*/
|
|
1021
|
+
async dropMaterializedView(viewName, options) {
|
|
1022
|
+
const schema = options?.schema;
|
|
1023
|
+
const viewNameWithSchema = schema
|
|
1024
|
+
? `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(viewName)}`
|
|
1025
|
+
: this.quoteIdentifier(viewName);
|
|
1026
|
+
let sql = `DROP MATERIALIZED VIEW`;
|
|
1027
|
+
if (options?.ifExists) {
|
|
1028
|
+
sql += ' IF EXISTS';
|
|
1029
|
+
}
|
|
1030
|
+
sql += ` ${viewNameWithSchema}`;
|
|
1031
|
+
if (options?.cascade) {
|
|
1032
|
+
sql += ' CASCADE';
|
|
1033
|
+
}
|
|
1034
|
+
await this.query(sql);
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Show all materialized views
|
|
1038
|
+
*/
|
|
1039
|
+
async showMaterializedViews() {
|
|
1040
|
+
const result = await this.query(`SELECT MVIEW_NAME FROM USER_MVIEWS ORDER BY MVIEW_NAME`);
|
|
1041
|
+
return result.rows.map((row) => row.MVIEW_NAME);
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Check if a materialized view exists
|
|
1045
|
+
* @param viewName - Name of the materialized view
|
|
1046
|
+
* @param schema - Optional schema name
|
|
1047
|
+
* @returns True if the materialized view exists
|
|
1048
|
+
*/
|
|
1049
|
+
async hasMaterializedView(viewName, schema) {
|
|
1050
|
+
let sql = `SELECT 1 FROM USER_MVIEWS WHERE MVIEW_NAME = ${this.escape(viewName)}`;
|
|
1051
|
+
if (schema) {
|
|
1052
|
+
sql += ` AND OWNER = ${this.escape(schema.toUpperCase())}`;
|
|
1053
|
+
}
|
|
1054
|
+
const result = await this.query(sql, { raw: true });
|
|
1055
|
+
return result.rows?.length > 0;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Add comment to materialized view
|
|
1059
|
+
*/
|
|
1060
|
+
async commentMaterializedView(viewName, comment, schema) {
|
|
1061
|
+
const viewNameWithSchema = schema
|
|
1062
|
+
? `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(viewName)}`
|
|
1063
|
+
: this.quoteIdentifier(viewName);
|
|
1064
|
+
const sql = `COMMENT ON MATERIALIZED VIEW ${viewNameWithSchema} IS ${this.escapeString(comment)}`;
|
|
1065
|
+
await this.query(sql);
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Create a materialized view log on a master table.
|
|
1069
|
+
*
|
|
1070
|
+
* FAST (incremental) refresh - as used by `refreshMaterializedView()` -
|
|
1071
|
+
* requires a materialized view log on every master table referenced by the
|
|
1072
|
+
* materialized view; without one, `DBMS_MVIEW.REFRESH(..., 'F')` fails with
|
|
1073
|
+
* ORA-23413. This creates that log.
|
|
1074
|
+
*
|
|
1075
|
+
* Oracle syntax:
|
|
1076
|
+
* ```sql
|
|
1077
|
+
* CREATE MATERIALIZED VIEW LOG ON table_name
|
|
1078
|
+
* WITH [ROWID] [, PRIMARY KEY] [, SEQUENCE] [(col1, col2, ...)] [INCLUDING NEW VALUES]
|
|
1079
|
+
* ```
|
|
1080
|
+
*/
|
|
1081
|
+
async createMaterializedViewLog(tableName, options) {
|
|
1082
|
+
const tableNameWithSchema = this.quoteTable(tableName, options?.schema);
|
|
1083
|
+
let sql = `CREATE MATERIALIZED VIEW LOG ON ${tableNameWithSchema}`;
|
|
1084
|
+
const withParts = [];
|
|
1085
|
+
if (options?.withRowid) {
|
|
1086
|
+
withParts.push('ROWID');
|
|
1087
|
+
}
|
|
1088
|
+
if (options?.withPrimaryKey) {
|
|
1089
|
+
withParts.push('PRIMARY KEY');
|
|
1090
|
+
}
|
|
1091
|
+
if (options?.sequence) {
|
|
1092
|
+
withParts.push('SEQUENCE');
|
|
1093
|
+
}
|
|
1094
|
+
// Oracle's own default (when neither ROWID nor PRIMARY KEY is
|
|
1095
|
+
// requested) is PRIMARY KEY - mirror that here so the generated SQL is
|
|
1096
|
+
// always explicit rather than relying on server-side defaulting. A bare
|
|
1097
|
+
// filter-column list is not valid on its own (Oracle requires at least
|
|
1098
|
+
// one of ROWID/PRIMARY KEY/SEQUENCE to precede it), so this also covers
|
|
1099
|
+
// the case where only `columns` was passed.
|
|
1100
|
+
if (withParts.length === 0) {
|
|
1101
|
+
withParts.push('PRIMARY KEY');
|
|
1102
|
+
}
|
|
1103
|
+
sql += ` WITH ${withParts.join(', ')}`;
|
|
1104
|
+
if (options?.columns && options.columns.length > 0) {
|
|
1105
|
+
const columnList = options.columns.map((c) => this.quoteIdentifier(c)).join(', ');
|
|
1106
|
+
sql += ` (${columnList})`;
|
|
1107
|
+
}
|
|
1108
|
+
// INCLUDING NEW VALUES defaults to true: required for fast refresh of
|
|
1109
|
+
// materialized views containing aggregate functions, and harmless
|
|
1110
|
+
// otherwise.
|
|
1111
|
+
sql += options?.includingNewValues === false ? ' EXCLUDING NEW VALUES' : ' INCLUDING NEW VALUES';
|
|
1112
|
+
await this.query(sql);
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Drop a materialized view log from a master table.
|
|
1116
|
+
* Oracle syntax: `DROP MATERIALIZED VIEW LOG ON table_name`
|
|
1117
|
+
* (Oracle has no `IF EXISTS` variant for this statement.)
|
|
1118
|
+
*/
|
|
1119
|
+
async dropMaterializedViewLog(tableName, options) {
|
|
1120
|
+
const tableNameWithSchema = this.quoteTable(tableName, options?.schema);
|
|
1121
|
+
const sql = `DROP MATERIALIZED VIEW LOG ON ${tableNameWithSchema}`;
|
|
1122
|
+
await this.query(sql);
|
|
1123
|
+
}
|
|
1124
|
+
// ==================== Column Operations ====================
|
|
1125
|
+
/**
|
|
1126
|
+
* Add a column
|
|
1127
|
+
*/
|
|
1128
|
+
async addColumn(tableName, columnName, definition) {
|
|
1129
|
+
const sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD ${this.buildColumnDefinition(columnName, definition)}`;
|
|
1130
|
+
await this.query(sql);
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Remove a column
|
|
1134
|
+
*/
|
|
1135
|
+
async removeColumn(tableName, columnName) {
|
|
1136
|
+
const sql = `ALTER TABLE ${this.quoteTable(tableName)} DROP COLUMN ${this.quoteIdentifier(columnName)}`;
|
|
1137
|
+
await this.query(sql);
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Change a column
|
|
1141
|
+
*/
|
|
1142
|
+
async changeColumn(tableName, columnName, definition) {
|
|
1143
|
+
const sql = `ALTER TABLE ${this.quoteTable(tableName)} MODIFY ${this.buildColumnDefinition(columnName, definition)}`;
|
|
1144
|
+
await this.query(sql);
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Describe a table
|
|
1148
|
+
*/
|
|
1149
|
+
async describeTable(tableName) {
|
|
1150
|
+
const result = await this.query(`SELECT COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE, DATA_DEFAULT, COLUMN_ID
|
|
1151
|
+
FROM USER_TAB_COLUMNS
|
|
1152
|
+
WHERE TABLE_NAME = UPPER('${tableName}')
|
|
1153
|
+
ORDER BY COLUMN_ID`);
|
|
1154
|
+
const description = {};
|
|
1155
|
+
for (const row of result.rows) {
|
|
1156
|
+
description[row.COLUMN_NAME.toLowerCase()] = {
|
|
1157
|
+
type: row.DATA_TYPE,
|
|
1158
|
+
allowNull: row.NULLABLE === 'Y',
|
|
1159
|
+
defaultValue: row.DATA_DEFAULT,
|
|
1160
|
+
primaryKey: false,
|
|
1161
|
+
autoIncrement: false,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
// Check for primary keys
|
|
1165
|
+
const pkResult = await this.query(`SELECT COLUMN_NAME FROM USER_CONS_COLUMNS
|
|
1166
|
+
WHERE TABLE_NAME = UPPER('${tableName}')
|
|
1167
|
+
AND CONSTRAINT_NAME IN (
|
|
1168
|
+
SELECT CONSTRAINT_NAME FROM USER_CONSTRAINTS
|
|
1169
|
+
WHERE CONSTRAINT_TYPE = 'P'
|
|
1170
|
+
)`);
|
|
1171
|
+
for (const row of pkResult.rows) {
|
|
1172
|
+
const colName = row.COLUMN_NAME.toLowerCase();
|
|
1173
|
+
if (description[colName]) {
|
|
1174
|
+
description[colName].primaryKey = true;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return description;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Rename a table
|
|
1181
|
+
*/
|
|
1182
|
+
async renameTable(oldName, newName) {
|
|
1183
|
+
const sql = `ALTER TABLE ${this.quoteTable(oldName)} RENAME TO ${this.quoteIdentifier(newName)}`;
|
|
1184
|
+
await this.query(sql);
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Show all tables
|
|
1188
|
+
*/
|
|
1189
|
+
async showTables() {
|
|
1190
|
+
const result = await this.query(`SELECT TABLE_NAME FROM USER_TABLES ORDER BY TABLE_NAME`);
|
|
1191
|
+
return result.rows.map((row) => row.TABLE_NAME);
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Get table status (Oracle implementation)
|
|
1195
|
+
*/
|
|
1196
|
+
async getTableStatus(tableName) {
|
|
1197
|
+
const sql = tableName
|
|
1198
|
+
? `SELECT TABLE_NAME, TABLE_TYPE FROM USER_TABLES WHERE TABLE_NAME = UPPER(:tableName)`
|
|
1199
|
+
: `SELECT TABLE_NAME, TABLE_TYPE FROM USER_TABLES`;
|
|
1200
|
+
const result = tableName
|
|
1201
|
+
? await this.query(sql, { bindings: { tableName } })
|
|
1202
|
+
: await this.query(sql);
|
|
1203
|
+
return result.rows;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Get table create statement (Oracle implementation)
|
|
1207
|
+
*/
|
|
1208
|
+
async getCreateTable(tableName) {
|
|
1209
|
+
const sql = `SELECT DBMS_METADATA.GET_DDL('TABLE', UPPER(:tableName)) AS CreateStatement FROM DUAL`;
|
|
1210
|
+
const result = await this.query(sql, { bindings: { tableName } });
|
|
1211
|
+
return result.rows[0]?.CreateStatement || '';
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Check if a table has partitions (Oracle implementation)
|
|
1215
|
+
*/
|
|
1216
|
+
async hasPartition(tableName) {
|
|
1217
|
+
const sql = `
|
|
1218
|
+
SELECT 1 FROM USER_TAB_PARTITIONS
|
|
1219
|
+
WHERE TABLE_NAME = UPPER(:tableName) AND ROWNUM = 1
|
|
1220
|
+
`;
|
|
1221
|
+
const result = await this.query(sql, { bindings: { tableName } });
|
|
1222
|
+
return result.rows.length > 0;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Show constraints for a table
|
|
1226
|
+
*/
|
|
1227
|
+
async showConstraints(tableName) {
|
|
1228
|
+
const sql = `
|
|
1229
|
+
SELECT
|
|
1230
|
+
c.constraint_name AS name,
|
|
1231
|
+
c.table_name AS tableName,
|
|
1232
|
+
c.constraint_type AS type,
|
|
1233
|
+
cc.column_name AS columnName
|
|
1234
|
+
FROM user_constraints c
|
|
1235
|
+
LEFT JOIN user_cons_columns cc
|
|
1236
|
+
ON c.constraint_name = cc.constraint_name
|
|
1237
|
+
WHERE c.table_name = '${tableName.toUpperCase()}'
|
|
1238
|
+
ORDER BY c.constraint_name
|
|
1239
|
+
`;
|
|
1240
|
+
const result = await this.query(sql);
|
|
1241
|
+
return result.rows;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Show indexes for a table
|
|
1245
|
+
*/
|
|
1246
|
+
async showIndexes(tableName) {
|
|
1247
|
+
const sql = `
|
|
1248
|
+
SELECT
|
|
1249
|
+
i.index_name AS name,
|
|
1250
|
+
i.table_name AS tableName,
|
|
1251
|
+
i.uniqueness AS uniqueness,
|
|
1252
|
+
ic.column_name AS columnName,
|
|
1253
|
+
ic.column_position AS columnPosition
|
|
1254
|
+
FROM user_indexes i
|
|
1255
|
+
LEFT JOIN user_ind_columns ic
|
|
1256
|
+
ON i.index_name = ic.index_name
|
|
1257
|
+
WHERE i.table_name = '${tableName.toUpperCase()}'
|
|
1258
|
+
ORDER BY i.index_name, ic.column_position
|
|
1259
|
+
`;
|
|
1260
|
+
const result = await this.query(sql);
|
|
1261
|
+
return result.rows;
|
|
1262
|
+
}
|
|
1263
|
+
// ==================== Index Operations ====================
|
|
1264
|
+
/**
|
|
1265
|
+
* Add an index
|
|
1266
|
+
*/
|
|
1267
|
+
async addIndex(tableName, indexName, fields, options) {
|
|
1268
|
+
const sql = this.buildCreateIndexSql(tableName, indexName, fields, options);
|
|
1269
|
+
await this.query(sql);
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Build CREATE INDEX SQL
|
|
1273
|
+
*/
|
|
1274
|
+
buildCreateIndexSql(tableName, indexName, fields, options) {
|
|
1275
|
+
const fieldsArr = fields || [];
|
|
1276
|
+
const fieldsSql = fieldsArr.map((f) => this.quoteIdentifier(f)).join(', ');
|
|
1277
|
+
let sql = `CREATE`;
|
|
1278
|
+
if (options?.unique) {
|
|
1279
|
+
sql += ' UNIQUE';
|
|
1280
|
+
}
|
|
1281
|
+
if (options?.type) {
|
|
1282
|
+
sql += ` ${options.type.toUpperCase()}`;
|
|
1283
|
+
}
|
|
1284
|
+
sql += ` INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteTable(tableName)} (${fieldsSql})`;
|
|
1285
|
+
if (options?.where) {
|
|
1286
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
1287
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1288
|
+
}
|
|
1289
|
+
return sql;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Remove an index
|
|
1293
|
+
*/
|
|
1294
|
+
async removeIndex(tableName, indexName) {
|
|
1295
|
+
const sql = `DROP INDEX ${this.quoteIdentifier(indexName)}`;
|
|
1296
|
+
await this.query(sql);
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Create an index with full options
|
|
1300
|
+
*/
|
|
1301
|
+
async createIndex(tableName, indexDef) {
|
|
1302
|
+
const sql = this.buildCreateIndexSql(tableName, indexDef.name, indexDef.fields, {
|
|
1303
|
+
unique: indexDef.unique,
|
|
1304
|
+
type: indexDef.type,
|
|
1305
|
+
using: indexDef.using,
|
|
1306
|
+
where: indexDef.where,
|
|
1307
|
+
});
|
|
1308
|
+
await this.query(sql);
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Drop an index
|
|
1312
|
+
*/
|
|
1313
|
+
async dropIndex(tableName, indexName, _options) {
|
|
1314
|
+
const sql = `DROP INDEX ${this.quoteIdentifier(indexName)}`;
|
|
1315
|
+
await this.query(sql);
|
|
1316
|
+
}
|
|
1317
|
+
// ==================== Constraint Operations ====================
|
|
1318
|
+
/**
|
|
1319
|
+
* Create a constraint
|
|
1320
|
+
*/
|
|
1321
|
+
async createConstraint(tableName, constraintDef) {
|
|
1322
|
+
const sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD ${this.buildConstraintDefinition(constraintDef)}`;
|
|
1323
|
+
await this.query(sql);
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Drop a constraint
|
|
1327
|
+
*/
|
|
1328
|
+
async dropConstraint(tableName, constraintName, options) {
|
|
1329
|
+
const sql = options?.ifExists
|
|
1330
|
+
? `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.quoteIdentifier(constraintName)}`
|
|
1331
|
+
: `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.quoteIdentifier(constraintName)} CASCADE`;
|
|
1332
|
+
await this.query(sql);
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Start a transaction
|
|
1336
|
+
*/
|
|
1337
|
+
async startTransaction(options) {
|
|
1338
|
+
const connection = await this.getConnection();
|
|
1339
|
+
if (options?.isolationLevel) {
|
|
1340
|
+
const level = options.isolationLevel.toUpperCase().trim();
|
|
1341
|
+
if (level === 'READ ONLY') {
|
|
1342
|
+
await connection.execute('SET TRANSACTION READ ONLY');
|
|
1343
|
+
}
|
|
1344
|
+
else if (OracleDialect.SUPPORTED_ISOLATION_LEVELS.has(level)) {
|
|
1345
|
+
await connection.execute(`SET TRANSACTION ISOLATION LEVEL ${level}`);
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
throw new errors_1.DatabaseError(`Oracle does not support isolation level '${options.isolationLevel}'. ` +
|
|
1349
|
+
`Oracle only supports READ COMMITTED and SERIALIZABLE (plus READ ONLY ` +
|
|
1350
|
+
`transactions), not the ANSI READ UNCOMMITTED/REPEATABLE READ levels.`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return new OracleTransaction(connection, options);
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Commit a transaction
|
|
1357
|
+
*/
|
|
1358
|
+
// ==================== Savepoint Methods ====================
|
|
1359
|
+
/**
|
|
1360
|
+
* Generate SQL for creating a savepoint
|
|
1361
|
+
*/
|
|
1362
|
+
createSavepointSQL(name) {
|
|
1363
|
+
const savepointName = name || `sp_${Date.now()}`;
|
|
1364
|
+
return `SAVEPOINT ${savepointName}`;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Generate SQL for releasing a savepoint
|
|
1368
|
+
*/
|
|
1369
|
+
releaseSavepointSQL(name) {
|
|
1370
|
+
return `RELEASE SAVEPOINT ${name}`;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Generate SQL for rolling back to a savepoint
|
|
1374
|
+
*/
|
|
1375
|
+
rollbackToSavepointSQL(name) {
|
|
1376
|
+
return `ROLLBACK TO SAVEPOINT ${name}`;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Commit a transaction
|
|
1380
|
+
*/
|
|
1381
|
+
async commitTransaction(transaction) {
|
|
1382
|
+
const oracleTx = transaction;
|
|
1383
|
+
if (oracleTx.connection) {
|
|
1384
|
+
await oracleTx.connection.commit();
|
|
1385
|
+
await oracleTx.connection.close();
|
|
1386
|
+
oracleTx.finished = true;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Rollback a transaction
|
|
1391
|
+
*/
|
|
1392
|
+
async rollbackTransaction(transaction) {
|
|
1393
|
+
const oracleTx = transaction;
|
|
1394
|
+
if (oracleTx.connection) {
|
|
1395
|
+
await oracleTx.connection.rollback();
|
|
1396
|
+
await oracleTx.connection.close();
|
|
1397
|
+
oracleTx.finished = true;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
// ==================== Data Type Mapping ====================
|
|
1401
|
+
/**
|
|
1402
|
+
* Get SQL for a data type
|
|
1403
|
+
*/
|
|
1404
|
+
getDataTypeSql(dataType) {
|
|
1405
|
+
if (typeof dataType === 'string') {
|
|
1406
|
+
return this.mapDataType(dataType);
|
|
1407
|
+
}
|
|
1408
|
+
const dt = dataType;
|
|
1409
|
+
return this.mapDataType(dataType.key, dt);
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Map ORM data type to Oracle data type
|
|
1413
|
+
*
|
|
1414
|
+
* `attrs` carries the optional `length`/`precision`/`scale` fields present
|
|
1415
|
+
* on types like `DataTypeString` and `DataTypeNumber`/`DataTypeDecimal`
|
|
1416
|
+
* (see `src/types/index.ts`) so that, e.g., `STRING(4000)` correctly emits
|
|
1417
|
+
* `VARCHAR2(4000)` and `DECIMAL(18,4)` emits `NUMBER(18,4)` instead of
|
|
1418
|
+
* silently falling back to the default sizes.
|
|
1419
|
+
*/
|
|
1420
|
+
mapDataType(typeKey, attrs) {
|
|
1421
|
+
const key = typeKey.toUpperCase();
|
|
1422
|
+
const length = attrs?.length;
|
|
1423
|
+
const precision = attrs?.precision;
|
|
1424
|
+
const scale = attrs?.scale;
|
|
1425
|
+
switch (key) {
|
|
1426
|
+
case 'STRING':
|
|
1427
|
+
return `VARCHAR2(${length ?? 255})`;
|
|
1428
|
+
case 'CHAR':
|
|
1429
|
+
return `CHAR(${length ?? 1})`;
|
|
1430
|
+
case 'TINYTEXT':
|
|
1431
|
+
return `VARCHAR2(${length ?? 255})`;
|
|
1432
|
+
case 'NUMBER':
|
|
1433
|
+
case 'DECIMAL':
|
|
1434
|
+
case 'NUMERIC':
|
|
1435
|
+
if (precision !== undefined && scale !== undefined) {
|
|
1436
|
+
return `NUMBER(${precision},${scale})`;
|
|
1437
|
+
}
|
|
1438
|
+
if (precision !== undefined) {
|
|
1439
|
+
return `NUMBER(${precision})`;
|
|
1440
|
+
}
|
|
1441
|
+
return 'NUMBER';
|
|
1442
|
+
case 'INTEGER':
|
|
1443
|
+
return `NUMBER(${length ?? 10})`;
|
|
1444
|
+
case 'BIGINT':
|
|
1445
|
+
return `NUMBER(${length ?? 19})`;
|
|
1446
|
+
case 'BINARY':
|
|
1447
|
+
return `RAW(${length ?? 255})`;
|
|
1448
|
+
case 'VARBINARY':
|
|
1449
|
+
return `RAW(${length ?? 2000})`;
|
|
1450
|
+
case 'NVARCHAR2':
|
|
1451
|
+
return `NVARCHAR2(${length ?? 255})`;
|
|
1452
|
+
case 'NCHAR':
|
|
1453
|
+
return `NCHAR(${length ?? 1})`;
|
|
1454
|
+
case 'RAW':
|
|
1455
|
+
return `RAW(${length ?? 255})`;
|
|
1456
|
+
default:
|
|
1457
|
+
break;
|
|
1458
|
+
}
|
|
1459
|
+
const typeMap = {
|
|
1460
|
+
// Text types
|
|
1461
|
+
TEXT: 'CLOB',
|
|
1462
|
+
MEDIUMTEXT: 'CLOB',
|
|
1463
|
+
LONGTEXT: 'CLOB',
|
|
1464
|
+
// Number types
|
|
1465
|
+
FLOAT: 'FLOAT',
|
|
1466
|
+
DOUBLE: 'FLOAT',
|
|
1467
|
+
// Boolean
|
|
1468
|
+
BOOLEAN: 'NUMBER(1)',
|
|
1469
|
+
// Oracle doesn't have native boolean, use NUMBER(1)
|
|
1470
|
+
// Date types
|
|
1471
|
+
DATE: 'DATE',
|
|
1472
|
+
DATETIME: 'TIMESTAMP',
|
|
1473
|
+
TIMESTAMP: 'TIMESTAMP',
|
|
1474
|
+
DATEONLY: 'DATE',
|
|
1475
|
+
TIME: 'VARCHAR2(8)',
|
|
1476
|
+
// Blob types
|
|
1477
|
+
BLOB: 'BLOB',
|
|
1478
|
+
BIT: 'RAW(1)',
|
|
1479
|
+
// JSON
|
|
1480
|
+
JSON: 'CLOB',
|
|
1481
|
+
JSONB: 'BLOB',
|
|
1482
|
+
// UUID
|
|
1483
|
+
UUID: 'RAW(16)',
|
|
1484
|
+
// Geometry
|
|
1485
|
+
GEOMETRY: 'SDO_GEOMETRY',
|
|
1486
|
+
// Oracle-specific types
|
|
1487
|
+
CLOB: 'CLOB',
|
|
1488
|
+
NCLOB: 'NCLOB',
|
|
1489
|
+
BFILE: 'BFILE',
|
|
1490
|
+
LONG_RAW: 'LONG RAW',
|
|
1491
|
+
XMLTYPE: 'XMLTYPE',
|
|
1492
|
+
};
|
|
1493
|
+
return typeMap[key] || 'VARCHAR2(255)';
|
|
1494
|
+
}
|
|
1495
|
+
// ==================== Query Builders ====================
|
|
1496
|
+
/**
|
|
1497
|
+
* Build WHERE clause
|
|
1498
|
+
*/
|
|
1499
|
+
buildWhereClause(where, options) {
|
|
1500
|
+
const values = [];
|
|
1501
|
+
const sqlParts = [];
|
|
1502
|
+
if (!where || Object.keys(where).length === 0) {
|
|
1503
|
+
return { sql: '', values };
|
|
1504
|
+
}
|
|
1505
|
+
for (const [key, value] of Object.entries(where)) {
|
|
1506
|
+
if (key === 'and' || key === 'or') {
|
|
1507
|
+
const conditions = value.map((cond) => {
|
|
1508
|
+
const result = this.buildWhereClause(cond, options);
|
|
1509
|
+
values.push(...result.values);
|
|
1510
|
+
return result.sql;
|
|
1511
|
+
});
|
|
1512
|
+
sqlParts.push(`(${conditions.join(` ${key.toUpperCase()} `)})`);
|
|
1513
|
+
}
|
|
1514
|
+
else if (key.startsWith('$')) {
|
|
1515
|
+
// Handle operators like $like, $in, etc.
|
|
1516
|
+
const result = this.buildOperatorWhere(key, value, options);
|
|
1517
|
+
if (result) {
|
|
1518
|
+
values.push(...result.values);
|
|
1519
|
+
sqlParts.push(result.sql);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
else {
|
|
1523
|
+
// Simple key = value
|
|
1524
|
+
if (value === null) {
|
|
1525
|
+
sqlParts.push(`${this.quoteIdentifier(key)} IS NULL`);
|
|
1526
|
+
}
|
|
1527
|
+
else if (Array.isArray(value)) {
|
|
1528
|
+
sqlParts.push(`${this.quoteIdentifier(key)} IN (${value.map(() => '?').join(', ')})`);
|
|
1529
|
+
values.push(...value);
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
sqlParts.push(`${this.quoteIdentifier(key)} = ?`);
|
|
1533
|
+
values.push(value);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return {
|
|
1538
|
+
sql: sqlParts.length > 0 ? sqlParts.join(' AND ') : '',
|
|
1539
|
+
values,
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Build operator WHERE clause
|
|
1544
|
+
*/
|
|
1545
|
+
buildOperatorWhere(key, value, _options) {
|
|
1546
|
+
const values = [];
|
|
1547
|
+
switch (key) {
|
|
1548
|
+
case '$like':
|
|
1549
|
+
return {
|
|
1550
|
+
sql: `${this.quoteIdentifier(Object.keys(value)[0])} LIKE ?`,
|
|
1551
|
+
values: [Object.values(value)[0]],
|
|
1552
|
+
};
|
|
1553
|
+
case '$notLike':
|
|
1554
|
+
return {
|
|
1555
|
+
sql: `${this.quoteIdentifier(Object.keys(value)[0])} NOT LIKE ?`,
|
|
1556
|
+
values: [Object.values(value)[0]],
|
|
1557
|
+
};
|
|
1558
|
+
case '$in':
|
|
1559
|
+
return {
|
|
1560
|
+
sql: `${this.quoteIdentifier(Object.keys(value)[0])} IN (?)`,
|
|
1561
|
+
values: [Object.values(value)[0]],
|
|
1562
|
+
};
|
|
1563
|
+
case '$notIn':
|
|
1564
|
+
return {
|
|
1565
|
+
sql: `${this.quoteIdentifier(Object.keys(value)[0])} NOT IN (?)`,
|
|
1566
|
+
values: [Object.values(value)[0]],
|
|
1567
|
+
};
|
|
1568
|
+
case '$between': {
|
|
1569
|
+
const betweenValues = Object.values(value)[0];
|
|
1570
|
+
return {
|
|
1571
|
+
sql: `${this.quoteIdentifier(Object.keys(value)[0])} BETWEEN ? AND ?`,
|
|
1572
|
+
values: [betweenValues[0], betweenValues[1]],
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
case '$notBetween': {
|
|
1576
|
+
const betweenValues = Object.values(value)[0];
|
|
1577
|
+
return {
|
|
1578
|
+
sql: `${this.quoteIdentifier(Object.keys(value)[0])} NOT BETWEEN ? AND ?`,
|
|
1579
|
+
values: [betweenValues[0], betweenValues[1]],
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
case '$gt':
|
|
1583
|
+
case '$gte':
|
|
1584
|
+
case '$lt':
|
|
1585
|
+
case '$lte':
|
|
1586
|
+
case '$eq':
|
|
1587
|
+
return {
|
|
1588
|
+
sql: `${this.quoteIdentifier(Object.keys(value)[0])} ${key.slice(1)} ?`,
|
|
1589
|
+
values: [Object.values(value)[0]],
|
|
1590
|
+
};
|
|
1591
|
+
case '$isNull':
|
|
1592
|
+
return {
|
|
1593
|
+
sql: `${this.quoteIdentifier(Object.keys(value)[0])} IS NULL`,
|
|
1594
|
+
values: [],
|
|
1595
|
+
};
|
|
1596
|
+
case '$notNull':
|
|
1597
|
+
return {
|
|
1598
|
+
sql: `${this.quoteIdentifier(Object.keys(value)[0])} IS NOT NULL`,
|
|
1599
|
+
values: [],
|
|
1600
|
+
};
|
|
1601
|
+
default:
|
|
1602
|
+
return null;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Build ORDER BY clause
|
|
1607
|
+
*/
|
|
1608
|
+
buildOrderClause(order, _options) {
|
|
1609
|
+
if (!order || (Array.isArray(order) && order.length === 0)) {
|
|
1610
|
+
return '';
|
|
1611
|
+
}
|
|
1612
|
+
const orderParts = Array.isArray(order)
|
|
1613
|
+
? order.map((o) => {
|
|
1614
|
+
const field = Object.keys(o)[0];
|
|
1615
|
+
const direction = Object.values(o)[0];
|
|
1616
|
+
return `${this.quoteIdentifier(field)} ${direction === 'DESC' ? 'DESC' : 'ASC'}`;
|
|
1617
|
+
})
|
|
1618
|
+
: Object.entries(order).map(([field, direction]) => {
|
|
1619
|
+
return `${this.quoteIdentifier(field)} ${direction === 'DESC' ? 'DESC' : 'ASC'}`;
|
|
1620
|
+
});
|
|
1621
|
+
return `ORDER BY ${orderParts.join(', ')}`;
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Build a list of ORDER BY items (without the leading `ORDER BY` keyword),
|
|
1625
|
+
* suitable for embedding inside `OVER (...)` / `WITHIN GROUP (...)` clauses.
|
|
1626
|
+
*/
|
|
1627
|
+
buildOrderByItems(order) {
|
|
1628
|
+
return this.buildOrderClause(order).replace(/^ORDER BY /, '');
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Build an Oracle analytic/window function expression, e.g.
|
|
1632
|
+
* `ROW_NUMBER() OVER (PARTITION BY "dept" ORDER BY "salary" DESC)`.
|
|
1633
|
+
*/
|
|
1634
|
+
buildWindowFunction(options) {
|
|
1635
|
+
const args = (options.args || [])
|
|
1636
|
+
.map((a) => (typeof a === 'number' ? a : this.quoteIdentifier(a)))
|
|
1637
|
+
.join(', ');
|
|
1638
|
+
const overParts = [];
|
|
1639
|
+
if (options.partitionBy) {
|
|
1640
|
+
const partitions = Array.isArray(options.partitionBy)
|
|
1641
|
+
? options.partitionBy
|
|
1642
|
+
: [options.partitionBy];
|
|
1643
|
+
overParts.push(`PARTITION BY ${partitions.map((p) => this.quoteIdentifier(p)).join(', ')}`);
|
|
1644
|
+
}
|
|
1645
|
+
if (options.orderBy) {
|
|
1646
|
+
overParts.push(`ORDER BY ${this.buildOrderByItems(options.orderBy)}`);
|
|
1647
|
+
}
|
|
1648
|
+
if (options.frame) {
|
|
1649
|
+
const frame = options.frame;
|
|
1650
|
+
overParts.push(frame.end
|
|
1651
|
+
? `${frame.type} BETWEEN ${frame.start} AND ${frame.end}`
|
|
1652
|
+
: `${frame.type} ${frame.start}`);
|
|
1653
|
+
}
|
|
1654
|
+
const over = overParts.length > 0 ? overParts.join(' ') : '';
|
|
1655
|
+
const sql = `${options.fn.toUpperCase()}(${args}) OVER (${over})`;
|
|
1656
|
+
return options.as ? `${sql} AS ${this.quoteIdentifier(options.as)}` : sql;
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Build a `WITH ... AS (...)` common table expression clause and prepend it
|
|
1660
|
+
* to a main query. A CTE is made recursive simply by supplying
|
|
1661
|
+
* `unionQuery` (Oracle has no `RECURSIVE` keyword - the self-reference
|
|
1662
|
+
* inside the UNION ALL member is what makes it recursive).
|
|
1663
|
+
*/
|
|
1664
|
+
buildCTE(ctes, mainQuery) {
|
|
1665
|
+
const list = Array.isArray(ctes) ? ctes : [ctes];
|
|
1666
|
+
const values = [];
|
|
1667
|
+
const cteParts = list.map((cte) => {
|
|
1668
|
+
const columns = cte.columns
|
|
1669
|
+
? ` (${cte.columns.map((c) => this.quoteIdentifier(c)).join(', ')})`
|
|
1670
|
+
: '';
|
|
1671
|
+
const body = cte.unionQuery ? `${cte.query} UNION ALL ${cte.unionQuery}` : cte.query;
|
|
1672
|
+
if (cte.values) {
|
|
1673
|
+
values.push(...cte.values);
|
|
1674
|
+
}
|
|
1675
|
+
return `${this.quoteIdentifier(cte.name)}${columns} AS (${body})`;
|
|
1676
|
+
});
|
|
1677
|
+
const sql = `WITH ${cteParts.join(', ')} ${mainQuery}`;
|
|
1678
|
+
return { sql, values };
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Build an Oracle hierarchical query using `START WITH ... CONNECT BY PRIOR`,
|
|
1682
|
+
* the pre-recursive-CTE idiom Oracle has supported since 8i and still
|
|
1683
|
+
* commonly used/expected in Oracle codebases.
|
|
1684
|
+
*/
|
|
1685
|
+
buildConnectByQuery(options) {
|
|
1686
|
+
const values = [];
|
|
1687
|
+
const baseAttributes = options.attributes
|
|
1688
|
+
? options.attributes.map((a) => this.quoteIdentifier(a))
|
|
1689
|
+
: ['*'];
|
|
1690
|
+
if (options.includePseudoColumns) {
|
|
1691
|
+
baseAttributes.push('LEVEL', 'CONNECT_BY_ISLEAF');
|
|
1692
|
+
}
|
|
1693
|
+
let sql = `SELECT ${baseAttributes.join(', ')} FROM ${this.quoteTable(options.tableName, options.schema)}`;
|
|
1694
|
+
if (options.where) {
|
|
1695
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
1696
|
+
if (whereClause.sql) {
|
|
1697
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1698
|
+
values.push(...whereClause.values);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
if (options.startWith) {
|
|
1702
|
+
sql += ` START WITH ${options.startWith}`;
|
|
1703
|
+
}
|
|
1704
|
+
sql += ` CONNECT BY ${options.nocycle ? 'NOCYCLE ' : ''}${options.connectByPrior}`;
|
|
1705
|
+
if (options.orderSiblingsBy) {
|
|
1706
|
+
sql += ` ORDER SIBLINGS BY ${this.buildOrderByItems(options.orderSiblingsBy)}`;
|
|
1707
|
+
}
|
|
1708
|
+
return { sql, values };
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Build LIMIT/OFFSET clause using ROWNUM
|
|
1712
|
+
*/
|
|
1713
|
+
buildLimitOffset(limit, offset) {
|
|
1714
|
+
// Oracle uses ROWNUM for pagination
|
|
1715
|
+
let sql = '';
|
|
1716
|
+
if (limit !== undefined && offset !== undefined) {
|
|
1717
|
+
sql = `WHERE ROWNUM <= ${parseInt(limit, 10) + parseInt(offset, 10)}`;
|
|
1718
|
+
}
|
|
1719
|
+
else if (limit !== undefined) {
|
|
1720
|
+
sql = `WHERE ROWNUM <= ${limit}`;
|
|
1721
|
+
}
|
|
1722
|
+
return sql;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Build INSERT query
|
|
1726
|
+
*/
|
|
1727
|
+
buildInsertQuery(tableName, values, options) {
|
|
1728
|
+
const fields = Object.keys(values);
|
|
1729
|
+
const placeholders = fields.map((_, i) => `:${i + 1}`).join(', ');
|
|
1730
|
+
const fieldNames = fields.map((f) => this.quoteIdentifier(f)).join(', ');
|
|
1731
|
+
let sql = `INSERT INTO ${this.quoteTable(tableName, options?.schema)} (${fieldNames}) VALUES (${placeholders})`;
|
|
1732
|
+
const bindValues = Object.values(values);
|
|
1733
|
+
if (options?.returning) {
|
|
1734
|
+
// Oracle has no `?` placeholder support at all - every bind, including
|
|
1735
|
+
// the OUT bind(s) used by `RETURNING ... INTO`, must be a positional
|
|
1736
|
+
// `:n` bind that continues numbering after the INSERT's own `:1..:n`
|
|
1737
|
+
// binds, and a corresponding placeholder value must be appended to the
|
|
1738
|
+
// values/binds array so the bind count sent to the driver matches the
|
|
1739
|
+
// bind count in the SQL text.
|
|
1740
|
+
let nextBindIndex = fields.length + 1;
|
|
1741
|
+
// node-oracledb needs an explicit bind descriptor (direction + type) to
|
|
1742
|
+
// recognize a positional bind as an OUT bind rather than an IN value;
|
|
1743
|
+
// a bare `null` would be sent as an IN bind of unknown type.
|
|
1744
|
+
const outBind = () => ({
|
|
1745
|
+
type: oracledb_1.default.STRING,
|
|
1746
|
+
dir: oracledb_1.default.BIND_OUT,
|
|
1747
|
+
maxSize: 4000,
|
|
1748
|
+
});
|
|
1749
|
+
if (options.returning === true) {
|
|
1750
|
+
sql += ` RETURNING ROWID INTO :${nextBindIndex}`;
|
|
1751
|
+
bindValues.push(outBind());
|
|
1752
|
+
}
|
|
1753
|
+
else {
|
|
1754
|
+
const returningFields = options.returning.map((f) => this.quoteIdentifier(f)).join(', ');
|
|
1755
|
+
const intoBinds = options.returning.map(() => `:${nextBindIndex++}`).join(', ');
|
|
1756
|
+
sql += ` RETURNING ${returningFields} INTO ${intoBinds}`;
|
|
1757
|
+
for (let i = 0; i < options.returning.length; i++) {
|
|
1758
|
+
bindValues.push(outBind());
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return { sql, values: bindValues };
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Build UPDATE query
|
|
1766
|
+
*/
|
|
1767
|
+
buildUpdateQuery(tableName, values, where, options) {
|
|
1768
|
+
const setClauses = Object.keys(values).map((key, i) => `${this.quoteIdentifier(key)} = :${i + 1}`);
|
|
1769
|
+
const setSql = setClauses.join(', ');
|
|
1770
|
+
let sql = `UPDATE ${this.quoteTable(tableName)} SET ${setSql}`;
|
|
1771
|
+
const whereClause = this.buildWhereClause(where);
|
|
1772
|
+
if (whereClause.sql) {
|
|
1773
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1774
|
+
}
|
|
1775
|
+
if (options?.limit) {
|
|
1776
|
+
// Oracle requires subquery for UPDATE with LIMIT
|
|
1777
|
+
sql = `UPDATE (${sql} WHERE ROWNUM <= ${options.limit}) SET ${setSql}`;
|
|
1778
|
+
}
|
|
1779
|
+
const bindValues = [...Object.values(values), ...whereClause.values];
|
|
1780
|
+
return { sql, values: bindValues };
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Build DELETE query
|
|
1784
|
+
*/
|
|
1785
|
+
buildDeleteQuery(tableName, where, options) {
|
|
1786
|
+
let sql = `DELETE FROM ${this.quoteTable(tableName)}`;
|
|
1787
|
+
const whereClause = this.buildWhereClause(where);
|
|
1788
|
+
if (whereClause.sql) {
|
|
1789
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1790
|
+
}
|
|
1791
|
+
if (options?.limit) {
|
|
1792
|
+
// Oracle requires subquery for DELETE with LIMIT
|
|
1793
|
+
sql = `DELETE FROM (${sql} WHERE ROWNUM <= ${options.limit})`;
|
|
1794
|
+
}
|
|
1795
|
+
return { sql, values: whereClause.values };
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Build SELECT query
|
|
1799
|
+
*/
|
|
1800
|
+
buildSelectQuery(options) {
|
|
1801
|
+
const values = [];
|
|
1802
|
+
// SELECT clause
|
|
1803
|
+
let sql = 'SELECT ';
|
|
1804
|
+
if (options.distinct) {
|
|
1805
|
+
sql += 'DISTINCT ';
|
|
1806
|
+
}
|
|
1807
|
+
if (options.attributes) {
|
|
1808
|
+
if (Array.isArray(options.attributes)) {
|
|
1809
|
+
sql += options.attributes.map((a) => this.quoteIdentifier(a)).join(', ');
|
|
1810
|
+
}
|
|
1811
|
+
else {
|
|
1812
|
+
const include = options.attributes.include
|
|
1813
|
+
?.map((a) => this.quoteIdentifier(a))
|
|
1814
|
+
.join(', ');
|
|
1815
|
+
const exclude = options.attributes.exclude
|
|
1816
|
+
?.map((a) => this.quoteIdentifier(a))
|
|
1817
|
+
.join(', ');
|
|
1818
|
+
if (include)
|
|
1819
|
+
sql += include;
|
|
1820
|
+
else
|
|
1821
|
+
sql += '*';
|
|
1822
|
+
if (exclude)
|
|
1823
|
+
sql = sql.replace('*', `* EXCEPT (${exclude})`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
else {
|
|
1827
|
+
sql += '*';
|
|
1828
|
+
}
|
|
1829
|
+
// FROM clause
|
|
1830
|
+
sql += ` FROM ${this.quoteTable(options.tableName, options.schema)}`;
|
|
1831
|
+
// JOIN clause (if any)
|
|
1832
|
+
if (options.include) {
|
|
1833
|
+
for (const include of options.include) {
|
|
1834
|
+
const joinType = include.join || 'JOIN';
|
|
1835
|
+
// Use the model name or tableName from the include options
|
|
1836
|
+
const tableName = include.table ||
|
|
1837
|
+
(typeof include.model === 'object' && 'tableName' in include.model
|
|
1838
|
+
? include.model.tableName
|
|
1839
|
+
: String(include.model));
|
|
1840
|
+
const schema = include.schema;
|
|
1841
|
+
const as = include.as;
|
|
1842
|
+
sql += ` ${joinType} ${this.quoteTable(tableName, schema)}`;
|
|
1843
|
+
if (include.on) {
|
|
1844
|
+
const onConditions = Object.entries(include.on).map(([key, value]) => `${this.quoteIdentifier(key)} = ${this.quoteIdentifier(value)}`);
|
|
1845
|
+
sql += ` ON (${onConditions.join(' AND ')})`;
|
|
1846
|
+
}
|
|
1847
|
+
if (as) {
|
|
1848
|
+
sql += ` ${this.quoteIdentifier(as)}`;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
// WHERE clause
|
|
1853
|
+
if (options.where) {
|
|
1854
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
1855
|
+
if (whereClause.sql) {
|
|
1856
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1857
|
+
values.push(...whereClause.values);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
// GROUP BY clause
|
|
1861
|
+
if (options.group) {
|
|
1862
|
+
const groupBy = Array.isArray(options.group)
|
|
1863
|
+
? options.group.map((g) => this.quoteIdentifier(g)).join(', ')
|
|
1864
|
+
: this.quoteIdentifier(options.group);
|
|
1865
|
+
sql += ` GROUP BY ${groupBy}`;
|
|
1866
|
+
}
|
|
1867
|
+
// HAVING clause
|
|
1868
|
+
if (options.having) {
|
|
1869
|
+
const havingClause = this.buildWhereClause(options.having);
|
|
1870
|
+
if (havingClause.sql) {
|
|
1871
|
+
sql += ` HAVING ${havingClause.sql}`;
|
|
1872
|
+
values.push(...havingClause.values);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
// ORDER BY clause
|
|
1876
|
+
if (options.order) {
|
|
1877
|
+
sql += ` ${this.buildOrderClause(options.order)}`;
|
|
1878
|
+
}
|
|
1879
|
+
// LIMIT/OFFSET using ROWNUM (Oracle specific)
|
|
1880
|
+
if (options.limit !== undefined || options.offset !== undefined) {
|
|
1881
|
+
const limit = options.limit ? parseInt(options.limit, 10) : 0;
|
|
1882
|
+
const offset = options.offset ? parseInt(options.offset, 10) : 0;
|
|
1883
|
+
// Wrap the query in a subquery to apply ROWNUM
|
|
1884
|
+
sql = `SELECT * FROM (${sql}) WHERE ROWNUM > ${offset}`;
|
|
1885
|
+
if (limit > 0) {
|
|
1886
|
+
sql += ` AND ROWNUM <= ${limit + offset}`;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
// FOR UPDATE (locking)
|
|
1890
|
+
if (options.lock) {
|
|
1891
|
+
sql += ' FOR UPDATE';
|
|
1892
|
+
if (options.lock === 'SHARE') {
|
|
1893
|
+
sql += ' NOWAIT';
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
return { sql, values };
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Build a general-purpose Oracle `MERGE INTO ... USING ... ON (...)` statement.
|
|
1900
|
+
* Supports composite (multi-column) match keys, conditional
|
|
1901
|
+
* `WHEN [NOT] MATCHED` predicates, and `WHEN MATCHED ... THEN DELETE`.
|
|
1902
|
+
*/
|
|
1903
|
+
buildMergeQuery(options) {
|
|
1904
|
+
const target = options.targetAlias
|
|
1905
|
+
? `${this.quoteTable(options.targetTable, options.targetSchema)} ${this.quoteIdentifier(options.targetAlias)}`
|
|
1906
|
+
: this.quoteTable(options.targetTable, options.targetSchema);
|
|
1907
|
+
const targetRef = options.targetAlias
|
|
1908
|
+
? this.quoteIdentifier(options.targetAlias)
|
|
1909
|
+
: this.quoteTable(options.targetTable, options.targetSchema);
|
|
1910
|
+
const usingClause = options.usingAlias
|
|
1911
|
+
? `${options.using} ${this.quoteIdentifier(options.usingAlias)}`
|
|
1912
|
+
: options.using;
|
|
1913
|
+
const usingRef = options.usingAlias ? this.quoteIdentifier(options.usingAlias) : options.using;
|
|
1914
|
+
// ANDing every match column together (rather than only the first one) is
|
|
1915
|
+
// what correctly supports composite conflict/join keys.
|
|
1916
|
+
const onCondition = options.onCondition ||
|
|
1917
|
+
options.on
|
|
1918
|
+
.map((col) => `${targetRef}.${this.quoteIdentifier(col)} = ${usingRef}.${this.quoteIdentifier(col)}`)
|
|
1919
|
+
.join(' AND ');
|
|
1920
|
+
let sql = `MERGE INTO ${target} USING ${usingClause} ON (${onCondition})`;
|
|
1921
|
+
if (options.whenMatchedUpdate && options.whenMatchedUpdate.length > 0) {
|
|
1922
|
+
const setClause = options.whenMatchedUpdate
|
|
1923
|
+
.map((col) => `${targetRef}.${this.quoteIdentifier(col)} = ${usingRef}.${this.quoteIdentifier(col)}`)
|
|
1924
|
+
.join(', ');
|
|
1925
|
+
sql += ` WHEN MATCHED THEN UPDATE SET ${setClause}`;
|
|
1926
|
+
if (options.whenMatchedCondition) {
|
|
1927
|
+
sql += ` WHERE ${options.whenMatchedCondition}`;
|
|
1928
|
+
}
|
|
1929
|
+
if (options.whenMatchedDelete) {
|
|
1930
|
+
sql +=
|
|
1931
|
+
typeof options.whenMatchedDelete === 'string'
|
|
1932
|
+
? ` DELETE WHERE ${options.whenMatchedDelete}`
|
|
1933
|
+
: ' DELETE';
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (options.whenNotMatchedInsert && options.whenNotMatchedInsert.length > 0) {
|
|
1937
|
+
const insertCols = options.whenNotMatchedInsert
|
|
1938
|
+
.map((c) => this.quoteIdentifier(c))
|
|
1939
|
+
.join(', ');
|
|
1940
|
+
const insertVals = options.whenNotMatchedInsert
|
|
1941
|
+
.map((c) => `${usingRef}.${this.quoteIdentifier(c)}`)
|
|
1942
|
+
.join(', ');
|
|
1943
|
+
sql += ` WHEN NOT MATCHED THEN INSERT (${insertCols}) VALUES (${insertVals})`;
|
|
1944
|
+
if (options.whenNotMatchedCondition) {
|
|
1945
|
+
sql += ` WHERE ${options.whenNotMatchedCondition}`;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return { sql, values: options.values || [] };
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Build UPSERT query (MERGE for Oracle)
|
|
1952
|
+
*/
|
|
1953
|
+
buildUpsertQuery(tableName, values, options) {
|
|
1954
|
+
const fields = Object.keys(values);
|
|
1955
|
+
const bindValues = Object.values(values);
|
|
1956
|
+
// Every conflict field participates in the ON clause (ANDed together),
|
|
1957
|
+
// correctly supporting composite conflict keys instead of only honoring
|
|
1958
|
+
// conflictFields[0].
|
|
1959
|
+
const conflictFields = options?.conflictFields && options.conflictFields.length > 0
|
|
1960
|
+
? options.conflictFields
|
|
1961
|
+
: [fields[0]];
|
|
1962
|
+
const updateFields = fields.filter((f) => !conflictFields.includes(f));
|
|
1963
|
+
const selectList = fields.map((f, i) => `:${i + 1} AS ${this.quoteIdentifier(f)}`).join(', ');
|
|
1964
|
+
const using = `(SELECT ${selectList} FROM DUAL)`;
|
|
1965
|
+
const merged = this.buildMergeQuery({
|
|
1966
|
+
targetTable: tableName,
|
|
1967
|
+
using,
|
|
1968
|
+
usingAlias: 'src',
|
|
1969
|
+
on: conflictFields,
|
|
1970
|
+
whenMatchedUpdate: updateFields.length > 0 ? updateFields : undefined,
|
|
1971
|
+
whenNotMatchedInsert: fields,
|
|
1972
|
+
});
|
|
1973
|
+
return { sql: merged.sql, values: bindValues };
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Build increment query
|
|
1977
|
+
*/
|
|
1978
|
+
buildIncrementQuery(tableName, fields, where, options) {
|
|
1979
|
+
const increment = options?.by || 1;
|
|
1980
|
+
let fieldsToIncrement;
|
|
1981
|
+
let incrementValues;
|
|
1982
|
+
if (typeof fields === 'string') {
|
|
1983
|
+
fieldsToIncrement = [fields];
|
|
1984
|
+
incrementValues = { [fields]: increment };
|
|
1985
|
+
}
|
|
1986
|
+
else if (Array.isArray(fields)) {
|
|
1987
|
+
fieldsToIncrement = fields;
|
|
1988
|
+
incrementValues = fields.reduce((acc, f) => ({ ...acc, [f]: increment }), {});
|
|
1989
|
+
}
|
|
1990
|
+
else {
|
|
1991
|
+
fieldsToIncrement = Object.keys(fields);
|
|
1992
|
+
incrementValues = fields;
|
|
1993
|
+
}
|
|
1994
|
+
const setClauses = fieldsToIncrement.map((f) => `${this.quoteIdentifier(f)} = ${this.quoteIdentifier(f)} + ${incrementValues[f]}`);
|
|
1995
|
+
let sql = `UPDATE ${this.quoteTable(tableName)} SET ${setClauses.join(', ')}`;
|
|
1996
|
+
const whereClause = this.buildWhereClause(where);
|
|
1997
|
+
if (whereClause.sql) {
|
|
1998
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1999
|
+
}
|
|
2000
|
+
return { sql, values: whereClause.values };
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Replace placeholders in SQL
|
|
2004
|
+
*/
|
|
2005
|
+
replaceReplacements(sql, replacements) {
|
|
2006
|
+
if (!replacements) {
|
|
2007
|
+
return sql;
|
|
2008
|
+
}
|
|
2009
|
+
if (Array.isArray(replacements)) {
|
|
2010
|
+
let result = sql;
|
|
2011
|
+
for (const value of replacements) {
|
|
2012
|
+
result = result.replace('?', this.escape(value));
|
|
2013
|
+
}
|
|
2014
|
+
return result;
|
|
2015
|
+
}
|
|
2016
|
+
let result = sql;
|
|
2017
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
2018
|
+
result = result.replace(new RegExp(`:${key}`, 'g'), this.escape(value));
|
|
2019
|
+
}
|
|
2020
|
+
return result;
|
|
2021
|
+
}
|
|
2022
|
+
// ==================== Extensions (Not Supported) ====================
|
|
2023
|
+
async createExtension(_extensionName, _options) {
|
|
2024
|
+
throw new Error('Extensions are not supported by Oracle. This is a PostgreSQL-specific feature.');
|
|
2025
|
+
}
|
|
2026
|
+
async dropExtension(_extensionName, _options) {
|
|
2027
|
+
throw new Error('Extensions are not supported by Oracle. This is a PostgreSQL-specific feature.');
|
|
2028
|
+
}
|
|
2029
|
+
async getExtensions() {
|
|
2030
|
+
throw new Error('Extensions are not supported by Oracle. This is a PostgreSQL-specific feature.');
|
|
2031
|
+
}
|
|
2032
|
+
async hasExtension(_extensionName) {
|
|
2033
|
+
throw new Error('Extensions are not supported by Oracle. This is a PostgreSQL-specific feature.');
|
|
2034
|
+
}
|
|
2035
|
+
// ==================== Foreign Data Wrappers (Not Supported) ====================
|
|
2036
|
+
buildCreateServerQuery(_name, _opts) {
|
|
2037
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
2038
|
+
}
|
|
2039
|
+
buildAlterServerQuery(_name, _opts) {
|
|
2040
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
2041
|
+
}
|
|
2042
|
+
buildDropServerQuery(_name, _opts) {
|
|
2043
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
2044
|
+
}
|
|
2045
|
+
buildCreateUserMappingQuery(_opts) {
|
|
2046
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
2047
|
+
}
|
|
2048
|
+
buildAlterUserMappingQuery(_opts) {
|
|
2049
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
2050
|
+
}
|
|
2051
|
+
buildDropUserMappingQuery(_serverName, _user, _opts) {
|
|
2052
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
2053
|
+
}
|
|
2054
|
+
buildCreateForeignTableQuery(_tableName, _opts) {
|
|
2055
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
2056
|
+
}
|
|
2057
|
+
buildDropForeignTableQuery(_tableName, _opts) {
|
|
2058
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
2059
|
+
}
|
|
2060
|
+
buildImportForeignSchemaQuery(_remoteSchema, _serverName, _opts) {
|
|
2061
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
2062
|
+
}
|
|
2063
|
+
getServersQuery() {
|
|
2064
|
+
throw new Error('Foreign Data Wrappers are not supported by Oracle. This is a PostgreSQL-specific feature. Consider using Oracle Database Links (CREATE DATABASE LINK) instead.');
|
|
2065
|
+
}
|
|
2066
|
+
// ==================== User Management ====================
|
|
2067
|
+
/**
|
|
2068
|
+
* Build a CREATE USER statement for Oracle.
|
|
2069
|
+
*
|
|
2070
|
+
* Oracle syntax: CREATE USER username IDENTIFIED BY password
|
|
2071
|
+
*/
|
|
2072
|
+
buildCreateUserQuery(username, options = {}) {
|
|
2073
|
+
let sql = 'CREATE USER';
|
|
2074
|
+
sql += ` "${this.escapeStringValue(username)}"`;
|
|
2075
|
+
if (options.password) {
|
|
2076
|
+
sql += ` IDENTIFIED BY "${this.escapeStringValue(options.password)}"`;
|
|
2077
|
+
}
|
|
2078
|
+
// Oracle uses PROFILE for resource limits
|
|
2079
|
+
if (options.maxConnections !== undefined) {
|
|
2080
|
+
sql += ` LIMIT SESSIONS_PER_USER ${Number(options.maxConnections)}`;
|
|
2081
|
+
}
|
|
2082
|
+
if (options.maxQueriesPerHour !== undefined || options.maxUpdatesPerHour !== undefined) {
|
|
2083
|
+
// Oracle uses PROFILE for these limits - simplified handling
|
|
2084
|
+
sql += ' /* Resource limits require PROFILE configuration */';
|
|
2085
|
+
}
|
|
2086
|
+
// Account lock status
|
|
2087
|
+
if (options.accountLocked === true) {
|
|
2088
|
+
sql += ' ACCOUNT LOCK';
|
|
2089
|
+
}
|
|
2090
|
+
else if (options.accountLocked === false) {
|
|
2091
|
+
sql += ' ACCOUNT UNLOCK';
|
|
2092
|
+
}
|
|
2093
|
+
// `CREATE USER IF NOT EXISTS` is 23c-only syntax (ORA-00922 on 19c/21c).
|
|
2094
|
+
// Use a portable existence check instead: run the plain DDL wrapped in a
|
|
2095
|
+
// PL/SQL block that silently ignores the "already exists" ORA codes.
|
|
2096
|
+
if (options.ifNotExists) {
|
|
2097
|
+
return this.wrapIgnoringOraErrors(sql, [
|
|
2098
|
+
OracleDialect.ORA_NAME_ALREADY_USED,
|
|
2099
|
+
OracleDialect.ORA_USER_ALREADY_EXISTS,
|
|
2100
|
+
]);
|
|
2101
|
+
}
|
|
2102
|
+
return sql;
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Build an ALTER USER statement for Oracle.
|
|
2106
|
+
*
|
|
2107
|
+
* Oracle syntax: ALTER USER username IDENTIFIED BY password
|
|
2108
|
+
*/
|
|
2109
|
+
buildAlterUserQuery(username, options) {
|
|
2110
|
+
let sql = `ALTER USER "${this.escapeStringValue(username)}"`;
|
|
2111
|
+
if (options.password) {
|
|
2112
|
+
sql += ` IDENTIFIED BY "${this.escapeStringValue(options.password)}"`;
|
|
2113
|
+
}
|
|
2114
|
+
// Account lock/unlock
|
|
2115
|
+
if (options.unlockAccount) {
|
|
2116
|
+
sql += ' ACCOUNT UNLOCK';
|
|
2117
|
+
}
|
|
2118
|
+
else if (options.accountLocked === true) {
|
|
2119
|
+
sql += ' ACCOUNT LOCK';
|
|
2120
|
+
}
|
|
2121
|
+
else if (options.accountLocked === false) {
|
|
2122
|
+
sql += ' ACCOUNT UNLOCK';
|
|
2123
|
+
}
|
|
2124
|
+
// Password expiry (Oracle uses PASSWORD EXPIRE)
|
|
2125
|
+
if (options.passwordExpirePolicy === 'DEFAULT') {
|
|
2126
|
+
sql += ' PASSWORD EXPIRE DEFAULT';
|
|
2127
|
+
}
|
|
2128
|
+
else if (options.passwordExpirePolicy === 'NEVER') {
|
|
2129
|
+
sql += ' PASSWORD EXPIRE NEVER';
|
|
2130
|
+
}
|
|
2131
|
+
else if (options.passwordExpirePolicy === 'INTERVAL') {
|
|
2132
|
+
sql += ' PASSWORD EXPIRE INTERVAL';
|
|
2133
|
+
}
|
|
2134
|
+
else if (typeof options.passwordExpirePolicy === 'number') {
|
|
2135
|
+
sql += ` PASSWORD EXPIRE INTERVAL ${options.passwordExpirePolicy} DAY`;
|
|
2136
|
+
}
|
|
2137
|
+
else if (options.resetExpiredPassword) {
|
|
2138
|
+
sql += ' PASSWORD EXPIRE';
|
|
2139
|
+
}
|
|
2140
|
+
// Resource limits would require PROFILE changes
|
|
2141
|
+
if (options.maxConnections !== undefined) {
|
|
2142
|
+
sql += ` /* SESSIONS_PER_USER limit requires PROFILE configuration */`;
|
|
2143
|
+
}
|
|
2144
|
+
return sql;
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Build a DROP USER statement for Oracle.
|
|
2148
|
+
*/
|
|
2149
|
+
buildDropUserQuery(username, options = {}) {
|
|
2150
|
+
let sql = 'DROP USER';
|
|
2151
|
+
sql += ` "${this.escapeStringValue(username)}"`;
|
|
2152
|
+
if (options.cascade) {
|
|
2153
|
+
sql += ' CASCADE';
|
|
2154
|
+
}
|
|
2155
|
+
// `DROP USER IF EXISTS` is 23c-only syntax (ORA-00922 on 19c/21c). Use a
|
|
2156
|
+
// portable existence check instead: run the plain DDL wrapped in a
|
|
2157
|
+
// PL/SQL block that silently ignores ORA-01918 ("user does not exist").
|
|
2158
|
+
if (options.ifExists) {
|
|
2159
|
+
return this.wrapIgnoringOraErrors(sql, [OracleDialect.ORA_USER_DOES_NOT_EXIST]);
|
|
2160
|
+
}
|
|
2161
|
+
return sql;
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Return a query that lists all Oracle users.
|
|
2165
|
+
*/
|
|
2166
|
+
getUsersQuery() {
|
|
2167
|
+
return `SELECT username AS "user",
|
|
2168
|
+
account_status AS "status",
|
|
2169
|
+
created AS "createTime",
|
|
2170
|
+
lock_date AS "lockDate"
|
|
2171
|
+
FROM dba_users
|
|
2172
|
+
ORDER BY username`;
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Build a GRANT statement for Oracle.
|
|
2176
|
+
*
|
|
2177
|
+
* Oracle syntax: GRANT privilege ON object TO user
|
|
2178
|
+
*/
|
|
2179
|
+
buildGrantQuery(options) {
|
|
2180
|
+
const { scopeSql, columnList } = this.buildPrivilegeScope(options.on);
|
|
2181
|
+
const privList = options.privileges.join(', ');
|
|
2182
|
+
// For column-level grants, format as: SELECT (col1, col2) ON table TO user
|
|
2183
|
+
let grantPriv;
|
|
2184
|
+
if (columnList) {
|
|
2185
|
+
grantPriv = `${privList} (${columnList})`;
|
|
2186
|
+
}
|
|
2187
|
+
else {
|
|
2188
|
+
grantPriv = privList;
|
|
2189
|
+
}
|
|
2190
|
+
const recipients = (Array.isArray(options.to) ? options.to : [options.to])
|
|
2191
|
+
.map((r) => `"${this.escapeStringValue(r)}"`)
|
|
2192
|
+
.join(', ');
|
|
2193
|
+
let sql = `GRANT ${grantPriv} ON ${scopeSql} TO ${recipients}`;
|
|
2194
|
+
if (options.withGrantOption) {
|
|
2195
|
+
sql += ' WITH GRANT OPTION';
|
|
2196
|
+
}
|
|
2197
|
+
return sql;
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Build a REVOKE statement for Oracle.
|
|
2201
|
+
*
|
|
2202
|
+
* Oracle syntax: REVOKE privilege ON object FROM user
|
|
2203
|
+
*/
|
|
2204
|
+
buildRevokeQuery(options) {
|
|
2205
|
+
const { scopeSql, columnList } = this.buildPrivilegeScope(options.on);
|
|
2206
|
+
const privList = options.privileges.join(', ');
|
|
2207
|
+
let revokePriv;
|
|
2208
|
+
if (columnList) {
|
|
2209
|
+
revokePriv = `${privList} (${columnList})`;
|
|
2210
|
+
}
|
|
2211
|
+
else {
|
|
2212
|
+
revokePriv = privList;
|
|
2213
|
+
}
|
|
2214
|
+
const targets = (Array.isArray(options.from) ? options.from : [options.from])
|
|
2215
|
+
.map((r) => `"${this.escapeStringValue(r)}"`)
|
|
2216
|
+
.join(', ');
|
|
2217
|
+
let sql;
|
|
2218
|
+
if (options.grantOptionFor) {
|
|
2219
|
+
sql = `REVOKE GRANT OPTION FOR ${revokePriv} ON ${scopeSql} FROM ${targets}`;
|
|
2220
|
+
}
|
|
2221
|
+
else {
|
|
2222
|
+
sql = `REVOKE ${revokePriv} ON ${scopeSql} FROM ${targets}`;
|
|
2223
|
+
}
|
|
2224
|
+
if (options.cascade) {
|
|
2225
|
+
sql += ' CASCADE';
|
|
2226
|
+
}
|
|
2227
|
+
return sql;
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Build a SHOW GRANTS query for Oracle.
|
|
2231
|
+
* Queries USER_TAB_PRIVS for object grants.
|
|
2232
|
+
*/
|
|
2233
|
+
buildShowGrantsQuery(username, _host) {
|
|
2234
|
+
const escaped = this.escapeStringValue(username);
|
|
2235
|
+
return [
|
|
2236
|
+
`SELECT grantee, privilege, table_name, table_schema, grantable`,
|
|
2237
|
+
`FROM user_tab_privs`,
|
|
2238
|
+
`WHERE grantee = UPPER('${escaped}')`,
|
|
2239
|
+
`UNION ALL`,
|
|
2240
|
+
`SELECT grantee, privilege, column_name, table_name, grantable`,
|
|
2241
|
+
`FROM user_col_privs`,
|
|
2242
|
+
`WHERE grantee = UPPER('${escaped}')`,
|
|
2243
|
+
`ORDER BY table_name, privilege;`,
|
|
2244
|
+
].join('\n');
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Oracle does not require FLUSH PRIVILEGES - return a no-op.
|
|
2248
|
+
*/
|
|
2249
|
+
buildFlushPrivilegesQuery() {
|
|
2250
|
+
return '-- Oracle applies privileges automatically; FLUSH PRIVILEGES is not required\nSELECT 1 FROM DUAL';
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Build a CREATE ROLE statement for Oracle.
|
|
2254
|
+
*/
|
|
2255
|
+
buildCreateRoleQuery(roleName, options = {}) {
|
|
2256
|
+
let sql = 'CREATE ROLE';
|
|
2257
|
+
sql += ` "${this.escapeStringValue(roleName)}"`;
|
|
2258
|
+
if (options.password) {
|
|
2259
|
+
sql += ` NOT IDENTIFIED`;
|
|
2260
|
+
// Oracle roles can be IDENTIFIED BY password or NOT IDENTIFIED
|
|
2261
|
+
// For roles, typically NOT IDENTIFIED (managed at schema level)
|
|
2262
|
+
}
|
|
2263
|
+
// `CREATE ROLE IF NOT EXISTS` is 23c-only syntax (ORA-00922 on 19c/21c).
|
|
2264
|
+
// Use a portable existence check instead: run the plain DDL wrapped in a
|
|
2265
|
+
// PL/SQL block that silently ignores the "already exists" ORA codes.
|
|
2266
|
+
if (options.ifNotExists) {
|
|
2267
|
+
return this.wrapIgnoringOraErrors(sql, [
|
|
2268
|
+
OracleDialect.ORA_NAME_ALREADY_USED,
|
|
2269
|
+
OracleDialect.ORA_ROLE_ALREADY_EXISTS,
|
|
2270
|
+
]);
|
|
2271
|
+
}
|
|
2272
|
+
return sql;
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Build a DROP ROLE statement for Oracle.
|
|
2276
|
+
*/
|
|
2277
|
+
buildDropRoleQuery(roleName, options = {}) {
|
|
2278
|
+
let sql = 'DROP ROLE';
|
|
2279
|
+
sql += ` "${this.escapeStringValue(roleName)}"`;
|
|
2280
|
+
// `DROP ROLE IF EXISTS` is 23c-only syntax (ORA-00922 on 19c/21c). Use a
|
|
2281
|
+
// portable existence check instead: run the plain DDL wrapped in a
|
|
2282
|
+
// PL/SQL block that silently ignores ORA-01919 ("role does not exist").
|
|
2283
|
+
if (options.ifExists) {
|
|
2284
|
+
return this.wrapIgnoringOraErrors(sql, [OracleDialect.ORA_ROLE_DOES_NOT_EXIST]);
|
|
2285
|
+
}
|
|
2286
|
+
return sql;
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Build a GRANT ROLE statement for Oracle.
|
|
2290
|
+
*/
|
|
2291
|
+
buildGrantRoleQuery(role, to, options = {}) {
|
|
2292
|
+
const recipients = (Array.isArray(to) ? to : [to])
|
|
2293
|
+
.map((r) => `"${this.escapeStringValue(r)}"`)
|
|
2294
|
+
.join(', ');
|
|
2295
|
+
let sql = `GRANT "${this.escapeStringValue(role)}" TO ${recipients}`;
|
|
2296
|
+
if (options.withAdminOption) {
|
|
2297
|
+
sql += ' WITH ADMIN OPTION';
|
|
2298
|
+
}
|
|
2299
|
+
return sql;
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Build a REVOKE ROLE statement for Oracle.
|
|
2303
|
+
*/
|
|
2304
|
+
buildRevokeRoleQuery(role, from, _options = {}) {
|
|
2305
|
+
const targets = (Array.isArray(from) ? from : [from])
|
|
2306
|
+
.map((r) => `"${this.escapeStringValue(r)}"`)
|
|
2307
|
+
.join(', ');
|
|
2308
|
+
return `REVOKE "${this.escapeStringValue(role)}" FROM ${targets}`;
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Return a query that lists all Oracle roles.
|
|
2312
|
+
*/
|
|
2313
|
+
getRolesQuery() {
|
|
2314
|
+
return `SELECT role AS "role" FROM dba_roles ORDER BY role`;
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Build Oracle privilege scope for GRANT/REVOKE.
|
|
2318
|
+
*/
|
|
2319
|
+
buildPrivilegeScope(scope) {
|
|
2320
|
+
switch (scope.level) {
|
|
2321
|
+
case 'global':
|
|
2322
|
+
return { scopeSql: 'DATABASE', columnList: null };
|
|
2323
|
+
case 'database':
|
|
2324
|
+
return { scopeSql: `"${this.escapeStringValue(scope.database)}".*`, columnList: null };
|
|
2325
|
+
case 'table': {
|
|
2326
|
+
const db = scope.database ? `"${this.escapeStringValue(scope.database)}".` : '';
|
|
2327
|
+
return { scopeSql: `${db}"${this.escapeStringValue(scope.table)}"`, columnList: null };
|
|
2328
|
+
}
|
|
2329
|
+
case 'column': {
|
|
2330
|
+
const db = scope.database ? `"${this.escapeStringValue(scope.database)}".` : '';
|
|
2331
|
+
const cols = scope.columns.map((c) => `"${this.escapeStringValue(c)}"`).join(', ');
|
|
2332
|
+
return {
|
|
2333
|
+
scopeSql: `${db}"${this.escapeStringValue(scope.table)}"`,
|
|
2334
|
+
columnList: cols,
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
case 'routine': {
|
|
2338
|
+
const db = scope.database ? `"${this.escapeStringValue(scope.database)}".` : '';
|
|
2339
|
+
const type = scope.routineType || 'PROCEDURE';
|
|
2340
|
+
return {
|
|
2341
|
+
scopeSql: `${type} ${db}"${this.escapeStringValue(scope.routine)}"`,
|
|
2342
|
+
columnList: null,
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
default:
|
|
2346
|
+
return { scopeSql: 'DATABASE', columnList: null };
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Escape a string value for Oracle (quote with double quotes).
|
|
2351
|
+
*/
|
|
2352
|
+
escapeStringValue(value) {
|
|
2353
|
+
return value.replace(/"/g, '""');
|
|
2354
|
+
}
|
|
2355
|
+
// ==================== Sequences ====================
|
|
2356
|
+
async createSequence(options) {
|
|
2357
|
+
const schema = options.schema || this.config?.schema || 'SYSTEM';
|
|
2358
|
+
const seqName = `${schema}.${this.escapeId(options.name)}`;
|
|
2359
|
+
let sql = 'CREATE SEQUENCE';
|
|
2360
|
+
if (options.replace) {
|
|
2361
|
+
sql = 'CREATE OR REPLACE SEQUENCE';
|
|
2362
|
+
}
|
|
2363
|
+
else if (options.ifNotExists) {
|
|
2364
|
+
// Check if exists first
|
|
2365
|
+
const exists = await this.hasSequence(options.name, schema);
|
|
2366
|
+
if (exists) {
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
if (options.temporary) {
|
|
2371
|
+
sql += ' TEMPORARY SEQUENCE';
|
|
2372
|
+
}
|
|
2373
|
+
sql += ` ${seqName}`;
|
|
2374
|
+
if (options.startWith !== undefined)
|
|
2375
|
+
sql += ` START WITH ${options.startWith}`;
|
|
2376
|
+
if (options.incrementBy !== undefined)
|
|
2377
|
+
sql += ` INCREMENT BY ${options.incrementBy}`;
|
|
2378
|
+
if (options.minvalue !== undefined)
|
|
2379
|
+
sql += ` MINVALUE ${options.minvalue}`;
|
|
2380
|
+
if (options.maxvalue !== undefined)
|
|
2381
|
+
sql += ` MAXVALUE ${options.maxvalue}`;
|
|
2382
|
+
if (options.cycle)
|
|
2383
|
+
sql += ' CYCLE';
|
|
2384
|
+
if (options.cache !== undefined)
|
|
2385
|
+
sql += ` CACHE ${options.cache}`;
|
|
2386
|
+
if (options.order)
|
|
2387
|
+
sql += ' ORDER';
|
|
2388
|
+
if (options.nocache)
|
|
2389
|
+
sql += ' NOCACHE';
|
|
2390
|
+
await this.query(sql);
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Drop a sequence.
|
|
2394
|
+
*
|
|
2395
|
+
* Like the other DDL paths in this dialect (tables, users, roles), Oracle's
|
|
2396
|
+
* `DROP SEQUENCE ... IF EXISTS` syntax is 23c-only and raises ORA-00922
|
|
2397
|
+
* against the 19c/21c versions this dialect targets. When `ifExists` is
|
|
2398
|
+
* requested, wrap the plain DDL so that ORA-02289 ("sequence does not
|
|
2399
|
+
* exist") is silently ignored instead (see {@link wrapIgnoringOraErrors}).
|
|
2400
|
+
*/
|
|
2401
|
+
async dropSequence(sequenceName, options) {
|
|
2402
|
+
const schema = options?.schema || this.config?.schema || 'SYSTEM';
|
|
2403
|
+
const seqName = `${schema}.${this.escapeId(sequenceName)}`;
|
|
2404
|
+
const plainSql = `DROP SEQUENCE ${seqName}`;
|
|
2405
|
+
const sql = options?.ifExists
|
|
2406
|
+
? this.wrapIgnoringOraErrors(plainSql, [OracleDialect.ORA_SEQUENCE_DOES_NOT_EXIST])
|
|
2407
|
+
: plainSql;
|
|
2408
|
+
await this.query(sql);
|
|
2409
|
+
}
|
|
2410
|
+
async nextSequenceValue(sequenceName) {
|
|
2411
|
+
const result = await this.query(`SELECT ${this.escapeId(sequenceName)}.NEXTVAL as value FROM DUAL`);
|
|
2412
|
+
return result.rows[0]?.value;
|
|
2413
|
+
}
|
|
2414
|
+
async hasSequence(sequenceName, schema) {
|
|
2415
|
+
const schemaName = schema || this.config?.schema || 'SYSTEM';
|
|
2416
|
+
const result = await this.query(`SELECT SEQUENCE_NAME FROM ALL_SEQUENCES WHERE SEQUENCE_NAME = UPPER('${sequenceName}') AND SEQUENCE_OWNER = UPPER('${schemaName}')`);
|
|
2417
|
+
return result.rows.length > 0;
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* List all sequences in the database (Oracle)
|
|
2421
|
+
* @returns Array of sequence names
|
|
2422
|
+
*/
|
|
2423
|
+
async listSequences() {
|
|
2424
|
+
const schemaName = this.config?.schema || 'SYSTEM';
|
|
2425
|
+
const sql = `SELECT SEQUENCE_NAME, SEQUENCE_OWNER FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = UPPER('${schemaName}') ORDER BY SEQUENCE_OWNER, SEQUENCE_NAME`;
|
|
2426
|
+
const result = await this.query(sql);
|
|
2427
|
+
return result.rows.map((row) => `${row.SEQUENCE_OWNER}.${row.SEQUENCE_NAME}`);
|
|
2428
|
+
}
|
|
2429
|
+
// ==================== Stored Procedures ====================
|
|
2430
|
+
async createStoredProcedure(options) {
|
|
2431
|
+
const schema = options.schema || this.config?.schema || 'SYSTEM';
|
|
2432
|
+
const procName = `${schema}.${this.escapeId(options.name)}`;
|
|
2433
|
+
// Build parameter list
|
|
2434
|
+
let paramList = '';
|
|
2435
|
+
if (options.params && options.params.length > 0) {
|
|
2436
|
+
paramList = options.params
|
|
2437
|
+
.map((param) => {
|
|
2438
|
+
const dir = param.mode === 'OUT' ? 'OUT' : param.mode === 'INOUT' ? 'IN OUT' : 'IN';
|
|
2439
|
+
return `${this.escapeId(param.name)} ${dir} ${param.type}`;
|
|
2440
|
+
})
|
|
2441
|
+
.join(', ');
|
|
2442
|
+
}
|
|
2443
|
+
let sql;
|
|
2444
|
+
if (options.returnType) {
|
|
2445
|
+
// It's a function
|
|
2446
|
+
sql = 'CREATE OR REPLACE FUNCTION';
|
|
2447
|
+
if (options.replace) {
|
|
2448
|
+
sql = 'CREATE OR REPLACE FUNCTION';
|
|
2449
|
+
}
|
|
2450
|
+
else if (options.ifNotExists) {
|
|
2451
|
+
const exists = await this.hasStoredProcedure(options.name, schema);
|
|
2452
|
+
if (exists) {
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
sql += ` ${procName}(${paramList}) RETURN ${options.returnType}`;
|
|
2457
|
+
}
|
|
2458
|
+
else {
|
|
2459
|
+
// It's a procedure
|
|
2460
|
+
sql = 'CREATE OR REPLACE PROCEDURE';
|
|
2461
|
+
if (options.replace) {
|
|
2462
|
+
sql = 'CREATE OR REPLACE PROCEDURE';
|
|
2463
|
+
}
|
|
2464
|
+
else if (options.ifNotExists) {
|
|
2465
|
+
const exists = await this.hasStoredProcedure(options.name, schema);
|
|
2466
|
+
if (exists) {
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
sql += ` ${procName}(${paramList})`;
|
|
2471
|
+
}
|
|
2472
|
+
// Add AS clause with body
|
|
2473
|
+
sql += ` AS\n${options.body}`;
|
|
2474
|
+
// Add comment if provided
|
|
2475
|
+
if (options.comment) {
|
|
2476
|
+
await this.commentProcedure(options.name, options.comment, schema);
|
|
2477
|
+
}
|
|
2478
|
+
await this.query(sql);
|
|
2479
|
+
}
|
|
2480
|
+
async dropStoredProcedure(procedureName, options) {
|
|
2481
|
+
const schema = options?.schema || this.config?.schema || 'SYSTEM';
|
|
2482
|
+
const procName = `${schema}.${this.escapeId(procedureName)}`;
|
|
2483
|
+
let sql = 'DROP PROCEDURE';
|
|
2484
|
+
if (options?.ifExists) {
|
|
2485
|
+
sql += ' IF EXISTS';
|
|
2486
|
+
}
|
|
2487
|
+
sql += ` ${procName}`;
|
|
2488
|
+
await this.query(sql);
|
|
2489
|
+
}
|
|
2490
|
+
/**
|
|
2491
|
+
* Drop a stored procedure (alias for dropStoredProcedure)
|
|
2492
|
+
*/
|
|
2493
|
+
async dropProcedure(procedureName, options) {
|
|
2494
|
+
return this.dropStoredProcedure(procedureName, options);
|
|
2495
|
+
}
|
|
2496
|
+
/**
|
|
2497
|
+
* Create a stored procedure (alias for createStoredProcedure)
|
|
2498
|
+
*/
|
|
2499
|
+
async createProcedure(options) {
|
|
2500
|
+
return this.createStoredProcedure(options);
|
|
2501
|
+
}
|
|
2502
|
+
async executeStoredProcedure(options) {
|
|
2503
|
+
const schema = options.schema || this.config?.schema || 'SYSTEM';
|
|
2504
|
+
const procName = schema === 'SYSTEM'
|
|
2505
|
+
? this.escapeId(options.procedureName)
|
|
2506
|
+
: `${schema}.${this.escapeId(options.procedureName)}`;
|
|
2507
|
+
// Build parameter list
|
|
2508
|
+
let paramList = '';
|
|
2509
|
+
if (options.params) {
|
|
2510
|
+
paramList = Object.entries(options.params)
|
|
2511
|
+
.map(([key, value]) => {
|
|
2512
|
+
if (typeof value === 'string') {
|
|
2513
|
+
return `'${value}'`;
|
|
2514
|
+
}
|
|
2515
|
+
return value;
|
|
2516
|
+
})
|
|
2517
|
+
.join(', ');
|
|
2518
|
+
}
|
|
2519
|
+
const sql = `BEGIN ${procName}(${paramList}); END;`;
|
|
2520
|
+
return await this.query(sql, { raw: true });
|
|
2521
|
+
}
|
|
2522
|
+
async hasStoredProcedure(procedureName, schema) {
|
|
2523
|
+
const schemaName = schema || this.config?.schema || 'SYSTEM';
|
|
2524
|
+
const result = await this.query(`SELECT OBJECT_NAME FROM ALL_PROCEDURES WHERE OBJECT_NAME = UPPER('${procedureName}') AND OWNER = UPPER('${schemaName}')`);
|
|
2525
|
+
return result.rows.length > 0;
|
|
2526
|
+
}
|
|
2527
|
+
async commentProcedure(procedureName, comment, schema) {
|
|
2528
|
+
await this.query(`COMMENT ON PROCEDURE ${schema}.${this.escapeId(procedureName)} IS '${this.escape(comment)}'`);
|
|
2529
|
+
}
|
|
2530
|
+
// ==================== Triggers ====================
|
|
2531
|
+
async createTrigger(options) {
|
|
2532
|
+
const schema = options.schema || this.config?.schema || 'SYSTEM';
|
|
2533
|
+
const triggerName = `${schema}.${this.escapeId(options.name)}`;
|
|
2534
|
+
const tableName = `${schema}.${this.escapeId(options.tableName)}`;
|
|
2535
|
+
// Build timing and events
|
|
2536
|
+
const timing = options.timing === 'INSTEAD OF' ? 'INSTEAD OF' : options.timing;
|
|
2537
|
+
const events = (options.events || []).join(' OR ');
|
|
2538
|
+
let sql = 'CREATE TRIGGER';
|
|
2539
|
+
if (options.replace) {
|
|
2540
|
+
sql = 'CREATE OR REPLACE TRIGGER';
|
|
2541
|
+
}
|
|
2542
|
+
sql += ` ${triggerName} ${timing} ${events} ON ${tableName}`;
|
|
2543
|
+
// Add FOR EACH ROW if specified
|
|
2544
|
+
if (options.level === 'ROW') {
|
|
2545
|
+
sql += ' FOR EACH ROW';
|
|
2546
|
+
}
|
|
2547
|
+
// Add WHEN clause if provided
|
|
2548
|
+
if (options.when) {
|
|
2549
|
+
sql += ` WHEN (${options.when})`;
|
|
2550
|
+
}
|
|
2551
|
+
// Add trigger body
|
|
2552
|
+
sql += `\nBEGIN\n${options.body}\nEND;`;
|
|
2553
|
+
await this.query(sql);
|
|
2554
|
+
}
|
|
2555
|
+
async dropTrigger(triggerName, tableName, options) {
|
|
2556
|
+
const schema = options?.schema || this.config?.schema || 'SYSTEM';
|
|
2557
|
+
const triggerFullName = `${schema}.${this.escapeId(triggerName)}`;
|
|
2558
|
+
let sql = 'DROP TRIGGER';
|
|
2559
|
+
if (options?.ifExists) {
|
|
2560
|
+
sql += ' IF EXISTS';
|
|
2561
|
+
}
|
|
2562
|
+
sql += ` ${triggerFullName}`;
|
|
2563
|
+
await this.query(sql);
|
|
2564
|
+
}
|
|
2565
|
+
async hasTrigger(triggerName, tableName, schema) {
|
|
2566
|
+
const schemaName = schema || this.config?.schema || 'SYSTEM';
|
|
2567
|
+
const result = await this.query(`SELECT TRIGGER_NAME FROM USER_TRIGGERS
|
|
2568
|
+
WHERE TRIGGER_NAME = UPPER('${triggerName}')
|
|
2569
|
+
AND TABLE_NAME = UPPER('${tableName}')`);
|
|
2570
|
+
return result.rows.length > 0;
|
|
2571
|
+
}
|
|
2572
|
+
// ==================== RLS (Row-Level Security) ====================
|
|
2573
|
+
/**
|
|
2574
|
+
* Create a security policy (RLS)
|
|
2575
|
+
*/
|
|
2576
|
+
async createPolicy(options) {
|
|
2577
|
+
const tableName = options.schema
|
|
2578
|
+
? `${this.quoteIdentifier(options.schema)}.${this.quoteIdentifier(options.tableName)}`
|
|
2579
|
+
: this.quoteTable(options.tableName);
|
|
2580
|
+
let sql = `BEGIN
|
|
2581
|
+
SYS.DBMS_RLS.ADD_POLICY(
|
|
2582
|
+
object_schema => ${options.schema ? `'${options.schema}'` : 'USER'},
|
|
2583
|
+
object_name => '${options.tableName}',
|
|
2584
|
+
policy_name => '${options.name}',
|
|
2585
|
+
function_schema => USER,
|
|
2586
|
+
policy_function => '${options.name}_func',
|
|
2587
|
+
`;
|
|
2588
|
+
if (options.roles && options.roles.length > 0) {
|
|
2589
|
+
sql += ` sec_relevant_cols => '${options.roles.join(',')}',
|
|
2590
|
+
`;
|
|
2591
|
+
}
|
|
2592
|
+
if (options.withCheck) {
|
|
2593
|
+
sql += ` update_check => TRUE,
|
|
2594
|
+
`;
|
|
2595
|
+
}
|
|
2596
|
+
sql += ` );
|
|
2597
|
+
END;`;
|
|
2598
|
+
// First create the policy function
|
|
2599
|
+
const funcName = `${options.name}_func`;
|
|
2600
|
+
let funcSql = `
|
|
2601
|
+
CREATE OR REPLACE FUNCTION ${funcName}(schema_var VARCHAR2, table_var VARCHAR2)
|
|
2602
|
+
RETURN VARCHAR2
|
|
2603
|
+
AS
|
|
2604
|
+
return_clause VARCHAR2(4000);
|
|
2605
|
+
BEGIN
|
|
2606
|
+
`;
|
|
2607
|
+
if (options.using) {
|
|
2608
|
+
funcSql += ` return_clause := '${options.using}';
|
|
2609
|
+
`;
|
|
2610
|
+
}
|
|
2611
|
+
else {
|
|
2612
|
+
funcSql += ` return_clause := '1=1';
|
|
2613
|
+
`;
|
|
2614
|
+
}
|
|
2615
|
+
funcSql += `
|
|
2616
|
+
RETURN return_clause;
|
|
2617
|
+
END;
|
|
2618
|
+
`;
|
|
2619
|
+
await this.query(funcSql);
|
|
2620
|
+
// Then add the policy using DBMS_RLS
|
|
2621
|
+
const policySql = `
|
|
2622
|
+
BEGIN
|
|
2623
|
+
SYS.DBMS_RLS.ADD_POLICY(
|
|
2624
|
+
object_schema => ${options.schema ? `'${options.schema}'` : 'NULL'},
|
|
2625
|
+
object_name => '${options.tableName}',
|
|
2626
|
+
policy_name => '${options.name}',
|
|
2627
|
+
policy_function => '${funcName}'
|
|
2628
|
+
);
|
|
2629
|
+
END;
|
|
2630
|
+
`;
|
|
2631
|
+
await this.query(policySql);
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* Drop a security policy
|
|
2635
|
+
*/
|
|
2636
|
+
async dropPolicy(policyName, tableName, options) {
|
|
2637
|
+
const schema = options?.schema || null;
|
|
2638
|
+
const sql = `
|
|
2639
|
+
BEGIN
|
|
2640
|
+
SYS.DBMS_RLS.DROP_POLICY(
|
|
2641
|
+
object_schema => ${schema ? `'${schema}'` : 'NULL'},
|
|
2642
|
+
object_name => '${tableName}',
|
|
2643
|
+
policy_name => '${policyName}'
|
|
2644
|
+
);
|
|
2645
|
+
END;
|
|
2646
|
+
`;
|
|
2647
|
+
await this.query(sql);
|
|
2648
|
+
}
|
|
2649
|
+
/**
|
|
2650
|
+
* Enable Row-Level Security on a table
|
|
2651
|
+
*/
|
|
2652
|
+
async enableRLS(tableName, schema) {
|
|
2653
|
+
const tableNameWithSchema = schema
|
|
2654
|
+
? `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`
|
|
2655
|
+
: this.quoteTable(tableName);
|
|
2656
|
+
const sql = `ALTER TABLE ${tableNameWithSchema} ENABLE ROW LEVEL SECURITY`;
|
|
2657
|
+
await this.query(sql);
|
|
2658
|
+
}
|
|
2659
|
+
/**
|
|
2660
|
+
* Disable Row-Level Security on a table
|
|
2661
|
+
*/
|
|
2662
|
+
async disableRLS(tableName, schema) {
|
|
2663
|
+
const tableNameWithSchema = schema
|
|
2664
|
+
? `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`
|
|
2665
|
+
: this.quoteTable(tableName);
|
|
2666
|
+
const sql = `ALTER TABLE ${tableNameWithSchema} DISABLE ROW LEVEL SECURITY`;
|
|
2667
|
+
await this.query(sql);
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* Check if a policy exists
|
|
2671
|
+
*/
|
|
2672
|
+
async hasPolicy(policyName, tableName) {
|
|
2673
|
+
const result = await this.query(`SELECT POLICY_NAME FROM DBA_POLICIES
|
|
2674
|
+
WHERE OBJECT_NAME = UPPER('${tableName}')
|
|
2675
|
+
AND POLICY_NAME = UPPER('${policyName}')`);
|
|
2676
|
+
return result.rows.length > 0;
|
|
2677
|
+
}
|
|
2678
|
+
// ==================== Comments (Not Fully Supported) ====================
|
|
2679
|
+
/**
|
|
2680
|
+
* Add comment to a table
|
|
2681
|
+
*/
|
|
2682
|
+
async commentTable(tableName, comment) {
|
|
2683
|
+
const sql = `COMMENT ON TABLE ${this.quoteTable(tableName)} IS ${this.escapeString(comment)}`;
|
|
2684
|
+
await this.query(sql);
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Add comment to a column
|
|
2688
|
+
*/
|
|
2689
|
+
async commentColumn(tableName, columnName, comment) {
|
|
2690
|
+
const sql = `COMMENT ON COLUMN ${this.quoteTable(tableName)}.${this.quoteIdentifier(columnName)} IS ${this.escapeString(comment)}`;
|
|
2691
|
+
await this.query(sql);
|
|
2692
|
+
}
|
|
2693
|
+
// ==================== Advanced Indexes ====================
|
|
2694
|
+
/**
|
|
2695
|
+
* Create a partial index (index with WHERE clause)
|
|
2696
|
+
*/
|
|
2697
|
+
async createPartialIndex(tableName, indexName, fields, where, options) {
|
|
2698
|
+
const fieldsSql = fields.map((f) => this.quoteIdentifier(f)).join(', ');
|
|
2699
|
+
let sql = `CREATE`;
|
|
2700
|
+
if (options?.unique) {
|
|
2701
|
+
sql += ' UNIQUE';
|
|
2702
|
+
}
|
|
2703
|
+
sql += ` INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteTable(tableName)} (${fieldsSql}) WHERE ${where}`;
|
|
2704
|
+
if (options?.tablespace) {
|
|
2705
|
+
sql += ` TABLESPACE ${options.tablespace}`;
|
|
2706
|
+
}
|
|
2707
|
+
if (options?.compress) {
|
|
2708
|
+
sql += ` COMPRESS`;
|
|
2709
|
+
}
|
|
2710
|
+
await this.query(sql);
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Create an expression index (functional index)
|
|
2714
|
+
*/
|
|
2715
|
+
async createExpressionIndex(tableName, indexName, expression, options) {
|
|
2716
|
+
let sql = `CREATE`;
|
|
2717
|
+
if (options?.unique) {
|
|
2718
|
+
sql += ' UNIQUE';
|
|
2719
|
+
}
|
|
2720
|
+
sql += ` INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteTable(tableName)} (${expression})`;
|
|
2721
|
+
if (options?.tablespace) {
|
|
2722
|
+
sql += ` TABLESPACE ${options.tablespace}`;
|
|
2723
|
+
}
|
|
2724
|
+
if (options?.compress) {
|
|
2725
|
+
sql += ` COMPRESS`;
|
|
2726
|
+
}
|
|
2727
|
+
await this.query(sql);
|
|
2728
|
+
}
|
|
2729
|
+
// ==================== Identity & Computed Columns ====================
|
|
2730
|
+
/**
|
|
2731
|
+
* Create an identity column (auto-increment)
|
|
2732
|
+
*/
|
|
2733
|
+
async createIdentityColumn(tableName, columnName, options) {
|
|
2734
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} MODIFY ${this.quoteIdentifier(columnName)} GENERATED ALWAYS AS IDENTITY`;
|
|
2735
|
+
if (options) {
|
|
2736
|
+
const optionsList = [];
|
|
2737
|
+
if (options.startWith !== undefined) {
|
|
2738
|
+
optionsList.push(`START WITH ${options.startWith}`);
|
|
2739
|
+
}
|
|
2740
|
+
if (options.incrementBy !== undefined) {
|
|
2741
|
+
optionsList.push(`INCREMENT BY ${options.incrementBy}`);
|
|
2742
|
+
}
|
|
2743
|
+
if (options.minvalue !== undefined) {
|
|
2744
|
+
optionsList.push(`MINVALUE ${options.minvalue}`);
|
|
2745
|
+
}
|
|
2746
|
+
if (options.maxvalue !== undefined) {
|
|
2747
|
+
optionsList.push(`MAXVALUE ${options.maxvalue}`);
|
|
2748
|
+
}
|
|
2749
|
+
if (options.cycle) {
|
|
2750
|
+
optionsList.push('CYCLE');
|
|
2751
|
+
}
|
|
2752
|
+
else {
|
|
2753
|
+
optionsList.push('NO CYCLE');
|
|
2754
|
+
}
|
|
2755
|
+
if (optionsList.length > 0) {
|
|
2756
|
+
sql += ` (${optionsList.join(', ')})`;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
await this.query(sql);
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Create a computed (virtual) column
|
|
2763
|
+
*/
|
|
2764
|
+
async createComputedColumn(tableName, columnName, expression, options) {
|
|
2765
|
+
const persisted = options?.persisted === true ? 'STORED' : 'VIRTUAL';
|
|
2766
|
+
const dataType = options?.type || 'VARCHAR2(4000)';
|
|
2767
|
+
const sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD ${this.quoteIdentifier(columnName)} ${dataType} GENERATED ALWAYS AS (${expression}) ${persisted}`;
|
|
2768
|
+
await this.query(sql);
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Bulk insert records into a table
|
|
2772
|
+
*/
|
|
2773
|
+
async bulkInsert(tableName, records, _options) {
|
|
2774
|
+
if (records.length === 0) {
|
|
2775
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
2776
|
+
}
|
|
2777
|
+
const columns = Object.keys(records[0]);
|
|
2778
|
+
const values = [];
|
|
2779
|
+
const placeholders = [];
|
|
2780
|
+
for (const record of records) {
|
|
2781
|
+
const rowPlaceholders = [];
|
|
2782
|
+
for (let i = 0; i < columns.length; i++) {
|
|
2783
|
+
rowPlaceholders.push(`:${i + 1}`);
|
|
2784
|
+
values.push(record[columns[i]]);
|
|
2785
|
+
}
|
|
2786
|
+
placeholders.push(`(${rowPlaceholders.join(', ')})`);
|
|
2787
|
+
}
|
|
2788
|
+
const sql = `INSERT INTO ${this.quoteTable(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES ${placeholders.join(', ')}`;
|
|
2789
|
+
return this.query(sql, { replacements: values });
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Add a foreign key to a table
|
|
2793
|
+
*/
|
|
2794
|
+
async addForeignKey(tableName, columnName, referencedTableName, referencedColumnName, options) {
|
|
2795
|
+
const constraintName = options?.name || `${tableName}_${columnName}_fkey`;
|
|
2796
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} FOREIGN KEY (${this.escapeId(columnName)}) REFERENCES ${this.quoteTable(referencedTableName)}(${this.escapeId(referencedColumnName)})`;
|
|
2797
|
+
const clauses = [];
|
|
2798
|
+
if (options?.onDelete) {
|
|
2799
|
+
clauses.push(`ON DELETE ${options.onDelete}`);
|
|
2800
|
+
}
|
|
2801
|
+
if (options?.onUpdate) {
|
|
2802
|
+
clauses.push(`ON UPDATE ${options.onUpdate}`);
|
|
2803
|
+
}
|
|
2804
|
+
if (clauses.length > 0) {
|
|
2805
|
+
sql += ' ' + clauses.join(' ');
|
|
2806
|
+
}
|
|
2807
|
+
await this.query(sql);
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Rename a column
|
|
2811
|
+
*/
|
|
2812
|
+
async renameColumn(tableName, oldColumnName, newColumnName) {
|
|
2813
|
+
const sql = `ALTER TABLE ${this.quoteTable(tableName)} RENAME COLUMN ${this.escapeId(oldColumnName)} TO ${this.escapeId(newColumnName)}`;
|
|
2814
|
+
await this.query(sql);
|
|
2815
|
+
}
|
|
2816
|
+
/**
|
|
2817
|
+
* Create a fulltext index (Oracle uses Oracle Text)
|
|
2818
|
+
*/
|
|
2819
|
+
async createFulltextIndex(tableName, indexName, fields, options) {
|
|
2820
|
+
// Unlike MySQL/Postgres/MSSQL FULLTEXT indexes, an Oracle Text
|
|
2821
|
+
// `CTXSYS.CONTEXT` index is defined `ON table(single_column)` - it can
|
|
2822
|
+
// only ever cover one column. There is no multi-column CONTEXT index
|
|
2823
|
+
// syntax (a "column list" like a B-tree composite index does not exist
|
|
2824
|
+
// for this INDEXTYPE); attempting to pass more than one column here
|
|
2825
|
+
// would previously produce SQL that either errors at DDL time or (worse)
|
|
2826
|
+
// silently indexes only the first field depending on driver behavior.
|
|
2827
|
+
//
|
|
2828
|
+
// We throw rather than silently fan out into N indexes under the same
|
|
2829
|
+
// `indexName`, since the caller only supplied a single index name and
|
|
2830
|
+
// Oracle index names must be unique - creating multiple indexes here
|
|
2831
|
+
// would require inventing per-column suffixes the caller never asked
|
|
2832
|
+
// for. Call this once per column instead, e.g.:
|
|
2833
|
+
// createFulltextIndex('articles', 'articles_title_ctx', ['title']);
|
|
2834
|
+
// createFulltextIndex('articles', 'articles_body_ctx', ['body']);
|
|
2835
|
+
if (fields.length !== 1) {
|
|
2836
|
+
throw new Error(`createFulltextIndex: Oracle Text (CTXSYS.CONTEXT) indexes are single-column only - ` +
|
|
2837
|
+
`got ${fields.length} fields (${fields.join(', ')}). Call createFulltextIndex() once ` +
|
|
2838
|
+
`per column with a distinct index name for each, e.g. ` +
|
|
2839
|
+
`createFulltextIndex('${tableName}', '<index_name>', ['${fields[0] ?? 'column'}']).`);
|
|
2840
|
+
}
|
|
2841
|
+
const sql = `CREATE INDEX ${this.escapeId(indexName)} ON ${this.quoteTable(tableName)} (${this.escapeId(fields[0])}) INDEXTYPE IS CTXSYS.CONTEXT`;
|
|
2842
|
+
await this.query(sql);
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Create a spatial index (Oracle uses Spatial indexing)
|
|
2846
|
+
*/
|
|
2847
|
+
async createSpatialIndex(tableName, indexName, fields, _options) {
|
|
2848
|
+
const sql = `CREATE INDEX ${this.escapeId(indexName)} ON ${this.quoteTable(tableName)} (${fields.map((f) => this.escapeId(f)).join(', ')}) INDEXTYPE IS MDSYS.SPATIAL_INDEX`;
|
|
2849
|
+
await this.query(sql);
|
|
2850
|
+
}
|
|
2851
|
+
// ==================== Spatial Functions ====================
|
|
2852
|
+
/**
|
|
2853
|
+
* ST_Distance - calculate distance between two geometries (Oracle)
|
|
2854
|
+
*/
|
|
2855
|
+
stDistance(geom1, geom2, srid) {
|
|
2856
|
+
const sridStr = srid ? `, ${srid}` : '';
|
|
2857
|
+
return `SDO_GEOM.SDO_DISTANCE(${geom1}, ${geom2}${sridStr})`;
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* ST_Within - check if geometry A is within geometry B (Oracle)
|
|
2861
|
+
*/
|
|
2862
|
+
stWithin(geom1, geom2, srid) {
|
|
2863
|
+
const sridStr = srid ? `, ${srid}` : '';
|
|
2864
|
+
return `SDO_GEOM.SDO_WITHIN_DISTANCE(${geom1}, ${geom2}, '${sridStr}') = 'TRUE'`;
|
|
2865
|
+
}
|
|
2866
|
+
/**
|
|
2867
|
+
* ST_Contains - check if geometry A contains geometry B (Oracle)
|
|
2868
|
+
*/
|
|
2869
|
+
stContains(geom1, geom2, srid) {
|
|
2870
|
+
const sridStr = srid ? `, ${srid}` : '';
|
|
2871
|
+
return `SDO_GEOM.SDO_CONTAINS(${geom1}, ${geom2}${sridStr}) = 'TRUE'`;
|
|
2872
|
+
}
|
|
2873
|
+
/**
|
|
2874
|
+
* ST_Intersects - check if geometries intersect (Oracle)
|
|
2875
|
+
*/
|
|
2876
|
+
stIntersects(geom1, geom2, srid) {
|
|
2877
|
+
const sridStr = srid ? `, ${srid}` : '';
|
|
2878
|
+
return `SDO_GEOM.SDO_INTERSECT(${geom1}, ${geom2}${sridStr}) = 'TRUE'`;
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* ST_DWithin - check if geometries are within a given distance (Oracle)
|
|
2882
|
+
*/
|
|
2883
|
+
stDWithin(geom1, geom2, distance, srid) {
|
|
2884
|
+
const sridStr = srid ? `, ${srid}` : '';
|
|
2885
|
+
return `SDO_GEOM.SDO_WITHIN_DISTANCE(${geom1}, ${geom2}, 'distance=${distance}${sridStr}') = 'TRUE'`;
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* ST_AsText - convert geometry to text representation (Oracle)
|
|
2889
|
+
*/
|
|
2890
|
+
stAsText(geom) {
|
|
2891
|
+
return `SDO_GEOM.SDO_GEOM_TOSQL(${geom})`;
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* ST_GeomFromText - create geometry from text (Oracle)
|
|
2895
|
+
*/
|
|
2896
|
+
stGeomFromText(wkt, srid) {
|
|
2897
|
+
return srid ? `SDO_GEOMETRY('${wkt}', ${srid})` : `SDO_GEOMETRY('${wkt}')`;
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Build a JSON_TABLE expression to shred a JSON document/array into relational rows
|
|
2901
|
+
* (Oracle 12c+). Useful to project a JSON array column into rows instead of a
|
|
2902
|
+
* correlated subquery.
|
|
2903
|
+
* Oracle: JSON_TABLE(expr, '$[*]' COLUMNS(name type PATH '$.path', ...)) alias
|
|
2904
|
+
*/
|
|
2905
|
+
buildJsonTable(jsonExpression, rowPath, columns, alias) {
|
|
2906
|
+
const columnDefs = columns
|
|
2907
|
+
.map((col) => {
|
|
2908
|
+
if (col.forOrdinality) {
|
|
2909
|
+
return `${this.escapeId(col.name)} FOR ORDINALITY`;
|
|
2910
|
+
}
|
|
2911
|
+
return `${this.escapeId(col.name)} ${col.type} PATH '${col.path}'`;
|
|
2912
|
+
})
|
|
2913
|
+
.join(', ');
|
|
2914
|
+
return `JSON_TABLE(${jsonExpression}, '${rowPath}' COLUMNS(${columnDefs})) ${this.escapeId(alias)}`;
|
|
2915
|
+
}
|
|
2916
|
+
// ==================== String Aggregation / Reporting ====================
|
|
2917
|
+
/**
|
|
2918
|
+
* Build a `LISTAGG(expr, delimiter) WITHIN GROUP (ORDER BY ...)` expression
|
|
2919
|
+
* (Oracle 11gR2+), the standard idiom for collapsing grouped rows into a
|
|
2920
|
+
* single delimited string.
|
|
2921
|
+
*/
|
|
2922
|
+
listAgg(expr, delimiter = ',', orderBy, overflow) {
|
|
2923
|
+
const orderByClause = orderBy ? this.buildOrderByItems(orderBy) : expr;
|
|
2924
|
+
const escapedDelimiter = delimiter.replace(/'/g, "''");
|
|
2925
|
+
const overflowClause = overflow ? ` ${overflow}` : '';
|
|
2926
|
+
return `LISTAGG(${expr}, '${escapedDelimiter}'${overflowClause}) WITHIN GROUP (ORDER BY ${orderByClause})`;
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* Build an Oracle `PIVOT` query, rotating rows into columns (11g+).
|
|
2930
|
+
*/
|
|
2931
|
+
buildPivotQuery(options) {
|
|
2932
|
+
const aggregates = Array.isArray(options.aggregate) ? options.aggregate : [options.aggregate];
|
|
2933
|
+
const aggregateSql = aggregates
|
|
2934
|
+
.map((agg) => typeof agg === 'string'
|
|
2935
|
+
? agg
|
|
2936
|
+
: `${agg.expr}${agg.as ? ` AS ${this.quoteIdentifier(agg.as)}` : ''}`)
|
|
2937
|
+
.join(', ');
|
|
2938
|
+
const pivotValues = options.pivotValues
|
|
2939
|
+
.map((v) => {
|
|
2940
|
+
if (typeof v === 'object') {
|
|
2941
|
+
const literal = typeof v.value === 'number' ? v.value : `'${String(v.value).replace(/'/g, "''")}'`;
|
|
2942
|
+
return v.as ? `${literal} AS ${this.quoteIdentifier(v.as)}` : `${literal}`;
|
|
2943
|
+
}
|
|
2944
|
+
const literal = typeof v === 'number' ? v : `'${String(v).replace(/'/g, "''")}'`;
|
|
2945
|
+
return `${literal}`;
|
|
2946
|
+
})
|
|
2947
|
+
.join(', ');
|
|
2948
|
+
const alias = options.alias ? ` ${this.quoteIdentifier(options.alias)}` : '';
|
|
2949
|
+
return `${options.sourceQuery} PIVOT (${aggregateSql} FOR ${this.quoteIdentifier(options.pivotColumn)} IN (${pivotValues}))${alias}`;
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* Build an Oracle `UNPIVOT` query, rotating columns into rows (11g+).
|
|
2953
|
+
*/
|
|
2954
|
+
buildUnpivotQuery(options) {
|
|
2955
|
+
const inColumns = options.inColumns
|
|
2956
|
+
.map((c) => typeof c === 'string'
|
|
2957
|
+
? this.quoteIdentifier(c)
|
|
2958
|
+
: `${this.quoteIdentifier(c.column)}${c.as ? ` AS '${c.as.replace(/'/g, "''")}'` : ''}`)
|
|
2959
|
+
.join(', ');
|
|
2960
|
+
const includeNulls = options.includeNulls ? 'INCLUDE NULLS ' : '';
|
|
2961
|
+
const alias = options.alias ? ` ${this.quoteIdentifier(options.alias)}` : '';
|
|
2962
|
+
return `${options.sourceQuery} UNPIVOT ${includeNulls}(${this.quoteIdentifier(options.valueColumn)} FOR ${this.quoteIdentifier(options.forColumn)} IN (${inColumns}))${alias}`;
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
exports.OracleDialect = OracleDialect;
|
|
2966
|
+
/** ORA-00955: name is already used by an existing object. */
|
|
2967
|
+
OracleDialect.ORA_NAME_ALREADY_USED = 955;
|
|
2968
|
+
/** ORA-01920: user name conflicts with another user or role name. */
|
|
2969
|
+
OracleDialect.ORA_USER_ALREADY_EXISTS = 1920;
|
|
2970
|
+
/** ORA-01921: role name conflicts with another user or role name. */
|
|
2971
|
+
OracleDialect.ORA_ROLE_ALREADY_EXISTS = 1921;
|
|
2972
|
+
/** ORA-00942: table or view does not exist. */
|
|
2973
|
+
OracleDialect.ORA_TABLE_DOES_NOT_EXIST = 942;
|
|
2974
|
+
/** ORA-01918: user does not exist. */
|
|
2975
|
+
OracleDialect.ORA_USER_DOES_NOT_EXIST = 1918;
|
|
2976
|
+
/** ORA-01919: role does not exist. */
|
|
2977
|
+
OracleDialect.ORA_ROLE_DOES_NOT_EXIST = 1919;
|
|
2978
|
+
/** ORA-02289: sequence does not exist. */
|
|
2979
|
+
OracleDialect.ORA_SEQUENCE_DOES_NOT_EXIST = 2289;
|
|
2980
|
+
// ==================== Transaction Operations ====================
|
|
2981
|
+
/**
|
|
2982
|
+
* Oracle only genuinely supports two isolation levels via
|
|
2983
|
+
* `SET TRANSACTION ISOLATION LEVEL`: READ COMMITTED (the default) and
|
|
2984
|
+
* SERIALIZABLE. It also supports `SET TRANSACTION READ ONLY`, which is not
|
|
2985
|
+
* an isolation level but is sometimes requested through the same option.
|
|
2986
|
+
* The ANSI four-level model (READ UNCOMMITTED, REPEATABLE READ) that other
|
|
2987
|
+
* dialects in this codebase assume has no equivalent in Oracle's
|
|
2988
|
+
* multiversion-concurrency-control engine, so rather than silently
|
|
2989
|
+
* downgrading to a different (and misleadingly named) guarantee, we throw
|
|
2990
|
+
* a clear error for anything Oracle can't actually provide.
|
|
2991
|
+
*/
|
|
2992
|
+
OracleDialect.SUPPORTED_ISOLATION_LEVELS = new Set(['READ COMMITTED', 'SERIALIZABLE']);
|
|
2993
|
+
/**
|
|
2994
|
+
* Create Oracle dialect instance
|
|
2995
|
+
*/
|
|
2996
|
+
function createOracleDialect(options) {
|
|
2997
|
+
return new OracleDialect(options || {});
|
|
2998
|
+
}
|
|
2999
|
+
exports.default = OracleDialect;
|