ts-prorm-orm 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1111 -0
- package/LICENSE +21 -0
- package/README.md +573 -0
- package/dist/audit/history-query.js +180 -0
- package/dist/audit/index.js +23 -0
- package/dist/audit/logger.js +236 -0
- package/dist/cache/cache-manager.js +84 -0
- package/dist/cache/index.js +16 -0
- package/dist/cache/redis-cluster-cache.js +557 -0
- package/dist/cli.js +2200 -0
- package/dist/compliance/audit-trail.js +68 -0
- package/dist/compliance/backup-verification.js +685 -0
- package/dist/compliance/breach-detector.js +505 -0
- package/dist/compliance/consent-record.js +227 -0
- package/dist/compliance/consent-versioning.js +331 -0
- package/dist/compliance/cross-border-log.js +530 -0
- package/dist/compliance/data-classifier.js +258 -0
- package/dist/compliance/data-lineage.js +565 -0
- package/dist/compliance/data-masker.js +303 -0
- package/dist/compliance/data-portability.js +265 -0
- package/dist/compliance/data-retention.js +195 -0
- package/dist/compliance/dsar-workflow.js +342 -0
- package/dist/compliance/field-encryption.js +223 -0
- package/dist/compliance/immutable-record.js +148 -0
- package/dist/compliance/index.js +260 -0
- package/dist/compliance/privacy-impact-assessment.js +364 -0
- package/dist/compliance/pseudonymization.js +269 -0
- package/dist/compliance/query-firewall.js +1182 -0
- package/dist/compliance/rate-limiter.js +580 -0
- package/dist/compliance/right-to-erasure.js +117 -0
- package/dist/compliance/row-level-security.js +246 -0
- package/dist/compliance/security-decorator.js +574 -0
- package/dist/compliance/security-monitor.js +525 -0
- package/dist/compliance/sensitive-data-discovery.js +476 -0
- package/dist/compliance/session-isolation.js +472 -0
- package/dist/compliance/tls-enforcer.js +108 -0
- package/dist/compliance/worm-storage.js +712 -0
- package/dist/connection-manager.js +198 -0
- package/dist/connection-pool.js +519 -0
- package/dist/decorators/audit.js +136 -0
- package/dist/decorators/belongs-to-many.js +115 -0
- package/dist/decorators/belongs-to.js +115 -0
- package/dist/decorators/check.js +435 -0
- package/dist/decorators/collate.js +329 -0
- package/dist/decorators/comment.js +205 -0
- package/dist/decorators/database-settings.js +236 -0
- package/dist/decorators/default.js +244 -0
- package/dist/decorators/encryption.js +235 -0
- package/dist/decorators/engine.js +97 -0
- package/dist/decorators/fk-constraints.js +594 -0
- package/dist/decorators/foreign-table.js +136 -0
- package/dist/decorators/generated.js +274 -0
- package/dist/decorators/has-many.js +127 -0
- package/dist/decorators/has-one.js +116 -0
- package/dist/decorators/hstore.js +129 -0
- package/dist/decorators/index.js +355 -0
- package/dist/decorators/json-column.js +83 -0
- package/dist/decorators/jsonb.js +101 -0
- package/dist/decorators/orm-decorators.js +425 -0
- package/dist/decorators/permissions.js +294 -0
- package/dist/decorators/procedure.js +210 -0
- package/dist/decorators/query-options.js +556 -0
- package/dist/decorators/range.js +167 -0
- package/dist/decorators/set-column.js +83 -0
- package/dist/decorators/spatial.js +114 -0
- package/dist/decorators/storage.js +580 -0
- package/dist/decorators/timezone.js +512 -0
- package/dist/decorators/trigger.js +90 -0
- package/dist/decorators/uuid.js +135 -0
- package/dist/decorators/view.js +258 -0
- package/dist/diagrams/chen-diagram.js +354 -0
- package/dist/diagrams/class-diagram.js +384 -0
- package/dist/diagrams/dependency-diagram.js +432 -0
- package/dist/diagrams/er-diagram.js +605 -0
- package/dist/diagrams/flow-diagram.js +394 -0
- package/dist/diagrams/gantt-diagram.js +411 -0
- package/dist/diagrams/index-diagram.js +353 -0
- package/dist/diagrams/index.js +184 -0
- package/dist/diagrams/migration-diagram.js +316 -0
- package/dist/diagrams/model-diagram.js +616 -0
- package/dist/diagrams/package-diagram.js +376 -0
- package/dist/diagrams/palette.js +85 -0
- package/dist/diagrams/relational-diagram.js +455 -0
- package/dist/diagrams/schemadoc-diagram.js +309 -0
- package/dist/diagrams/sequence-diagram.js +307 -0
- package/dist/diagrams/state-diagram.js +344 -0
- package/dist/diagrams/svg-dom.js +111 -0
- package/dist/diagrams/tree-diagram.js +250 -0
- package/dist/dialects/clickhouse/index.js +1742 -0
- package/dist/dialects/cockroachdb/index.js +4677 -0
- package/dist/dialects/cratedb/index.js +441 -0
- package/dist/dialects/databricks/index.js +517 -0
- package/dist/dialects/db2/index.js +2225 -0
- package/dist/dialects/dialect.js +720 -0
- package/dist/dialects/duckdb/index.js +2014 -0
- package/dist/dialects/exasol/index.js +357 -0
- package/dist/dialects/firebird/index.js +495 -0
- package/dist/dialects/greenplum/index.js +457 -0
- package/dist/dialects/hana/index.js +1741 -0
- package/dist/dialects/mariadb/index.js +3590 -0
- package/dist/dialects/mssql/index.js +2296 -0
- package/dist/dialects/mysql/index.js +4058 -0
- package/dist/dialects/oracle/index.js +2999 -0
- package/dist/dialects/postgres/index.js +5319 -0
- package/dist/dialects/query-stream-helper.js +152 -0
- package/dist/dialects/questdb/index.js +433 -0
- package/dist/dialects/redshift/index.js +2277 -0
- package/dist/dialects/singlestore/index.js +354 -0
- package/dist/dialects/snowflake/index.js +2082 -0
- package/dist/dialects/spanner/index.js +1768 -0
- package/dist/dialects/sqlite/index.js +3382 -0
- package/dist/dialects/tidb/index.js +377 -0
- package/dist/dialects/timescaledb/index.js +164 -0
- package/dist/dialects/trino/index.js +420 -0
- package/dist/dialects/turso/index.js +227 -0
- package/dist/dialects/vertica/index.js +317 -0
- package/dist/dialects/yugabytedb/index.js +424 -0
- package/dist/errors/index.js +472 -0
- package/dist/errors/utils.js +340 -0
- package/dist/errors.js +21 -0
- package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
- package/dist/extensions/catalog/mssql-features.js +147 -0
- package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
- package/dist/extensions/catalog/oracle-db2-features.js +196 -0
- package/dist/extensions/catalog/postgres-extensions.js +516 -0
- package/dist/extensions/index.js +71 -0
- package/dist/extensions/types.js +13 -0
- package/dist/foreign-data.js +173 -0
- package/dist/hooks/hooks-manager.js +350 -0
- package/dist/hooks/index.js +37 -0
- package/dist/index.js +463 -0
- package/dist/logging.js +335 -0
- package/dist/migrations/index.js +40 -0
- package/dist/migrations/migration.js +196 -0
- package/dist/migrations/migrator.js +411 -0
- package/dist/migrations/prormmigration.js +275 -0
- package/dist/migrations/query-interface.js +435 -0
- package/dist/migrations/seeder.js +353 -0
- package/dist/models/associations.js +852 -0
- package/dist/models/constraints.js +288 -0
- package/dist/models/data-types.js +2518 -0
- package/dist/models/decorators.js +445 -0
- package/dist/models/index.js +33 -0
- package/dist/models/indexes.js +531 -0
- package/dist/models/methods.js +382 -0
- package/dist/models/model-manager.js +103 -0
- package/dist/models/model.js +5349 -0
- package/dist/models/operators.js +67 -0
- package/dist/models/scopes.js +189 -0
- package/dist/models/typescript-types.js +26 -0
- package/dist/nosql/aerospike/index.js +205 -0
- package/dist/nosql/allegrograph/index.js +169 -0
- package/dist/nosql/arangodb/index.js +364 -0
- package/dist/nosql/azure-blob/index.js +206 -0
- package/dist/nosql/beanstalkd/index.js +231 -0
- package/dist/nosql/beequeue/index.js +210 -0
- package/dist/nosql/bigchaindb/index.js +195 -0
- package/dist/nosql/bigtable/index.js +224 -0
- package/dist/nosql/blazegraph/index.js +173 -0
- package/dist/nosql/bullmq/index.js +191 -0
- package/dist/nosql/cassandra/index.js +174 -0
- package/dist/nosql/chroma/index.js +190 -0
- package/dist/nosql/cloudflare-kv/index.js +220 -0
- package/dist/nosql/coherence/index.js +200 -0
- package/dist/nosql/cosmosdb/index.js +157 -0
- package/dist/nosql/couchbase/index.js +213 -0
- package/dist/nosql/dax/index.js +212 -0
- package/dist/nosql/deno-kv/index.js +206 -0
- package/dist/nosql/dgraph/index.js +171 -0
- package/dist/nosql/doris/index.js +169 -0
- package/dist/nosql/druid/index.js +162 -0
- package/dist/nosql/dynamodb/index.js +937 -0
- package/dist/nosql/elasticsearch/index.js +377 -0
- package/dist/nosql/etcd/index.js +502 -0
- package/dist/nosql/eventhubs/index.js +213 -0
- package/dist/nosql/eventstore/index.js +254 -0
- package/dist/nosql/faunadb/index.js +188 -0
- package/dist/nosql/firestore/index.js +177 -0
- package/dist/nosql/fluree/index.js +148 -0
- package/dist/nosql/fuseki/index.js +170 -0
- package/dist/nosql/gcs/index.js +172 -0
- package/dist/nosql/gearman/index.js +160 -0
- package/dist/nosql/geode/index.js +196 -0
- package/dist/nosql/graphdb/index.js +169 -0
- package/dist/nosql/graylog/index.js +188 -0
- package/dist/nosql/gridgain/index.js +171 -0
- package/dist/nosql/hazelcast/index.js +162 -0
- package/dist/nosql/hbase/index.js +230 -0
- package/dist/nosql/ignite/index.js +173 -0
- package/dist/nosql/immudb/index.js +184 -0
- package/dist/nosql/index.js +232 -0
- package/dist/nosql/infinispan/index.js +200 -0
- package/dist/nosql/influxdb/index.js +0 -0
- package/dist/nosql/kafka/index.js +234 -0
- package/dist/nosql/keyspaces/index.js +189 -0
- package/dist/nosql/kinesis/index.js +253 -0
- package/dist/nosql/leveldb/index.js +153 -0
- package/dist/nosql/lmdb/index.js +160 -0
- package/dist/nosql/loki/index.js +201 -0
- package/dist/nosql/marklogic/index.js +204 -0
- package/dist/nosql/materialize/index.js +145 -0
- package/dist/nosql/meilisearch/index.js +154 -0
- package/dist/nosql/memcached/index.js +223 -0
- package/dist/nosql/milvus/index.js +410 -0
- package/dist/nosql/minio/index.js +265 -0
- package/dist/nosql/momento/index.js +179 -0
- package/dist/nosql/mongodb/index.js +461 -0
- package/dist/nosql/nats/index.js +247 -0
- package/dist/nosql/nedb/index.js +164 -0
- package/dist/nosql/neo4j/index.js +450 -0
- package/dist/nosql/neptune/index.js +470 -0
- package/dist/nosql/nsq/index.js +200 -0
- package/dist/nosql/opensearch/index.js +186 -0
- package/dist/nosql/orientdb/index.js +175 -0
- package/dist/nosql/papertrail/index.js +200 -0
- package/dist/nosql/pinecone/index.js +0 -0
- package/dist/nosql/pinot/index.js +133 -0
- package/dist/nosql/pouchdb/index.js +172 -0
- package/dist/nosql/prometheus/index.js +174 -0
- package/dist/nosql/provendb/index.js +147 -0
- package/dist/nosql/pubsub/index.js +187 -0
- package/dist/nosql/pulsar/index.js +232 -0
- package/dist/nosql/qdrant/index.js +295 -0
- package/dist/nosql/qldb/index.js +192 -0
- package/dist/nosql/r2/index.js +281 -0
- package/dist/nosql/rabbitmq/index.js +237 -0
- package/dist/nosql/ravendb/index.js +175 -0
- package/dist/nosql/redis/index.js +607 -0
- package/dist/nosql/redpanda/index.js +237 -0
- package/dist/nosql/resque/index.js +203 -0
- package/dist/nosql/rethinkdb/index.js +232 -0
- package/dist/nosql/rocksdb/index.js +152 -0
- package/dist/nosql/rockset/index.js +126 -0
- package/dist/nosql/s3/index.js +298 -0
- package/dist/nosql/scylladb/index.js +178 -0
- package/dist/nosql/signoz/index.js +227 -0
- package/dist/nosql/sns/index.js +201 -0
- package/dist/nosql/solr/index.js +231 -0
- package/dist/nosql/splunk/index.js +227 -0
- package/dist/nosql/sqs/index.js +246 -0
- package/dist/nosql/stardog/index.js +169 -0
- package/dist/nosql/starrocks/index.js +170 -0
- package/dist/nosql/store.js +2 -0
- package/dist/nosql/sumologic/index.js +213 -0
- package/dist/nosql/surrealdb/index.js +179 -0
- package/dist/nosql/terminusdb/index.js +151 -0
- package/dist/nosql/tigergraph/index.js +469 -0
- package/dist/nosql/typesense/index.js +148 -0
- package/dist/nosql/unqlite/index.js +157 -0
- package/dist/nosql/upstash/index.js +167 -0
- package/dist/nosql/vercel-kv/index.js +166 -0
- package/dist/nosql/victoriametrics/index.js +213 -0
- package/dist/nosql/virtuoso/index.js +169 -0
- package/dist/nosql/weaviate/index.js +276 -0
- package/dist/operators/index.js +108 -0
- package/dist/operators.js +2690 -0
- package/dist/prisma-migrate/index.js +38 -0
- package/dist/prisma-migrate/migration-generator.js +125 -0
- package/dist/prisma-migrate/model-generator.js +172 -0
- package/dist/prisma-migrate/relations.js +100 -0
- package/dist/prisma-migrate/schema-parser.js +167 -0
- package/dist/prisma-migrate/type-mapper.js +40 -0
- package/dist/prorm.js +6832 -0
- package/dist/query-builders/cte-builder.js +80 -0
- package/dist/query-builders/functions/aggregate.js +390 -0
- package/dist/query-builders/functions/conditional.js +503 -0
- package/dist/query-builders/functions/datetime.js +695 -0
- package/dist/query-builders/functions/fulltext.js +439 -0
- package/dist/query-builders/functions/index.js +93 -0
- package/dist/query-builders/functions/json.js +427 -0
- package/dist/query-builders/functions/math.js +399 -0
- package/dist/query-builders/functions/string.js +518 -0
- package/dist/query-builders/functions/window.js +328 -0
- package/dist/query-builders/include-builder.js +323 -0
- package/dist/query-builders/index-expression-builder.js +242 -0
- package/dist/query-builders/index.js +161 -0
- package/dist/query-builders/insert-builder.js +164 -0
- package/dist/query-builders/model-helpers.js +38 -0
- package/dist/query-builders/order-limit-builder.js +239 -0
- package/dist/query-builders/sql-compiler.js +2040 -0
- package/dist/query-builders/subquery-builder.js +152 -0
- package/dist/query-builders/update-builder.js +182 -0
- package/dist/query-builders/view-builder.js +218 -0
- package/dist/query-builders/where-builder.js +1532 -0
- package/dist/query-interface.js +622 -0
- package/dist/query-optimizers/batch-optimizer.js +426 -0
- package/dist/query-optimizers/explain-plans.js +415 -0
- package/dist/query-optimizers/index.js +51 -0
- package/dist/query-optimizers/prepared-statement-cache.js +423 -0
- package/dist/query-optimizers/query-hints.js +438 -0
- package/dist/query-optimizers/query-optimizer.js +278 -0
- package/dist/query-optimizers/slow-query-logger.js +271 -0
- package/dist/replica-manager.js +507 -0
- package/dist/schema/index.js +15 -0
- package/dist/schema/migration-generator.js +374 -0
- package/dist/schema/schema-differ.js +549 -0
- package/dist/schema/types.js +6 -0
- package/dist/sql-constants.js +300 -0
- package/dist/sqlite-advanced.js +1047 -0
- package/dist/streams/index.js +12 -0
- package/dist/streams/transforms.js +178 -0
- package/dist/transaction.js +456 -0
- package/dist/types/index.js +180 -0
- package/dist/user-management.js +127 -0
- package/dist/utils/date.js +300 -0
- package/dist/utils/index.js +1079 -0
- package/dist/utils/string.js +161 -0
- package/dist/validators/index.js +19 -0
- package/dist/validators/validator.js +911 -0
- package/package.json +190 -0
|
@@ -0,0 +1,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;
|