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