ts-prorm-orm 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/CHANGELOG.md +1111 -0
  2. package/LICENSE +21 -0
  3. package/README.md +573 -0
  4. package/dist/audit/history-query.js +180 -0
  5. package/dist/audit/index.js +23 -0
  6. package/dist/audit/logger.js +236 -0
  7. package/dist/cache/cache-manager.js +84 -0
  8. package/dist/cache/index.js +16 -0
  9. package/dist/cache/redis-cluster-cache.js +557 -0
  10. package/dist/cli.js +2200 -0
  11. package/dist/compliance/audit-trail.js +68 -0
  12. package/dist/compliance/backup-verification.js +685 -0
  13. package/dist/compliance/breach-detector.js +505 -0
  14. package/dist/compliance/consent-record.js +227 -0
  15. package/dist/compliance/consent-versioning.js +331 -0
  16. package/dist/compliance/cross-border-log.js +530 -0
  17. package/dist/compliance/data-classifier.js +258 -0
  18. package/dist/compliance/data-lineage.js +565 -0
  19. package/dist/compliance/data-masker.js +303 -0
  20. package/dist/compliance/data-portability.js +265 -0
  21. package/dist/compliance/data-retention.js +195 -0
  22. package/dist/compliance/dsar-workflow.js +342 -0
  23. package/dist/compliance/field-encryption.js +223 -0
  24. package/dist/compliance/immutable-record.js +148 -0
  25. package/dist/compliance/index.js +260 -0
  26. package/dist/compliance/privacy-impact-assessment.js +364 -0
  27. package/dist/compliance/pseudonymization.js +269 -0
  28. package/dist/compliance/query-firewall.js +1182 -0
  29. package/dist/compliance/rate-limiter.js +580 -0
  30. package/dist/compliance/right-to-erasure.js +117 -0
  31. package/dist/compliance/row-level-security.js +246 -0
  32. package/dist/compliance/security-decorator.js +574 -0
  33. package/dist/compliance/security-monitor.js +525 -0
  34. package/dist/compliance/sensitive-data-discovery.js +476 -0
  35. package/dist/compliance/session-isolation.js +472 -0
  36. package/dist/compliance/tls-enforcer.js +108 -0
  37. package/dist/compliance/worm-storage.js +712 -0
  38. package/dist/connection-manager.js +198 -0
  39. package/dist/connection-pool.js +519 -0
  40. package/dist/decorators/audit.js +136 -0
  41. package/dist/decorators/belongs-to-many.js +115 -0
  42. package/dist/decorators/belongs-to.js +115 -0
  43. package/dist/decorators/check.js +435 -0
  44. package/dist/decorators/collate.js +329 -0
  45. package/dist/decorators/comment.js +205 -0
  46. package/dist/decorators/database-settings.js +236 -0
  47. package/dist/decorators/default.js +244 -0
  48. package/dist/decorators/encryption.js +235 -0
  49. package/dist/decorators/engine.js +97 -0
  50. package/dist/decorators/fk-constraints.js +594 -0
  51. package/dist/decorators/foreign-table.js +136 -0
  52. package/dist/decorators/generated.js +274 -0
  53. package/dist/decorators/has-many.js +127 -0
  54. package/dist/decorators/has-one.js +116 -0
  55. package/dist/decorators/hstore.js +129 -0
  56. package/dist/decorators/index.js +355 -0
  57. package/dist/decorators/json-column.js +83 -0
  58. package/dist/decorators/jsonb.js +101 -0
  59. package/dist/decorators/orm-decorators.js +425 -0
  60. package/dist/decorators/permissions.js +294 -0
  61. package/dist/decorators/procedure.js +210 -0
  62. package/dist/decorators/query-options.js +556 -0
  63. package/dist/decorators/range.js +167 -0
  64. package/dist/decorators/set-column.js +83 -0
  65. package/dist/decorators/spatial.js +114 -0
  66. package/dist/decorators/storage.js +580 -0
  67. package/dist/decorators/timezone.js +512 -0
  68. package/dist/decorators/trigger.js +90 -0
  69. package/dist/decorators/uuid.js +135 -0
  70. package/dist/decorators/view.js +258 -0
  71. package/dist/diagrams/chen-diagram.js +354 -0
  72. package/dist/diagrams/class-diagram.js +384 -0
  73. package/dist/diagrams/dependency-diagram.js +432 -0
  74. package/dist/diagrams/er-diagram.js +605 -0
  75. package/dist/diagrams/flow-diagram.js +394 -0
  76. package/dist/diagrams/gantt-diagram.js +411 -0
  77. package/dist/diagrams/index-diagram.js +353 -0
  78. package/dist/diagrams/index.js +184 -0
  79. package/dist/diagrams/migration-diagram.js +316 -0
  80. package/dist/diagrams/model-diagram.js +616 -0
  81. package/dist/diagrams/package-diagram.js +376 -0
  82. package/dist/diagrams/palette.js +85 -0
  83. package/dist/diagrams/relational-diagram.js +455 -0
  84. package/dist/diagrams/schemadoc-diagram.js +309 -0
  85. package/dist/diagrams/sequence-diagram.js +307 -0
  86. package/dist/diagrams/state-diagram.js +344 -0
  87. package/dist/diagrams/svg-dom.js +111 -0
  88. package/dist/diagrams/tree-diagram.js +250 -0
  89. package/dist/dialects/clickhouse/index.js +1742 -0
  90. package/dist/dialects/cockroachdb/index.js +4677 -0
  91. package/dist/dialects/cratedb/index.js +441 -0
  92. package/dist/dialects/databricks/index.js +517 -0
  93. package/dist/dialects/db2/index.js +2225 -0
  94. package/dist/dialects/dialect.js +720 -0
  95. package/dist/dialects/duckdb/index.js +2014 -0
  96. package/dist/dialects/exasol/index.js +357 -0
  97. package/dist/dialects/firebird/index.js +495 -0
  98. package/dist/dialects/greenplum/index.js +457 -0
  99. package/dist/dialects/hana/index.js +1741 -0
  100. package/dist/dialects/mariadb/index.js +3590 -0
  101. package/dist/dialects/mssql/index.js +2296 -0
  102. package/dist/dialects/mysql/index.js +4058 -0
  103. package/dist/dialects/oracle/index.js +2999 -0
  104. package/dist/dialects/postgres/index.js +5319 -0
  105. package/dist/dialects/query-stream-helper.js +152 -0
  106. package/dist/dialects/questdb/index.js +433 -0
  107. package/dist/dialects/redshift/index.js +2277 -0
  108. package/dist/dialects/singlestore/index.js +354 -0
  109. package/dist/dialects/snowflake/index.js +2082 -0
  110. package/dist/dialects/spanner/index.js +1768 -0
  111. package/dist/dialects/sqlite/index.js +3382 -0
  112. package/dist/dialects/tidb/index.js +377 -0
  113. package/dist/dialects/timescaledb/index.js +164 -0
  114. package/dist/dialects/trino/index.js +420 -0
  115. package/dist/dialects/turso/index.js +227 -0
  116. package/dist/dialects/vertica/index.js +317 -0
  117. package/dist/dialects/yugabytedb/index.js +424 -0
  118. package/dist/errors/index.js +472 -0
  119. package/dist/errors/utils.js +340 -0
  120. package/dist/errors.js +21 -0
  121. package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
  122. package/dist/extensions/catalog/mssql-features.js +147 -0
  123. package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
  124. package/dist/extensions/catalog/oracle-db2-features.js +196 -0
  125. package/dist/extensions/catalog/postgres-extensions.js +516 -0
  126. package/dist/extensions/index.js +71 -0
  127. package/dist/extensions/types.js +13 -0
  128. package/dist/foreign-data.js +173 -0
  129. package/dist/hooks/hooks-manager.js +350 -0
  130. package/dist/hooks/index.js +37 -0
  131. package/dist/index.js +463 -0
  132. package/dist/logging.js +335 -0
  133. package/dist/migrations/index.js +40 -0
  134. package/dist/migrations/migration.js +196 -0
  135. package/dist/migrations/migrator.js +411 -0
  136. package/dist/migrations/prormmigration.js +275 -0
  137. package/dist/migrations/query-interface.js +435 -0
  138. package/dist/migrations/seeder.js +353 -0
  139. package/dist/models/associations.js +852 -0
  140. package/dist/models/constraints.js +288 -0
  141. package/dist/models/data-types.js +2518 -0
  142. package/dist/models/decorators.js +445 -0
  143. package/dist/models/index.js +33 -0
  144. package/dist/models/indexes.js +531 -0
  145. package/dist/models/methods.js +382 -0
  146. package/dist/models/model-manager.js +103 -0
  147. package/dist/models/model.js +5349 -0
  148. package/dist/models/operators.js +67 -0
  149. package/dist/models/scopes.js +189 -0
  150. package/dist/models/typescript-types.js +26 -0
  151. package/dist/nosql/aerospike/index.js +205 -0
  152. package/dist/nosql/allegrograph/index.js +169 -0
  153. package/dist/nosql/arangodb/index.js +364 -0
  154. package/dist/nosql/azure-blob/index.js +206 -0
  155. package/dist/nosql/beanstalkd/index.js +231 -0
  156. package/dist/nosql/beequeue/index.js +210 -0
  157. package/dist/nosql/bigchaindb/index.js +195 -0
  158. package/dist/nosql/bigtable/index.js +224 -0
  159. package/dist/nosql/blazegraph/index.js +173 -0
  160. package/dist/nosql/bullmq/index.js +191 -0
  161. package/dist/nosql/cassandra/index.js +174 -0
  162. package/dist/nosql/chroma/index.js +190 -0
  163. package/dist/nosql/cloudflare-kv/index.js +220 -0
  164. package/dist/nosql/coherence/index.js +200 -0
  165. package/dist/nosql/cosmosdb/index.js +157 -0
  166. package/dist/nosql/couchbase/index.js +213 -0
  167. package/dist/nosql/dax/index.js +212 -0
  168. package/dist/nosql/deno-kv/index.js +206 -0
  169. package/dist/nosql/dgraph/index.js +171 -0
  170. package/dist/nosql/doris/index.js +169 -0
  171. package/dist/nosql/druid/index.js +162 -0
  172. package/dist/nosql/dynamodb/index.js +937 -0
  173. package/dist/nosql/elasticsearch/index.js +377 -0
  174. package/dist/nosql/etcd/index.js +502 -0
  175. package/dist/nosql/eventhubs/index.js +213 -0
  176. package/dist/nosql/eventstore/index.js +254 -0
  177. package/dist/nosql/faunadb/index.js +188 -0
  178. package/dist/nosql/firestore/index.js +177 -0
  179. package/dist/nosql/fluree/index.js +148 -0
  180. package/dist/nosql/fuseki/index.js +170 -0
  181. package/dist/nosql/gcs/index.js +172 -0
  182. package/dist/nosql/gearman/index.js +160 -0
  183. package/dist/nosql/geode/index.js +196 -0
  184. package/dist/nosql/graphdb/index.js +169 -0
  185. package/dist/nosql/graylog/index.js +188 -0
  186. package/dist/nosql/gridgain/index.js +171 -0
  187. package/dist/nosql/hazelcast/index.js +162 -0
  188. package/dist/nosql/hbase/index.js +230 -0
  189. package/dist/nosql/ignite/index.js +173 -0
  190. package/dist/nosql/immudb/index.js +184 -0
  191. package/dist/nosql/index.js +232 -0
  192. package/dist/nosql/infinispan/index.js +200 -0
  193. package/dist/nosql/influxdb/index.js +0 -0
  194. package/dist/nosql/kafka/index.js +234 -0
  195. package/dist/nosql/keyspaces/index.js +189 -0
  196. package/dist/nosql/kinesis/index.js +253 -0
  197. package/dist/nosql/leveldb/index.js +153 -0
  198. package/dist/nosql/lmdb/index.js +160 -0
  199. package/dist/nosql/loki/index.js +201 -0
  200. package/dist/nosql/marklogic/index.js +204 -0
  201. package/dist/nosql/materialize/index.js +145 -0
  202. package/dist/nosql/meilisearch/index.js +154 -0
  203. package/dist/nosql/memcached/index.js +223 -0
  204. package/dist/nosql/milvus/index.js +410 -0
  205. package/dist/nosql/minio/index.js +265 -0
  206. package/dist/nosql/momento/index.js +179 -0
  207. package/dist/nosql/mongodb/index.js +461 -0
  208. package/dist/nosql/nats/index.js +247 -0
  209. package/dist/nosql/nedb/index.js +164 -0
  210. package/dist/nosql/neo4j/index.js +450 -0
  211. package/dist/nosql/neptune/index.js +470 -0
  212. package/dist/nosql/nsq/index.js +200 -0
  213. package/dist/nosql/opensearch/index.js +186 -0
  214. package/dist/nosql/orientdb/index.js +175 -0
  215. package/dist/nosql/papertrail/index.js +200 -0
  216. package/dist/nosql/pinecone/index.js +0 -0
  217. package/dist/nosql/pinot/index.js +133 -0
  218. package/dist/nosql/pouchdb/index.js +172 -0
  219. package/dist/nosql/prometheus/index.js +174 -0
  220. package/dist/nosql/provendb/index.js +147 -0
  221. package/dist/nosql/pubsub/index.js +187 -0
  222. package/dist/nosql/pulsar/index.js +232 -0
  223. package/dist/nosql/qdrant/index.js +295 -0
  224. package/dist/nosql/qldb/index.js +192 -0
  225. package/dist/nosql/r2/index.js +281 -0
  226. package/dist/nosql/rabbitmq/index.js +237 -0
  227. package/dist/nosql/ravendb/index.js +175 -0
  228. package/dist/nosql/redis/index.js +607 -0
  229. package/dist/nosql/redpanda/index.js +237 -0
  230. package/dist/nosql/resque/index.js +203 -0
  231. package/dist/nosql/rethinkdb/index.js +232 -0
  232. package/dist/nosql/rocksdb/index.js +152 -0
  233. package/dist/nosql/rockset/index.js +126 -0
  234. package/dist/nosql/s3/index.js +298 -0
  235. package/dist/nosql/scylladb/index.js +178 -0
  236. package/dist/nosql/signoz/index.js +227 -0
  237. package/dist/nosql/sns/index.js +201 -0
  238. package/dist/nosql/solr/index.js +231 -0
  239. package/dist/nosql/splunk/index.js +227 -0
  240. package/dist/nosql/sqs/index.js +246 -0
  241. package/dist/nosql/stardog/index.js +169 -0
  242. package/dist/nosql/starrocks/index.js +170 -0
  243. package/dist/nosql/store.js +2 -0
  244. package/dist/nosql/sumologic/index.js +213 -0
  245. package/dist/nosql/surrealdb/index.js +179 -0
  246. package/dist/nosql/terminusdb/index.js +151 -0
  247. package/dist/nosql/tigergraph/index.js +469 -0
  248. package/dist/nosql/typesense/index.js +148 -0
  249. package/dist/nosql/unqlite/index.js +157 -0
  250. package/dist/nosql/upstash/index.js +167 -0
  251. package/dist/nosql/vercel-kv/index.js +166 -0
  252. package/dist/nosql/victoriametrics/index.js +213 -0
  253. package/dist/nosql/virtuoso/index.js +169 -0
  254. package/dist/nosql/weaviate/index.js +276 -0
  255. package/dist/operators/index.js +108 -0
  256. package/dist/operators.js +2690 -0
  257. package/dist/prisma-migrate/index.js +38 -0
  258. package/dist/prisma-migrate/migration-generator.js +125 -0
  259. package/dist/prisma-migrate/model-generator.js +172 -0
  260. package/dist/prisma-migrate/relations.js +100 -0
  261. package/dist/prisma-migrate/schema-parser.js +167 -0
  262. package/dist/prisma-migrate/type-mapper.js +40 -0
  263. package/dist/prorm.js +6832 -0
  264. package/dist/query-builders/cte-builder.js +80 -0
  265. package/dist/query-builders/functions/aggregate.js +390 -0
  266. package/dist/query-builders/functions/conditional.js +503 -0
  267. package/dist/query-builders/functions/datetime.js +695 -0
  268. package/dist/query-builders/functions/fulltext.js +439 -0
  269. package/dist/query-builders/functions/index.js +93 -0
  270. package/dist/query-builders/functions/json.js +427 -0
  271. package/dist/query-builders/functions/math.js +399 -0
  272. package/dist/query-builders/functions/string.js +518 -0
  273. package/dist/query-builders/functions/window.js +328 -0
  274. package/dist/query-builders/include-builder.js +323 -0
  275. package/dist/query-builders/index-expression-builder.js +242 -0
  276. package/dist/query-builders/index.js +161 -0
  277. package/dist/query-builders/insert-builder.js +164 -0
  278. package/dist/query-builders/model-helpers.js +38 -0
  279. package/dist/query-builders/order-limit-builder.js +239 -0
  280. package/dist/query-builders/sql-compiler.js +2040 -0
  281. package/dist/query-builders/subquery-builder.js +152 -0
  282. package/dist/query-builders/update-builder.js +182 -0
  283. package/dist/query-builders/view-builder.js +218 -0
  284. package/dist/query-builders/where-builder.js +1532 -0
  285. package/dist/query-interface.js +622 -0
  286. package/dist/query-optimizers/batch-optimizer.js +426 -0
  287. package/dist/query-optimizers/explain-plans.js +415 -0
  288. package/dist/query-optimizers/index.js +51 -0
  289. package/dist/query-optimizers/prepared-statement-cache.js +423 -0
  290. package/dist/query-optimizers/query-hints.js +438 -0
  291. package/dist/query-optimizers/query-optimizer.js +278 -0
  292. package/dist/query-optimizers/slow-query-logger.js +271 -0
  293. package/dist/replica-manager.js +507 -0
  294. package/dist/schema/index.js +15 -0
  295. package/dist/schema/migration-generator.js +374 -0
  296. package/dist/schema/schema-differ.js +549 -0
  297. package/dist/schema/types.js +6 -0
  298. package/dist/sql-constants.js +300 -0
  299. package/dist/sqlite-advanced.js +1047 -0
  300. package/dist/streams/index.js +12 -0
  301. package/dist/streams/transforms.js +178 -0
  302. package/dist/transaction.js +456 -0
  303. package/dist/types/index.js +180 -0
  304. package/dist/user-management.js +127 -0
  305. package/dist/utils/date.js +300 -0
  306. package/dist/utils/index.js +1079 -0
  307. package/dist/utils/string.js +161 -0
  308. package/dist/validators/index.js +19 -0
  309. package/dist/validators/validator.js +911 -0
  310. package/package.json +190 -0
@@ -0,0 +1,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;