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,4058 @@
1
+ "use strict";
2
+ /**
3
+ * MySQL dialect implementation for the TypeScript ORM
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.MySQLTransaction = exports.MySQLDialect = void 0;
10
+ exports.createMySQLDialect = createMySQLDialect;
11
+ const promise_1 = __importDefault(require("mysql2/promise"));
12
+ const prorm_1 = require("../../prorm");
13
+ const operators_1 = require("../../operators");
14
+ /**
15
+ * Default retry options for connection
16
+ */
17
+ const DEFAULT_RETRY_OPTIONS = {
18
+ max: 3,
19
+ timeout: 1000,
20
+ match: [
21
+ 'ECONNREFUSED',
22
+ 'ENOTFOUND',
23
+ 'ETIMEDOUT',
24
+ 'ER_ACCESS_DENIED_ERROR',
25
+ 'ER_BAD_DB_ERROR',
26
+ 'connect timeout',
27
+ 'connection timeout',
28
+ 'Too many connections',
29
+ 'Lock wait timeout',
30
+ ],
31
+ backoff: false,
32
+ backoffMultiplier: 2,
33
+ backoffMax: 10000,
34
+ };
35
+ /**
36
+ * Default pool options for MySQL
37
+ */
38
+ const DEFAULT_POOL_OPTIONS = {
39
+ max: 10,
40
+ min: 0,
41
+ acquire: 60000,
42
+ idle: 30000,
43
+ evict: 1000,
44
+ handleDisconnects: true,
45
+ onConnect: undefined,
46
+ onDisconnect: undefined,
47
+ enableEvents: false,
48
+ logPoolOperations: false,
49
+ };
50
+ /**
51
+ * MySQL dialect class that implements the Dialect interface
52
+ */
53
+ class MySQLDialect {
54
+ constructor(config) {
55
+ this.name = 'mysql';
56
+ this.library = 'mysql2';
57
+ this.pool = null;
58
+ this._isConnected = false;
59
+ this.poolOptions = config.pool || {};
60
+ this.retryOptions = config.retry || {};
61
+ this.config = {
62
+ host: 'localhost',
63
+ port: 3306,
64
+ waitForConnections: true,
65
+ connectionLimit: this.poolOptions.max || DEFAULT_POOL_OPTIONS.max,
66
+ queueLimit: 0,
67
+ ...config,
68
+ };
69
+ }
70
+ /**
71
+ * Execute a function with retry logic
72
+ */
73
+ async withRetry(fn) {
74
+ const retryConfig = { ...DEFAULT_RETRY_OPTIONS, ...this.retryOptions };
75
+ let lastError;
76
+ const { backoff, backoffMultiplier, backoffMax, max, match, timeout } = retryConfig;
77
+ for (let attempt = 0; attempt <= max; attempt++) {
78
+ try {
79
+ return await fn();
80
+ }
81
+ catch (error) {
82
+ lastError = error;
83
+ const errorMessage = lastError.message;
84
+ // Check if this error should be retried
85
+ const shouldRetry = match.some((pattern) => errorMessage.toLowerCase().includes(pattern.toLowerCase()));
86
+ if (!shouldRetry || attempt === max) {
87
+ throw lastError;
88
+ }
89
+ // Calculate delay - use exponential backoff if enabled
90
+ let delay;
91
+ if (backoff) {
92
+ delay = timeout * Math.pow(backoffMultiplier || 2, attempt);
93
+ delay = Math.min(delay, backoffMax || 10000);
94
+ }
95
+ else {
96
+ delay = timeout;
97
+ }
98
+ // Add some jitter to avoid thundering herd
99
+ const jitter = Math.random() * 100;
100
+ await new Promise((resolve) => setTimeout(resolve, delay + jitter));
101
+ }
102
+ }
103
+ throw lastError;
104
+ }
105
+ /**
106
+ * Connect to the MySQL database with retry logic
107
+ */
108
+ async connect() {
109
+ await this.withRetry(async () => {
110
+ const poolConfig = {
111
+ host: this.config.host,
112
+ port: this.config.port,
113
+ user: this.config.username,
114
+ password: this.config.password,
115
+ database: this.config.database,
116
+ waitForConnections: this.config.waitForConnections,
117
+ connectionLimit: this.poolOptions.max || DEFAULT_POOL_OPTIONS.max,
118
+ queueLimit: this.config.queueLimit,
119
+ timezone: this.config.timezone || 'Z',
120
+ charset: this.config.charset || 'utf8mb4',
121
+ ssl: this.config.ssl,
122
+ supportBigNumbers: true,
123
+ bigNumberStrings: false,
124
+ dateStrings: false,
125
+ debug: this.config.debug,
126
+ trace: this.config.trace,
127
+ };
128
+ // Add connection timeout if configured
129
+ if (this.config.connectTimeout) {
130
+ poolConfig.connectTimeout = this.config.connectTimeout;
131
+ }
132
+ // Add idle timeout if configured
133
+ if (this.config.idleTimeout) {
134
+ poolConfig.idleTimeout = this.config.idleTimeout;
135
+ }
136
+ this.pool = promise_1.default.createPool(poolConfig);
137
+ // Test the connection
138
+ const connection = await this.pool.getConnection();
139
+ connection.release();
140
+ this._isConnected = true;
141
+ });
142
+ }
143
+ /**
144
+ * Disconnect from the MySQL database
145
+ */
146
+ async disconnect() {
147
+ if (this.pool) {
148
+ await this.pool.end();
149
+ this.pool = null;
150
+ this._isConnected = false;
151
+ }
152
+ }
153
+ /**
154
+ * Get the current connection pool
155
+ */
156
+ getConnection() {
157
+ return this.pool;
158
+ }
159
+ /**
160
+ * Check if connected
161
+ */
162
+ isConnected() {
163
+ return this._isConnected && this.pool !== null;
164
+ }
165
+ /**
166
+ * Execute a raw SQL query with retry support
167
+ */
168
+ async query(sql, options) {
169
+ // Default retry options for queries
170
+ const defaultQueryRetryOptions = {
171
+ max: 3,
172
+ timeout: 1000,
173
+ match: [
174
+ 'ECONNREFUSED',
175
+ 'ENOTFOUND',
176
+ 'ETIMEDOUT',
177
+ 'connect timeout',
178
+ 'connection timeout',
179
+ 'Too many connections',
180
+ 'Lock wait timeout',
181
+ 'Deadlock',
182
+ ],
183
+ backoff: false,
184
+ backoffMultiplier: 2,
185
+ backoffMax: 10000,
186
+ };
187
+ // Use retry options from query options, or fall back to dialect-level retry config
188
+ const retryOptions = options?.retry;
189
+ const effectiveRetry = retryOptions
190
+ ? {
191
+ ...defaultQueryRetryOptions,
192
+ ...retryOptions,
193
+ match: retryOptions.match || defaultQueryRetryOptions.match,
194
+ }
195
+ : this.retryOptions
196
+ ? { ...defaultQueryRetryOptions, ...this.retryOptions }
197
+ : null;
198
+ const executeQuery = async () => {
199
+ if (!this.pool) {
200
+ throw new Error('Not connected to database');
201
+ }
202
+ const isSelect = sql.trim().toUpperCase().startsWith('SELECT') ||
203
+ sql.trim().toUpperCase().startsWith('SHOW') ||
204
+ sql.trim().toUpperCase().startsWith('DESCRIBE') ||
205
+ sql.trim().toUpperCase().startsWith('EXPLAIN');
206
+ try {
207
+ // Apply replacements so parameterized queries (e.g. bulkInsert's
208
+ // `VALUES (?, ?)`) actually receive their bound values. Positional
209
+ // (array) replacements are bound by the driver; named (object)
210
+ // replacements are inlined via replaceReplacements.
211
+ const replacements = options?.replacements;
212
+ let finalSql = sql;
213
+ let bindValues;
214
+ if (replacements !== undefined && replacements !== null) {
215
+ if (Array.isArray(replacements)) {
216
+ bindValues = replacements;
217
+ }
218
+ else {
219
+ finalSql = this.replaceReplacements(sql, replacements);
220
+ }
221
+ }
222
+ const [rows, fields] = (await (bindValues !== undefined
223
+ ? this.pool.query(finalSql, bindValues)
224
+ : this.pool.query(finalSql)));
225
+ // Transform fields
226
+ const fieldInfo = fields
227
+ ? fields.map((field) => ({
228
+ name: field.name,
229
+ type: String(field.type),
230
+ length: field.length ?? 0,
231
+ tableID: 0,
232
+ columnID: 0,
233
+ nullable: true,
234
+ isEnum: false,
235
+ isPrimaryKey: false,
236
+ }))
237
+ : [];
238
+ if (isSelect) {
239
+ return {
240
+ rows: rows,
241
+ rowCount: rows.length,
242
+ fields: fieldInfo,
243
+ };
244
+ }
245
+ else {
246
+ const result = rows;
247
+ return {
248
+ rows: [],
249
+ rowCount: result?.affectedRows || 0,
250
+ fields: fieldInfo,
251
+ };
252
+ }
253
+ }
254
+ catch (error) {
255
+ throw new Error(`MySQL Query error: ${error}`);
256
+ }
257
+ };
258
+ // If retry options are provided, use retry logic
259
+ if (effectiveRetry && effectiveRetry.max > 0) {
260
+ return this.executeWithRetry(executeQuery, effectiveRetry);
261
+ }
262
+ return executeQuery();
263
+ }
264
+ /**
265
+ * Execute a query and stream results using MySQL2 stream
266
+ * Uses MySQL2's stream API for efficient memory usage with large datasets
267
+ * @param sql - The SQL query string
268
+ * @param options - Stream options
269
+ * @returns Readable stream for streaming results
270
+ */
271
+ queryStream(sql, options) {
272
+ const { Readable } = require('stream');
273
+ const highWaterMark = options?.highWaterMark || 1000;
274
+ // Create a custom readable stream
275
+ const stream = new Readable({
276
+ objectMode: true,
277
+ highWaterMark,
278
+ read() { },
279
+ });
280
+ if (!this.pool) {
281
+ stream.destroy(new Error('Not connected to database'));
282
+ return stream;
283
+ }
284
+ // Use mysql2's createQueryStream for streaming
285
+ const mysql = require('mysql2/promise');
286
+ // @ts-ignore - createQueryStream is available in mysql2
287
+ const queryStream = this.pool.createQueryStream(sql);
288
+ queryStream.on('result', (row) => {
289
+ // For each row, push to our stream
290
+ // If stream buffer is full, pause the query
291
+ if (options?.mapToModel && options?.model) {
292
+ // Map to model instance if requested
293
+ const instance = new options.model();
294
+ Object.assign(instance, row);
295
+ const canContinue = stream.push(instance);
296
+ if (!canContinue) {
297
+ queryStream.pause();
298
+ }
299
+ }
300
+ else {
301
+ const canContinue = stream.push(row);
302
+ if (!canContinue) {
303
+ queryStream.pause();
304
+ }
305
+ }
306
+ });
307
+ queryStream.on('error', (queryErr) => {
308
+ stream.destroy(queryErr);
309
+ });
310
+ queryStream.on('end', () => {
311
+ stream.push(null);
312
+ });
313
+ return stream;
314
+ }
315
+ /**
316
+ * Execute a function with retry logic for query execution
317
+ */
318
+ async executeWithRetry(fn, retryOptions) {
319
+ let lastError;
320
+ const { backoff, backoffMultiplier, backoffMax } = retryOptions;
321
+ for (let attempt = 0; attempt <= retryOptions.max; attempt++) {
322
+ try {
323
+ return await fn();
324
+ }
325
+ catch (error) {
326
+ lastError = error;
327
+ const errorMessage = lastError.message;
328
+ // Check if this error should be retried
329
+ const shouldRetry = retryOptions.match.some((pattern) => errorMessage.toLowerCase().includes(pattern.toLowerCase()));
330
+ if (!shouldRetry || attempt === retryOptions.max) {
331
+ throw lastError;
332
+ }
333
+ // Calculate delay - use exponential backoff if enabled
334
+ let delay;
335
+ if (backoff) {
336
+ delay = retryOptions.timeout * Math.pow(backoffMultiplier || 2, attempt);
337
+ delay = Math.min(delay, backoffMax || 10000);
338
+ }
339
+ else {
340
+ delay = retryOptions.timeout;
341
+ }
342
+ // Add some jitter to avoid thundering herd
343
+ const jitter = Math.random() * 100;
344
+ await new Promise((resolve) => setTimeout(resolve, delay + jitter));
345
+ }
346
+ }
347
+ throw lastError;
348
+ }
349
+ /**
350
+ * Escape a value for use in a query
351
+ */
352
+ escape(value) {
353
+ // Handle Literal (raw SQL) values - insert directly without escaping
354
+ if (value instanceof prorm_1.Literal) {
355
+ return value.val;
356
+ }
357
+ if (value === null) {
358
+ return 'NULL';
359
+ }
360
+ if (typeof value === 'string') {
361
+ return `'${this.escapeString(value)}'`;
362
+ }
363
+ if (typeof value === 'number') {
364
+ return String(value);
365
+ }
366
+ if (typeof value === 'bigint') {
367
+ return value.toString();
368
+ }
369
+ if (typeof value === 'boolean') {
370
+ // Emit the TRUE/FALSE keyword literals rather than 1/0. Both compare
371
+ // identically for ordinary numeric/TINYINT boolean columns, but only
372
+ // the keyword form compares equal to a JSON boolean extracted via
373
+ // JSON_EXTRACT()/-> — `JSON_EXTRACT(col, '$.path') = 1` never matches
374
+ // a JSON `true` value on a real MySQL server, while `= TRUE` does.
375
+ return value ? 'TRUE' : 'FALSE';
376
+ }
377
+ if (value instanceof Date) {
378
+ return `'${this.formatDate(value)}'`;
379
+ }
380
+ if (Buffer.isBuffer(value)) {
381
+ return `X'${value.toString('hex')}'`;
382
+ }
383
+ // JSON stringify objects
384
+ return `'${this.escapeString(JSON.stringify(value))}'`;
385
+ }
386
+ /**
387
+ * Build a JSON extraction query for MySQL
388
+ * MySQL: JSON_EXTRACT(column, '$.path') and JSON_UNQUOTE(JSON_EXTRACT(...))
389
+ */
390
+ buildJsonExtract(column, path, asText = true) {
391
+ const columnRef = this.escapeId(column);
392
+ const normalizedPath = path.startsWith('$.') ? path : `$.${path}`;
393
+ if (asText) {
394
+ // MySQL needs JSON_UNQUOTE to get text value
395
+ return `JSON_UNQUOTE(JSON_EXTRACT(${columnRef}, '${normalizedPath}'))`;
396
+ }
397
+ return `JSON_EXTRACT(${columnRef}, '${normalizedPath}')`;
398
+ }
399
+ /**
400
+ * Build a JSON contains query for MySQL
401
+ * MySQL: JSON_CONTAINS(column, value[, path])
402
+ */
403
+ buildJsonContains(column, value, path) {
404
+ const columnRef = this.escapeId(column);
405
+ const values = [JSON.stringify(value)];
406
+ if (path) {
407
+ const normalizedPath = path.startsWith('$.') ? path : `$.${path}`;
408
+ return {
409
+ sql: `JSON_CONTAINS(${columnRef}, ?, '${normalizedPath}')`,
410
+ values,
411
+ };
412
+ }
413
+ return {
414
+ sql: `JSON_CONTAINS(${columnRef}, ?)`,
415
+ values,
416
+ };
417
+ }
418
+ /**
419
+ * Build a JSON has key query for MySQL
420
+ * MySQL: JSON_CONTAINS_PATH(column, 'one'|'all', '$.key')
421
+ */
422
+ buildJsonHasKey(column, key, type = 'one') {
423
+ const columnRef = this.escapeId(column);
424
+ const normalizedKey = key.startsWith('$.') ? key : `$.${key}`;
425
+ return `JSON_CONTAINS_PATH(${columnRef}, '${type}', '${normalizedKey}')`;
426
+ }
427
+ /**
428
+ * Build a JSON path query for MySQL
429
+ */
430
+ buildJsonPathQuery(column, path, value, operator = '=') {
431
+ const values = [];
432
+ const columnRef = this.escapeId(column);
433
+ const normalizedPath = path.startsWith('$.') ? path : `$.${path}`;
434
+ const jsonExtract = this.buildJsonExtract(column, normalizedPath, true);
435
+ if (value !== undefined) {
436
+ values.push(value);
437
+ return { sql: `${jsonExtract} ${operator} ?`, values };
438
+ }
439
+ return { sql: jsonExtract, values };
440
+ }
441
+ /**
442
+ * Build a JSON_SET expression to write values at given paths.
443
+ * MySQL: JSON_SET(column, '$.path1', ?, '$.path2', ?, ...)
444
+ * Unlike JSON_REPLACE, JSON_SET creates the path if it does not already exist;
445
+ * unlike JSON_INSERT, it will overwrite an existing value at that path.
446
+ */
447
+ buildJsonSet(column, pathValues) {
448
+ const columnRef = this.escapeId(column);
449
+ const values = [];
450
+ const parts = [];
451
+ for (const [path, value] of Object.entries(pathValues)) {
452
+ const normalizedPath = path.startsWith('$') ? path : `$.${path}`;
453
+ parts.push(`'${normalizedPath}', ?`);
454
+ values.push(value);
455
+ }
456
+ return {
457
+ sql: `JSON_SET(${columnRef}, ${parts.join(', ')})`,
458
+ values,
459
+ };
460
+ }
461
+ /**
462
+ * Build a JSON_REPLACE expression, updating values only at paths that already exist.
463
+ * MySQL: JSON_REPLACE(column, '$.path1', ?, ...)
464
+ */
465
+ buildJsonReplace(column, pathValues) {
466
+ const columnRef = this.escapeId(column);
467
+ const values = [];
468
+ const parts = [];
469
+ for (const [path, value] of Object.entries(pathValues)) {
470
+ const normalizedPath = path.startsWith('$') ? path : `$.${path}`;
471
+ parts.push(`'${normalizedPath}', ?`);
472
+ values.push(value);
473
+ }
474
+ return {
475
+ sql: `JSON_REPLACE(${columnRef}, ${parts.join(', ')})`,
476
+ values,
477
+ };
478
+ }
479
+ /**
480
+ * Build a JSON_INSERT expression, inserting values only at paths that do not already exist.
481
+ * MySQL: JSON_INSERT(column, '$.path1', ?, ...)
482
+ */
483
+ buildJsonInsert(column, pathValues) {
484
+ const columnRef = this.escapeId(column);
485
+ const values = [];
486
+ const parts = [];
487
+ for (const [path, value] of Object.entries(pathValues)) {
488
+ const normalizedPath = path.startsWith('$') ? path : `$.${path}`;
489
+ parts.push(`'${normalizedPath}', ?`);
490
+ values.push(value);
491
+ }
492
+ return {
493
+ sql: `JSON_INSERT(${columnRef}, ${parts.join(', ')})`,
494
+ values,
495
+ };
496
+ }
497
+ /**
498
+ * Build a JSON_REMOVE expression to remove data at the given paths.
499
+ * MySQL: JSON_REMOVE(column, '$.path1', '$.path2', ...)
500
+ */
501
+ buildJsonRemove(column, paths) {
502
+ const columnRef = this.escapeId(column);
503
+ const pathList = paths
504
+ .map((path) => `'${path.startsWith('$') ? path : `$.${path}`}'`)
505
+ .join(', ');
506
+ return `JSON_REMOVE(${columnRef}, ${pathList})`;
507
+ }
508
+ /**
509
+ * Build a JSON_MERGE_PATCH expression (RFC 7396 merge patch semantics: object keys
510
+ * set to JSON null remove the key; non-object values fully replace).
511
+ * MySQL: JSON_MERGE_PATCH(column, ?, ?, ...)
512
+ */
513
+ buildJsonMergePatch(column, ...documents) {
514
+ const columnRef = this.escapeId(column);
515
+ const values = documents.map((doc) => (typeof doc === 'string' ? doc : JSON.stringify(doc)));
516
+ const placeholders = values.map(() => '?').join(', ');
517
+ return {
518
+ sql: `JSON_MERGE_PATCH(${columnRef}, ${placeholders})`,
519
+ values,
520
+ };
521
+ }
522
+ /**
523
+ * Build a JSON_MERGE_PRESERVE expression (auto-wraps/merges without dropping keys or
524
+ * array members, unlike JSON_MERGE_PATCH).
525
+ * MySQL: JSON_MERGE_PRESERVE(column, ?, ?, ...)
526
+ */
527
+ buildJsonMergePreserve(column, ...documents) {
528
+ const columnRef = this.escapeId(column);
529
+ const values = documents.map((doc) => (typeof doc === 'string' ? doc : JSON.stringify(doc)));
530
+ const placeholders = values.map(() => '?').join(', ');
531
+ return {
532
+ sql: `JSON_MERGE_PRESERVE(${columnRef}, ${placeholders})`,
533
+ values,
534
+ };
535
+ }
536
+ /**
537
+ * Build a JSON_ARRAYAGG aggregate expression.
538
+ * MySQL: JSON_ARRAYAGG(expr) - aggregates a column/expression (typically grouped)
539
+ * into a single JSON array.
540
+ */
541
+ buildJsonArrayAgg(expression, alias) {
542
+ const columnExpr = /[\s(]/.test(expression) ? expression : this.escapeId(expression);
543
+ const sql = `JSON_ARRAYAGG(${columnExpr})`;
544
+ return alias ? `${sql} AS ${this.escapeId(alias)}` : sql;
545
+ }
546
+ /**
547
+ * Build a JSON_OBJECTAGG aggregate expression.
548
+ * MySQL: JSON_OBJECTAGG(key, value) - aggregates key/value pairs (typically grouped)
549
+ * into a single JSON object.
550
+ */
551
+ buildJsonObjectAgg(keyExpression, valueExpression, alias) {
552
+ const keyExpr = /[\s(]/.test(keyExpression) ? keyExpression : this.escapeId(keyExpression);
553
+ const valueExpr = /[\s(]/.test(valueExpression)
554
+ ? valueExpression
555
+ : this.escapeId(valueExpression);
556
+ const sql = `JSON_OBJECTAGG(${keyExpr}, ${valueExpr})`;
557
+ return alias ? `${sql} AS ${this.escapeId(alias)}` : sql;
558
+ }
559
+ /**
560
+ * Build a JSON_TABLE expression to shred a JSON document/array into relational rows
561
+ * (MySQL 8.0+). Useful to project a JSON array column into rows instead of a
562
+ * correlated subquery.
563
+ * MySQL: JSON_TABLE(expr, '$[*]' COLUMNS(name type PATH '$.path', ...)) AS alias
564
+ */
565
+ buildJsonTable(jsonExpression, rowPath, columns, alias) {
566
+ const columnDefs = columns
567
+ .map((col) => {
568
+ if (col.forOrdinality) {
569
+ return `${this.escapeId(col.name)} FOR ORDINALITY`;
570
+ }
571
+ return `${this.escapeId(col.name)} ${col.type} PATH '${col.path}'`;
572
+ })
573
+ .join(', ');
574
+ return `JSON_TABLE(${jsonExpression}, '${rowPath}' COLUMNS(${columnDefs})) AS ${this.escapeId(alias)}`;
575
+ }
576
+ /**
577
+ * Escape a string for SQL
578
+ */
579
+ escapeString(str) {
580
+ return str.replace(/'/g, "''").replace(/\\/g, '\\\\');
581
+ }
582
+ /**
583
+ * Format a date for MySQL
584
+ */
585
+ formatDate(date) {
586
+ const year = date.getFullYear();
587
+ const month = String(date.getMonth() + 1).padStart(2, '0');
588
+ const day = String(date.getDate()).padStart(2, '0');
589
+ const hours = String(date.getHours()).padStart(2, '0');
590
+ const minutes = String(date.getMinutes()).padStart(2, '0');
591
+ const seconds = String(date.getSeconds()).padStart(2, '0');
592
+ const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
593
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
594
+ }
595
+ /**
596
+ * Escape an identifier (table name, column name, etc.)
597
+ */
598
+ escapeId(identifier) {
599
+ const id = String(identifier ?? '');
600
+ return `\`${id.replace(/`/g, '``')}\``;
601
+ }
602
+ /**
603
+ * Quote an identifier (column name, table name)
604
+ * MySQL uses backticks for identifiers
605
+ */
606
+ quoteIdentifier(identifier) {
607
+ return this.escapeId(identifier);
608
+ }
609
+ /**
610
+ * Quote a table name
611
+ *
612
+ * Callers frequently pass a `schema.table` compound name (e.g.
613
+ * `` this.quoteTable(`${schema}.${name}`) ``). `escapeId()` alone wraps
614
+ * the *whole* string in a single pair of backticks, which makes the dot a
615
+ * literal character in one identifier (`` `schema.table` ``) rather than
616
+ * a real schema qualifier (`` `schema`.`table` ``) — against a real
617
+ * MySQL server this creates/references a table whose actual name
618
+ * literally contains a dot, which then doesn't match how e.g.
619
+ * `INFORMATION_SCHEMA.TABLES.TABLE_NAME` reports it elsewhere. Quote each
620
+ * dot-separated segment independently so `schema.table` round-trips
621
+ * correctly; a bare name (no dot) is unaffected.
622
+ */
623
+ quoteTable(tableName) {
624
+ const parts = String(tableName ?? '').split('.');
625
+ return parts.map((part) => this.escapeId(part)).join('.');
626
+ }
627
+ /**
628
+ * Get the database version
629
+ */
630
+ async getDatabaseVersion() {
631
+ if (!this.pool) {
632
+ throw new Error('Not connected to database');
633
+ }
634
+ const result = await this.query('SELECT VERSION() as version');
635
+ return result.rows[0]?.version || 'Unknown';
636
+ }
637
+ /**
638
+ * Create a new table
639
+ */
640
+ async createTable(tableName, columns, options) {
641
+ const columnDefs = [];
642
+ for (const [columnName, definition] of Object.entries(columns)) {
643
+ columnDefs.push(this.getColumnDefinitionSql(columnName, definition));
644
+ }
645
+ // Add constraints from options
646
+ if (options?.constraints) {
647
+ for (const constraint of options.constraints) {
648
+ const constraintSql = this.buildConstraintSql(constraint);
649
+ if (constraintSql) {
650
+ columnDefs.push(constraintSql);
651
+ }
652
+ }
653
+ }
654
+ let sql = 'CREATE TABLE';
655
+ if (options?.ifNotExists) {
656
+ sql += ' IF NOT EXISTS';
657
+ }
658
+ sql += ` ${this.escapeId(tableName)} (${columnDefs.join(', ')})`;
659
+ // Add engine (MySQL-specific)
660
+ if (options?.engine) {
661
+ sql += ` ENGINE=${options.engine}`;
662
+ }
663
+ // Add charset
664
+ if (options?.charset) {
665
+ sql += ` DEFAULT CHARSET=${options.charset}`;
666
+ }
667
+ // Add collation
668
+ if (options?.collate) {
669
+ sql += ` COLLATE ${options.collate}`;
670
+ }
671
+ // Add table comment (MySQL 5.5.3+)
672
+ if (options?.comment) {
673
+ sql += ` COMMENT='${this.escapeString(options.comment)}'`;
674
+ }
675
+ // Add initial auto-increment value
676
+ if (options?.initialAutoIncrement) {
677
+ sql += ` AUTO_INCREMENT=${options.initialAutoIncrement}`;
678
+ }
679
+ // Add row format
680
+ if (options?.rowFormat) {
681
+ sql += ` ROW_FORMAT=${options.rowFormat}`;
682
+ }
683
+ // Add unique keys (defined at table level)
684
+ // Support both old format (Record<string, string[]>) and new format (UniqueKeyOptions[])
685
+ if (options?.uniqueKeys) {
686
+ if (Array.isArray(options.uniqueKeys)) {
687
+ // New format: UniqueKeyOptions[]
688
+ for (const uk of options.uniqueKeys) {
689
+ const uniqueName = uk.name || `${tableName}_${uk.fields.join('_')}_key`;
690
+ columnDefs.push(`UNIQUE KEY \`${uniqueName}\` (${uk.fields.map((f) => this.escapeId(f)).join(', ')})`);
691
+ }
692
+ }
693
+ else {
694
+ // Legacy format: Record<string, string[]>
695
+ for (const [keyName, fields] of Object.entries(options.uniqueKeys)) {
696
+ const uniqueName = keyName || `uq_${tableName}_${fields.join('_')}`;
697
+ columnDefs.push(`UNIQUE KEY \`${uniqueName}\` (${fields.map((f) => this.escapeId(f)).join(', ')})`);
698
+ }
699
+ }
700
+ }
701
+ await this.query(sql);
702
+ // Create indexes after table creation
703
+ if (options?.indexes) {
704
+ for (const index of options.indexes) {
705
+ await this.addIndex(tableName, index.name || `idx_${tableName}_${index.fields.join('_')}`, index.fields, {
706
+ unique: index.unique,
707
+ type: index.type,
708
+ using: index.using,
709
+ where: index.where,
710
+ expression: index.expression,
711
+ include: index.include,
712
+ });
713
+ }
714
+ }
715
+ }
716
+ /**
717
+ * Build constraint SQL for table creation (MySQL)
718
+ */
719
+ buildConstraintSql(constraint) {
720
+ const name = constraint.name ? `CONSTRAINT \`${constraint.name}\` ` : '';
721
+ const fields = constraint.fields?.map((f) => this.escapeId(f)).join(', ') || '';
722
+ switch (constraint.type) {
723
+ case 'PRIMARY KEY':
724
+ return `${name}PRIMARY KEY (${fields})`;
725
+ case 'UNIQUE':
726
+ return `${name}UNIQUE (${fields})`;
727
+ case 'FOREIGN KEY':
728
+ if (!constraint.references)
729
+ return null;
730
+ // Handle composite foreign keys (string | string[])
731
+ const refField = constraint.references.field;
732
+ const refFieldSql = Array.isArray(refField)
733
+ ? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
734
+ : `(${this.escapeId(refField)})`;
735
+ let fkSql = `${name}FOREIGN KEY (${fields}) `;
736
+ fkSql += `REFERENCES ${this.escapeId(constraint.references.table)}${refFieldSql}`;
737
+ if (constraint.references.onDelete) {
738
+ fkSql += ` ON DELETE ${constraint.references.onDelete}`;
739
+ }
740
+ if (constraint.references.onUpdate) {
741
+ fkSql += ` ON UPDATE ${constraint.references.onUpdate}`;
742
+ }
743
+ return fkSql;
744
+ case 'CHECK':
745
+ if (!constraint.check)
746
+ return null;
747
+ return `${name}CHECK (${constraint.check})`;
748
+ default:
749
+ return null;
750
+ }
751
+ }
752
+ /**
753
+ * Generate column definition SQL for MySQL
754
+ */
755
+ getColumnDefinitionSql(columnName, definition) {
756
+ let sql = `${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type)}`;
757
+ // Column-level CHARACTER SET / COLLATE (MySQL only supports this on
758
+ // string types, but MySQL itself will raise a clear error for
759
+ // incompatible types - we don't need to validate that here). These must
760
+ // come immediately after the data type, before NOT NULL/DEFAULT/etc.
761
+ if (definition.charset) {
762
+ sql += ` CHARACTER SET ${this.escapeId(definition.charset)}`;
763
+ }
764
+ if (definition.collate) {
765
+ sql += ` COLLATE ${this.escapeId(definition.collate)}`;
766
+ }
767
+ if (definition.allowNull === false) {
768
+ sql += ' NOT NULL';
769
+ }
770
+ if (definition.defaultValue !== undefined) {
771
+ sql += ` DEFAULT ${this.getDefaultValue(definition.defaultValue)}`;
772
+ }
773
+ // Column visibility (MySQL 8.0+). Must come after DEFAULT and before
774
+ // AUTO_INCREMENT/UNIQUE/PRIMARY KEY per the MySQL column_definition
775
+ // grammar. Columns are VISIBLE by default, so only emit the keyword
776
+ // when the column is explicitly marked invisible.
777
+ if (definition.invisible) {
778
+ sql += ' INVISIBLE';
779
+ }
780
+ if (definition.primaryKey) {
781
+ sql += ' PRIMARY KEY';
782
+ }
783
+ if (definition.autoIncrement) {
784
+ sql += ' AUTO_INCREMENT';
785
+ }
786
+ if (definition.unique) {
787
+ if (typeof definition.unique === 'string') {
788
+ sql += ` UNIQUE KEY ${this.escapeId(definition.unique)}`;
789
+ }
790
+ else {
791
+ sql += ' UNIQUE';
792
+ }
793
+ }
794
+ if (definition.references) {
795
+ // Handle composite foreign keys (string | string[])
796
+ const refField = definition.references.field;
797
+ const refFieldSql = Array.isArray(refField)
798
+ ? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
799
+ : `(${this.escapeId(refField)})`;
800
+ sql += ` REFERENCES ${this.escapeId(definition.references.table)}${refFieldSql}`;
801
+ if (definition.references.onDelete) {
802
+ sql += ` ON DELETE ${definition.references.onDelete}`;
803
+ }
804
+ if (definition.references.onUpdate) {
805
+ sql += ` ON UPDATE ${definition.references.onUpdate}`;
806
+ }
807
+ }
808
+ if (definition.comment) {
809
+ sql += ` COMMENT '${this.escapeString(definition.comment)}'`;
810
+ }
811
+ return sql;
812
+ }
813
+ /**
814
+ * Get default value SQL
815
+ */
816
+ getDefaultValue(value) {
817
+ if (value === null) {
818
+ return 'NULL';
819
+ }
820
+ if (typeof value === 'string') {
821
+ // Check for MySQL functions
822
+ if (value.toUpperCase().includes('CURRENT_') || value.toUpperCase() === 'NULL') {
823
+ return value;
824
+ }
825
+ return `'${this.escapeString(value)}'`;
826
+ }
827
+ if (typeof value === 'number' || typeof value === 'boolean') {
828
+ return String(value);
829
+ }
830
+ if (value instanceof Date) {
831
+ return `'${this.formatDate(value)}'`;
832
+ }
833
+ // Handle DataType instances with toDefaultValue method (e.g., DataTypes.NOW)
834
+ if (typeof value === 'object' &&
835
+ value !== null &&
836
+ typeof value.toDefaultValue === 'function') {
837
+ return value.toDefaultValue();
838
+ }
839
+ return `'${this.escapeString(String(value))}'`;
840
+ }
841
+ /**
842
+ * Drop a table
843
+ */
844
+ async dropTable(tableName, options) {
845
+ let sql = 'DROP TABLE';
846
+ if (options?.ifExists) {
847
+ sql += ' IF EXISTS';
848
+ }
849
+ sql += ` ${this.escapeId(tableName)}`;
850
+ if (options?.cascade) {
851
+ sql += ' CASCADE';
852
+ }
853
+ await this.query(sql);
854
+ }
855
+ /**
856
+ * Create a partitioned table (MySQL 5.1+)
857
+ *
858
+ * MySQL supports RANGE, LIST, HASH, and KEY partitioning.
859
+ * Partitions are defined in the CREATE TABLE statement.
860
+ *
861
+ * @param tableName - Name of the table to create
862
+ * @param columns - Column definitions
863
+ * @param options - Table options including partition configuration
864
+ */
865
+ async createPartitionedTable(tableName, columns, options) {
866
+ const columnDefs = [];
867
+ for (const [columnName, definition] of Object.entries(columns)) {
868
+ columnDefs.push(this.getColumnDefinitionSql(columnName, definition));
869
+ }
870
+ // Add constraints from options
871
+ if (options?.constraints) {
872
+ for (const constraint of options.constraints) {
873
+ const constraintSql = this.buildConstraintSql(constraint);
874
+ if (constraintSql) {
875
+ columnDefs.push(constraintSql);
876
+ }
877
+ }
878
+ }
879
+ let sql = 'CREATE TABLE';
880
+ if (options?.ifNotExists) {
881
+ sql += ' IF NOT EXISTS';
882
+ }
883
+ sql += ` ${this.escapeId(tableName)} (${columnDefs.join(', ')})`;
884
+ // Add engine (MySQL-specific)
885
+ if (options?.engine) {
886
+ sql += ` ENGINE=${options.engine}`;
887
+ }
888
+ // Add partitioning
889
+ if (options?.partitionBy) {
890
+ const { type, column } = options.partitionBy;
891
+ const columnsStr = Array.isArray(column) ? column.join(', ') : column;
892
+ // Plain `PARTITION BY RANGE (...)`/`PARTITION BY LIST (...)` require the
893
+ // partitioning expression to evaluate to an integer; a real MySQL server
894
+ // rejects non-integer columns (e.g. VARCHAR) with "VALUES value for
895
+ // partition '...' must have type INT". The `COLUMNS` variant (MySQL
896
+ // 5.5+) supports non-integer column types (strings, dates, etc.) with
897
+ // identical partition-definition syntax, so use it unconditionally for
898
+ // RANGE/LIST partitioning to support both integer and non-integer
899
+ // partitioning columns.
900
+ const partitionKeyword = type === 'range' || type === 'list' ? `${type.toUpperCase()} COLUMNS` : type.toUpperCase();
901
+ sql += ` PARTITION BY ${partitionKeyword} (${columnsStr})`;
902
+ // Add partition definitions
903
+ // Handle partitions being a number (e.g., partitions: 2 for KEY partitioning)
904
+ const partitions = Array.isArray(options.partitions)
905
+ ? options.partitions
906
+ : typeof options.partitions === 'number'
907
+ ? []
908
+ : [];
909
+ if (partitions.length > 0) {
910
+ sql += ' (';
911
+ const partitionDefs = [];
912
+ for (const partition of partitions) {
913
+ let partitionSql = `PARTITION ${this.escapeId(partition.name)}`;
914
+ if (partition.bound) {
915
+ const bound = partition.bound;
916
+ // Check for list partition
917
+ if ('values' in bound && bound.values && Array.isArray(bound.values)) {
918
+ const values = bound.values
919
+ .map((v) => (typeof v === 'string' ? `'${v}'` : v))
920
+ .join(', ');
921
+ partitionSql += ` VALUES IN (${values})`;
922
+ }
923
+ // Check for hash partition
924
+ else if ('modulus' in bound && bound.modulus !== undefined) {
925
+ const hashBound = bound;
926
+ partitionSql += ` VALUES WITH (MODULUS ${hashBound.modulus}, REMAINDER ${hashBound.remainder})`;
927
+ }
928
+ // Default to range partition
929
+ else {
930
+ const rangeBound = bound;
931
+ const from = rangeBound.from;
932
+ const to = rangeBound.to || 'MAXVALUE';
933
+ const fromStr = from instanceof Date ? from.toISOString().split('T')[0] : from;
934
+ const toStr = to instanceof Date ? to.toISOString().split('T')[0] : to;
935
+ partitionSql += ` VALUES LESS THAN (${toStr})`;
936
+ }
937
+ }
938
+ partitionDefs.push(partitionSql);
939
+ }
940
+ sql += partitionDefs.join(', ') + ')';
941
+ }
942
+ }
943
+ await this.query(sql);
944
+ }
945
+ /**
946
+ * Create a partition for an existing partitioned table (MySQL 5.1+)
947
+ *
948
+ * @param options - Partition creation options
949
+ */
950
+ async createPartition(options) {
951
+ let sql = `ALTER TABLE ${this.escapeId(options.parentTable)} ADD PARTITION (`;
952
+ if (options.bound) {
953
+ const bound = options.bound;
954
+ // Check for list partition
955
+ if ('values' in bound && bound.values && Array.isArray(bound.values)) {
956
+ const values = bound.values.map((v) => (typeof v === 'string' ? `'${v}'` : v)).join(', ');
957
+ sql += `PARTITION ${this.escapeId(options.name)} VALUES IN (${values})`;
958
+ }
959
+ // Check for hash partition
960
+ else if ('modulus' in bound && bound.modulus !== undefined) {
961
+ const hashBound = bound;
962
+ sql += `PARTITION ${this.escapeId(options.name)} VALUES WITH (MODULUS ${hashBound.modulus}, REMAINDER ${hashBound.remainder})`;
963
+ }
964
+ // Default to range partition
965
+ else {
966
+ const rangeBound = bound;
967
+ const from = rangeBound.from;
968
+ const to = rangeBound.to || 'MAXVALUE';
969
+ const fromStr = from instanceof Date ? from.toISOString().split('T')[0] : from;
970
+ const toStr = to instanceof Date ? to.toISOString().split('T')[0] : to;
971
+ sql += `PARTITION ${this.escapeId(options.name)} VALUES LESS THAN (${toStr})`;
972
+ }
973
+ }
974
+ else {
975
+ sql += `PARTITION ${this.escapeId(options.name)}`;
976
+ }
977
+ sql += ')';
978
+ await this.query(sql);
979
+ }
980
+ /**
981
+ * Attach a partition (MySQL uses ADD PARTITION instead)
982
+ * Provided for PostgreSQL compatibility
983
+ *
984
+ * @param options - Partition attachment options
985
+ */
986
+ async attachPartition(options) {
987
+ // MySQL uses ADD PARTITION instead of ATTACH PARTITION
988
+ // This is for compatibility - we just add the partition
989
+ let sql = `ALTER TABLE ${this.escapeId(options.parentTable)} ADD PARTITION (`;
990
+ sql += `PARTITION ${this.escapeId(options.partitionName)}`;
991
+ sql += ')';
992
+ await this.query(sql);
993
+ }
994
+ /**
995
+ * Detach a partition (MySQL does not support this operation)
996
+ *
997
+ * @param options - Partition detachment options
998
+ */
999
+ async detachPartition(_options) {
1000
+ throw new Error('MySQL does not support detaching partitions. Use DROP PARTITION instead.');
1001
+ }
1002
+ /**
1003
+ * Drop a partition (MySQL 5.1+)
1004
+ *
1005
+ * @param partitionName - Name of the partition to drop
1006
+ * @param options - Drop options including partition name and optional parent table
1007
+ */
1008
+ async dropPartition(partitionName, options) {
1009
+ // MySQL requires the parent table name
1010
+ // If parentTable is not provided in options, look it up from
1011
+ // INFORMATION_SCHEMA.PARTITIONS instead of guessing at the table name by
1012
+ // splitting the partition name on underscores — that heuristic is
1013
+ // ambiguous (both table and partition names may themselves contain
1014
+ // underscores) and cannot be made correct in general.
1015
+ let parentTable = options?.parentTable;
1016
+ if (!parentTable) {
1017
+ const lookupSql = `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA = ${this.escape(this.config.database || '')} AND PARTITION_NAME = ${this.escape(partitionName)} LIMIT 1`;
1018
+ const lookupResult = await this.query(lookupSql);
1019
+ if (lookupResult.rows.length === 0) {
1020
+ throw new Error(`Could not infer the parent table for partition '${partitionName}': no matching partition was found in INFORMATION_SCHEMA.PARTITIONS. Pass options.parentTable explicitly.`);
1021
+ }
1022
+ parentTable = lookupResult.rows[0].TABLE_NAME;
1023
+ }
1024
+ let sql = 'ALTER TABLE';
1025
+ sql += ` ${this.escapeId(parentTable)} DROP PARTITION ${this.escapeId(partitionName)}`;
1026
+ await this.query(sql);
1027
+ }
1028
+ /**
1029
+ * Add a partition to an existing partitioned table (MySQL 5.1+)
1030
+ * @param tableName - Name of the partitioned table
1031
+ * @param partitionName - Name for the new partition
1032
+ * @param partitionSpec - Partition specification
1033
+ */
1034
+ async addPartition(tableName, partitionName, partitionSpec) {
1035
+ // The partition name must be part of the ADD PARTITION definition itself
1036
+ // (`ADD PARTITION (PARTITION <name> VALUES ...)`) — omitting it produces
1037
+ // an unnamed/invalid partition definition and MySQL rejects the statement.
1038
+ let sql = `ALTER TABLE ${this.escapeId(tableName)} ADD PARTITION (PARTITION ${this.escapeId(partitionName)}`;
1039
+ if (partitionSpec.values) {
1040
+ sql += ` VALUES ${partitionSpec.values}`;
1041
+ }
1042
+ else if (partitionSpec.forValues) {
1043
+ sql += ` VALUES ${partitionSpec.forValues}`;
1044
+ }
1045
+ sql += ')';
1046
+ await this.query(sql);
1047
+ }
1048
+ /**
1049
+ * List all partitions for a table (MySQL 5.1+)
1050
+ *
1051
+ * @param tableName - Name of the table
1052
+ * @returns Array of partition metadata
1053
+ */
1054
+ async listPartitions(tableName) {
1055
+ const sql = `SELECT PARTITION_NAME, PARTITION_ORDINAL_POSITION, PARTITION_DESCRIPTION, TABLE_ROWS FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA = ${this.escape(this.config.database || '')} AND TABLE_NAME = ${this.escape(tableName)} AND PARTITION_NAME IS NOT NULL ORDER BY PARTITION_ORDINAL_POSITION`;
1056
+ const result = await this.query(sql);
1057
+ return result.rows.map((row) => ({
1058
+ name: row.PARTITION_NAME,
1059
+ ordinal: row.PARTITION_ORDINAL_POSITION,
1060
+ description: row.PARTITION_DESCRIPTION,
1061
+ rows: row.TABLE_ROWS,
1062
+ }));
1063
+ }
1064
+ /**
1065
+ * Check if a table has partitions (MySQL 5.1+)
1066
+ *
1067
+ * @param tableName - Name of the table to check
1068
+ * @returns Promise<boolean> - True if the table has partitions
1069
+ */
1070
+ async hasPartition(tableName) {
1071
+ const sql = `SELECT PARTITION_NAME FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA = ${this.escape(this.config.database || '')} AND TABLE_NAME = ${this.escape(tableName)}`;
1072
+ const result = await this.query(sql);
1073
+ return result.rows.length > 0;
1074
+ }
1075
+ /**
1076
+ * Add a column to a table
1077
+ */
1078
+ async addColumn(tableName, columnName, definition) {
1079
+ const sql = `ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${this.getColumnDefinitionSql(columnName, definition)}`;
1080
+ await this.query(sql);
1081
+ }
1082
+ /**
1083
+ * Remove a column from a table
1084
+ */
1085
+ async removeColumn(tableName, columnName) {
1086
+ const sql = `ALTER TABLE ${this.escapeId(tableName)} DROP COLUMN ${this.escapeId(columnName)}`;
1087
+ await this.query(sql);
1088
+ }
1089
+ /**
1090
+ * Change a column definition
1091
+ */
1092
+ async changeColumn(tableName, columnName, definition) {
1093
+ const sql = `ALTER TABLE ${this.escapeId(tableName)} CHANGE ${this.escapeId(columnName)} ${this.getColumnDefinitionSql(columnName, definition)}`;
1094
+ await this.query(sql);
1095
+ }
1096
+ /**
1097
+ * Show all tables in the database
1098
+ */
1099
+ async showTables() {
1100
+ const sql = 'SHOW TABLES';
1101
+ const result = await this.query(sql);
1102
+ const tableKey = `Tables_in_${this.config.database}`;
1103
+ return result.rows.map((row) => row[tableKey]);
1104
+ }
1105
+ /**
1106
+ * Get table status (MySQL implementation)
1107
+ */
1108
+ async getTableStatus(tableName) {
1109
+ let sql = 'SHOW TABLE STATUS';
1110
+ if (tableName) {
1111
+ sql += ` LIKE '${this.escapeString(tableName)}'`;
1112
+ }
1113
+ const result = await this.query(sql);
1114
+ return result.rows;
1115
+ }
1116
+ /**
1117
+ * Get table create statement (MySQL implementation)
1118
+ */
1119
+ async getCreateTable(tableName) {
1120
+ const sql = `SHOW CREATE TABLE ${this.escapeId(tableName)}`;
1121
+ const result = await this.query(sql);
1122
+ return result.rows[0]?.['Create Table'] || '';
1123
+ }
1124
+ /**
1125
+ * Show constraints for a table
1126
+ */
1127
+ async showConstraints(tableName) {
1128
+ const sql = `
1129
+ SELECT
1130
+ CONSTRAINT_NAME as name,
1131
+ TABLE_NAME as tableName,
1132
+ CONSTRAINT_TYPE as type
1133
+ FROM information_schema.TABLE_CONSTRAINTS
1134
+ WHERE TABLE_SCHEMA = ${this.escape(this.config.database || '')}
1135
+ AND TABLE_NAME = ${this.escape(tableName)}
1136
+ `;
1137
+ const result = await this.query(sql);
1138
+ return result.rows;
1139
+ }
1140
+ /**
1141
+ * Show indexes for a table
1142
+ */
1143
+ async showIndexes(tableName) {
1144
+ const sql = `
1145
+ SELECT
1146
+ INDEX_NAME as name,
1147
+ TABLE_NAME as tableName,
1148
+ NON_UNIQUE as nonUnique,
1149
+ SEQ_IN_INDEX as seqInIndex,
1150
+ COLUMN_NAME as columnName
1151
+ FROM information_schema.STATISTICS
1152
+ WHERE TABLE_SCHEMA = ${this.escape(this.config.database || '')}
1153
+ AND TABLE_NAME = ${this.escape(tableName)}
1154
+ ORDER BY INDEX_NAME, SEQ_IN_INDEX
1155
+ `;
1156
+ const result = await this.query(sql);
1157
+ return result.rows;
1158
+ }
1159
+ /**
1160
+ * Describe a table (get column information)
1161
+ */
1162
+ async describeTable(tableName) {
1163
+ const sql = `DESCRIBE ${this.escapeId(tableName)}`;
1164
+ const result = await this.query(sql);
1165
+ const description = {};
1166
+ for (const row of result.rows) {
1167
+ const rowData = row;
1168
+ description[rowData.Field] = {
1169
+ type: rowData.Type,
1170
+ allowNull: rowData.Null === 'YES',
1171
+ defaultValue: rowData.Default,
1172
+ primaryKey: rowData.Key === 'PRI',
1173
+ autoIncrement: rowData.Extra?.includes('auto_increment') || false,
1174
+ };
1175
+ }
1176
+ return description;
1177
+ }
1178
+ /**
1179
+ * Rename a table
1180
+ */
1181
+ async renameTable(oldName, newName) {
1182
+ const sql = `ALTER TABLE ${this.escapeId(oldName)} RENAME TO ${this.escapeId(newName)}`;
1183
+ await this.query(sql);
1184
+ }
1185
+ // ============================================================================
1186
+ // Schema methods (MySQL uses databases instead of schemas, these are not supported)
1187
+ // ============================================================================
1188
+ /**
1189
+ * Create a schema - Not supported by MySQL (use CREATE DATABASE instead)
1190
+ */
1191
+ async createSchema(_schema) {
1192
+ throw new Error('Not supported by MySQL: schemas are called databases. Use CREATE DATABASE instead.');
1193
+ }
1194
+ /**
1195
+ * Drop a schema - Not supported by MySQL (use DROP DATABASE instead)
1196
+ */
1197
+ async dropSchema(_schema, _options) {
1198
+ throw new Error('Not supported by MySQL: schemas are called databases. Use DROP DATABASE instead.');
1199
+ }
1200
+ /**
1201
+ * Show all schemas - Not supported by MySQL (use SHOW DATABASES instead)
1202
+ */
1203
+ async showAllSchemas() {
1204
+ throw new Error('Not supported by MySQL: schemas are called databases. Use SHOW DATABASES instead.');
1205
+ }
1206
+ /**
1207
+ * List all schemas (databases) in MySQL
1208
+ */
1209
+ async listSchemas() {
1210
+ const result = await this.query('SHOW DATABASES');
1211
+ return result.rows.map((row) => row.Database);
1212
+ }
1213
+ // ============================================================================
1214
+ // Extension methods (PostgreSQL-specific, not supported by MySQL)
1215
+ // ============================================================================
1216
+ /**
1217
+ * Create a PostgreSQL extension - Not supported by MySQL
1218
+ */
1219
+ async createExtension(_extensionName, _options) {
1220
+ throw new Error('Extensions are not supported by MySQL. This is a PostgreSQL-specific feature.');
1221
+ }
1222
+ /**
1223
+ * Drop a PostgreSQL extension - Not supported by MySQL
1224
+ */
1225
+ async dropExtension(_extensionName, _options) {
1226
+ throw new Error('Extensions are not supported by MySQL. This is a PostgreSQL-specific feature.');
1227
+ }
1228
+ /**
1229
+ * Get all installed PostgreSQL extensions - Not supported by MySQL
1230
+ */
1231
+ async getExtensions() {
1232
+ throw new Error('Extensions are not supported by MySQL. This is a PostgreSQL-specific feature.');
1233
+ }
1234
+ /**
1235
+ * Check if a PostgreSQL extension is installed - Not supported by MySQL
1236
+ */
1237
+ async hasExtension(_extensionName) {
1238
+ throw new Error('Extensions are not supported by MySQL. This is a PostgreSQL-specific feature.');
1239
+ }
1240
+ // ============================================================================
1241
+ // View methods
1242
+ // ============================================================================
1243
+ /**
1244
+ * Create a database view
1245
+ */
1246
+ async createView(viewName, query, options) {
1247
+ const sql = options?.replace
1248
+ ? `CREATE OR REPLACE VIEW ${this.escapeId(viewName)} AS ${query}`
1249
+ : `CREATE VIEW ${this.escapeId(viewName)} AS ${query}`;
1250
+ await this.query(sql);
1251
+ }
1252
+ /**
1253
+ * Drop a database view
1254
+ */
1255
+ async dropView(viewName, options) {
1256
+ const sql = options?.ifExists
1257
+ ? `DROP VIEW IF EXISTS ${this.escapeId(viewName)}`
1258
+ : `DROP VIEW ${this.escapeId(viewName)}`;
1259
+ await this.query(sql);
1260
+ }
1261
+ /**
1262
+ * Show all views in the database
1263
+ */
1264
+ async showViews() {
1265
+ const result = await this.query("SHOW FULL TABLES WHERE Table_type = 'VIEW'");
1266
+ return result.rows.map((row) => row[`Tables_in_${this.config.database}`]);
1267
+ }
1268
+ // ==================== Materialized View Emulation ====================
1269
+ // MySQL does not support materialized views natively.
1270
+ // We emulate them using a table + event scheduler combination.
1271
+ /**
1272
+ * Create a materialized view (emulated using table + event)
1273
+ * Creates a table from the query and optionally creates a scheduled event to refresh it
1274
+ */
1275
+ async createMaterializedView(options) {
1276
+ const schema = options.schema || this.config.database;
1277
+ const tableName = this.quoteTable(`${schema}.${options.name}`);
1278
+ // Build the CREATE TABLE statement (mimicking materialized view)
1279
+ let sql = 'CREATE TABLE';
1280
+ if (options.replace) {
1281
+ sql = 'CREATE OR REPLACE TABLE';
1282
+ }
1283
+ else if (options.ifNotExists) {
1284
+ sql = 'CREATE TABLE IF NOT EXISTS';
1285
+ }
1286
+ sql += ` ${tableName} AS ${options.query}`;
1287
+ // Execute the table creation
1288
+ await this.query(sql);
1289
+ // Mark this table as a materialized view by adding a comment
1290
+ if (options.comment) {
1291
+ await this.query(`ALTER TABLE ${tableName} COMMENT ${this.escape(options.comment)}`);
1292
+ }
1293
+ // Add metadata to track this as a materialized view
1294
+ await this.query(`
1295
+ CREATE TABLE IF NOT EXISTS ${this.quoteTable(`${schema}._materialized_views`)} (
1296
+ view_name VARCHAR(255) PRIMARY KEY,
1297
+ query_text TEXT NOT NULL,
1298
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1299
+ refresh_event VARCHAR(255),
1300
+ unique_index VARCHAR(255)
1301
+ )
1302
+ `);
1303
+ // Store the materialized view metadata
1304
+ await this.query(`INSERT INTO ${this.quoteTable(`${schema}._materialized_views`)} (view_name, query_text) VALUES (${this.escape(options.name)}, ${this.escape(options.query)}) ON DUPLICATE KEY UPDATE query_text = ${this.escape(options.query)}`);
1305
+ }
1306
+ /**
1307
+ * Refresh a materialized view
1308
+ * Truncates the table and re-inserts data from the query
1309
+ */
1310
+ async refreshMaterializedView(viewName, options) {
1311
+ const schema = options?.schema || this.config.database;
1312
+ // Get the query from metadata
1313
+ const result = await this.query(`SELECT query_text FROM ${this.quoteTable(`${schema}._materialized_views`)} WHERE view_name = ${this.escape(viewName)}`, { plain: true });
1314
+ const row = result.rows[0];
1315
+ if (!row || !row.query_text) {
1316
+ throw new Error(`Materialized view '${viewName}' does not exist in schema '${schema}'`);
1317
+ }
1318
+ const tableName = this.quoteTable(`${schema}.${viewName}`);
1319
+ const query = row.query_text;
1320
+ // Truncate and re-insert
1321
+ await this.query(`TRUNCATE TABLE ${tableName}`);
1322
+ await this.query(`INSERT INTO ${tableName} ${query}`);
1323
+ }
1324
+ /**
1325
+ * Drop a materialized view
1326
+ * Drops the table and removes metadata, optionally drops the refresh event
1327
+ */
1328
+ async dropMaterializedView(viewName, options) {
1329
+ const schema = options?.schema || this.config.database;
1330
+ const tableName = this.quoteTable(`${schema}.${viewName}`);
1331
+ // Get the refresh event name from metadata
1332
+ const metadataResult = await this.query(`SELECT refresh_event FROM ${this.quoteTable(`${schema}._materialized_views`)} WHERE view_name = ${this.escape(viewName)}`, { plain: true });
1333
+ const metadataRow = metadataResult.rows[0];
1334
+ // Drop the refresh event if it exists
1335
+ if (metadataRow?.refresh_event) {
1336
+ try {
1337
+ await this.dropEvent(metadataRow.refresh_event, { schema, ifExists: true });
1338
+ }
1339
+ catch {
1340
+ // Ignore if event doesn't exist
1341
+ }
1342
+ }
1343
+ // Drop the table
1344
+ const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
1345
+ await this.query(`DROP TABLE ${ifExists}${tableName}`);
1346
+ // Remove metadata
1347
+ await this.query(`DELETE FROM ${this.quoteTable(`${schema}._materialized_views`)} WHERE view_name = ${this.escape(viewName)}`);
1348
+ }
1349
+ /**
1350
+ * Check if a materialized view exists
1351
+ */
1352
+ async hasMaterializedView(viewName, schema) {
1353
+ const db = schema || this.config.database;
1354
+ const result = await this.query(`SELECT view_name FROM ${this.quoteTable(`${db}._materialized_views`)} WHERE view_name = ${this.escape(viewName)}`, { plain: true });
1355
+ return !!result;
1356
+ }
1357
+ /**
1358
+ * Show all materialized views (tables created as materialized views)
1359
+ */
1360
+ async showMaterializedViews(schema) {
1361
+ const db = schema || this.config.database;
1362
+ const result = await this.query(`SELECT view_name FROM ${this.quoteTable(`${db}._materialized_views`)} ORDER BY view_name`);
1363
+ return result.rows.map((row) => row.view_name);
1364
+ }
1365
+ // ==================== Event Scheduler ====================
1366
+ /**
1367
+ * Create an event (MySQL event scheduler)
1368
+ */
1369
+ async createEvent(options) {
1370
+ const schema = options.schema || this.config.database;
1371
+ const eventName = this.quoteIdentifier(options.name);
1372
+ // Build schedule clause
1373
+ let scheduleClause = '';
1374
+ if (options.schedule.type === 'AT') {
1375
+ scheduleClause = `AT ${options.schedule.timestamp}`;
1376
+ }
1377
+ else if (options.schedule.type === 'EVERY') {
1378
+ scheduleClause = `EVERY ${options.schedule.intervalValue} ${options.schedule.intervalUnit}`;
1379
+ if (options.schedule.starts) {
1380
+ scheduleClause += ` STARTS ${options.schedule.starts}`;
1381
+ }
1382
+ if (options.schedule.ends) {
1383
+ scheduleClause += ` ENDS ${options.schedule.ends}`;
1384
+ }
1385
+ }
1386
+ let sql = 'CREATE EVENT';
1387
+ if (options.ifNotExists) {
1388
+ sql = 'CREATE EVENT IF NOT EXISTS';
1389
+ }
1390
+ else if (options.replace) {
1391
+ sql = 'CREATE OR REPLACE EVENT';
1392
+ }
1393
+ sql += ` ${schema}.${eventName} ON SCHEDULE ${scheduleClause}`;
1394
+ // Add ON COMPLETION clause
1395
+ if (options.onCompletion === 'DROP') {
1396
+ sql += ' ON COMPLETION DROP';
1397
+ }
1398
+ else {
1399
+ sql += ' ON COMPLETION PRESERVE';
1400
+ }
1401
+ // Add status
1402
+ if (options.status === 'DISABLED') {
1403
+ sql += ' DISABLED';
1404
+ }
1405
+ else {
1406
+ sql += ' ENABLED';
1407
+ }
1408
+ sql += `\nDO\nBEGIN\n${options.body}\nEND`;
1409
+ // Add comment if provided
1410
+ if (options.comment) {
1411
+ sql += `\nCOMMENT ${this.escape(options.comment)}`;
1412
+ }
1413
+ await this.query(sql);
1414
+ }
1415
+ /**
1416
+ * Drop an event
1417
+ */
1418
+ async dropEvent(eventName, options) {
1419
+ const schema = options?.schema || this.config.database;
1420
+ const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
1421
+ const sql = `DROP EVENT ${ifExists}${schema}.${this.quoteIdentifier(eventName)}`;
1422
+ await this.query(sql);
1423
+ }
1424
+ /**
1425
+ * Show all events in the database
1426
+ */
1427
+ async showEvents(schema) {
1428
+ const db = schema || this.config.database;
1429
+ const result = await this.query(`SHOW EVENTS FROM ${db}`);
1430
+ return result.rows;
1431
+ }
1432
+ /**
1433
+ * Alter an event (change schedule, body, or status)
1434
+ */
1435
+ async alterEvent(options) {
1436
+ const schema = options.schema || this.config.database;
1437
+ const eventName = this.quoteIdentifier(options.name);
1438
+ let sql = `ALTER EVENT ${schema}.${eventName}`;
1439
+ // Add schedule
1440
+ if (options.schedule) {
1441
+ let scheduleClause = '';
1442
+ if (options.schedule.type === 'AT') {
1443
+ scheduleClause = `AT ${options.schedule.timestamp}`;
1444
+ }
1445
+ else if (options.schedule.type === 'EVERY') {
1446
+ scheduleClause = `EVERY ${options.schedule.intervalValue} ${options.schedule.intervalUnit}`;
1447
+ if (options.schedule.starts) {
1448
+ scheduleClause += ` STARTS ${options.schedule.starts}`;
1449
+ }
1450
+ if (options.schedule.ends) {
1451
+ scheduleClause += ` ENDS ${options.schedule.ends}`;
1452
+ }
1453
+ }
1454
+ sql += ` ON SCHEDULE ${scheduleClause}`;
1455
+ }
1456
+ // Add status
1457
+ if (options.status) {
1458
+ sql += ` ${options.status}`;
1459
+ }
1460
+ // Add comment
1461
+ if (options.comment) {
1462
+ sql += ` COMMENT ${this.escape(options.comment)}`;
1463
+ }
1464
+ // Add new body
1465
+ if (options.body) {
1466
+ sql += `\nDO\nBEGIN\n${options.body}\nEND`;
1467
+ }
1468
+ await this.query(sql);
1469
+ }
1470
+ // ==================== Stored Procedures ====================
1471
+ /**
1472
+ * Create a stored procedure with full PL/SQL support
1473
+ * Supports IN, OUT, INOUT parameters, local variables, IF/ELSE, WHILE loops, cursors
1474
+ */
1475
+ async createStoredProcedure(options) {
1476
+ const schema = options.schema || this.config.database;
1477
+ const procName = this.quoteIdentifier(options.name);
1478
+ // Check if procedure already exists and replace if requested
1479
+ if (options.replace) {
1480
+ try {
1481
+ await this.dropStoredProcedure(options.name, { schema, ifExists: true });
1482
+ }
1483
+ catch {
1484
+ // Ignore if doesn't exist
1485
+ }
1486
+ }
1487
+ else if (options.ifNotExists) {
1488
+ const exists = await this.hasStoredProcedure(options.name, schema);
1489
+ if (exists) {
1490
+ return;
1491
+ }
1492
+ }
1493
+ let sql = `CREATE PROCEDURE ${schema}.${procName}`;
1494
+ // Add parameters with full mode support (IN, OUT, INOUT). MySQL requires
1495
+ // the parameter-list parentheses even when there are no parameters
1496
+ // (`CREATE PROCEDURE name() BEGIN ... END`) — omitting them entirely is
1497
+ // a syntax error against a real MySQL server.
1498
+ if (options.params && options.params.length > 0) {
1499
+ const params = options.params
1500
+ .map((p) => {
1501
+ const mode = p.mode ? `${p.mode} ` : '';
1502
+ const paramName = this.quoteIdentifier(p.name);
1503
+ const defaultPart = p.defaultValue !== undefined ? ` DEFAULT ${this.escape(p.defaultValue)}` : '';
1504
+ return `${mode}${paramName} ${p.type}${defaultPart}`;
1505
+ })
1506
+ .join(', ');
1507
+ sql += ` (${params})`;
1508
+ }
1509
+ else {
1510
+ sql += ' ()';
1511
+ }
1512
+ // Add language and other options
1513
+ if (options.language) {
1514
+ sql += `\nLANGUAGE ${options.language}`;
1515
+ }
1516
+ sql += `\nBEGIN\n${options.body}\nEND`;
1517
+ // Add comment if provided
1518
+ if (options.comment) {
1519
+ await this.query(`CREATE PROCEDURE ${schema}.${procName} COMMENT ${this.escape(options.comment)}`);
1520
+ }
1521
+ await this.query(sql);
1522
+ }
1523
+ /**
1524
+ * Create a stored procedure with body builder for complex procedures
1525
+ * Supports: IN parameters, OUT parameters, INOUT parameters, local variables,
1526
+ * IF/ELSE, WHILE loops, cursors
1527
+ */
1528
+ async createProcedure(options) {
1529
+ const schema = options.schema || this.config.database;
1530
+ const procName = this.quoteIdentifier(options.name);
1531
+ // Check if procedure already exists
1532
+ if (options.replace) {
1533
+ try {
1534
+ await this.dropStoredProcedure(options.name, { schema, ifExists: true });
1535
+ }
1536
+ catch {
1537
+ // Ignore if doesn't exist
1538
+ }
1539
+ }
1540
+ else if (options.ifNotExists) {
1541
+ const exists = await this.hasStoredProcedure(options.name, schema);
1542
+ if (exists) {
1543
+ return;
1544
+ }
1545
+ }
1546
+ let sql = `CREATE PROCEDURE ${schema}.${procName}`;
1547
+ // Add parameters with mode support (IN, OUT, INOUT). MySQL requires the
1548
+ // parameter-list parentheses even when there are no parameters.
1549
+ if (options.params && options.params.length > 0) {
1550
+ const params = options.params
1551
+ .map((p) => {
1552
+ const mode = p.mode ? `${p.mode} ` : '';
1553
+ const paramName = this.quoteIdentifier(p.name);
1554
+ const defaultPart = p.defaultValue !== undefined ? ` DEFAULT ${this.escape(p.defaultValue)}` : '';
1555
+ return `${mode}${paramName} ${p.type}${defaultPart}`;
1556
+ })
1557
+ .join(', ');
1558
+ sql += ` (${params})`;
1559
+ }
1560
+ else {
1561
+ sql += ' ()';
1562
+ }
1563
+ sql += `\nBEGIN\n${options.body}\nEND`;
1564
+ await this.query(sql);
1565
+ // Add comment if provided
1566
+ if (options.comment) {
1567
+ await this.query(`ALTER PROCEDURE ${schema}.${procName} COMMENT ${this.escape(options.comment)}`);
1568
+ }
1569
+ }
1570
+ /**
1571
+ * Build a stored procedure body with helper methods
1572
+ * Provides a fluent API for building complex procedure bodies
1573
+ */
1574
+ buildProcedureBody() {
1575
+ return new ProcedureBodyBuilder(this);
1576
+ }
1577
+ /**
1578
+ * Execute a stored procedure with enhanced return handling
1579
+ * Supports OUT parameters and result sets
1580
+ */
1581
+ async executeStoredProcedure(options) {
1582
+ const schema = options.schema || this.config.database;
1583
+ const procName = `${schema}.${this.quoteIdentifier(options.procedureName)}`;
1584
+ let sql = `CALL ${procName}`;
1585
+ if (options.params && Object.keys(options.params).length > 0) {
1586
+ const params = Object.entries(options.params)
1587
+ .map(([key, value]) => {
1588
+ // Handle OUT parameters - they need @ prefix in MySQL
1589
+ if (key.startsWith('@')) {
1590
+ return value !== undefined ? this.escape(value) : 'NULL';
1591
+ }
1592
+ return this.escape(value);
1593
+ })
1594
+ .join(', ');
1595
+ sql += `(${params})`;
1596
+ }
1597
+ const result = await this.query(sql, { timeout: options.timeout });
1598
+ // Return the result - MySQL returns results via result sets, not affectedRows
1599
+ return result;
1600
+ }
1601
+ /**
1602
+ * Drop a stored procedure
1603
+ */
1604
+ async dropStoredProcedure(procedureName, options) {
1605
+ const schema = options?.schema || this.config.database;
1606
+ const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
1607
+ const cascade = options?.cascade ? ' CASCADE' : '';
1608
+ const sql = `DROP PROCEDURE ${ifExists}${schema}.${this.quoteIdentifier(procedureName)}${cascade}`;
1609
+ await this.query(sql);
1610
+ }
1611
+ /**
1612
+ * Drop a stored procedure (alias for dropStoredProcedure)
1613
+ */
1614
+ async dropProcedure(procedureName, options) {
1615
+ return this.dropStoredProcedure(procedureName, options);
1616
+ }
1617
+ /**
1618
+ * Check if a stored procedure exists
1619
+ */
1620
+ async hasStoredProcedure(procedureName, schema) {
1621
+ const db = schema || this.config.database;
1622
+ const sql = `SELECT ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ${this.escape(db || '')} AND ROUTINE_NAME = ${this.escape(procedureName)} AND ROUTINE_TYPE = 'PROCEDURE'`;
1623
+ // `query()` does not special-case the `plain` option — it always returns
1624
+ // the full { rows, rowCount, fields } QueryResult, so `!!result` was
1625
+ // always true (even with zero matching rows). Check row count instead.
1626
+ const result = await this.query(sql, { plain: true });
1627
+ return result.rows.length > 0;
1628
+ }
1629
+ /**
1630
+ * List all stored procedures in the given (or configured) schema
1631
+ *
1632
+ * @param schema - Optional schema to list procedures from
1633
+ * @returns Array of procedure names
1634
+ */
1635
+ async listProcedures(schema) {
1636
+ const db = schema || this.config.database;
1637
+ const sql = `SELECT ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ${this.escape(db || '')} AND ROUTINE_TYPE = 'PROCEDURE' ORDER BY ROUTINE_NAME`;
1638
+ const result = await this.query(sql);
1639
+ return result.rows.map((row) => row.ROUTINE_NAME);
1640
+ }
1641
+ /**
1642
+ * Create a foreign data wrapper (MySQL - not supported)
1643
+ * @param fdwName - Foreign data wrapper name
1644
+ * @param options - Options including handler function
1645
+ */
1646
+ async createForeignDataWrapper(_fdwName, _options) {
1647
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1648
+ }
1649
+ async dropForeignDataWrapper(_fdwName, _options) {
1650
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1651
+ }
1652
+ async createForeignServer(_serverName, _fdwName, _options) {
1653
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1654
+ }
1655
+ async dropForeignServer(_serverName, _options) {
1656
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1657
+ }
1658
+ async createForeignTable(_tableName, _columns, _options) {
1659
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1660
+ }
1661
+ async changeOwner(_newOwner, _tableName) {
1662
+ throw new Error('changeOwner is not supported in MySQL');
1663
+ }
1664
+ async addConstraint(_tableName, _options) {
1665
+ throw new Error('addConstraint is not supported in MySQL');
1666
+ }
1667
+ async removeConstraint(_tableName, _constraintName) {
1668
+ throw new Error('removeConstraint is not supported in MySQL');
1669
+ }
1670
+ async createSecurityPolicy(_policyName, _tableName, _options) {
1671
+ throw new Error('Security policies are not supported in MySQL');
1672
+ }
1673
+ async dropSecurityPolicy(_policyName, _tableName) {
1674
+ throw new Error('Security policies are not supported in MySQL');
1675
+ }
1676
+ // ==================== Triggers ====================
1677
+ /**
1678
+ * Create a trigger
1679
+ */
1680
+ async createTrigger(options) {
1681
+ const timing = options.timing;
1682
+ const events = (options.events || []).join(' OR ');
1683
+ const level = options.level || 'ROW';
1684
+ const tableName = this.quoteTable(options.tableName);
1685
+ let sql = `CREATE TRIGGER ${this.quoteIdentifier(options.name)} ${timing} ${events} ON ${tableName} FOR EACH ${level}`;
1686
+ if (options.timing === 'INSTEAD OF') {
1687
+ sql = `CREATE TRIGGER ${this.quoteIdentifier(options.name)} INSTEAD OF ${events} ON ${tableName}`;
1688
+ }
1689
+ sql += `\nBEGIN\n${options.body}\nEND`;
1690
+ if (options.replace) {
1691
+ sql = sql.replace('CREATE TRIGGER', 'CREATE OR REPLACE TRIGGER');
1692
+ }
1693
+ await this.query(sql);
1694
+ }
1695
+ /**
1696
+ * Drop a trigger
1697
+ */
1698
+ async dropTrigger(triggerName, tableName, options) {
1699
+ const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
1700
+ const cascade = options?.cascade ? ' CASCADE' : '';
1701
+ const sql = `DROP TRIGGER ${ifExists}${this.quoteIdentifier(triggerName)}`;
1702
+ await this.query(sql);
1703
+ }
1704
+ /**
1705
+ * Check if a trigger exists
1706
+ */
1707
+ async hasTrigger(triggerName, tableName) {
1708
+ const sql = `SELECT TRIGGER_NAME FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_NAME = ${this.escape(triggerName)} AND EVENT_OBJECT_TABLE = ${this.escape(tableName)}`;
1709
+ // `query()` does not special-case the `plain` option — it always returns
1710
+ // the full { rows, rowCount, fields } QueryResult, so `!!result` was
1711
+ // always true (even with zero matching rows). Check row count instead.
1712
+ const result = await this.query(sql, { plain: true });
1713
+ return result.rows.length > 0;
1714
+ }
1715
+ /**
1716
+ * List all triggers, optionally filtered to a single table
1717
+ *
1718
+ * @param tableName - Optional table name to filter triggers by
1719
+ * @returns Array of trigger names
1720
+ */
1721
+ async listTriggers(tableName) {
1722
+ const schema = this.config.database;
1723
+ let sql = `SELECT TRIGGER_NAME FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ${this.escape(schema || '')}`;
1724
+ if (tableName) {
1725
+ sql += ` AND EVENT_OBJECT_TABLE = ${this.escape(tableName)}`;
1726
+ }
1727
+ const result = await this.query(sql);
1728
+ return result.rows.map((row) => row.TRIGGER_NAME);
1729
+ }
1730
+ // ==================== Sequences ====================
1731
+ // MySQL doesn't have native sequences - use AUTO_INCREMENT with a sequence table
1732
+ /**
1733
+ * Create a sequence table with AUTO_INCREMENT
1734
+ */
1735
+ async createSequence(options) {
1736
+ const schema = options.schema || this.config.database;
1737
+ const tableName = this.quoteTable(`${schema}.${options.name}`);
1738
+ // Build the CREATE TABLE statement
1739
+ let sql = 'CREATE TABLE';
1740
+ if (options.replace) {
1741
+ sql = 'CREATE OR REPLACE TABLE';
1742
+ }
1743
+ else if (options.ifNotExists) {
1744
+ sql = 'CREATE TABLE IF NOT EXISTS';
1745
+ }
1746
+ sql += ` ${tableName} (id INT AUTO_INCREMENT PRIMARY KEY)`;
1747
+ if (options.startWith) {
1748
+ // MySQL doesn't support START WITH in CREATE TABLE,
1749
+ // so we insert an initial row to set the auto_increment value
1750
+ await this.query(sql);
1751
+ if (options.startWith > 1) {
1752
+ const alterSql = `ALTER TABLE ${tableName} AUTO_INCREMENT = ${options.startWith}`;
1753
+ await this.query(alterSql);
1754
+ }
1755
+ return;
1756
+ }
1757
+ await this.query(sql);
1758
+ }
1759
+ /**
1760
+ * Drop a sequence table
1761
+ */
1762
+ async dropSequence(sequenceName, options) {
1763
+ const schema = options?.schema || this.config.database;
1764
+ const tableName = this.quoteTable(`${schema}.${sequenceName}`);
1765
+ const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
1766
+ const sql = `DROP TABLE ${ifExists}${tableName}`;
1767
+ await this.query(sql);
1768
+ }
1769
+ /**
1770
+ * Get next value from AUTO_INCREMENT
1771
+ */
1772
+ async nextSequenceValue(sequenceName) {
1773
+ const schema = this.config.database;
1774
+ const tableName = this.quoteTable(`${schema}.${sequenceName}`);
1775
+ // Insert a dummy row to get the next auto_increment value, then delete it
1776
+ // This is the standard way to get next value in MySQL without a separate sequence
1777
+ const sql = `INSERT INTO ${tableName} () VALUES ()`;
1778
+ await this.query(sql);
1779
+ // Get the last auto_increment value
1780
+ const result = await this.query('SELECT LAST_INSERT_ID() as value');
1781
+ return result.rows[0]?.value;
1782
+ }
1783
+ /**
1784
+ * Check if a sequence table exists
1785
+ */
1786
+ async hasSequence(sequenceName) {
1787
+ const schema = this.config.database;
1788
+ const sql = `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ${this.escape(schema || '')} AND TABLE_NAME = ${this.escape(sequenceName)}`;
1789
+ const result = await this.query(sql);
1790
+ return result.rows.length > 0;
1791
+ }
1792
+ /**
1793
+ * List all sequences in the database (MySQL - simulated via tables)
1794
+ * @returns Array of sequence names
1795
+ */
1796
+ async listSequences() {
1797
+ const schema = this.config.database;
1798
+ const sql = `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ${this.escape(schema || '')} AND TABLE_NAME LIKE '%_sequence%'`;
1799
+ const result = await this.query(sql);
1800
+ return result.rows.map((row) => row.TABLE_NAME);
1801
+ }
1802
+ // ==================== Row-Level Security (RLS) ====================
1803
+ async createPolicy(_options) {
1804
+ throw new Error('Row-level security is not supported by MySQL');
1805
+ }
1806
+ async dropPolicy(_policyName, _tableName, _options) {
1807
+ throw new Error('Row-level security is not supported by MySQL');
1808
+ }
1809
+ async enableRLS(_tableName, _schema) {
1810
+ throw new Error('Row-level security is not supported by MySQL');
1811
+ }
1812
+ async enableRowLevelSecurity(_tableName, _schema) {
1813
+ throw new Error('Row-level security is not supported by MySQL');
1814
+ }
1815
+ async disableRLS(_tableName, _schema) {
1816
+ throw new Error('Row-level security is not supported by MySQL');
1817
+ }
1818
+ async hasPolicy(_policyName, _tableName) {
1819
+ return false;
1820
+ }
1821
+ // ==================== Comments ====================
1822
+ /**
1823
+ * Add comment to a table
1824
+ */
1825
+ async commentTable(tableName, comment) {
1826
+ const sql = `ALTER TABLE ${this.quoteTable(tableName)} COMMENT = ${this.escape(comment)}`;
1827
+ await this.query(sql);
1828
+ }
1829
+ /**
1830
+ * Add comment to a column
1831
+ */
1832
+ async commentColumn(tableName, columnName, comment) {
1833
+ const sql = `ALTER TABLE ${this.quoteTable(tableName)} MODIFY ${this.quoteIdentifier(columnName)} COMMENT ${this.escape(comment)}`;
1834
+ await this.query(sql);
1835
+ }
1836
+ // ==================== Advanced Indexes ====================
1837
+ /**
1838
+ * Create a partial index - NOT SUPPORTED by MySQL at any version.
1839
+ *
1840
+ * MySQL has no `CREATE INDEX ... WHERE ...` (filtered/partial index)
1841
+ * syntax at any version, unlike PostgreSQL/SQL Server/SQLite. A previous
1842
+ * version of this method built that syntax anyway, which a real MySQL
1843
+ * server rejects with a syntax error — reject it explicitly instead,
1844
+ * consistent with how `createIndex()` handles a `where` clause.
1845
+ */
1846
+ async createPartialIndex(_tableName, _indexName, _fields, _where, _options) {
1847
+ throw new Error('MySQL does not support partial/filtered indexes (CREATE INDEX ... WHERE ...). ' +
1848
+ 'This is a PostgreSQL/SQL Server/SQLite-only feature. Consider a generated ' +
1849
+ 'column plus a regular index, or a CHECK constraint, as a workaround.');
1850
+ }
1851
+ /**
1852
+ * Create an expression index - Not supported in MySQL < 8.0
1853
+ */
1854
+ async createExpressionIndex(tableName, indexName, expression, options) {
1855
+ const fieldsSql = `((${expression}))`;
1856
+ let sql = `CREATE INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteTable(tableName)} ${fieldsSql}`;
1857
+ if (options?.unique)
1858
+ sql = sql.replace('CREATE INDEX', 'CREATE UNIQUE INDEX');
1859
+ await this.query(sql);
1860
+ }
1861
+ // ==================== Identity & Computed Columns ====================
1862
+ /**
1863
+ * Create an identity column - Not fully supported in MySQL
1864
+ */
1865
+ async createIdentityColumn(tableName, columnName, options) {
1866
+ const startWith = options?.startWith || 1;
1867
+ const sql = `ALTER TABLE ${this.quoteTable(tableName)} MODIFY ${this.quoteIdentifier(columnName)} INT AUTO_INCREMENT = ${startWith}`;
1868
+ await this.query(sql);
1869
+ }
1870
+ /**
1871
+ * Create a computed column - Not supported in MySQL (except in generated columns)
1872
+ */
1873
+ async createComputedColumn(tableName, columnName, expression, options) {
1874
+ const persisted = options?.persisted ? 'STORED' : 'VIRTUAL';
1875
+ const type = options?.type || 'INT';
1876
+ const sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD COLUMN ${this.quoteIdentifier(columnName)} ${type} GENERATED ALWAYS AS (${expression}) ${persisted}`;
1877
+ await this.query(sql);
1878
+ }
1879
+ // ============================================================================
1880
+ // Foreign Data Wrapper methods (PostgreSQL-specific, not supported by MySQL)
1881
+ // ============================================================================
1882
+ /**
1883
+ * Build CREATE SERVER query - Not supported by MySQL
1884
+ */
1885
+ buildCreateServerQuery(_name, _opts) {
1886
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1887
+ }
1888
+ /**
1889
+ * Build ALTER SERVER query - Not supported by MySQL
1890
+ */
1891
+ buildAlterServerQuery(_name, _opts) {
1892
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1893
+ }
1894
+ /**
1895
+ * Build DROP SERVER query - Not supported by MySQL
1896
+ */
1897
+ buildDropServerQuery(_name, _opts) {
1898
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1899
+ }
1900
+ /**
1901
+ * Build CREATE USER MAPPING query - Not supported by MySQL
1902
+ */
1903
+ buildCreateUserMappingQuery(_opts) {
1904
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1905
+ }
1906
+ /**
1907
+ * Build ALTER USER MAPPING query - Not supported by MySQL
1908
+ */
1909
+ buildAlterUserMappingQuery(_opts) {
1910
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1911
+ }
1912
+ /**
1913
+ * Build DROP USER MAPPING query - Not supported by MySQL
1914
+ */
1915
+ buildDropUserMappingQuery(_serverName, _user, _opts) {
1916
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1917
+ }
1918
+ /**
1919
+ * Build CREATE FOREIGN TABLE query - Not supported by MySQL
1920
+ */
1921
+ buildCreateForeignTableQuery(_tableName, _opts) {
1922
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1923
+ }
1924
+ /**
1925
+ * Build DROP FOREIGN TABLE query - Not supported by MySQL
1926
+ */
1927
+ buildDropForeignTableQuery(_tableName, _opts) {
1928
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1929
+ }
1930
+ /**
1931
+ * Build IMPORT FOREIGN SCHEMA query - Not supported by MySQL
1932
+ */
1933
+ buildImportForeignSchemaQuery(_remoteSchema, _serverName, _opts) {
1934
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1935
+ }
1936
+ /**
1937
+ * Get servers query - Not supported by MySQL
1938
+ */
1939
+ getServersQuery() {
1940
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
1941
+ }
1942
+ // ============================================================================
1943
+ // User management methods (MySQL-specific implementation)
1944
+ // ============================================================================
1945
+ // ============================================================================
1946
+ // ============================================================================
1947
+ // Utility methods
1948
+ // ============================================================================
1949
+ /**
1950
+ * Replace placeholders in SQL with actual values
1951
+ * Supports both named (:param) and positional (?) placeholders
1952
+ */
1953
+ replaceReplacements(sql, replacements) {
1954
+ if (!replacements) {
1955
+ return sql;
1956
+ }
1957
+ if (Array.isArray(replacements)) {
1958
+ // Positional replacements (?)
1959
+ let result = sql;
1960
+ for (const value of replacements) {
1961
+ result = result.replace(/\?/, this.escape(value));
1962
+ }
1963
+ return result;
1964
+ }
1965
+ // Named replacements (:param)
1966
+ let result = sql;
1967
+ for (const [key, value] of Object.entries(replacements)) {
1968
+ const escapedValue = this.escape(value);
1969
+ // Match :paramName with optional word boundary
1970
+ result = result.replace(new RegExp(`:${key}(?![\\w])`, 'g'), escapedValue);
1971
+ }
1972
+ return result;
1973
+ }
1974
+ // ============================================================================
1975
+ // Subquery includes helper
1976
+ // ============================================================================
1977
+ /**
1978
+ * Build a subquery for includes with LIMIT/OFFSET support
1979
+ */
1980
+ buildSubqueryIncludes(sql, includes, tableName, queryValues) {
1981
+ // Build WHERE clause for the main query to filter by included model's foreign key
1982
+ const include = includes[0];
1983
+ const model = include.model;
1984
+ const through = include.through;
1985
+ const throughTable = through?.tableName || `${tableName}_${model.tableName}`;
1986
+ // For many-to-many relationships, we need a subquery with JOIN
1987
+ if (include.through) {
1988
+ const whereClause = this.buildWhereClause(include.where ?? {});
1989
+ const onCondition = include.on
1990
+ ? this.buildWhereClause(include.on)
1991
+ : `${this.escapeId(tableName)}.${this.escapeId(include.foreignKey || 'id')} = ${this.escapeId(throughTable)}.${this.escapeId(include.sourceKey || 'id')}`;
1992
+ sql = `${sql} WHERE ${onCondition}`;
1993
+ if (whereClause.sql) {
1994
+ sql += ` AND ${whereClause.sql}`;
1995
+ queryValues.push(...whereClause.values);
1996
+ }
1997
+ // Add LIMIT/OFFSET for pagination
1998
+ sql += this.buildLimitOffset(include.limit, include.offset);
1999
+ }
2000
+ else {
2001
+ // For regular hasMany/belongsTo, use subquery with IN
2002
+ const foreignKey = include.foreignKey || `${tableName}_id`;
2003
+ const subquery = `SELECT ${this.escapeId(foreignKey)} FROM ${this.escapeId(model.tableName || include.model.toString())}`;
2004
+ const whereClause = include.where
2005
+ ? this.buildWhereClause(include.where)
2006
+ : { sql: '1=1', values: [] };
2007
+ sql = `${sql} WHERE ${this.escapeId(foreignKey)} IN (${subquery} WHERE ${whereClause.sql})`;
2008
+ queryValues.push(...whereClause.values);
2009
+ }
2010
+ return sql;
2011
+ }
2012
+ /**
2013
+ * Add an index to a table
2014
+ * Supports expression indexes and INCLUDE columns (MySQL 8.0+)
2015
+ */
2016
+ async addIndex(tableName, indexName, fields = [], options) {
2017
+ let sql = 'CREATE';
2018
+ if (options?.unique) {
2019
+ sql += ' UNIQUE';
2020
+ }
2021
+ // MySQL's index-type modifier (FULLTEXT/SPATIAL) goes BEFORE the INDEX
2022
+ // keyword: `CREATE FULLTEXT INDEX name ON ...`, not `CREATE INDEX
2023
+ // FULLTEXT name ON ...` (which is a syntax error).
2024
+ if (options?.type) {
2025
+ sql += ` ${options.type} INDEX`;
2026
+ }
2027
+ else {
2028
+ sql += ' INDEX';
2029
+ }
2030
+ sql += ` ${this.escapeId(indexName)} ON ${this.escapeId(tableName)}`;
2031
+ // Handle expression index (MySQL 8.0+)
2032
+ if (options?.expression) {
2033
+ sql += ` ((${options.expression}))`;
2034
+ }
2035
+ else {
2036
+ sql += ` (${fields.map((f) => this.escapeId(f)).join(', ')})`;
2037
+ }
2038
+ if (options?.using) {
2039
+ sql += ` USING ${options.using}`;
2040
+ }
2041
+ // MySQL has no `INCLUDE (...)` covering-index syntax (that's a
2042
+ // PostgreSQL 11+ / SQL Server construct). Emitting it would produce a
2043
+ // syntax error against a real MySQL server, so reject it explicitly
2044
+ // instead of generating invalid SQL.
2045
+ if (options?.include && options.include.length > 0) {
2046
+ throw new Error('MySQL does not support INCLUDE (...) covering-index columns. ' +
2047
+ 'This is a PostgreSQL/SQL Server-only feature; consider adding the ' +
2048
+ 'columns to the index key list itself instead.');
2049
+ }
2050
+ // MySQL has no partial/filtered index support at any version (unlike
2051
+ // PostgreSQL/SQL Server/SQLite) — there is no `WHERE` clause on
2052
+ // `CREATE INDEX`. Emitting one would produce a syntax error against a
2053
+ // real MySQL server, so reject it explicitly instead.
2054
+ if (options?.where && Object.keys(options.where).length > 0) {
2055
+ throw new Error('MySQL does not support partial/filtered indexes (CREATE INDEX ... WHERE ...). ' +
2056
+ 'This is a PostgreSQL/SQL Server/SQLite-only feature. Consider a generated ' +
2057
+ 'column plus a regular index, or a CHECK constraint, as a workaround.');
2058
+ }
2059
+ // Index visibility (MySQL 8.0+). INVISIBLE goes at the end of the
2060
+ // CREATE INDEX statement. Indexes are VISIBLE by default.
2061
+ if (options?.invisible) {
2062
+ sql += ' INVISIBLE';
2063
+ }
2064
+ await this.query(sql);
2065
+ }
2066
+ /**
2067
+ * Remove an index from a table
2068
+ */
2069
+ async removeIndex(tableName, indexName) {
2070
+ const sql = `DROP INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)}`;
2071
+ await this.query(sql);
2072
+ }
2073
+ /**
2074
+ * Create an index on a table with full options support
2075
+ * Supports expression indexes and INCLUDE columns (MySQL 8.0+)
2076
+ */
2077
+ async createIndex(tableName, indexDef) {
2078
+ const fields = indexDef.fields || [];
2079
+ let sql = 'CREATE';
2080
+ if (indexDef.unique) {
2081
+ sql += ' UNIQUE';
2082
+ }
2083
+ // MySQL's index-type modifier (FULLTEXT/SPATIAL) goes BEFORE the INDEX
2084
+ // keyword: `CREATE FULLTEXT INDEX name ON ...`, not `CREATE INDEX
2085
+ // FULLTEXT name ON ...` (which is a syntax error).
2086
+ if (indexDef.type) {
2087
+ sql += ` ${indexDef.type} INDEX`;
2088
+ }
2089
+ else {
2090
+ sql += ' INDEX';
2091
+ }
2092
+ sql += ` ${this.escapeId(indexDef.name)} ON ${this.escapeId(tableName)}`;
2093
+ // Handle expression index (MySQL 8.0+)
2094
+ if (indexDef.expression) {
2095
+ sql += ` ((${indexDef.expression}))`;
2096
+ }
2097
+ else {
2098
+ // Each field may be a bare column name or "column DIRECTION"
2099
+ // (e.g. "created_at DESC") — quote only the identifier portion so
2100
+ // the direction keyword isn't mangled into the escaped identifier.
2101
+ sql += ` (${fields
2102
+ .map((f) => {
2103
+ const match = f.match(/^(.+?)\s+(ASC|DESC)$/i);
2104
+ return match ? `${this.escapeId(match[1])} ${match[2].toUpperCase()}` : this.escapeId(f);
2105
+ })
2106
+ .join(', ')})`;
2107
+ }
2108
+ if (indexDef.using) {
2109
+ sql += ` USING ${indexDef.using}`;
2110
+ }
2111
+ // MySQL has no `INCLUDE (...)` covering-index syntax (that's a
2112
+ // PostgreSQL 11+ / SQL Server construct). Emitting it would produce a
2113
+ // syntax error against a real MySQL server, so reject it explicitly
2114
+ // instead of generating invalid SQL.
2115
+ if (indexDef.include && indexDef.include.length > 0) {
2116
+ throw new Error('MySQL does not support INCLUDE (...) covering-index columns. ' +
2117
+ 'This is a PostgreSQL/SQL Server-only feature; consider adding the ' +
2118
+ 'columns to the index key list itself instead.');
2119
+ }
2120
+ // MySQL has no partial/filtered index support at any version (unlike
2121
+ // PostgreSQL/SQL Server/SQLite) — there is no `WHERE` clause on
2122
+ // `CREATE INDEX`. Emitting one would produce a syntax error against a
2123
+ // real MySQL server, so reject it explicitly instead.
2124
+ if (indexDef.where && Object.keys(indexDef.where).length > 0) {
2125
+ throw new Error('MySQL does not support partial/filtered indexes (CREATE INDEX ... WHERE ...). ' +
2126
+ 'This is a PostgreSQL/SQL Server/SQLite-only feature. Consider a generated ' +
2127
+ 'column plus a regular index, or a CHECK constraint, as a workaround.');
2128
+ }
2129
+ // Index visibility (MySQL 8.0+). INVISIBLE goes at the end of the
2130
+ // CREATE INDEX statement. Indexes are VISIBLE by default.
2131
+ if (indexDef.invisible) {
2132
+ sql += ' INVISIBLE';
2133
+ }
2134
+ await this.query(sql);
2135
+ }
2136
+ /**
2137
+ * Toggle visibility of an existing column (MySQL 8.0+).
2138
+ *
2139
+ * Uses `ALTER TABLE ... ALTER COLUMN ... SET VISIBLE|INVISIBLE`, which
2140
+ * only changes metadata (fast, in-place) rather than rewriting the
2141
+ * table like `MODIFY COLUMN` would. Useful for zero-downtime column
2142
+ * rollout: add the column invisible, backfill it, then flip it visible
2143
+ * once verified.
2144
+ *
2145
+ * @param tableName - Name of the table
2146
+ * @param columnName - Name of the column to toggle
2147
+ * @param invisible - true to hide the column, false to make it visible
2148
+ */
2149
+ async alterColumnVisibility(tableName, columnName, invisible) {
2150
+ const sql = `ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} SET ${invisible ? 'INVISIBLE' : 'VISIBLE'}`;
2151
+ await this.query(sql);
2152
+ }
2153
+ /**
2154
+ * Toggle visibility of an existing index (MySQL 8.0+).
2155
+ *
2156
+ * Uses `ALTER TABLE ... ALTER INDEX ... VISIBLE|INVISIBLE`, which only
2157
+ * changes metadata rather than rebuilding the index. Useful for testing
2158
+ * the impact of dropping an index (or rolling one out) without the cost
2159
+ * of a destructive DROP/CREATE INDEX cycle. Note: a table's primary key
2160
+ * cannot be made invisible.
2161
+ *
2162
+ * @param tableName - Name of the table
2163
+ * @param indexName - Name of the index to toggle
2164
+ * @param invisible - true to hide the index from the optimizer, false to make it visible
2165
+ */
2166
+ async alterIndexVisibility(tableName, indexName, invisible) {
2167
+ const sql = `ALTER TABLE ${this.escapeId(tableName)} ALTER INDEX ${this.escapeId(indexName)} ${invisible ? 'INVISIBLE' : 'VISIBLE'}`;
2168
+ await this.query(sql);
2169
+ }
2170
+ /**
2171
+ * Build a FULLTEXT index on a table
2172
+ * MySQL-specific full-text search index
2173
+ */
2174
+ async createFulltextIndex(tableName, indexName, fields, options) {
2175
+ const sql = `CREATE FULLTEXT INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)} (${fields.map((f) => this.escapeId(f)).join(', ')})${options?.parser ? ` WITH PARSER ${options.parser}` : ''}`;
2176
+ await this.query(sql);
2177
+ }
2178
+ /**
2179
+ * Build a SPATIAL index on a table
2180
+ * MySQL-specific spatial index for geometry columns
2181
+ * Requires MySQL 5.7+ and InnoDB
2182
+ */
2183
+ async createSpatialIndex(tableName, indexName, fields, options) {
2184
+ // MySQL requires NOT NULL constraint on spatial columns for SPATIAL index
2185
+ const sql = `CREATE SPATIAL INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)} (${fields.map((f) => this.escapeId(f)).join(', ')})`;
2186
+ await this.query(sql);
2187
+ }
2188
+ /**
2189
+ * Build a multi-valued index expression for a JSON array column (MySQL 8.0.17+).
2190
+ * Multi-valued indexes let `JSON_CONTAINS`/`MEMBER OF` perform many-to-many-in-JSON
2191
+ * lookups efficiently against a scalar-typed JSON array.
2192
+ * MySQL: (CAST(column->'$[*]' AS <castType> ARRAY))
2193
+ */
2194
+ buildMultiValuedIndexExpression(jsonColumn, castType, path = '$[*]') {
2195
+ const columnRef = this.escapeId(jsonColumn);
2196
+ return `CAST(${columnRef}->'${path}' AS ${castType} ARRAY)`;
2197
+ }
2198
+ /**
2199
+ * Create a multi-valued index on a JSON array column (MySQL 8.0.17+).
2200
+ *
2201
+ * @example
2202
+ * // CREATE INDEX idx_tags ON products ((CAST(tags->'$[*]' AS CHAR(20) ARRAY)))
2203
+ * await dialect.createMultiValuedIndex('products', 'idx_tags', 'tags', { castType: 'CHAR(20)' });
2204
+ */
2205
+ async createMultiValuedIndex(tableName, indexName, jsonColumn, options) {
2206
+ const expression = this.buildMultiValuedIndexExpression(jsonColumn, options.castType, options.path);
2207
+ const sql = `CREATE INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)} ((${expression}))`;
2208
+ await this.query(sql);
2209
+ }
2210
+ /**
2211
+ * Bulk insert records into a table
2212
+ */
2213
+ async bulkInsert(tableName, records, _options) {
2214
+ if (records.length === 0) {
2215
+ return { rows: [], rowCount: 0, fields: [] };
2216
+ }
2217
+ const columns = Object.keys(records[0]);
2218
+ const values = [];
2219
+ const placeholders = [];
2220
+ for (const record of records) {
2221
+ const rowPlaceholders = [];
2222
+ for (let i = 0; i < columns.length; i++) {
2223
+ rowPlaceholders.push('?');
2224
+ values.push(record[columns[i]]);
2225
+ }
2226
+ placeholders.push(`(${rowPlaceholders.join(', ')})`);
2227
+ }
2228
+ const sql = `INSERT INTO ${this.quoteTable(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES ${placeholders.join(', ')}`;
2229
+ return this.query(sql, { replacements: values });
2230
+ }
2231
+ /**
2232
+ * Add a foreign key to a table
2233
+ */
2234
+ async addForeignKey(tableName, columnName, referencedTableName, referencedColumnName, options) {
2235
+ const constraintName = options?.name || `${tableName}_${columnName}_fkey`;
2236
+ const sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} FOREIGN KEY (${this.escapeId(columnName)}) REFERENCES ${this.quoteTable(referencedTableName)}(${this.escapeId(referencedColumnName)})`;
2237
+ const clauses = [];
2238
+ if (options?.onDelete) {
2239
+ clauses.push(`ON DELETE ${options.onDelete}`);
2240
+ }
2241
+ if (options?.onUpdate) {
2242
+ clauses.push(`ON UPDATE ${options.onUpdate}`);
2243
+ }
2244
+ if (clauses.length > 0) {
2245
+ await this.query(sql + ' ' + clauses.join(' '));
2246
+ }
2247
+ else {
2248
+ await this.query(sql);
2249
+ }
2250
+ }
2251
+ /**
2252
+ * Rename a column
2253
+ */
2254
+ async renameColumn(tableName, oldColumnName, newColumnName) {
2255
+ // MySQL uses CHANGE COLUMN syntax
2256
+ const describe = await this.describeTable(tableName);
2257
+ const columnDef = describe[oldColumnName];
2258
+ if (!columnDef) {
2259
+ throw new Error(`Column ${oldColumnName} does not exist in table ${tableName}`);
2260
+ }
2261
+ let sql = `ALTER TABLE ${this.quoteTable(tableName)} CHANGE ${this.escapeId(oldColumnName)} ${this.escapeId(newColumnName)} ${columnDef.type}`;
2262
+ if (!columnDef.allowNull) {
2263
+ sql += ' NOT NULL';
2264
+ }
2265
+ if (columnDef.defaultValue !== undefined) {
2266
+ sql += ` DEFAULT ${this.escape(columnDef.defaultValue)}`;
2267
+ }
2268
+ await this.query(sql);
2269
+ }
2270
+ /**
2271
+ * Create a user mapping (MySQL doesn't support user mappings)
2272
+ */
2273
+ async createUserMapping(_userName, _serverName, _options) {
2274
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
2275
+ }
2276
+ /**
2277
+ * Drop a user mapping (MySQL doesn't support user mappings)
2278
+ */
2279
+ async dropUserMapping(_userName, _serverName, _options) {
2280
+ throw new Error('Foreign Data Wrappers are not supported by MySQL. This is a PostgreSQL-specific feature.');
2281
+ }
2282
+ /**
2283
+ * ST_Distance - calculate distance between two geometries
2284
+ * MySQL 8.0+ spatial function
2285
+ */
2286
+ stDistance(geom1, geom2, srid) {
2287
+ const sridStr = srid ? `, ${srid}` : '';
2288
+ return `ST_Distance(${geom1}, ST_GeomFromText(${geom2}${sridStr}))`;
2289
+ }
2290
+ /**
2291
+ * ST_Within - check if geometry A is within geometry B
2292
+ * MySQL 8.0+ spatial function
2293
+ */
2294
+ stWithin(geom1, geom2, srid) {
2295
+ const sridStr = srid ? `, ${srid}` : '';
2296
+ return `ST_Within(${geom1}, ST_GeomFromText(${geom2}${sridStr}))`;
2297
+ }
2298
+ /**
2299
+ * ST_Contains - check if geometry A contains geometry B
2300
+ * MySQL 8.0+ spatial function
2301
+ */
2302
+ stContains(geom1, geom2, srid) {
2303
+ const sridStr = srid ? `, ${srid}` : '';
2304
+ return `ST_Contains(${geom1}, ST_GeomFromText(${geom2}${sridStr}))`;
2305
+ }
2306
+ /**
2307
+ * ST_Intersects - check if two geometries intersect
2308
+ * MySQL 8.0+ spatial function
2309
+ */
2310
+ stIntersects(geom1, geom2, srid) {
2311
+ const sridStr = srid ? `, ${srid}` : '';
2312
+ return `ST_Intersects(${geom1}, ST_GeomFromText(${geom2}${sridStr}))`;
2313
+ }
2314
+ /**
2315
+ * ST_DWithin - check if geometries are within a given distance
2316
+ * MySQL 8.0+ spatial function
2317
+ */
2318
+ stDWithin(geom1, geom2, distance, srid) {
2319
+ const sridStr = srid ? `, ${srid}` : '';
2320
+ return `ST_DWithin(${geom1}, ST_GeomFromText(${geom2}${sridStr}), ${distance})`;
2321
+ }
2322
+ /**
2323
+ * ST_GeomFromText - create geometry from WKT text
2324
+ */
2325
+ stGeomFromText(wkt, srid) {
2326
+ const sridStr = srid ? `, ${srid}` : '';
2327
+ return `ST_GeomFromText(${this.escape(wkt)}${sridStr})`;
2328
+ }
2329
+ /**
2330
+ * ST_GeomFromGeoJSON - create geometry from GeoJSON
2331
+ */
2332
+ stGeomFromGeoJSON(geojson, srid) {
2333
+ const sridStr = srid ? `, ${srid}` : '';
2334
+ return `ST_GeomFromGeoJSON(${this.escape(geojson)}${sridStr})`;
2335
+ }
2336
+ /**
2337
+ * ST_AsGeoJSON - convert geometry to GeoJSON
2338
+ */
2339
+ stAsGeoJSON(geom) {
2340
+ return `ST_AsGeoJSON(${geom})`;
2341
+ }
2342
+ /**
2343
+ * ST_AsText - convert geometry to WKT
2344
+ */
2345
+ stAsText(geom) {
2346
+ return `ST_AsText(${geom})`;
2347
+ }
2348
+ /**
2349
+ * Build a MATCH AGAINST full-text search expression
2350
+ * SQL: MATCH(columns) AGAINST(searchTerm [IN NATURAL LANGUAGE MODE | IN BOOLEAN MODE])
2351
+ */
2352
+ buildMatchAgainst(columns, searchTerm, options) {
2353
+ const cols = Array.isArray(columns) ? columns : [columns];
2354
+ const columnList = cols.map((c) => this.escapeId(c)).join(', ');
2355
+ const mode = options?.mode || 'natural';
2356
+ let modeStr = mode === 'boolean' ? 'IN BOOLEAN MODE' : 'IN NATURAL LANGUAGE MODE';
2357
+ if (options?.modifier) {
2358
+ modeStr =
2359
+ options.modifier === 'WITH QUERY EXPANSION'
2360
+ ? 'WITH QUERY EXPANSION'
2361
+ : 'IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION';
2362
+ }
2363
+ return `MATCH(${columnList}) AGAINST(${this.escape(searchTerm)} ${modeStr})`;
2364
+ }
2365
+ /**
2366
+ * Drop an index from a table
2367
+ */
2368
+ async dropIndex(tableName, indexName, options) {
2369
+ let sql = 'DROP INDEX';
2370
+ if (options?.ifExists) {
2371
+ sql += ' IF EXISTS';
2372
+ }
2373
+ sql += ` ${this.escapeId(indexName)} ON ${this.escapeId(tableName)}`;
2374
+ if (options?.cascade) {
2375
+ sql += ' CASCADE';
2376
+ }
2377
+ await this.query(sql);
2378
+ }
2379
+ /**
2380
+ * Create a constraint on a table
2381
+ */
2382
+ async createConstraint(tableName, constraintDef) {
2383
+ const fields = constraintDef.fields?.map((f) => this.escapeId(f)).join(', ');
2384
+ switch (constraintDef.type) {
2385
+ case 'PRIMARY KEY':
2386
+ if (!fields) {
2387
+ throw new Error('Primary key constraint requires fields');
2388
+ }
2389
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintDef.name)} PRIMARY KEY (${fields})`);
2390
+ break;
2391
+ case 'UNIQUE':
2392
+ if (!fields) {
2393
+ throw new Error('Unique constraint requires fields');
2394
+ }
2395
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintDef.name)} UNIQUE (${fields})`);
2396
+ break;
2397
+ case 'FOREIGN KEY': {
2398
+ if (!fields || !constraintDef.references) {
2399
+ throw new Error('Foreign key constraint requires fields and references');
2400
+ }
2401
+ // Handle composite foreign keys (string | string[])
2402
+ const refField = constraintDef.references.field;
2403
+ const refFieldSql = Array.isArray(refField)
2404
+ ? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
2405
+ : `(${this.escapeId(refField)})`;
2406
+ let sql = `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintDef.name)} `;
2407
+ sql += `FOREIGN KEY (${fields}) REFERENCES ${this.escapeId(constraintDef.references.table)}${refFieldSql}`;
2408
+ if (constraintDef.references.onDelete) {
2409
+ sql += ` ON DELETE ${constraintDef.references.onDelete}`;
2410
+ }
2411
+ if (constraintDef.references.onUpdate) {
2412
+ sql += ` ON UPDATE ${constraintDef.references.onUpdate}`;
2413
+ }
2414
+ await this.query(sql);
2415
+ break;
2416
+ }
2417
+ case 'CHECK':
2418
+ if (!constraintDef.check) {
2419
+ throw new Error('Check constraint requires a check expression');
2420
+ }
2421
+ await this.query(`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintDef.name)} CHECK (${constraintDef.check})`);
2422
+ break;
2423
+ default:
2424
+ throw new Error(`Unknown constraint type: ${constraintDef.type}`);
2425
+ }
2426
+ }
2427
+ /**
2428
+ * Drop a constraint from a table
2429
+ */
2430
+ async dropConstraint(tableName, constraintName, options) {
2431
+ let sql = `ALTER TABLE ${this.escapeId(tableName)} DROP CONSTRAINT`;
2432
+ if (options?.ifExists) {
2433
+ sql += ' IF EXISTS';
2434
+ }
2435
+ sql += ` ${this.escapeId(constraintName)}`;
2436
+ if (options?.cascade) {
2437
+ sql += ' CASCADE';
2438
+ }
2439
+ await this.query(sql);
2440
+ }
2441
+ /**
2442
+ * Begin a new transaction
2443
+ */
2444
+ async startTransaction(options) {
2445
+ if (!this.pool) {
2446
+ throw new Error('Not connected to database');
2447
+ }
2448
+ const connection = await this.pool.getConnection();
2449
+ if (options?.isolationLevel) {
2450
+ await connection.query(`SET TRANSACTION ISOLATION LEVEL ${options.isolationLevel.replace(/_/g, ' ')}`);
2451
+ }
2452
+ await connection.beginTransaction();
2453
+ const transaction = new MySQLTransaction(connection, options);
2454
+ return transaction;
2455
+ }
2456
+ /**
2457
+ * Commit a transaction
2458
+ */
2459
+ async commitTransaction(transaction) {
2460
+ const mysqlTx = transaction;
2461
+ if (mysqlTx.connection) {
2462
+ await mysqlTx.connection.commit();
2463
+ mysqlTx.connection.release();
2464
+ mysqlTx.finished = true;
2465
+ }
2466
+ }
2467
+ /**
2468
+ * Rollback a transaction
2469
+ */
2470
+ async rollbackTransaction(transaction) {
2471
+ const mysqlTx = transaction;
2472
+ if (mysqlTx.connection) {
2473
+ await mysqlTx.connection.rollback();
2474
+ mysqlTx.connection.release();
2475
+ mysqlTx.finished = true;
2476
+ }
2477
+ }
2478
+ /**
2479
+ * Get the SQL for a data type
2480
+ */
2481
+ getDataTypeSql(dataType) {
2482
+ if (typeof dataType === 'string') {
2483
+ return dataType;
2484
+ }
2485
+ if (!dataType || typeof dataType !== 'object') {
2486
+ return 'VARCHAR(255)';
2487
+ }
2488
+ const dt = dataType;
2489
+ switch (dt.key) {
2490
+ case 'STRING':
2491
+ return `VARCHAR(${dt.length || 255})`;
2492
+ case 'CHAR':
2493
+ return `CHAR(${dt.length || 1})`;
2494
+ case 'TEXT':
2495
+ if (dt.length === 65535)
2496
+ return 'TEXT';
2497
+ if (dt.length === 16777215)
2498
+ return 'MEDIUMTEXT';
2499
+ if (dt.length === 4294967295)
2500
+ return 'LONGTEXT';
2501
+ return dt.length ? `VARCHAR(${dt.length})` : 'TEXT';
2502
+ case 'INTEGER':
2503
+ let intType = 'INT';
2504
+ if (dt.length === 1)
2505
+ intType = 'TINYINT';
2506
+ else if (dt.length === 2)
2507
+ intType = 'SMALLINT';
2508
+ else if (dt.length === 3)
2509
+ intType = 'MEDIUMINT';
2510
+ else if (dt.length === 8)
2511
+ intType = 'BIGINT';
2512
+ if (dt.unsigned)
2513
+ intType += ' UNSIGNED';
2514
+ return intType;
2515
+ case 'BIGINT':
2516
+ let bigIntType = 'BIGINT';
2517
+ if (dt.unsigned)
2518
+ bigIntType += ' UNSIGNED';
2519
+ return bigIntType;
2520
+ case 'FLOAT':
2521
+ return dt.length ? `FLOAT(${dt.length},${dt.decimals || 2})` : 'FLOAT';
2522
+ case 'DOUBLE':
2523
+ return dt.length ? `DOUBLE(${dt.length},${dt.decimals || 2})` : 'DOUBLE';
2524
+ case 'DECIMAL':
2525
+ return `DECIMAL(${dt.precision || 10},${dt.scale || 0})`;
2526
+ case 'BOOLEAN':
2527
+ return 'TINYINT(1)';
2528
+ case 'DATE':
2529
+ return dt.precision ? `DATETIME(${dt.precision})` : 'DATETIME';
2530
+ case 'DATEONLY':
2531
+ return 'DATE';
2532
+ case 'TIME':
2533
+ return dt.precision ? `TIME(${dt.precision})` : 'TIME';
2534
+ case 'BLOB':
2535
+ if (dt.length === 'tiny')
2536
+ return 'TINYBLOB';
2537
+ if (dt.length === 'medium')
2538
+ return 'MEDIUMBLOB';
2539
+ if (dt.length === 'long')
2540
+ return 'LONGBLOB';
2541
+ return 'BLOB';
2542
+ case 'ENUM':
2543
+ const values = dt.values || [];
2544
+ return `ENUM(${values.map((v) => `'${v}'`).join(',')})`;
2545
+ case 'JSON':
2546
+ return 'JSON';
2547
+ case 'JSONB':
2548
+ return 'JSON';
2549
+ case 'UUID':
2550
+ return 'CHAR(36)';
2551
+ case 'GEOMETRY': {
2552
+ const sridSql = dt.srid ? ` SRID ${dt.srid}` : '';
2553
+ if (dt.type) {
2554
+ return `${String(dt.type).toUpperCase()}${sridSql}`;
2555
+ }
2556
+ return `GEOMETRY${sridSql}`;
2557
+ }
2558
+ case 'GEOGRAPHY': {
2559
+ // MySQL has no native GEOGRAPHY type; store as (typed) GEOMETRY.
2560
+ const sridSql = dt.srid ? ` SRID ${dt.srid}` : '';
2561
+ if (dt.type) {
2562
+ return `${String(dt.type).toUpperCase()}${sridSql}`;
2563
+ }
2564
+ return `GEOMETRY${sridSql}`;
2565
+ }
2566
+ case 'POINT':
2567
+ case 'LINESTRING':
2568
+ case 'POLYGON':
2569
+ case 'MULTIPOINT':
2570
+ case 'MULTILINESTRING':
2571
+ case 'MULTIPOLYGON':
2572
+ case 'GEOMETRYCOLLECTION': {
2573
+ const sridSql = dt.srid ? ` SRID ${dt.srid}` : '';
2574
+ return `${dt.key}${sridSql}`;
2575
+ }
2576
+ case 'BIT':
2577
+ return `BIT(${dt.length || 1})`;
2578
+ case 'YEAR':
2579
+ return dt.length ? `YEAR(${dt.length})` : 'YEAR';
2580
+ case 'SET': {
2581
+ const setValues = dt.values || [];
2582
+ return `SET(${setValues.map((v) => `'${v}'`).join(',')})`;
2583
+ }
2584
+ case 'VIRTUAL':
2585
+ // Virtual fields don't create a database column - return empty string
2586
+ return '';
2587
+ default:
2588
+ return 'VARCHAR(255)';
2589
+ }
2590
+ }
2591
+ /**
2592
+ * Build a WHERE clause from a WhereOptions object
2593
+ */
2594
+ buildWhereClause(where, options) {
2595
+ const values = [];
2596
+ if (!where ||
2597
+ (Object.keys(where).length === 0 && Object.getOwnPropertySymbols(where).length === 0)) {
2598
+ return { sql: '', values };
2599
+ }
2600
+ const buildCondition = (condition) => {
2601
+ if (!condition) {
2602
+ return { sql: '', values: [] };
2603
+ }
2604
+ if (typeof condition !== 'object') {
2605
+ values.push(condition);
2606
+ return { sql: '?', values: [condition] };
2607
+ }
2608
+ // Operator keys (Op.isNotNull, Op.match, Op.json('path'), ...) are
2609
+ // Symbols, which Object.entries()/Object.keys() silently drop. Without
2610
+ // this normalization step every Symbol-keyed operator condition here
2611
+ // was falling through to the `= ?` equality branch below, binding the
2612
+ // raw operator-value object as an opaque param (e.g. producing
2613
+ // `WHERE email = ?` bound to `{ [Symbol(isNotNull)]: true }`) instead
2614
+ // of the intended SQL — a real MySQL server then either matches
2615
+ // nothing or rejects the query outright.
2616
+ const rawCond = condition;
2617
+ const topSymbols = Object.getOwnPropertySymbols(rawCond);
2618
+ const normalizedCond = { ...rawCond };
2619
+ for (const sym of topSymbols) {
2620
+ const strKey = (0, operators_1.operatorToWhereKey)(sym);
2621
+ if (strKey)
2622
+ normalizedCond[strKey] = rawCond[sym];
2623
+ }
2624
+ const cond = normalizedCond;
2625
+ // Handle logical operators
2626
+ if (cond.$and || cond.$or || cond.$not) {
2627
+ const conditions = [];
2628
+ if (cond.$and) {
2629
+ const andConditions = cond.$and.map((c) => {
2630
+ const result = buildCondition(c);
2631
+ return result.sql;
2632
+ });
2633
+ conditions.push(`(${andConditions.join(' AND ')})`);
2634
+ }
2635
+ if (cond.$or) {
2636
+ const orConditions = cond.$or.map((c) => {
2637
+ const result = buildCondition(c);
2638
+ return result.sql;
2639
+ });
2640
+ conditions.push(`(${orConditions.join(' OR ')})`);
2641
+ }
2642
+ if (cond.$not) {
2643
+ const result = buildCondition(cond.$not);
2644
+ conditions.push(`NOT (${result.sql})`);
2645
+ }
2646
+ return { sql: conditions.join(' AND '), values };
2647
+ }
2648
+ // Handle field conditions
2649
+ const fieldConditions = [];
2650
+ for (const [key, value] of Object.entries(cond)) {
2651
+ if (key.startsWith('$'))
2652
+ continue;
2653
+ if (value && typeof value === 'object') {
2654
+ // Same Symbol-key normalization as above, but for the nested
2655
+ // per-field operator object (e.g. `{ email: { [Op.isNotNull]: true } }`).
2656
+ const rawValueObj = value;
2657
+ const symbolKeys = Object.getOwnPropertySymbols(rawValueObj);
2658
+ const normalizedObj = {
2659
+ ...rawValueObj,
2660
+ };
2661
+ for (const sym of symbolKeys) {
2662
+ // Op.json(path) creates Symbol.for(`json:${path}`) — a family of
2663
+ // distinct symbols, not the fixed set operatorToWhereKey()
2664
+ // recognizes — so handle it directly here rather than via
2665
+ // operatorToWhereKey().
2666
+ const symDesc = sym.description || '';
2667
+ if (symDesc.startsWith('json:')) {
2668
+ normalizedObj[`$json:${symDesc.substring(5)}`] = rawValueObj[sym];
2669
+ continue;
2670
+ }
2671
+ const strKey = (0, operators_1.operatorToWhereKey)(sym);
2672
+ if (strKey)
2673
+ normalizedObj[strKey] = rawValueObj[sym];
2674
+ }
2675
+ const valueObj = normalizedObj;
2676
+ const jsonPathEntry = Object.keys(valueObj).find((k) => k.startsWith('$json:'));
2677
+ if (valueObj.$eq !== undefined) {
2678
+ values.push(valueObj.$eq);
2679
+ fieldConditions.push(`${this.escapeId(key)} = ?`);
2680
+ }
2681
+ else if (valueObj.$ne !== undefined) {
2682
+ values.push(valueObj.$ne);
2683
+ fieldConditions.push(`${this.escapeId(key)} != ?`);
2684
+ }
2685
+ else if (valueObj.$gt !== undefined) {
2686
+ values.push(valueObj.$gt);
2687
+ fieldConditions.push(`${this.escapeId(key)} > ?`);
2688
+ }
2689
+ else if (valueObj.$gte !== undefined) {
2690
+ values.push(valueObj.$gte);
2691
+ fieldConditions.push(`${this.escapeId(key)} >= ?`);
2692
+ }
2693
+ else if (valueObj.$lt !== undefined) {
2694
+ values.push(valueObj.$lt);
2695
+ fieldConditions.push(`${this.escapeId(key)} < ?`);
2696
+ }
2697
+ else if (valueObj.$lte !== undefined) {
2698
+ values.push(valueObj.$lte);
2699
+ fieldConditions.push(`${this.escapeId(key)} <= ?`);
2700
+ }
2701
+ else if (valueObj.$like !== undefined) {
2702
+ values.push(valueObj.$like);
2703
+ fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
2704
+ }
2705
+ else if (valueObj.$notLike !== undefined) {
2706
+ values.push(valueObj.$notLike);
2707
+ fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
2708
+ }
2709
+ else if (valueObj.$startsWith !== undefined) {
2710
+ values.push(valueObj.$startsWith + '%');
2711
+ fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
2712
+ }
2713
+ else if (valueObj.$notStartsWith !== undefined) {
2714
+ values.push(valueObj.$notStartsWith + '%');
2715
+ fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
2716
+ }
2717
+ else if (valueObj.$endsWith !== undefined) {
2718
+ values.push('%' + valueObj.$endsWith);
2719
+ fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
2720
+ }
2721
+ else if (valueObj.$notEndsWith !== undefined) {
2722
+ values.push('%' + valueObj.$notEndsWith);
2723
+ fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
2724
+ }
2725
+ else if (valueObj.$substring !== undefined) {
2726
+ values.push('%' + valueObj.$substring + '%');
2727
+ fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
2728
+ }
2729
+ else if (valueObj.$notSubstring !== undefined) {
2730
+ values.push('%' + valueObj.$notSubstring + '%');
2731
+ fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
2732
+ }
2733
+ else if (valueObj.$in) {
2734
+ const inValues = valueObj.$in;
2735
+ values.push(...inValues);
2736
+ fieldConditions.push(`${this.escapeId(key)} IN (${inValues.map(() => '?').join(',')})`);
2737
+ }
2738
+ else if (valueObj.$notIn) {
2739
+ const notInValues = valueObj.$notIn;
2740
+ values.push(...notInValues);
2741
+ fieldConditions.push(`${this.escapeId(key)} NOT IN (${notInValues.map(() => '?').join(',')})`);
2742
+ }
2743
+ else if (valueObj.$between) {
2744
+ const between = valueObj.$between;
2745
+ values.push(between[0], between[1]);
2746
+ fieldConditions.push(`${this.escapeId(key)} BETWEEN ? AND ?`);
2747
+ }
2748
+ else if (valueObj.$notBetween) {
2749
+ const notBetween = valueObj.$notBetween;
2750
+ values.push(notBetween[0], notBetween[1]);
2751
+ fieldConditions.push(`${this.escapeId(key)} NOT BETWEEN ? AND ?`);
2752
+ }
2753
+ else if (valueObj.$isNull !== undefined) {
2754
+ if (valueObj.$isNull) {
2755
+ fieldConditions.push(`${this.escapeId(key)} IS NULL`);
2756
+ }
2757
+ else {
2758
+ fieldConditions.push(`${this.escapeId(key)} IS NOT NULL`);
2759
+ }
2760
+ }
2761
+ else if (valueObj.$isNotNull !== undefined) {
2762
+ if (valueObj.$isNotNull) {
2763
+ fieldConditions.push(`${this.escapeId(key)} IS NOT NULL`);
2764
+ }
2765
+ else {
2766
+ fieldConditions.push(`${this.escapeId(key)} IS NULL`);
2767
+ }
2768
+ }
2769
+ else if (valueObj.$col !== undefined) {
2770
+ fieldConditions.push(`${this.escapeId(key)} = ${this.escapeId(String(valueObj.$col))}`);
2771
+ }
2772
+ else if (valueObj.$regexp !== undefined) {
2773
+ values.push(valueObj.$regexp);
2774
+ fieldConditions.push(`${this.escapeId(key)} REGEXP ?`);
2775
+ }
2776
+ else if (valueObj.$notRegexp !== undefined) {
2777
+ values.push(valueObj.$notRegexp);
2778
+ fieldConditions.push(`${this.escapeId(key)} NOT REGEXP ?`);
2779
+ }
2780
+ else if (valueObj.$match !== undefined) {
2781
+ // MySQL full-text search: MATCH(columns) AGAINST(term [mode]).
2782
+ // Format: { $match: 'term' } or { $match: { columns, mode, searchTerm } }
2783
+ const matchValue = valueObj.$match;
2784
+ const searchTerm = typeof matchValue === 'string'
2785
+ ? matchValue
2786
+ : (matchValue?.searchTerm ?? '');
2787
+ const matchColumns = matchValue?.columns || [key];
2788
+ const mode = matchValue?.mode === 'boolean' ? 'IN BOOLEAN MODE' : 'IN NATURAL LANGUAGE MODE';
2789
+ const columnList = matchColumns.map((c) => this.escapeId(c)).join(', ');
2790
+ values.push(searchTerm);
2791
+ fieldConditions.push(`MATCH(${columnList}) AGAINST(? ${mode})`);
2792
+ }
2793
+ else if (jsonPathEntry) {
2794
+ // Op.json('path') -> Symbol.for('json:path'), normalized above to `$json:path`.
2795
+ const jsonPath = jsonPathEntry.substring(6);
2796
+ const normalizedPath = jsonPath.startsWith('$.') ? jsonPath : `$.${jsonPath}`;
2797
+ const compareValue = valueObj[jsonPathEntry];
2798
+ if (compareValue !== undefined) {
2799
+ values.push(compareValue);
2800
+ fieldConditions.push(`JSON_EXTRACT(${this.escapeId(key)}, '${normalizedPath}') = ?`);
2801
+ }
2802
+ else {
2803
+ fieldConditions.push(`JSON_EXTRACT(${this.escapeId(key)}, '${normalizedPath}') IS NOT NULL`);
2804
+ }
2805
+ }
2806
+ else {
2807
+ values.push(value);
2808
+ fieldConditions.push(`${this.escapeId(key)} = ?`);
2809
+ }
2810
+ }
2811
+ else {
2812
+ values.push(value);
2813
+ fieldConditions.push(`${this.escapeId(key)} = ?`);
2814
+ }
2815
+ }
2816
+ return { sql: fieldConditions.join(' AND '), values };
2817
+ };
2818
+ const result = buildCondition(where);
2819
+ return { sql: result.sql, values: result.values };
2820
+ }
2821
+ /**
2822
+ * Build an ORDER BY clause
2823
+ */
2824
+ buildOrderClause(order, options) {
2825
+ const orderArray = order;
2826
+ if (!orderArray || !Array.isArray(orderArray) || orderArray.length === 0) {
2827
+ return '';
2828
+ }
2829
+ const orderParts = [];
2830
+ for (const item of orderArray) {
2831
+ if (Array.isArray(item)) {
2832
+ const field = typeof item[0] === 'string' ? this.escapeId(item[0]) : item[0];
2833
+ const direction = item[1] ? ` ${item[1]}` : '';
2834
+ orderParts.push(`${field}${direction}`);
2835
+ }
2836
+ else if (typeof item === 'string') {
2837
+ orderParts.push(item);
2838
+ }
2839
+ else if (item && typeof item === 'object') {
2840
+ const itemArr = item;
2841
+ const [modelOrString, field] = itemArr;
2842
+ const model = typeof modelOrString === 'string'
2843
+ ? modelOrString
2844
+ : modelOrString.tableName || '';
2845
+ const fieldStr = typeof field === 'string' ? field : '';
2846
+ orderParts.push(`${model ? `${this.escapeId(model)}.` : ''}${this.escapeId(fieldStr)}`);
2847
+ }
2848
+ }
2849
+ return orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
2850
+ }
2851
+ /**
2852
+ * Build a LIMIT/OFFSET clause
2853
+ */
2854
+ buildLimitOffset(limit, offset) {
2855
+ let sql = '';
2856
+ if (limit !== undefined) {
2857
+ sql += ` LIMIT ${Number(limit)}`;
2858
+ if (offset !== undefined) {
2859
+ sql += ` OFFSET ${Number(offset)}`;
2860
+ }
2861
+ }
2862
+ else if (offset !== undefined) {
2863
+ // MySQL does not support OFFSET without LIMIT. Use the documented
2864
+ // maximum unsigned BIGINT as the LIMIT to effectively return all rows.
2865
+ sql += ` LIMIT 18446744073709551615 OFFSET ${Number(offset)}`;
2866
+ }
2867
+ return sql;
2868
+ }
2869
+ /**
2870
+ * Build an INSERT query
2871
+ */
2872
+ buildInsertQuery(tableName, values, options) {
2873
+ const columns = Object.keys(values);
2874
+ const processedValues = [];
2875
+ const placeholders = [];
2876
+ // Process each value - handle Literal differently
2877
+ for (const value of Object.values(values)) {
2878
+ if (value instanceof prorm_1.Literal) {
2879
+ // For Literal values, inline the SQL directly
2880
+ placeholders.push(value.val);
2881
+ }
2882
+ else {
2883
+ // For regular values, use parameterized placeholder
2884
+ placeholders.push('?');
2885
+ processedValues.push(value);
2886
+ }
2887
+ }
2888
+ let sql = `INSERT INTO ${this.escapeId(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${placeholders.join(', ')})`;
2889
+ if (options?.upsert) {
2890
+ // The `VALUES(col)` function form is deprecated as of MySQL 8.0.20 and
2891
+ // will be removed in a future release. Use the row-alias form instead.
2892
+ sql += ' AS new_vals';
2893
+ const updateColumns = columns
2894
+ .map((c) => `${this.escapeId(c)} = new_vals.${this.escapeId(c)}`)
2895
+ .join(', ');
2896
+ const conflictFields = options.conflictFields || columns;
2897
+ sql += ` ON DUPLICATE KEY UPDATE ${updateColumns}`;
2898
+ }
2899
+ return { sql, values: processedValues };
2900
+ }
2901
+ /**
2902
+ * Build an UPSERT query for MySQL
2903
+ * Uses ON DUPLICATE KEY UPDATE syntax
2904
+ */
2905
+ buildUpsertQuery(tableName, values, options) {
2906
+ const columns = Object.keys(values);
2907
+ const processedValues = [];
2908
+ const placeholders = [];
2909
+ // Process each value - handle Literal differently
2910
+ for (const value of Object.values(values)) {
2911
+ if (value instanceof prorm_1.Literal) {
2912
+ // For Literal values, inline the SQL directly
2913
+ placeholders.push(value.val);
2914
+ }
2915
+ else {
2916
+ // For regular values, use parameterized placeholder
2917
+ placeholders.push('?');
2918
+ processedValues.push(value);
2919
+ }
2920
+ }
2921
+ // Determine which fields to update
2922
+ const updateFields = options?.updateOnDuplicate && options.updateOnDuplicate.length > 0
2923
+ ? options.updateOnDuplicate
2924
+ : columns;
2925
+ let sql = `INSERT INTO ${this.escapeId(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${placeholders.join(', ')})`;
2926
+ // The `VALUES(col)` function form is deprecated as of MySQL 8.0.20 and
2927
+ // will be removed in a future release. Use the row-alias form instead.
2928
+ sql += ' AS new_vals';
2929
+ // Add ON DUPLICATE KEY UPDATE clause
2930
+ const updateClauses = updateFields
2931
+ .map((c) => `${this.escapeId(c)} = new_vals.${this.escapeId(c)}`)
2932
+ .join(', ');
2933
+ sql += ` ON DUPLICATE KEY UPDATE ${updateClauses}`;
2934
+ // MySQL (unlike PostgreSQL/SQLite/MariaDB 10.5+) does not support the
2935
+ // RETURNING clause at all. Emitting it would produce a syntax error
2936
+ // against a real MySQL server, so fail fast instead of generating
2937
+ // invalid SQL. Callers should instead re-select the row after the
2938
+ // upsert (e.g. using LAST_INSERT_ID() or the known unique key).
2939
+ if (options?.returning) {
2940
+ throw new Error('MySQL does not support the RETURNING clause. Re-fetch the row after the upsert instead.');
2941
+ }
2942
+ return { sql, values: processedValues };
2943
+ }
2944
+ /**
2945
+ * Build an increment query
2946
+ * @param tableName - Table name
2947
+ * @param fields - Fields to increment
2948
+ * @param where - Where clause
2949
+ * @param options - Query options (by: number)
2950
+ */
2951
+ buildIncrementQuery(tableName, fields, where, options) {
2952
+ const by = options?.by ?? 1;
2953
+ const setClauses = [];
2954
+ const queryValues = [];
2955
+ // Handle different field formats
2956
+ if (typeof fields === 'string') {
2957
+ // Single field: increment('count')
2958
+ setClauses.push(`${this.escapeId(fields)} = ${this.escapeId(fields)} + ?`);
2959
+ queryValues.push(by);
2960
+ }
2961
+ else if (Array.isArray(fields)) {
2962
+ // Array of fields: increment(['count', 'value'], { by: 5 })
2963
+ for (const field of fields) {
2964
+ setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + ?`);
2965
+ queryValues.push(by);
2966
+ }
2967
+ }
2968
+ else {
2969
+ // Object: increment({ count: 1, value: 10 })
2970
+ for (const [field, value] of Object.entries(fields)) {
2971
+ setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + ?`);
2972
+ queryValues.push(value);
2973
+ }
2974
+ }
2975
+ // Build WHERE clause
2976
+ const whereClause = this.buildWhereClause(where);
2977
+ queryValues.push(...whereClause.values);
2978
+ const sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')} WHERE ${whereClause.sql}`;
2979
+ return { sql, values: queryValues };
2980
+ }
2981
+ /**
2982
+ * Build an UPDATE query
2983
+ */
2984
+ buildUpdateQuery(tableName, values, where, options) {
2985
+ const setClauses = [];
2986
+ const queryValues = [];
2987
+ for (const [key, value] of Object.entries(values)) {
2988
+ if (value instanceof prorm_1.Literal) {
2989
+ // For Literal values, inline the SQL directly
2990
+ setClauses.push(`${this.escapeId(key)} = ${value.val}`);
2991
+ }
2992
+ else {
2993
+ // For regular values, use parameterized placeholder
2994
+ setClauses.push(`${this.escapeId(key)} = ?`);
2995
+ queryValues.push(value);
2996
+ }
2997
+ }
2998
+ const whereClause = this.buildWhereClause(where);
2999
+ let sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')}`;
3000
+ if (whereClause.sql) {
3001
+ sql += ` WHERE ${whereClause.sql}`;
3002
+ }
3003
+ if (options?.limit) {
3004
+ sql += ` LIMIT ${options.limit}`;
3005
+ }
3006
+ return { sql, values: [...queryValues, ...whereClause.values] };
3007
+ }
3008
+ /**
3009
+ * Build a DELETE query
3010
+ */
3011
+ buildDeleteQuery(tableName, where, options) {
3012
+ const whereClause = this.buildWhereClause(where);
3013
+ let sql = `DELETE FROM ${this.escapeId(tableName)}`;
3014
+ if (whereClause.sql) {
3015
+ sql += ` WHERE ${whereClause.sql}`;
3016
+ }
3017
+ if (options?.limit) {
3018
+ sql += ` LIMIT ${options.limit}`;
3019
+ }
3020
+ return { sql, values: whereClause.values };
3021
+ }
3022
+ /**
3023
+ * Check if a value is a function expression (fn('COUNT', ...))
3024
+ */
3025
+ isFnExpression(value) {
3026
+ return value !== null && typeof value === 'object' && value.__type === 'fn';
3027
+ }
3028
+ /**
3029
+ * Check if a value is a column expression (col('name'))
3030
+ */
3031
+ isColExpression(value) {
3032
+ return value !== null && typeof value === 'object' && value.__type === 'col';
3033
+ }
3034
+ /**
3035
+ * Check if a value is a literal expression (literal('sql'))
3036
+ */
3037
+ isLiteralExpression(value) {
3038
+ return value !== null && typeof value === 'object' && value.__type === 'literal';
3039
+ }
3040
+ /**
3041
+ * Compile a function expression to SQL
3042
+ */
3043
+ compileFnExpression(fnExpr) {
3044
+ const args = fnExpr.args.map((arg) => {
3045
+ if (this.isColExpression(arg)) {
3046
+ return this.escapeId(arg.col);
3047
+ }
3048
+ else if (typeof arg === 'string') {
3049
+ return this.escape(arg);
3050
+ }
3051
+ else if (this.isFnExpression(arg)) {
3052
+ return this.compileFnExpression(arg);
3053
+ }
3054
+ else {
3055
+ return String(arg);
3056
+ }
3057
+ });
3058
+ return `${fnExpr.fn}(${args.join(', ')})`;
3059
+ }
3060
+ /**
3061
+ * Build SELECT clause with support for function expressions and aliases
3062
+ * Handles: 'field', ['field'], ['field', 'alias'], [fn('COUNT', 'id'), 'count']
3063
+ */
3064
+ buildSelectClause(attributes) {
3065
+ if (!attributes)
3066
+ return '*';
3067
+ if (Array.isArray(attributes)) {
3068
+ const parts = attributes.map((attr) => {
3069
+ // Handle array format: [expression, alias] or [fn, alias]
3070
+ if (Array.isArray(attr)) {
3071
+ const [expr, alias] = attr;
3072
+ let sql;
3073
+ if (this.isFnExpression(expr)) {
3074
+ sql = this.compileFnExpression(expr);
3075
+ }
3076
+ else if (this.isColExpression(expr)) {
3077
+ sql = this.escapeId(expr.col);
3078
+ }
3079
+ else if (this.isLiteralExpression(expr)) {
3080
+ sql = expr.sql;
3081
+ }
3082
+ else if (typeof expr === 'string') {
3083
+ // Check if it's a column reference or literal SQL
3084
+ if (expr.includes('(') || expr.includes(' ')) {
3085
+ sql = expr; // Literal SQL
3086
+ }
3087
+ else {
3088
+ sql = this.escapeId(expr);
3089
+ }
3090
+ }
3091
+ else {
3092
+ sql = String(expr);
3093
+ }
3094
+ return `${sql} AS ${this.escapeId(alias)}`;
3095
+ }
3096
+ else if (this.isFnExpression(attr)) {
3097
+ // Function without alias - let DB assign a name
3098
+ return this.compileFnExpression(attr);
3099
+ }
3100
+ else if (this.isColExpression(attr)) {
3101
+ return this.escapeId(attr.col);
3102
+ }
3103
+ else if (this.isLiteralExpression(attr)) {
3104
+ return attr.sql;
3105
+ }
3106
+ else if (typeof attr === 'string') {
3107
+ // Plain column name
3108
+ return this.escapeId(attr);
3109
+ }
3110
+ return String(attr);
3111
+ });
3112
+ return parts.join(', ');
3113
+ }
3114
+ else if (attributes.include) {
3115
+ return attributes.include.map((a) => this.escapeId(a)).join(', ');
3116
+ }
3117
+ else if (attributes.exclude) {
3118
+ // MySQL doesn't support EXCEPT, we handle this differently
3119
+ return '*';
3120
+ }
3121
+ return '*';
3122
+ }
3123
+ /**
3124
+ * Build GROUP BY clause with support for function expressions and aliases
3125
+ * Handles: 'field', ['field1', 'field2'], [[fn('COUNT', 'id'), 'count']]
3126
+ */
3127
+ buildGroupByClause(group) {
3128
+ if (!group)
3129
+ return '';
3130
+ const groupArray = Array.isArray(group) ? group : [group];
3131
+ if (groupArray.length === 0)
3132
+ return '';
3133
+ const parts = groupArray.map((field) => {
3134
+ if (Array.isArray(field)) {
3135
+ // Array format: [expression, alias] or [fn, alias]
3136
+ const [expr, alias] = field;
3137
+ let sql;
3138
+ if (this.isFnExpression(expr)) {
3139
+ sql = this.compileFnExpression(expr);
3140
+ }
3141
+ else if (this.isColExpression(expr)) {
3142
+ sql = this.escapeId(expr.col);
3143
+ }
3144
+ else if (this.isLiteralExpression(expr)) {
3145
+ sql = expr.sql;
3146
+ }
3147
+ else if (typeof expr === 'string') {
3148
+ sql = this.escapeId(expr);
3149
+ }
3150
+ else {
3151
+ sql = String(expr);
3152
+ }
3153
+ // Alias is optional in GROUP BY
3154
+ if (alias) {
3155
+ return `${sql} AS ${this.escapeId(alias)}`;
3156
+ }
3157
+ return sql;
3158
+ }
3159
+ else if (this.isFnExpression(field)) {
3160
+ return this.compileFnExpression(field);
3161
+ }
3162
+ else if (this.isColExpression(field)) {
3163
+ return this.escapeId(field.col);
3164
+ }
3165
+ else if (this.isLiteralExpression(field)) {
3166
+ return field.sql;
3167
+ }
3168
+ else if (typeof field === 'string') {
3169
+ if (field.includes('.')) {
3170
+ return field
3171
+ .split('.')
3172
+ .map((part) => this.escapeId(part))
3173
+ .join('.');
3174
+ }
3175
+ return this.escapeId(field);
3176
+ }
3177
+ return String(field);
3178
+ });
3179
+ return `GROUP BY ${parts.join(', ')}`;
3180
+ }
3181
+ /**
3182
+ * Build a WHERE clause condition using standard operators (Op.gt, Op.eq, etc.)
3183
+ */
3184
+ buildCondition(condition, values, paramChar = '?') {
3185
+ if (!condition || (typeof condition === 'object' && Object.keys(condition).length === 0)) {
3186
+ return '';
3187
+ }
3188
+ // Handle logical operators
3189
+ if ('$and' in condition) {
3190
+ const andParts = condition.$and.map((c) => this.buildCondition(c, values, paramChar));
3191
+ return `(${andParts.join(' AND ')})`;
3192
+ }
3193
+ if ('$or' in condition) {
3194
+ const orParts = condition.$or.map((c) => this.buildCondition(c, values, paramChar));
3195
+ return `(${orParts.join(' OR ')})`;
3196
+ }
3197
+ if ('$not' in condition) {
3198
+ return `NOT (${this.buildCondition(condition.$not, values, paramChar)})`;
3199
+ }
3200
+ // Handle regular conditions
3201
+ const conditions = [];
3202
+ for (const [key, value] of Object.entries(condition)) {
3203
+ if (key.startsWith('$'))
3204
+ continue; // Skip logical operators
3205
+ let sql = '';
3206
+ // Check if value is an operator object
3207
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
3208
+ const opKeys = Object.keys(value);
3209
+ if (opKeys.length === 1) {
3210
+ const opKey = opKeys[0];
3211
+ const opValue = value[opKey];
3212
+ // Map Op symbol to string
3213
+ if (opKey === 'Symbol(eq)' || opKey === 'Symbol(operators.eq)') {
3214
+ sql = `${this.escapeId(key)} = ${paramChar}`;
3215
+ values.push(opValue);
3216
+ }
3217
+ else if (opKey === 'Symbol(ne)' || opKey === 'Symbol(operators.ne)') {
3218
+ sql = `${this.escapeId(key)} != ${paramChar}`;
3219
+ values.push(opValue);
3220
+ }
3221
+ else if (opKey === 'Symbol(gt)' || opKey === 'Symbol(operators.gt)') {
3222
+ sql = `${this.escapeId(key)} > ${paramChar}`;
3223
+ values.push(opValue);
3224
+ }
3225
+ else if (opKey === 'Symbol(gte)' || opKey === 'Symbol(operators.gte)') {
3226
+ sql = `${this.escapeId(key)} >= ${paramChar}`;
3227
+ values.push(opValue);
3228
+ }
3229
+ else if (opKey === 'Symbol(lt)' || opKey === 'Symbol(operators.lt)') {
3230
+ sql = `${this.escapeId(key)} < ${paramChar}`;
3231
+ values.push(opValue);
3232
+ }
3233
+ else if (opKey === 'Symbol(lte)' || opKey === 'Symbol(operators.lte)') {
3234
+ sql = `${this.escapeId(key)} <= ${paramChar}`;
3235
+ values.push(opValue);
3236
+ }
3237
+ else if (opKey === 'Symbol(in)' || opKey === 'Symbol(operators.in)') {
3238
+ sql = `${this.escapeId(key)} IN (${paramChar})`;
3239
+ values.push(opValue);
3240
+ }
3241
+ else if (opKey === 'Symbol(notIn)' || opKey === 'Symbol(operators.notIn)') {
3242
+ sql = `${this.escapeId(key)} NOT IN (${paramChar})`;
3243
+ values.push(opValue);
3244
+ }
3245
+ else if (opKey === 'Symbol(between)' || opKey === 'Symbol(operators.between)') {
3246
+ sql = `${this.escapeId(key)} BETWEEN ${paramChar} AND ${paramChar}`;
3247
+ values.push(...opValue);
3248
+ }
3249
+ else if (opKey === 'Symbol(notBetween)' || opKey === 'Symbol(operators.notBetween)') {
3250
+ sql = `${this.escapeId(key)} NOT BETWEEN ${paramChar} AND ${paramChar}`;
3251
+ values.push(...opValue);
3252
+ }
3253
+ else if (opKey === 'Symbol(isNull)' || opKey === 'Symbol(operators.isNull)') {
3254
+ sql = `${this.escapeId(key)} IS NULL`;
3255
+ }
3256
+ else if (opKey === 'Symbol(isNotNull)' || opKey === 'Symbol(operators.isNotNull)') {
3257
+ sql = `${this.escapeId(key)} IS NOT NULL`;
3258
+ }
3259
+ else if (opKey === 'Symbol(like)' || opKey === 'Symbol(operators.like)') {
3260
+ sql = `${this.escapeId(key)} LIKE ${paramChar}`;
3261
+ values.push(opValue);
3262
+ }
3263
+ else if (opKey === 'Symbol(notLike)' || opKey === 'Symbol(operators.notLike)') {
3264
+ sql = `${this.escapeId(key)} NOT LIKE ${paramChar}`;
3265
+ values.push(opValue);
3266
+ }
3267
+ else if (opKey === 'Symbol(iLike)' || opKey === 'Symbol(operators.iLike)') {
3268
+ sql = `${this.escapeId(key)} ILIKE ${paramChar}`;
3269
+ values.push(opValue);
3270
+ }
3271
+ else if (opKey === 'Symbol(notILike)' || opKey === 'Symbol(operators.notILike)') {
3272
+ sql = `${this.escapeId(key)} NOT ILIKE ${paramChar}`;
3273
+ values.push(opValue);
3274
+ }
3275
+ }
3276
+ if (!sql) {
3277
+ // Fallback: treat as equals
3278
+ sql = `${this.escapeId(key)} = ${paramChar}`;
3279
+ values.push(value);
3280
+ }
3281
+ }
3282
+ else if (Array.isArray(value)) {
3283
+ // Handle IN arrays
3284
+ sql = `${this.escapeId(key)} IN (${value.map(() => paramChar).join(', ')})`;
3285
+ values.push(...value);
3286
+ }
3287
+ else {
3288
+ // Simple equality
3289
+ sql = `${this.escapeId(key)} = ${paramChar}`;
3290
+ values.push(value);
3291
+ }
3292
+ if (sql) {
3293
+ conditions.push(sql);
3294
+ }
3295
+ }
3296
+ return conditions.join(' AND ');
3297
+ }
3298
+ /**
3299
+ * Build a HAVING clause with support for standard Op operators
3300
+ */
3301
+ buildHavingClause(having) {
3302
+ const values = [];
3303
+ if (!having || (typeof having === 'object' && Object.keys(having).length === 0)) {
3304
+ return { sql: '', values };
3305
+ }
3306
+ // Use the condition builder with Op operator support
3307
+ const sql = this.buildCondition(having, values);
3308
+ return { sql, values };
3309
+ }
3310
+ /**
3311
+ * Build a `WITH [RECURSIVE] name (cols) AS (query), ...` prefix for the
3312
+ * given CTE definitions. MySQL 8.0+ supports standard recursive CTE syntax
3313
+ * (a single `RECURSIVE` keyword applies to the whole `WITH` clause, not
3314
+ * per-CTE).
3315
+ */
3316
+ buildWithClause(ctes, recursive, queryValues) {
3317
+ if (!ctes || ctes.length === 0)
3318
+ return '';
3319
+ const parts = ctes.map((cte) => {
3320
+ const columns = cte.columns && cte.columns.length > 0
3321
+ ? ` (${cte.columns.map((c) => this.escapeId(c)).join(', ')})`
3322
+ : '';
3323
+ let cteSql;
3324
+ if (typeof cte.query === 'string') {
3325
+ cteSql = cte.query;
3326
+ }
3327
+ else {
3328
+ cteSql = cte.query.sql;
3329
+ if (cte.query.values && cte.query.values.length > 0) {
3330
+ queryValues.push(...cte.query.values);
3331
+ }
3332
+ }
3333
+ return `${this.escapeId(cte.name)}${columns} AS (${cteSql})`;
3334
+ });
3335
+ return `WITH ${recursive ? 'RECURSIVE ' : ''}${parts.join(', ')} `;
3336
+ }
3337
+ /**
3338
+ * Build a SELECT query
3339
+ */
3340
+ buildSelectQuery(options) {
3341
+ const queryValues = [];
3342
+ // Build WITH [RECURSIVE] clause (Common Table Expressions), MySQL 8.0+
3343
+ const withClause = this.buildWithClause(options.with, options.withRecursive, queryValues);
3344
+ // Build SELECT clause
3345
+ const selectSql = this.buildSelectClause(options.attributes);
3346
+ // Handle DISTINCT (for COUNT(DISTINCT col))
3347
+ let distinctClause = '';
3348
+ if (options.distinct) {
3349
+ if (options.col) {
3350
+ distinctClause = `DISTINCT ${this.escapeId(options.col)} `;
3351
+ }
3352
+ else {
3353
+ distinctClause = 'DISTINCT ';
3354
+ }
3355
+ }
3356
+ let sql = `${withClause}SELECT ${distinctClause}${selectSql} FROM ${this.escapeId(options.tableName)}`;
3357
+ // Build JOIN clause (for includes) or use subquery IN pattern
3358
+ if (options.include && options.include.length > 0) {
3359
+ if (options.subQuery) {
3360
+ // Use subquery IN pattern for includes
3361
+ sql = this.buildSubqueryIncludes(sql, options.include, options.tableName, queryValues);
3362
+ }
3363
+ else {
3364
+ // Use JOIN for includes (default behavior)
3365
+ for (const include of options.include) {
3366
+ const joinType = include.required ? 'INNER JOIN' : 'LEFT JOIN';
3367
+ const modelTableName = include.model.tableName;
3368
+ const alias = include.as || modelTableName;
3369
+ const tableNameStr = modelTableName || include.model.toString();
3370
+ sql += ` ${joinType} ${this.escapeId(tableNameStr)} ${this.escapeId(alias)}`;
3371
+ if (include.where) {
3372
+ const whereClause = this.buildWhereClause(include.where);
3373
+ sql += ` ON ${whereClause.sql}`;
3374
+ queryValues.push(...whereClause.values);
3375
+ }
3376
+ }
3377
+ }
3378
+ }
3379
+ // Build WHERE clause
3380
+ if (options.where) {
3381
+ const whereClause = this.buildWhereClause(options.where);
3382
+ if (whereClause.sql) {
3383
+ sql += ` WHERE ${whereClause.sql}`;
3384
+ queryValues.push(...whereClause.values);
3385
+ }
3386
+ }
3387
+ // Build GROUP BY clause
3388
+ if (options.group) {
3389
+ const groupByClause = this.buildGroupByClause(options.group);
3390
+ if (groupByClause) {
3391
+ sql += ` ${groupByClause}`;
3392
+ }
3393
+ }
3394
+ // Build HAVING clause
3395
+ if (options.having) {
3396
+ const havingClause = this.buildHavingClause(options.having);
3397
+ if (havingClause.sql) {
3398
+ sql += ` HAVING ${havingClause.sql}`;
3399
+ queryValues.push(...havingClause.values);
3400
+ }
3401
+ }
3402
+ // Build ORDER BY clause
3403
+ if (options.order) {
3404
+ const orderClause = this.buildOrderClause(options.order);
3405
+ if (orderClause) {
3406
+ sql += ` ${orderClause}`;
3407
+ }
3408
+ }
3409
+ // Build LIMIT/OFFSET clause
3410
+ sql += this.buildLimitOffset(options.limit, options.offset);
3411
+ // Handle row-level locking
3412
+ if (options.lock) {
3413
+ // lock: true is equivalent to 'UPDATE'
3414
+ let lockType;
3415
+ let nowait = false;
3416
+ let skipLocked = false;
3417
+ if (options.lock === true) {
3418
+ lockType = 'UPDATE';
3419
+ }
3420
+ else if (typeof options.lock === 'string') {
3421
+ lockType = options.lock;
3422
+ }
3423
+ else if (typeof options.lock === 'object') {
3424
+ // Handle { level: 'UPDATE', nowait: true } or { level: 'UPDATE', skipLocked: true }
3425
+ lockType = options.lock.level || (options.lock.of ? 'UPDATE' : undefined);
3426
+ nowait = options.lock.nowait === true;
3427
+ skipLocked = options.lock.skipLocked === true;
3428
+ }
3429
+ if (lockType === 'UPDATE') {
3430
+ sql += ' FOR UPDATE';
3431
+ // MySQL 8.0+ supports table-scoped locking via `FOR UPDATE OF <table>`
3432
+ if (typeof options.lock === 'object' && options.lock.of) {
3433
+ const model = options.lock.of;
3434
+ if (model.tableName) {
3435
+ sql += ` OF ${this.escapeId(model.tableName)}`;
3436
+ }
3437
+ }
3438
+ // MySQL 8.0+ supports NOWAIT and SKIP LOCKED
3439
+ if (nowait) {
3440
+ sql += ' NOWAIT';
3441
+ }
3442
+ else if (skipLocked) {
3443
+ sql += ' SKIP LOCKED';
3444
+ }
3445
+ }
3446
+ else if (lockType === 'SHARE') {
3447
+ sql += ' LOCK IN SHARE MODE';
3448
+ // MySQL 8.0+ supports NOWAIT for SHARE mode
3449
+ if (nowait) {
3450
+ sql += ' NOWAIT';
3451
+ }
3452
+ // MySQL doesn't support SKIP LOCKED for LOCK IN SHARE MODE
3453
+ }
3454
+ }
3455
+ // Build UNION clause if provided
3456
+ if (options.union && options.union.length > 0) {
3457
+ const unionType = options.unionType || 'UNION';
3458
+ for (const unionQuery of options.union) {
3459
+ const unionModel = unionQuery.model;
3460
+ const unionTableName = unionModel.tableName || unionModel.name || '';
3461
+ // Build the union query
3462
+ let unionSql = `SELECT * FROM ${this.escapeId(unionTableName)}`;
3463
+ // Add WHERE clause for union query
3464
+ if (unionQuery.where) {
3465
+ const whereClause = this.buildWhereClause(unionQuery.where);
3466
+ if (whereClause.sql) {
3467
+ unionSql += ` WHERE ${whereClause.sql}`;
3468
+ queryValues.push(...whereClause.values);
3469
+ }
3470
+ }
3471
+ // Add ORDER BY for union query
3472
+ if (unionQuery.order) {
3473
+ const orderClause = this.buildOrderClause(unionQuery.order);
3474
+ if (orderClause) {
3475
+ unionSql += ` ${orderClause}`;
3476
+ }
3477
+ }
3478
+ // Add LIMIT for union query
3479
+ if (unionQuery.limit) {
3480
+ unionSql += ` LIMIT ${unionQuery.limit}`;
3481
+ }
3482
+ // Add OFFSET for union query
3483
+ if (unionQuery.offset) {
3484
+ unionSql += ` OFFSET ${unionQuery.offset}`;
3485
+ }
3486
+ sql += ` ${unionType} ${unionSql}`;
3487
+ }
3488
+ }
3489
+ return { sql, values: queryValues };
3490
+ }
3491
+ // ---------------------------------------------------------------------------
3492
+ // User / privilege management
3493
+ // ---------------------------------------------------------------------------
3494
+ /**
3495
+ * Escape a password string – replaces single quotes so it is safe to embed
3496
+ * between single-quote delimiters in SQL.
3497
+ */
3498
+ escapePassword(pw) {
3499
+ return pw.replace(/'/g, "\\'");
3500
+ }
3501
+ /**
3502
+ * Format a MySQL user identity as 'username'@'host'
3503
+ */
3504
+ formatUserIdentity(username, host = '%') {
3505
+ return `'${username}'@'${host}'`;
3506
+ }
3507
+ /**
3508
+ * Format a MySQL privilege scope to the ON … part of a GRANT / REVOKE.
3509
+ * Returns { scopeSql, columnPrefix } where columnPrefix is the
3510
+ * "SELECT (col1, col2)" fragment that must precede ON for column-level grants.
3511
+ */
3512
+ formatPrivilegeScope(scope) {
3513
+ switch (scope.level) {
3514
+ case 'global':
3515
+ return { scopeSql: '*.*', columnPrefix: null };
3516
+ case 'database':
3517
+ return { scopeSql: `\`${scope.database}\`.*`, columnPrefix: null };
3518
+ case 'table': {
3519
+ const db = scope.database ? `\`${scope.database}\`.` : '';
3520
+ return { scopeSql: `${db}\`${scope.table}\``, columnPrefix: null };
3521
+ }
3522
+ case 'column': {
3523
+ const db = scope.database ? `\`${scope.database}\`.` : '';
3524
+ const cols = scope.columns.map((c) => `\`${c}\``).join(', ');
3525
+ return {
3526
+ scopeSql: `${db}\`${scope.table}\``,
3527
+ columnPrefix: cols,
3528
+ };
3529
+ }
3530
+ case 'routine': {
3531
+ const db = scope.database ? `\`${scope.database}\`.` : '';
3532
+ const type = scope.routineType || 'PROCEDURE';
3533
+ return { scopeSql: `${type} ${db}\`${scope.routine}\``, columnPrefix: null };
3534
+ }
3535
+ }
3536
+ }
3537
+ buildCreateUserQuery(username, options = {}) {
3538
+ const host = options.host ?? '%';
3539
+ const identity = this.formatUserIdentity(username, host);
3540
+ let sql = 'CREATE USER';
3541
+ if (options.ifNotExists)
3542
+ sql += ' IF NOT EXISTS';
3543
+ sql += ` ${identity}`;
3544
+ // Authentication
3545
+ if (options.authPlugin && options.password) {
3546
+ sql += ` IDENTIFIED WITH ${options.authPlugin} BY '${this.escapePassword(options.password)}'`;
3547
+ }
3548
+ else if (options.authPlugin && options.passwordHash) {
3549
+ sql += ` IDENTIFIED WITH ${options.authPlugin} AS '${this.escapePassword(options.passwordHash)}'`;
3550
+ }
3551
+ else if (options.password) {
3552
+ sql += ` IDENTIFIED BY '${this.escapePassword(options.password)}'`;
3553
+ }
3554
+ // SSL
3555
+ if (options.requireSSL === true) {
3556
+ sql += ' REQUIRE SSL';
3557
+ }
3558
+ // Resource limits
3559
+ const limits = [];
3560
+ if (options.maxQueriesPerHour != null)
3561
+ limits.push(`MAX_QUERIES_PER_HOUR ${options.maxQueriesPerHour}`);
3562
+ if (options.maxUpdatesPerHour != null)
3563
+ limits.push(`MAX_UPDATES_PER_HOUR ${options.maxUpdatesPerHour}`);
3564
+ if (options.maxConnectionsPerHour != null)
3565
+ limits.push(`MAX_CONNECTIONS_PER_HOUR ${options.maxConnectionsPerHour}`);
3566
+ if (options.maxConnections != null)
3567
+ limits.push(`MAX_USER_CONNECTIONS ${options.maxConnections}`);
3568
+ if (limits.length > 0)
3569
+ sql += ` WITH ${limits.join(' ')}`;
3570
+ // Password expiry
3571
+ if (options.expirePassword != null) {
3572
+ if (options.expirePassword === true) {
3573
+ sql += ' PASSWORD EXPIRE';
3574
+ }
3575
+ else if (options.expirePassword === false) {
3576
+ sql += ' PASSWORD EXPIRE NEVER';
3577
+ }
3578
+ else if (options.expirePassword instanceof Date) {
3579
+ // MySQL doesn't support a specific expiry date; emit EXPIRE DEFAULT
3580
+ sql += ' PASSWORD EXPIRE DEFAULT';
3581
+ }
3582
+ }
3583
+ // Account lock
3584
+ if (options.accountLocked === true)
3585
+ sql += ' ACCOUNT LOCK';
3586
+ if (options.accountLocked === false)
3587
+ sql += ' ACCOUNT UNLOCK';
3588
+ // Comment (MySQL 8.0.21+)
3589
+ if (options.comment) {
3590
+ sql += ` COMMENT '${options.comment.replace(/'/g, "\\'")}'`;
3591
+ }
3592
+ return sql;
3593
+ }
3594
+ buildAlterUserQuery(username, options) {
3595
+ const host = options.host ?? '%';
3596
+ const identity = this.formatUserIdentity(username, host);
3597
+ let sql = `ALTER USER ${identity}`;
3598
+ // Authentication
3599
+ if (options.authPlugin && options.password) {
3600
+ sql += ` IDENTIFIED WITH ${options.authPlugin} BY '${this.escapePassword(options.password)}'`;
3601
+ }
3602
+ else if (options.password) {
3603
+ sql += ` IDENTIFIED BY '${this.escapePassword(options.password)}'`;
3604
+ }
3605
+ // SSL
3606
+ if (options.requireSSL === true) {
3607
+ sql += ' REQUIRE SSL';
3608
+ }
3609
+ else if (options.requireSSL === 'NONE' || options.requireSSL === false) {
3610
+ sql += ' REQUIRE NONE';
3611
+ }
3612
+ // Resource limits
3613
+ const limits = [];
3614
+ if (options.maxQueriesPerHour != null)
3615
+ limits.push(`MAX_QUERIES_PER_HOUR ${options.maxQueriesPerHour}`);
3616
+ if (options.maxUpdatesPerHour != null)
3617
+ limits.push(`MAX_UPDATES_PER_HOUR ${options.maxUpdatesPerHour}`);
3618
+ if (options.maxConnectionsPerHour != null)
3619
+ limits.push(`MAX_CONNECTIONS_PER_HOUR ${options.maxConnectionsPerHour}`);
3620
+ if (options.maxConnections != null)
3621
+ limits.push(`MAX_USER_CONNECTIONS ${options.maxConnections}`);
3622
+ if (limits.length > 0)
3623
+ sql += ` WITH ${limits.join(' ')}`;
3624
+ // Password expiry policy
3625
+ if (options.passwordExpirePolicy != null) {
3626
+ const policy = options.passwordExpirePolicy;
3627
+ if (policy === 'DEFAULT')
3628
+ sql += ' PASSWORD EXPIRE DEFAULT';
3629
+ else if (policy === 'NEVER')
3630
+ sql += ' PASSWORD EXPIRE NEVER';
3631
+ else if (policy === 'INTERVAL')
3632
+ sql += ' PASSWORD EXPIRE INTERVAL';
3633
+ else if (typeof policy === 'number')
3634
+ sql += ` PASSWORD EXPIRE INTERVAL ${policy} DAY`;
3635
+ }
3636
+ // Account lock / unlock
3637
+ if (options.unlockAccount)
3638
+ sql += ' ACCOUNT UNLOCK';
3639
+ else if (options.accountLocked)
3640
+ sql += ' ACCOUNT LOCK';
3641
+ return sql;
3642
+ }
3643
+ buildDropUserQuery(username, options = {}) {
3644
+ const host = options.host ?? '%';
3645
+ const identity = this.formatUserIdentity(username, host);
3646
+ let sql = 'DROP USER';
3647
+ if (options.ifExists)
3648
+ sql += ' IF EXISTS';
3649
+ sql += ` ${identity}`;
3650
+ return sql;
3651
+ }
3652
+ getUsersQuery() {
3653
+ return 'SELECT User, Host, plugin, password_expired, account_locked FROM mysql.user';
3654
+ }
3655
+ /**
3656
+ * Create a database user
3657
+ */
3658
+ async createUser(username, options) {
3659
+ const sql = this.buildCreateUserQuery(username, options);
3660
+ await this.query(sql);
3661
+ }
3662
+ buildGrantQuery(options) {
3663
+ const { scopeSql, columnPrefix } = this.formatPrivilegeScope(options.on);
3664
+ const privList = options.privileges.join(', ');
3665
+ // For column-level grants the privilege list needs to wrap the columns
3666
+ let grantPriv;
3667
+ if (columnPrefix !== null) {
3668
+ // e.g. SELECT (`col1`, `col2`) – columnPrefix already has backtick-quoted cols
3669
+ grantPriv = `${privList} (${columnPrefix})`;
3670
+ }
3671
+ else {
3672
+ grantPriv = privList;
3673
+ }
3674
+ const recipients = (Array.isArray(options.to) ? options.to : [options.to])
3675
+ .map((u) => {
3676
+ // If the string already contains '@' treat it as a full identity
3677
+ if (u.includes('@'))
3678
+ return u;
3679
+ return this.formatUserIdentity(u, options.host ?? '%');
3680
+ })
3681
+ .join(', ');
3682
+ let sql = `GRANT ${grantPriv} ON ${scopeSql} TO ${recipients}`;
3683
+ if (options.withGrantOption)
3684
+ sql += ' WITH GRANT OPTION';
3685
+ return sql;
3686
+ }
3687
+ buildRevokeQuery(options) {
3688
+ const { scopeSql, columnPrefix } = this.formatPrivilegeScope(options.on);
3689
+ const privList = options.privileges.join(', ');
3690
+ let revokePriv;
3691
+ if (columnPrefix !== null) {
3692
+ revokePriv = `${privList} (${columnPrefix})`;
3693
+ }
3694
+ else {
3695
+ revokePriv = privList;
3696
+ }
3697
+ const targets = (Array.isArray(options.from) ? options.from : [options.from])
3698
+ .map((u) => {
3699
+ if (u.includes('@'))
3700
+ return u;
3701
+ return this.formatUserIdentity(u, options.host ?? '%');
3702
+ })
3703
+ .join(', ');
3704
+ let sql;
3705
+ if (options.grantOptionFor) {
3706
+ sql = `REVOKE GRANT OPTION FOR ${revokePriv} ON ${scopeSql} FROM ${targets}`;
3707
+ }
3708
+ else {
3709
+ sql = `REVOKE ${revokePriv} ON ${scopeSql} FROM ${targets}`;
3710
+ }
3711
+ return sql;
3712
+ }
3713
+ buildShowGrantsQuery(username, host = '%') {
3714
+ return `SHOW GRANTS FOR ${this.formatUserIdentity(username, host)}`;
3715
+ }
3716
+ buildFlushPrivilegesQuery() {
3717
+ return 'FLUSH PRIVILEGES';
3718
+ }
3719
+ // ---- Roles (MySQL 8+) ----
3720
+ buildCreateRoleQuery(roleName, options = {}) {
3721
+ let sql = 'CREATE ROLE';
3722
+ if (options.ifNotExists)
3723
+ sql += ' IF NOT EXISTS';
3724
+ sql += ` '${roleName}'`;
3725
+ return sql;
3726
+ }
3727
+ buildDropRoleQuery(roleName, options = {}) {
3728
+ let sql = 'DROP ROLE';
3729
+ if (options.ifExists)
3730
+ sql += ' IF EXISTS';
3731
+ sql += ` '${roleName}'`;
3732
+ return sql;
3733
+ }
3734
+ buildGrantRoleQuery(role, to, options = {}) {
3735
+ const recipients = (Array.isArray(to) ? to : [to])
3736
+ .map((u) => (u.includes('@') ? u : this.formatUserIdentity(u, '%')))
3737
+ .join(', ');
3738
+ let sql = `GRANT '${role}' TO ${recipients}`;
3739
+ if (options.withAdminOption)
3740
+ sql += ' WITH ADMIN OPTION';
3741
+ return sql;
3742
+ }
3743
+ buildRevokeRoleQuery(role, from, _options = {}) {
3744
+ const targets = (Array.isArray(from) ? from : [from])
3745
+ .map((u) => (u.includes('@') ? u : this.formatUserIdentity(u, '%')))
3746
+ .join(', ');
3747
+ return `REVOKE '${role}' FROM ${targets}`;
3748
+ }
3749
+ getRolesQuery() {
3750
+ return 'SELECT role_name AS role FROM information_schema.APPLICABLE_ROLES';
3751
+ }
3752
+ /**
3753
+ * Generate SQL for creating a MySQL database
3754
+ */
3755
+ createDatabaseSQL(options) {
3756
+ const parts = [];
3757
+ parts.push(`CREATE DATABASE ${this.quoteIdentifier(options.name)}`);
3758
+ if (options.encoding) {
3759
+ parts.push(`CHARACTER SET = '${options.encoding}'`);
3760
+ }
3761
+ if (options.collate) {
3762
+ parts.push(`COLLATE = '${options.collate}'`);
3763
+ }
3764
+ return parts.join(' ');
3765
+ }
3766
+ /**
3767
+ * Generate SQL for dropping a MySQL database
3768
+ */
3769
+ dropDatabaseSQL(name) {
3770
+ return `DROP DATABASE IF EXISTS ${this.quoteIdentifier(name)}`;
3771
+ }
3772
+ /**
3773
+ * Generate SQL for creating a savepoint
3774
+ */
3775
+ createSavepointSQL(name) {
3776
+ const savepointName = name || `sp_${Date.now()}`;
3777
+ return `SAVEPOINT ${savepointName}`;
3778
+ }
3779
+ /**
3780
+ * Generate SQL for releasing a savepoint
3781
+ */
3782
+ releaseSavepointSQL(name) {
3783
+ return `RELEASE SAVEPOINT ${name}`;
3784
+ }
3785
+ /**
3786
+ * Generate SQL for rolling back to a savepoint
3787
+ */
3788
+ rollbackToSavepointSQL(name) {
3789
+ return `ROLLBACK TO SAVEPOINT ${name}`;
3790
+ }
3791
+ }
3792
+ exports.MySQLDialect = MySQLDialect;
3793
+ /**
3794
+ * MySQL-specific transaction class
3795
+ */
3796
+ class MySQLTransaction {
3797
+ constructor(connection, options) {
3798
+ this.finished = false;
3799
+ this.parent = null;
3800
+ this.savepoints = [];
3801
+ this.connection = null;
3802
+ this.savepointCount = 0;
3803
+ this.id = `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
3804
+ this.options = options || {};
3805
+ this.connection = connection;
3806
+ }
3807
+ async commit() {
3808
+ if (this.connection) {
3809
+ await this.connection.commit();
3810
+ }
3811
+ this.finished = true;
3812
+ }
3813
+ async rollback() {
3814
+ if (this.connection) {
3815
+ await this.connection.rollback();
3816
+ }
3817
+ this.finished = true;
3818
+ }
3819
+ /**
3820
+ * Create a savepoint
3821
+ */
3822
+ async createSavepoint(name) {
3823
+ const savepointName = name || `sp_${this.savepointCount++}`;
3824
+ if (this.connection) {
3825
+ await this.connection.query(`SAVEPOINT ${savepointName}`);
3826
+ this.savepoints.push(savepointName);
3827
+ }
3828
+ return savepointName;
3829
+ }
3830
+ /**
3831
+ * Rollback to a savepoint
3832
+ */
3833
+ async rollbackToSavepoint(name) {
3834
+ if (this.connection) {
3835
+ await this.connection.query(`ROLLBACK TO SAVEPOINT ${name}`);
3836
+ }
3837
+ }
3838
+ /**
3839
+ * Release a savepoint
3840
+ */
3841
+ async releaseSavepoint(name) {
3842
+ if (this.connection) {
3843
+ await this.connection.query(`RELEASE SAVEPOINT ${name}`);
3844
+ this.savepoints = this.savepoints.filter((sp) => sp !== name);
3845
+ }
3846
+ }
3847
+ }
3848
+ exports.MySQLTransaction = MySQLTransaction;
3849
+ // ==================== Procedure Body Builder ====================
3850
+ /**
3851
+ * Helper class for building stored procedure bodies with full PL/SQL support
3852
+ * Supports: IN/OUT/INOUT parameters, local variables, IF/ELSE, WHILE loops, cursors
3853
+ */
3854
+ class ProcedureBodyBuilder {
3855
+ constructor(dialect) {
3856
+ this.statements = [];
3857
+ this.dialect = dialect;
3858
+ }
3859
+ /**
3860
+ * Declare a local variable
3861
+ */
3862
+ declare(name, type, defaultValue) {
3863
+ const defaultPart = defaultValue !== undefined
3864
+ ? ` DEFAULT ${this.dialect.escape(defaultValue)}`
3865
+ : '';
3866
+ this.statements.push(`DECLARE ${this.dialect.quoteIdentifier(name)} ${type}${defaultPart};`);
3867
+ return this;
3868
+ }
3869
+ /**
3870
+ * Set a variable value
3871
+ */
3872
+ set(name, value) {
3873
+ this.statements.push(`SET ${this.dialect.quoteIdentifier(name)} = ${this.dialect.escape(value)};`);
3874
+ return this;
3875
+ }
3876
+ /**
3877
+ * Set a variable from a SELECT query (single value)
3878
+ */
3879
+ setFromSelect(name, query) {
3880
+ this.statements.push(`SET ${this.dialect.quoteIdentifier(name)} = (${query});`);
3881
+ return this;
3882
+ }
3883
+ /**
3884
+ * IF condition
3885
+ */
3886
+ if(condition, then) {
3887
+ this.statements.push(`IF ${condition} THEN`);
3888
+ const thenBuilder = new ProcedureBodyBuilder(this.dialect);
3889
+ then(thenBuilder);
3890
+ this.statements.push(...thenBuilder.statements.map((s) => ' ' + s));
3891
+ this.statements.push('END IF;');
3892
+ return this;
3893
+ }
3894
+ /**
3895
+ * IF/ELSE condition
3896
+ */
3897
+ ifElse(condition, then, else_) {
3898
+ this.statements.push(`IF ${condition} THEN`);
3899
+ const thenBuilder = new ProcedureBodyBuilder(this.dialect);
3900
+ then(thenBuilder);
3901
+ this.statements.push(...thenBuilder.statements.map((s) => ' ' + s));
3902
+ this.statements.push('ELSE');
3903
+ const elseBuilder = new ProcedureBodyBuilder(this.dialect);
3904
+ else_(elseBuilder);
3905
+ this.statements.push(...elseBuilder.statements.map((s) => ' ' + s));
3906
+ this.statements.push('END IF;');
3907
+ return this;
3908
+ }
3909
+ /**
3910
+ * WHILE loop
3911
+ */
3912
+ whileLoop(condition, do_) {
3913
+ this.statements.push(`WHILE ${condition} DO`);
3914
+ const loopBuilder = new ProcedureBodyBuilder(this.dialect);
3915
+ do_(loopBuilder);
3916
+ this.statements.push(...loopBuilder.statements.map((s) => ' ' + s));
3917
+ this.statements.push('END WHILE;');
3918
+ return this;
3919
+ }
3920
+ /**
3921
+ * REPEAT loop (do-while)
3922
+ */
3923
+ repeatLoop(do_, condition) {
3924
+ this.statements.push('REPEAT');
3925
+ const loopBuilder = new ProcedureBodyBuilder(this.dialect);
3926
+ do_(loopBuilder);
3927
+ this.statements.push(...loopBuilder.statements.map((s) => ' ' + s));
3928
+ this.statements.push(`UNTIL ${condition} END REPEAT;`);
3929
+ return this;
3930
+ }
3931
+ /**
3932
+ * FOR loop over a query
3933
+ */
3934
+ forLoop(variableName, query, do_) {
3935
+ const itemVar = this.dialect.quoteIdentifier(variableName);
3936
+ this.statements.push(`FOR ${itemVar} IN (${query}) DO`);
3937
+ const loopBuilder = new ProcedureBodyBuilder(this.dialect);
3938
+ do_(loopBuilder, variableName);
3939
+ this.statements.push(...loopBuilder.statements.map((s) => ' ' + s));
3940
+ this.statements.push('END FOR;');
3941
+ return this;
3942
+ }
3943
+ /**
3944
+ * Declare and open a cursor
3945
+ */
3946
+ declareCursor(name, query) {
3947
+ this.statements.push(`DECLARE ${this.dialect.quoteIdentifier(name)} CURSOR FOR ${query};`);
3948
+ return this;
3949
+ }
3950
+ /**
3951
+ * Open a cursor
3952
+ */
3953
+ openCursor(name) {
3954
+ this.statements.push(`OPEN ${this.dialect.quoteIdentifier(name)};`);
3955
+ return this;
3956
+ }
3957
+ /**
3958
+ * Fetch from cursor into variables
3959
+ */
3960
+ fetchCursor(name, into) {
3961
+ const intoVars = into.map((v) => this.dialect.quoteIdentifier(v)).join(', ');
3962
+ this.statements.push(`FETCH ${this.dialect.quoteIdentifier(name)} INTO ${intoVars};`);
3963
+ return this;
3964
+ }
3965
+ /**
3966
+ * Close a cursor
3967
+ */
3968
+ closeCursor(name) {
3969
+ this.statements.push(`CLOSE ${this.dialect.quoteIdentifier(name)};`);
3970
+ return this;
3971
+ }
3972
+ /**
3973
+ * Execute a raw SQL statement
3974
+ */
3975
+ sql(statement) {
3976
+ this.statements.push(statement);
3977
+ return this;
3978
+ }
3979
+ /**
3980
+ * Call another stored procedure
3981
+ */
3982
+ call(procedureName, params) {
3983
+ let sql = `CALL ${procedureName}`;
3984
+ if (params && Object.keys(params).length > 0) {
3985
+ const paramStr = Object.entries(params)
3986
+ .map(([key, value]) => this.dialect.escape(value))
3987
+ .join(', ');
3988
+ sql += `(${paramStr})`;
3989
+ }
3990
+ sql += ';';
3991
+ this.statements.push(sql);
3992
+ return this;
3993
+ }
3994
+ /**
3995
+ * Insert statement
3996
+ */
3997
+ insert(tableName, data) {
3998
+ const columns = Object.keys(data).map((k) => this.dialect.quoteIdentifier(k)).join(', ');
3999
+ const values = Object.values(data).map((v) => this.dialect.escape(v)).join(', ');
4000
+ this.statements.push(`INSERT INTO ${tableName} (${columns}) VALUES (${values});`);
4001
+ return this;
4002
+ }
4003
+ /**
4004
+ * Update statement
4005
+ */
4006
+ update(tableName, data, where) {
4007
+ const setClause = Object.entries(data)
4008
+ .map(([k, v]) => `${this.dialect.quoteIdentifier(k)} = ${this.dialect.escape(v)}`)
4009
+ .join(', ');
4010
+ let sql = `UPDATE ${tableName} SET ${setClause}`;
4011
+ if (where) {
4012
+ sql += ` WHERE ${where}`;
4013
+ }
4014
+ sql += ';';
4015
+ this.statements.push(sql);
4016
+ return this;
4017
+ }
4018
+ /**
4019
+ * Delete statement
4020
+ */
4021
+ delete(tableName, where) {
4022
+ let sql = `DELETE FROM ${tableName}`;
4023
+ if (where) {
4024
+ sql += ` WHERE ${where}`;
4025
+ }
4026
+ sql += ';';
4027
+ this.statements.push(sql);
4028
+ return this;
4029
+ }
4030
+ /**
4031
+ * SELECT INTO statement
4032
+ */
4033
+ selectInto(variables, query) {
4034
+ const vars = variables.map((v) => this.dialect.quoteIdentifier(v)).join(', ');
4035
+ this.statements.push(`SELECT ${vars} INTO ${vars} FROM ${query};`);
4036
+ return this;
4037
+ }
4038
+ /**
4039
+ * Add a comment
4040
+ */
4041
+ comment(text) {
4042
+ this.statements.push(`-- ${text}`);
4043
+ return this;
4044
+ }
4045
+ /**
4046
+ * Build the final procedure body
4047
+ */
4048
+ build() {
4049
+ return this.statements.join('\n');
4050
+ }
4051
+ }
4052
+ /**
4053
+ * Create a new MySQL dialect instance
4054
+ */
4055
+ function createMySQLDialect(options) {
4056
+ return new MySQLDialect(options || {});
4057
+ }
4058
+ exports.default = MySQLDialect;