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