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,2296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MSSQL (SQL Server) dialect implementation for the TypeScript ORM
|
|
4
|
+
* Experimental implementation using the mssql driver
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.MSSQLDialect = void 0;
|
|
8
|
+
exports.createMSSQLDialect = createMSSQLDialect;
|
|
9
|
+
const mssql_1 = require("mssql");
|
|
10
|
+
const types_1 = require("../../types");
|
|
11
|
+
const query_stream_helper_1 = require("../query-stream-helper");
|
|
12
|
+
const prorm_1 = require("../../prorm");
|
|
13
|
+
/**
|
|
14
|
+
* Default retry options for connection
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_RETRY_OPTIONS = {
|
|
17
|
+
max: 3,
|
|
18
|
+
timeout: 1000,
|
|
19
|
+
match: [
|
|
20
|
+
'ECONNREFUSED',
|
|
21
|
+
'ENOTFOUND',
|
|
22
|
+
'ETIMEDOUT',
|
|
23
|
+
'Login failed',
|
|
24
|
+
'connection timeout',
|
|
25
|
+
'Too many connections',
|
|
26
|
+
'Lock wait timeout',
|
|
27
|
+
],
|
|
28
|
+
backoff: false,
|
|
29
|
+
backoffMultiplier: 2,
|
|
30
|
+
backoffMax: 10000,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Table partitioning has shipped in SQL Server Standard Edition since 2016
|
|
34
|
+
* SP1 (it previously required Enterprise Edition) — the older "Enterprise
|
|
35
|
+
* Edition required" message was factually outdated. The generic
|
|
36
|
+
* partitioning API used by this ORM (declarative range/list/hash
|
|
37
|
+
* partitions) also doesn't map cleanly onto SQL Server's actual mechanism,
|
|
38
|
+
* which is `CREATE PARTITION FUNCTION` + `CREATE PARTITION SCHEME` — a
|
|
39
|
+
* structurally different, session/database-level object model rather than
|
|
40
|
+
* per-table declarative partitioning as in Postgres/MySQL. That mechanism
|
|
41
|
+
* isn't modeled here; use raw SQL via `query()` to create partition
|
|
42
|
+
* functions/schemes directly.
|
|
43
|
+
*/
|
|
44
|
+
const PARTITIONING_UNSUPPORTED_MESSAGE = 'Table partitioning is available in SQL Server Standard Edition (2016 SP1+) and Enterprise ' +
|
|
45
|
+
'Edition, but requires CREATE PARTITION FUNCTION / CREATE PARTITION SCHEME objects, which are ' +
|
|
46
|
+
'structurally different from declarative per-table partitioning and are not modeled by this ' +
|
|
47
|
+
'generic partitioning API. Use raw SQL via query() to create partition functions/schemes directly.';
|
|
48
|
+
/**
|
|
49
|
+
* MSSQL dialect class that implements the Dialect interface
|
|
50
|
+
*/
|
|
51
|
+
class MSSQLDialect {
|
|
52
|
+
constructor(config) {
|
|
53
|
+
this.name = 'mssql';
|
|
54
|
+
this.library = 'mssql';
|
|
55
|
+
this.pool = null;
|
|
56
|
+
this._isConnected = false;
|
|
57
|
+
this.poolOptions = config.pool || {};
|
|
58
|
+
this.retryOptions = config.retry || {};
|
|
59
|
+
this.config = {
|
|
60
|
+
host: 'localhost',
|
|
61
|
+
port: 1433,
|
|
62
|
+
database: 'master',
|
|
63
|
+
username: 'sa',
|
|
64
|
+
password: '',
|
|
65
|
+
...config,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async withRetry(fn) {
|
|
69
|
+
const retryConfig = { ...DEFAULT_RETRY_OPTIONS, ...this.retryOptions };
|
|
70
|
+
let lastError;
|
|
71
|
+
const { backoff, backoffMultiplier, backoffMax } = retryConfig;
|
|
72
|
+
const maxRetries = retryConfig.max ?? 3;
|
|
73
|
+
const matchPatterns = retryConfig.match ?? [];
|
|
74
|
+
const timeout = retryConfig.timeout ?? 1000;
|
|
75
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
76
|
+
try {
|
|
77
|
+
return await fn();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
lastError = error;
|
|
81
|
+
const errorMessage = lastError.message;
|
|
82
|
+
const shouldRetry = matchPatterns.some((pattern) => errorMessage.toLowerCase().includes(pattern.toLowerCase()));
|
|
83
|
+
if (!shouldRetry || attempt === maxRetries) {
|
|
84
|
+
throw lastError;
|
|
85
|
+
}
|
|
86
|
+
let delay;
|
|
87
|
+
if (backoff) {
|
|
88
|
+
delay = timeout * Math.pow(backoffMultiplier || 2, attempt);
|
|
89
|
+
delay = Math.min(delay, backoffMax || 10000);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
delay = timeout;
|
|
93
|
+
}
|
|
94
|
+
const jitter = Math.random() * 100;
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw lastError;
|
|
99
|
+
}
|
|
100
|
+
async connect() {
|
|
101
|
+
await this.withRetry(async () => {
|
|
102
|
+
const poolConfig = {
|
|
103
|
+
server: this.config.host || 'localhost',
|
|
104
|
+
port: this.config.port || 1433,
|
|
105
|
+
database: this.config.database || 'master',
|
|
106
|
+
user: this.config.username || 'sa',
|
|
107
|
+
password: this.config.password || '',
|
|
108
|
+
pool: {
|
|
109
|
+
max: this.poolOptions.max || 10,
|
|
110
|
+
min: this.poolOptions.min || 0,
|
|
111
|
+
idleTimeoutMillis: this.poolOptions.idle || 30000,
|
|
112
|
+
},
|
|
113
|
+
options: {
|
|
114
|
+
encrypt: this.config.encrypt ?? true,
|
|
115
|
+
trustServerCertificate: this.config.trustServerCertificate ?? true,
|
|
116
|
+
enableArithAbort: this.config.enableArithAbort ?? true,
|
|
117
|
+
connectTimeout: this.config.connectionTimeout || 30000,
|
|
118
|
+
requestTimeout: this.config.requestTimeout || 30000,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
this.pool = new mssql_1.ConnectionPool(poolConfig);
|
|
122
|
+
try {
|
|
123
|
+
await this.pool.connect();
|
|
124
|
+
this._isConnected = true;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
this._isConnected = false;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async disconnect() {
|
|
133
|
+
if (this.pool) {
|
|
134
|
+
await this.pool.close();
|
|
135
|
+
this.pool = null;
|
|
136
|
+
this._isConnected = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
getConnection() {
|
|
140
|
+
return this.pool;
|
|
141
|
+
}
|
|
142
|
+
isConnected() {
|
|
143
|
+
return this._isConnected && this.pool !== null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Stream query results by paging through `sql` via repeated
|
|
147
|
+
* dialect-appropriate LIMIT/OFFSET queries (see
|
|
148
|
+
* `createPaginatedQueryStream()` in `src/dialects/query-stream-helper.ts`)
|
|
149
|
+
* instead of loading the whole result set into memory at once.
|
|
150
|
+
* @param sql - The SELECT statement to stream
|
|
151
|
+
* @param options - Streaming options (batch size, backpressure watermark, model mapping)
|
|
152
|
+
*/
|
|
153
|
+
queryStream(sql, options) {
|
|
154
|
+
return (0, query_stream_helper_1.createPaginatedQueryStream)(this, sql, options);
|
|
155
|
+
}
|
|
156
|
+
async query(sql, _options) {
|
|
157
|
+
if (!this.pool) {
|
|
158
|
+
throw new Error('Not connected to database');
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
// Named (@param) bind values are passed under different option keys
|
|
162
|
+
// (`replacements` or `bindings`) depending on the call site, either as
|
|
163
|
+
// a named object (@tableName) or a positional array (@p1, @p2, ...).
|
|
164
|
+
// Without wiring these into a parameterized request, SQL Server would
|
|
165
|
+
// receive the literal "@param" text and fail with
|
|
166
|
+
// "must declare scalar variable".
|
|
167
|
+
const bindValues = (_options?.replacements ?? _options?.bindings);
|
|
168
|
+
let result;
|
|
169
|
+
if (Array.isArray(bindValues) && bindValues.length > 0) {
|
|
170
|
+
const request = this.pool.request();
|
|
171
|
+
bindValues.forEach((value, index) => request.input(`p${index + 1}`, value));
|
|
172
|
+
result = await request.query(sql);
|
|
173
|
+
}
|
|
174
|
+
else if (bindValues && !Array.isArray(bindValues) && Object.keys(bindValues).length > 0) {
|
|
175
|
+
const request = this.pool.request();
|
|
176
|
+
for (const [key, value] of Object.entries(bindValues)) {
|
|
177
|
+
request.input(key, value);
|
|
178
|
+
}
|
|
179
|
+
result = await request.query(sql);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
result = await this.pool.query(sql);
|
|
183
|
+
}
|
|
184
|
+
const rows = result.recordset ? Array.from(result.recordset) : [];
|
|
185
|
+
const rowCount = result.rowsAffected?.[0] || 0;
|
|
186
|
+
const fields = [];
|
|
187
|
+
const recordset = result.recordset;
|
|
188
|
+
if (recordset && recordset.columns) {
|
|
189
|
+
for (const [name, col] of Object.entries(recordset.columns)) {
|
|
190
|
+
const colObj = col;
|
|
191
|
+
fields.push({
|
|
192
|
+
name: name,
|
|
193
|
+
type: colObj.type || 'unknown',
|
|
194
|
+
length: colObj.length || 0,
|
|
195
|
+
tableID: 0,
|
|
196
|
+
columnID: 0,
|
|
197
|
+
nullable: colObj.nullable !== false,
|
|
198
|
+
isEnum: false,
|
|
199
|
+
isPrimaryKey: false,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { rows, rowCount, fields, lastInsertRowid: undefined };
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
throw new Error(`Query failed: ${error.message}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
escape(value) {
|
|
210
|
+
if (value instanceof prorm_1.Literal) {
|
|
211
|
+
return value.val;
|
|
212
|
+
}
|
|
213
|
+
if (value === null)
|
|
214
|
+
return 'NULL';
|
|
215
|
+
if (value === undefined)
|
|
216
|
+
return 'NULL';
|
|
217
|
+
if (typeof value === 'boolean')
|
|
218
|
+
return value ? '1' : '0';
|
|
219
|
+
if (typeof value === 'number')
|
|
220
|
+
return String(value);
|
|
221
|
+
if (value instanceof Date)
|
|
222
|
+
return this.formatDate(value);
|
|
223
|
+
if (Buffer.isBuffer(value))
|
|
224
|
+
return `0x${value.toString('hex')}`;
|
|
225
|
+
if (Array.isArray(value) || typeof value === 'object') {
|
|
226
|
+
return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
|
|
227
|
+
}
|
|
228
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
229
|
+
}
|
|
230
|
+
formatDate(date) {
|
|
231
|
+
const year = date.getFullYear();
|
|
232
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
233
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
234
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
235
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
236
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
237
|
+
const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
|
|
238
|
+
return `'${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}'`;
|
|
239
|
+
}
|
|
240
|
+
escapeId(identifier) {
|
|
241
|
+
const id = String(identifier ?? '');
|
|
242
|
+
return `[${id.replace(/\]/g, ']]')}]`;
|
|
243
|
+
}
|
|
244
|
+
quoteIdentifier(identifier) {
|
|
245
|
+
return this.escapeId(identifier);
|
|
246
|
+
}
|
|
247
|
+
quoteTable(tableName, schema) {
|
|
248
|
+
if (schema) {
|
|
249
|
+
return `${this.escapeId(schema)}.${this.escapeId(tableName)}`;
|
|
250
|
+
}
|
|
251
|
+
return this.escapeId(tableName);
|
|
252
|
+
}
|
|
253
|
+
async getDatabaseVersion() {
|
|
254
|
+
const result = await this.query('SELECT @@VERSION AS version');
|
|
255
|
+
return result.rows[0]?.version || 'unknown';
|
|
256
|
+
}
|
|
257
|
+
async createTable(tableName, columns, options) {
|
|
258
|
+
const columnDefs = [];
|
|
259
|
+
for (const [columnName, definition] of Object.entries(columns)) {
|
|
260
|
+
columnDefs.push(this.getColumnDefinitionSql(columnName, definition));
|
|
261
|
+
}
|
|
262
|
+
if (options?.indexes) {
|
|
263
|
+
for (const index of options.indexes) {
|
|
264
|
+
const indexName = index.name || `${tableName}_${index.fields.join('_')}_idx`;
|
|
265
|
+
const unique = index.unique ? 'UNIQUE ' : '';
|
|
266
|
+
const fields = index.fields.map((f) => this.escapeId(f)).join(', ');
|
|
267
|
+
columnDefs.push(`${unique}INDEX ${indexName} (${fields})`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (options?.constraints) {
|
|
271
|
+
for (const constraint of options.constraints) {
|
|
272
|
+
const constraintSql = this.buildConstraintSql(constraint);
|
|
273
|
+
if (constraintSql)
|
|
274
|
+
columnDefs.push(constraintSql);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
let withClause = '';
|
|
278
|
+
if (options?.temporal) {
|
|
279
|
+
const temporal = options.temporal;
|
|
280
|
+
const startCol = temporal.startColumnName || 'ValidFrom';
|
|
281
|
+
const endCol = temporal.endColumnName || 'ValidTo';
|
|
282
|
+
const hidden = temporal.hiddenPeriodColumns === false ? '' : ' HIDDEN';
|
|
283
|
+
// Hidden period columns tracking each row's system-time validity window.
|
|
284
|
+
columnDefs.push(`${this.escapeId(startCol)} DATETIME2 GENERATED ALWAYS AS ROW START${hidden} NOT NULL ` +
|
|
285
|
+
`DEFAULT SYSUTCDATETIME()`);
|
|
286
|
+
columnDefs.push(`${this.escapeId(endCol)} DATETIME2 GENERATED ALWAYS AS ROW END${hidden} NOT NULL ` +
|
|
287
|
+
`DEFAULT CONVERT(DATETIME2, '9999-12-31 23:59:59.9999999')`);
|
|
288
|
+
columnDefs.push(`PERIOD FOR SYSTEM_TIME (${this.escapeId(startCol)}, ${this.escapeId(endCol)})`);
|
|
289
|
+
const historyTableName = temporal.historyTableName || `${tableName}History`;
|
|
290
|
+
const historySchema = temporal.historySchema;
|
|
291
|
+
const historyTableRef = historySchema
|
|
292
|
+
? `${this.escapeId(historySchema)}.${this.escapeId(historyTableName)}`
|
|
293
|
+
: this.escapeId(historyTableName);
|
|
294
|
+
const versioningParts = [`HISTORY_TABLE = ${historyTableRef}`];
|
|
295
|
+
if (temporal.historyRetentionPeriod) {
|
|
296
|
+
versioningParts.push(`HISTORY_RETENTION_PERIOD = ${temporal.historyRetentionPeriod}`);
|
|
297
|
+
}
|
|
298
|
+
withClause = ` WITH (SYSTEM_VERSIONING = ON (${versioningParts.join(', ')}))`;
|
|
299
|
+
}
|
|
300
|
+
let sql = 'CREATE TABLE';
|
|
301
|
+
sql += ` ${this.escapeId(tableName)} (${columnDefs.join(', ')})`;
|
|
302
|
+
sql += withClause;
|
|
303
|
+
await this.query(sql);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Disable system-versioning on a temporal table. Required before the
|
|
307
|
+
* table (or its schema) can be dropped or structurally altered in ways
|
|
308
|
+
* incompatible with an active `PERIOD FOR SYSTEM_TIME`.
|
|
309
|
+
*/
|
|
310
|
+
async disableSystemVersioning(tableName, schema) {
|
|
311
|
+
const table = this.quoteTable(tableName, schema);
|
|
312
|
+
await this.query(`ALTER TABLE ${table} SET (SYSTEM_VERSIONING = OFF)`);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Drop a system-versioned temporal table: disables SYSTEM_VERSIONING on
|
|
316
|
+
* the current table, then drops the table itself. Pass
|
|
317
|
+
* `dropHistoryTable`/`historyTableName` to also drop the associated
|
|
318
|
+
* history table.
|
|
319
|
+
*/
|
|
320
|
+
async dropTemporalTable(tableName, options) {
|
|
321
|
+
await this.disableSystemVersioning(tableName, options?.schema);
|
|
322
|
+
await this.dropTable(tableName, options);
|
|
323
|
+
if (options?.dropHistoryTable) {
|
|
324
|
+
const historyTableName = options.historyTableName || `${tableName}History`;
|
|
325
|
+
await this.dropTable(historyTableName, { ifExists: true, schema: options.schema });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
getColumnDefinitionSql(columnName, definition) {
|
|
329
|
+
let sql = `${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type)}`;
|
|
330
|
+
// SPARSE must appear immediately after the data type, before the
|
|
331
|
+
// NULL/NOT NULL clause, per T-SQL column-definition syntax:
|
|
332
|
+
// column_name <data_type> [ SPARSE ] [ NULL | NOT NULL ] ...
|
|
333
|
+
// SQL Server requires SPARSE columns to allow NULL — fail fast with a
|
|
334
|
+
// clear error rather than emitting DDL SQL Server will reject.
|
|
335
|
+
if (definition.sparse) {
|
|
336
|
+
if (definition.allowNull === false) {
|
|
337
|
+
throw new Error(`Column "${columnName}" cannot be SPARSE and NOT NULL — SQL Server requires SPARSE columns to allow NULL.`);
|
|
338
|
+
}
|
|
339
|
+
sql += ' SPARSE';
|
|
340
|
+
}
|
|
341
|
+
if (definition.allowNull === false)
|
|
342
|
+
sql += ' NOT NULL';
|
|
343
|
+
if (definition.defaultValue !== undefined) {
|
|
344
|
+
if (definition.defaultValue === null)
|
|
345
|
+
sql += ' DEFAULT NULL';
|
|
346
|
+
else if (typeof definition.defaultValue === 'string')
|
|
347
|
+
sql += ` DEFAULT ${definition.defaultValue}`;
|
|
348
|
+
else
|
|
349
|
+
sql += ` DEFAULT ${this.formatValue(definition.defaultValue)}`;
|
|
350
|
+
}
|
|
351
|
+
if (definition.primaryKey)
|
|
352
|
+
sql += ' PRIMARY KEY';
|
|
353
|
+
if (definition.autoIncrement)
|
|
354
|
+
sql += ' IDENTITY(1,1)';
|
|
355
|
+
if (definition.unique) {
|
|
356
|
+
if (typeof definition.unique === 'string')
|
|
357
|
+
sql += ` UNIQUE ${this.escapeId(definition.unique)}`;
|
|
358
|
+
else
|
|
359
|
+
sql += ' UNIQUE';
|
|
360
|
+
}
|
|
361
|
+
if (definition.references) {
|
|
362
|
+
const refField = definition.references.field;
|
|
363
|
+
const refFieldSql = Array.isArray(refField)
|
|
364
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
365
|
+
: `(${this.escapeId(refField)})`;
|
|
366
|
+
sql += ` REFERENCES ${this.escapeId(definition.references.table)}${refFieldSql}`;
|
|
367
|
+
if (definition.references.onDelete)
|
|
368
|
+
sql += ` ON DELETE ${definition.references.onDelete}`;
|
|
369
|
+
if (definition.references.onUpdate)
|
|
370
|
+
sql += ` ON UPDATE ${definition.references.onUpdate}`;
|
|
371
|
+
}
|
|
372
|
+
return sql;
|
|
373
|
+
}
|
|
374
|
+
buildConstraintSql(constraint) {
|
|
375
|
+
const name = constraint.name ? `CONSTRAINT ${this.escapeId(constraint.name)} ` : '';
|
|
376
|
+
const fields = constraint.fields?.map((f) => this.escapeId(f)).join(', ') || '';
|
|
377
|
+
switch (constraint.type) {
|
|
378
|
+
case 'PRIMARY KEY':
|
|
379
|
+
return `${name}PRIMARY KEY (${fields})`;
|
|
380
|
+
case 'UNIQUE':
|
|
381
|
+
return `${name}UNIQUE (${fields})`;
|
|
382
|
+
case 'CHECK':
|
|
383
|
+
return `${name}CHECK (${constraint.check})`;
|
|
384
|
+
case 'FOREIGN KEY':
|
|
385
|
+
if (!constraint.references)
|
|
386
|
+
return null;
|
|
387
|
+
const refField = constraint.references.field;
|
|
388
|
+
const refFieldSql = Array.isArray(refField)
|
|
389
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
390
|
+
: `(${this.escapeId(refField)})`;
|
|
391
|
+
let fkSql = `${name}FOREIGN KEY (${fields}) `;
|
|
392
|
+
fkSql += `REFERENCES ${this.escapeId(constraint.references.table)}${refFieldSql}`;
|
|
393
|
+
if (constraint.references.onDelete)
|
|
394
|
+
fkSql += ` ON DELETE ${constraint.references.onDelete}`;
|
|
395
|
+
if (constraint.references.onUpdate)
|
|
396
|
+
fkSql += ` ON UPDATE ${constraint.references.onUpdate}`;
|
|
397
|
+
return fkSql;
|
|
398
|
+
default:
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async dropTable(tableName, options) {
|
|
403
|
+
if (options?.cascade) {
|
|
404
|
+
throw new Error('MSSQL DROP TABLE does not support CASCADE. T-SQL has no cascading drop for tables; ' +
|
|
405
|
+
'drop dependent foreign keys/objects manually in dependency order before dropping this table.');
|
|
406
|
+
}
|
|
407
|
+
let sql = 'DROP TABLE';
|
|
408
|
+
if (options?.ifExists)
|
|
409
|
+
sql += ' IF EXISTS';
|
|
410
|
+
sql += ` ${this.escapeId(tableName)}`;
|
|
411
|
+
await this.query(sql);
|
|
412
|
+
}
|
|
413
|
+
async createPartitionedTable(_tableName, _columns, _options) {
|
|
414
|
+
throw new Error(PARTITIONING_UNSUPPORTED_MESSAGE);
|
|
415
|
+
}
|
|
416
|
+
async createPartition(_options) {
|
|
417
|
+
throw new Error(PARTITIONING_UNSUPPORTED_MESSAGE);
|
|
418
|
+
}
|
|
419
|
+
async attachPartition(_options) {
|
|
420
|
+
throw new Error(PARTITIONING_UNSUPPORTED_MESSAGE);
|
|
421
|
+
}
|
|
422
|
+
async detachPartition(_options) {
|
|
423
|
+
throw new Error(PARTITIONING_UNSUPPORTED_MESSAGE);
|
|
424
|
+
}
|
|
425
|
+
async dropPartition(_partitionName, _options) {
|
|
426
|
+
throw new Error(PARTITIONING_UNSUPPORTED_MESSAGE);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Add a partition to an existing partitioned table (MSSQL)
|
|
430
|
+
* @param tableName - Name of the partitioned table
|
|
431
|
+
* @param partitionName - Name for the new partition
|
|
432
|
+
* @param partitionSpec - Partition specification
|
|
433
|
+
*/
|
|
434
|
+
async addPartition(_tableName, _partitionName, _partitionSpec) {
|
|
435
|
+
throw new Error(PARTITIONING_UNSUPPORTED_MESSAGE);
|
|
436
|
+
}
|
|
437
|
+
async createView(viewName, query, options) {
|
|
438
|
+
// T-SQL has no `CREATE OR REPLACE VIEW` syntax. Use `CREATE OR ALTER VIEW`
|
|
439
|
+
// (SQL Server 2016 SP1+), which is the correct equivalent.
|
|
440
|
+
let sql = options?.replace ? 'CREATE OR ALTER VIEW' : 'CREATE VIEW';
|
|
441
|
+
sql += ` ${this.escapeId(viewName)} AS ${query}`;
|
|
442
|
+
await this.query(sql);
|
|
443
|
+
}
|
|
444
|
+
async dropView(viewName, options) {
|
|
445
|
+
if (options?.cascade) {
|
|
446
|
+
throw new Error('MSSQL DROP VIEW does not support CASCADE. T-SQL has no cascading drop for views; ' +
|
|
447
|
+
'drop dependent objects manually in dependency order before dropping this view.');
|
|
448
|
+
}
|
|
449
|
+
let sql = 'DROP VIEW';
|
|
450
|
+
if (options?.ifExists)
|
|
451
|
+
sql += ' IF EXISTS';
|
|
452
|
+
sql += ` ${this.escapeId(viewName)}`;
|
|
453
|
+
await this.query(sql);
|
|
454
|
+
}
|
|
455
|
+
async createMaterializedView(options) {
|
|
456
|
+
const schema = options.schema || 'dbo';
|
|
457
|
+
const viewName = options.name;
|
|
458
|
+
// MSSQL uses indexed views as equivalent to materialized views
|
|
459
|
+
// First create the view
|
|
460
|
+
let sql = `CREATE VIEW ${this.escapeId(schema)}.${this.escapeId(viewName)} AS ${options.query}`;
|
|
461
|
+
if (options.replace) {
|
|
462
|
+
// T-SQL has no `CREATE OR REPLACE VIEW`. `CREATE OR ALTER VIEW`
|
|
463
|
+
// (SQL Server 2016 SP1+) is the correct equivalent and redefines the
|
|
464
|
+
// view in a single statement (note: this drops any indexes on the
|
|
465
|
+
// view, so callers should recreate `uniqueIndex` afterwards, which
|
|
466
|
+
// this method already does below).
|
|
467
|
+
sql = `CREATE OR ALTER VIEW ${this.escapeId(schema)}.${this.escapeId(viewName)} AS ${options.query}`;
|
|
468
|
+
}
|
|
469
|
+
else if (options.ifNotExists) {
|
|
470
|
+
await this.query(`IF NOT EXISTS (SELECT * FROM sys.views WHERE name = '${viewName}' AND schema_id = SCHEMA_ID('${schema}')) ${sql}`);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
await this.query(sql);
|
|
474
|
+
// Add a unique clustered index to make it a "materialized" view
|
|
475
|
+
// Note: MSSQL indexed views require all columns in the SELECT to be included in the unique index
|
|
476
|
+
// For simplicity, we create the index without specific columns - this will fail if view has multiple columns
|
|
477
|
+
// Users should manually add the unique index for production use
|
|
478
|
+
if (options.uniqueIndex) {
|
|
479
|
+
// Try to create index on a placeholder - this may fail for complex views
|
|
480
|
+
// In production, user should manually create the unique clustered index
|
|
481
|
+
await this.query(`-- Note: MSSQL indexed views require unique clustered index on all columns
|
|
482
|
+
-- Please create manually: CREATE UNIQUE CLUSTERED INDEX ${this.escapeId(options.uniqueIndex)} ON ${this.escapeId(schema)}.${this.escapeId(viewName)} (column1, column2, ...)`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async refreshMaterializedView(viewName, _options) {
|
|
486
|
+
// In MSSQL indexed views are automatically maintained
|
|
487
|
+
// To "refresh", we need to rebuild the index
|
|
488
|
+
const parts = viewName.split('.');
|
|
489
|
+
const vName = parts[parts.length - 1];
|
|
490
|
+
const schema = parts.length > 1 ? parts[0] : 'dbo';
|
|
491
|
+
// Rebuild all indexes on the view
|
|
492
|
+
await this.query(`ALTER INDEX ALL ON ${this.escapeId(schema)}.${this.escapeId(vName)} REBUILD`);
|
|
493
|
+
}
|
|
494
|
+
async dropMaterializedView(viewName, options) {
|
|
495
|
+
if (options?.cascade) {
|
|
496
|
+
throw new Error('MSSQL DROP VIEW does not support CASCADE. T-SQL has no cascading drop for views; ' +
|
|
497
|
+
'drop dependent objects manually in dependency order before dropping this view.');
|
|
498
|
+
}
|
|
499
|
+
const parts = viewName.split('.');
|
|
500
|
+
const vName = parts[parts.length - 1];
|
|
501
|
+
const schema = parts.length > 1 ? parts[0] : 'dbo';
|
|
502
|
+
const sql = options?.ifExists
|
|
503
|
+
? `IF EXISTS (SELECT * FROM sys.views WHERE name = '${vName}' AND schema_id = SCHEMA_ID('${schema}')) DROP VIEW ${this.escapeId(schema)}.${this.escapeId(vName)}`
|
|
504
|
+
: `DROP VIEW ${this.escapeId(schema)}.${this.escapeId(vName)}`;
|
|
505
|
+
await this.query(sql);
|
|
506
|
+
}
|
|
507
|
+
async hasMaterializedView(viewName) {
|
|
508
|
+
const parts = viewName.split('.');
|
|
509
|
+
const vName = parts[parts.length - 1];
|
|
510
|
+
const schema = parts.length > 1 ? parts[0] : 'dbo';
|
|
511
|
+
const sql = `SELECT name FROM sys.views WHERE name = '${vName}' AND schema_id = SCHEMA_ID('${schema}')`;
|
|
512
|
+
const result = await this.query(sql);
|
|
513
|
+
return result.rows.length > 0;
|
|
514
|
+
}
|
|
515
|
+
async createStoredProcedure(options) {
|
|
516
|
+
const schema = options.schema || 'dbo';
|
|
517
|
+
const procName = options.name;
|
|
518
|
+
let sql = 'CREATE PROCEDURE ';
|
|
519
|
+
if (options.ifNotExists) {
|
|
520
|
+
sql = `IF NOT EXISTS (SELECT * FROM sys.procedures WHERE name = '${procName}' AND schema_id = SCHEMA_ID('${schema}')) CREATE PROCEDURE `;
|
|
521
|
+
}
|
|
522
|
+
sql += `${this.escapeId(schema)}.${this.escapeId(procName)}`;
|
|
523
|
+
// Add parameters
|
|
524
|
+
if (options.params && options.params.length > 0) {
|
|
525
|
+
const params = options.params
|
|
526
|
+
.map((p) => {
|
|
527
|
+
const mode = p.mode === 'OUT' ? 'OUTPUT' : p.mode === 'INOUT' ? 'OUTPUT' : '';
|
|
528
|
+
return `@${p.name} ${p.type} ${mode}`.trim();
|
|
529
|
+
})
|
|
530
|
+
.join(', ');
|
|
531
|
+
sql += ` (${params})`;
|
|
532
|
+
}
|
|
533
|
+
if (options.comment) {
|
|
534
|
+
sql += `\nAS -- ${options.comment}`;
|
|
535
|
+
}
|
|
536
|
+
sql += `\nAS\nBEGIN\n${options.body}\nEND`;
|
|
537
|
+
if (options.replace) {
|
|
538
|
+
// For replace, we need to drop and recreate
|
|
539
|
+
await this.query(`DROP PROCEDURE IF EXISTS ${this.escapeId(schema)}.${this.escapeId(procName)}`);
|
|
540
|
+
sql = sql.replace('IF NOT EXISTS', '').replace('CREATE PROCEDURE ', 'CREATE PROCEDURE ');
|
|
541
|
+
}
|
|
542
|
+
await this.query(sql);
|
|
543
|
+
}
|
|
544
|
+
async dropStoredProcedure(procedureName, options) {
|
|
545
|
+
const schema = options?.schema || 'dbo';
|
|
546
|
+
const sql = options?.ifExists
|
|
547
|
+
? `IF EXISTS (SELECT * FROM sys.procedures WHERE name = '${procedureName}' AND schema_id = SCHEMA_ID('${schema}')) DROP PROCEDURE ${this.escapeId(schema)}.${this.escapeId(procedureName)}`
|
|
548
|
+
: `DROP PROCEDURE ${this.escapeId(schema)}.${this.escapeId(procedureName)}`;
|
|
549
|
+
await this.query(sql);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Drop a stored procedure (alias for dropStoredProcedure)
|
|
553
|
+
*/
|
|
554
|
+
async dropProcedure(procedureName, options) {
|
|
555
|
+
return this.dropStoredProcedure(procedureName, options);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Create a stored procedure (alias for createStoredProcedure)
|
|
559
|
+
*/
|
|
560
|
+
async createProcedure(options) {
|
|
561
|
+
return this.createStoredProcedure(options);
|
|
562
|
+
}
|
|
563
|
+
async executeStoredProcedure(options) {
|
|
564
|
+
const schema = options.schema || 'dbo';
|
|
565
|
+
const procName = `${this.escapeId(schema)}.${this.escapeId(options.procedureName)}`;
|
|
566
|
+
let sql = `EXEC ${procName}`;
|
|
567
|
+
if (options.params && Object.keys(options.params).length > 0) {
|
|
568
|
+
const params = Object.entries(options.params)
|
|
569
|
+
.map(([name, value]) => {
|
|
570
|
+
if (value === undefined || value === null) {
|
|
571
|
+
return `@${name} = NULL`;
|
|
572
|
+
}
|
|
573
|
+
return `@${name} = ${this.escape(value)}`;
|
|
574
|
+
})
|
|
575
|
+
.join(', ');
|
|
576
|
+
sql += ` ${params}`;
|
|
577
|
+
}
|
|
578
|
+
return this.query(sql, { timeout: options.timeout });
|
|
579
|
+
}
|
|
580
|
+
async hasStoredProcedure(procedureName, schema) {
|
|
581
|
+
const db = schema || 'dbo';
|
|
582
|
+
const sql = `SELECT name FROM sys.procedures WHERE name = @procedureName AND schema_id = SCHEMA_ID(@schema)`;
|
|
583
|
+
const result = await this.query(sql, { bindings: { procedureName, schema: db } });
|
|
584
|
+
return result.rows.length > 0;
|
|
585
|
+
}
|
|
586
|
+
async createTrigger(options) {
|
|
587
|
+
const timing = options.timing;
|
|
588
|
+
const events = (options.events || []).join(' OR ');
|
|
589
|
+
const tableName = options.schema
|
|
590
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.tableName)}`
|
|
591
|
+
: this.quoteTable(options.tableName);
|
|
592
|
+
let sql = `CREATE TRIGGER ${this.escapeId(options.name)} ${timing} ${events} ON ${tableName}`;
|
|
593
|
+
if (options.timing === 'INSTEAD OF') {
|
|
594
|
+
sql = `CREATE TRIGGER ${this.escapeId(options.name)} INSTEAD OF ${events} ON ${tableName}`;
|
|
595
|
+
}
|
|
596
|
+
sql += `\nAS\nBEGIN\n${options.body}\nEND`;
|
|
597
|
+
if (options.replace) {
|
|
598
|
+
// T-SQL has no `CREATE OR REPLACE TRIGGER` syntax. `CREATE OR ALTER
|
|
599
|
+
// TRIGGER` (SQL Server 2016 SP1+) is the correct equivalent.
|
|
600
|
+
sql = sql.replace('CREATE TRIGGER', 'CREATE OR ALTER TRIGGER');
|
|
601
|
+
}
|
|
602
|
+
await this.query(sql);
|
|
603
|
+
}
|
|
604
|
+
async dropTrigger(triggerName, tableName, options) {
|
|
605
|
+
const sql = options?.ifExists
|
|
606
|
+
? `IF EXISTS (SELECT * FROM sys.triggers WHERE name = '${triggerName}') DROP TRIGGER ${this.escapeId(triggerName)}`
|
|
607
|
+
: `DROP TRIGGER ${this.escapeId(triggerName)}`;
|
|
608
|
+
await this.query(sql);
|
|
609
|
+
}
|
|
610
|
+
async hasTrigger(triggerName, tableName) {
|
|
611
|
+
const sql = `SELECT name FROM sys.triggers t
|
|
612
|
+
JOIN sys.tables tb ON t.parent_id = tb.object_id
|
|
613
|
+
WHERE t.name = @triggerName AND tb.name = @tableName`;
|
|
614
|
+
const result = await this.query(sql, { bindings: { triggerName, tableName } });
|
|
615
|
+
return result.rows.length > 0;
|
|
616
|
+
}
|
|
617
|
+
async createSequence(options) {
|
|
618
|
+
const schema = options.schema || 'dbo';
|
|
619
|
+
const seqName = options.name;
|
|
620
|
+
let sql = 'CREATE SEQUENCE ';
|
|
621
|
+
if (options.ifNotExists) {
|
|
622
|
+
sql = `IF NOT EXISTS (SELECT * FROM sys.sequences WHERE name = '${seqName}' AND schema_id = SCHEMA_ID('${schema}')) CREATE SEQUENCE `;
|
|
623
|
+
}
|
|
624
|
+
sql += `${this.escapeId(schema)}.${this.escapeId(seqName)}`;
|
|
625
|
+
const parts = [];
|
|
626
|
+
if (options.startWith !== undefined) {
|
|
627
|
+
parts.push(`START WITH ${options.startWith}`);
|
|
628
|
+
}
|
|
629
|
+
if (options.incrementBy !== undefined) {
|
|
630
|
+
parts.push(`INCREMENT BY ${options.incrementBy}`);
|
|
631
|
+
}
|
|
632
|
+
if (options.minvalue !== undefined) {
|
|
633
|
+
parts.push(`MINVALUE ${options.minvalue}`);
|
|
634
|
+
}
|
|
635
|
+
if (options.maxvalue !== undefined) {
|
|
636
|
+
parts.push(`MAXVALUE ${options.maxvalue}`);
|
|
637
|
+
}
|
|
638
|
+
if (options.cycle) {
|
|
639
|
+
parts.push('CYCLE');
|
|
640
|
+
}
|
|
641
|
+
if (options.cache !== undefined) {
|
|
642
|
+
parts.push(`CACHE ${options.cache}`);
|
|
643
|
+
}
|
|
644
|
+
if (parts.length > 0) {
|
|
645
|
+
sql += `\n${parts.join('\n')}`;
|
|
646
|
+
}
|
|
647
|
+
if (options.replace) {
|
|
648
|
+
await this.query(`DROP SEQUENCE IF EXISTS ${this.escapeId(schema)}.${this.escapeId(seqName)}`);
|
|
649
|
+
sql = sql.replace('IF NOT EXISTS', '').replace('CREATE SEQUENCE ', 'CREATE SEQUENCE ');
|
|
650
|
+
}
|
|
651
|
+
await this.query(sql);
|
|
652
|
+
}
|
|
653
|
+
async dropSequence(sequenceName, options) {
|
|
654
|
+
const schema = options?.schema || 'dbo';
|
|
655
|
+
const sql = options?.ifExists
|
|
656
|
+
? `IF EXISTS (SELECT * FROM sys.sequences WHERE name = '${sequenceName}' AND schema_id = SCHEMA_ID('${schema}')) DROP SEQUENCE ${this.escapeId(schema)}.${this.escapeId(sequenceName)}`
|
|
657
|
+
: `DROP SEQUENCE ${this.escapeId(schema)}.${this.escapeId(sequenceName)}`;
|
|
658
|
+
await this.query(sql);
|
|
659
|
+
}
|
|
660
|
+
async nextSequenceValue(sequenceName) {
|
|
661
|
+
// Extract schema from sequence name if present
|
|
662
|
+
const parts = sequenceName.split('.');
|
|
663
|
+
const seqName = parts[parts.length - 1];
|
|
664
|
+
const schema = parts.length > 1 ? parts[0] : 'dbo';
|
|
665
|
+
const sql = `SELECT NEXT VALUE FOR ${this.escapeId(schema)}.${this.escapeId(seqName)} AS next_val`;
|
|
666
|
+
const result = await this.query(sql);
|
|
667
|
+
return result.rows[0]?.next_val;
|
|
668
|
+
}
|
|
669
|
+
async hasSequence(sequenceName) {
|
|
670
|
+
const parts = sequenceName.split('.');
|
|
671
|
+
const seqName = parts[parts.length - 1];
|
|
672
|
+
const schema = parts.length > 1 ? parts[0] : 'dbo';
|
|
673
|
+
const sql = `SELECT name FROM sys.sequences WHERE name = @seqName AND schema_id = SCHEMA_ID(@schema)`;
|
|
674
|
+
const result = await this.query(sql, { bindings: { seqName, schema } });
|
|
675
|
+
return result.rows.length > 0;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* List all sequences in the database (MSSQL)
|
|
679
|
+
* @returns Array of sequence names
|
|
680
|
+
*/
|
|
681
|
+
async listSequences() {
|
|
682
|
+
const sql = `SELECT s.name AS sequence_name, SCHEMA_NAME(s.schema_id) AS schema_name FROM sys.sequences s ORDER BY schema_name, sequence_name`;
|
|
683
|
+
const result = await this.query(sql);
|
|
684
|
+
return result.rows.map((row) => `${row.schema_name}.${row.sequence_name}`);
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Create a security policy with predicate function (MSSQL RLS)
|
|
688
|
+
*/
|
|
689
|
+
async createPolicy(options) {
|
|
690
|
+
const schema = options.schema || 'dbo';
|
|
691
|
+
const tableName = `${this.escapeId(schema)}.${this.escapeId(options.tableName)}`;
|
|
692
|
+
const policyName = this.escapeId(options.name);
|
|
693
|
+
// SQL Server RLS uses CREATE SECURITY POLICY
|
|
694
|
+
let sql = `CREATE SECURITY POLICY ${policyName}`;
|
|
695
|
+
if (options.predicateFunction) {
|
|
696
|
+
sql += `\nADD FILTER PREDICATE ${options.predicateFunction}(${options.column || 'NULL'})`;
|
|
697
|
+
}
|
|
698
|
+
else if (options.using) {
|
|
699
|
+
// If no predicate function is provided but using clause exists,
|
|
700
|
+
// we need a predicate function (not directly supported in CREATE SECURITY POLICY)
|
|
701
|
+
// The user should provide predicateFunction for proper MSSQL RLS
|
|
702
|
+
throw new Error('MSSQL RLS requires a predicate function. Use predicateFunction option to specify the security predicate function (e.g., "dbo.fn_SecurityPredicate")');
|
|
703
|
+
}
|
|
704
|
+
sql += `\nON ${tableName}`;
|
|
705
|
+
await this.query(sql);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Drop a security policy (MSSQL RLS)
|
|
709
|
+
*/
|
|
710
|
+
async dropPolicy(policyName, _tableName, options) {
|
|
711
|
+
// T-SQL's DROP SECURITY POLICY takes no `ON <table>` clause and no
|
|
712
|
+
// CASCADE — a security policy is dropped by name alone (it may have
|
|
713
|
+
// predicates on multiple tables). `tableName` is accepted for parity
|
|
714
|
+
// with other dialects' dropPolicy signatures but unused here.
|
|
715
|
+
const schema = options?.schema || 'dbo';
|
|
716
|
+
const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
|
|
717
|
+
const sql = `DROP SECURITY POLICY ${ifExists}${this.escapeId(schema)}.${this.escapeId(policyName)}`;
|
|
718
|
+
await this.query(sql);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Enable row-level security on a table (MSSQL)
|
|
722
|
+
* Note: In SQL Server, RLS is automatically enabled when a security policy is created.
|
|
723
|
+
* This method can be used to explicitly enable RLS if needed.
|
|
724
|
+
*/
|
|
725
|
+
async enableRLS(tableName, schema) {
|
|
726
|
+
const schemaName = schema || 'dbo';
|
|
727
|
+
// In SQL Server, RLS is enabled by creating a security policy.
|
|
728
|
+
// This method is kept for API compatibility but doesn't need to do anything
|
|
729
|
+
// as createPolicy will handle enabling RLS.
|
|
730
|
+
// However, we can verify the table exists.
|
|
731
|
+
const sql = `SELECT OBJECT_ID('${schemaName}.${tableName}') AS TableId`;
|
|
732
|
+
const result = await this.query(sql);
|
|
733
|
+
if (!result.rows || result.rows.length === 0) {
|
|
734
|
+
throw new Error(`Table ${schemaName}.${tableName} does not exist`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Disable row-level security on a table (MSSQL)
|
|
739
|
+
* This drops all security policies associated with the table
|
|
740
|
+
*/
|
|
741
|
+
async disableRLS(tableName, schema) {
|
|
742
|
+
const schemaName = schema || 'dbo';
|
|
743
|
+
// Find and drop all security policies for this table
|
|
744
|
+
const sql = `
|
|
745
|
+
SELECT name FROM sys.security_policies
|
|
746
|
+
WHERE schema_id = SCHEMA_ID('${schemaName}')
|
|
747
|
+
AND OBJECT_ID(CONCAT('${schemaName}.', name)) IN (
|
|
748
|
+
SELECT object_id FROM sys.security_predicates
|
|
749
|
+
WHERE class = 1 -- TABLE
|
|
750
|
+
AND schema_id = SCHEMA_ID('${schemaName}')
|
|
751
|
+
AND object_id = OBJECT_ID('${schemaName}.${tableName}')
|
|
752
|
+
)
|
|
753
|
+
`;
|
|
754
|
+
const result = await this.query(sql);
|
|
755
|
+
if (result.rows && result.rows.length > 0) {
|
|
756
|
+
for (const row of result.rows) {
|
|
757
|
+
const dropSql = `DROP SECURITY POLICY ${this.escapeId(schemaName)}.${this.escapeId(row.name)} ON ${this.escapeId(schemaName)}.${this.escapeId(tableName)}`;
|
|
758
|
+
await this.query(dropSql);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Check if a security policy exists (MSSQL RLS)
|
|
764
|
+
*/
|
|
765
|
+
async hasPolicy(policyName, tableName) {
|
|
766
|
+
const sql = `
|
|
767
|
+
SELECT sp.name AS policyname
|
|
768
|
+
FROM sys.security_policies sp
|
|
769
|
+
INNER JOIN sys.security_predicates spred ON sp.object_id = spred.object_id
|
|
770
|
+
WHERE sp.name = @policyName
|
|
771
|
+
AND spred.parent_object_id = OBJECT_ID(@tableName)
|
|
772
|
+
`;
|
|
773
|
+
const result = await this.query(sql, { bindings: { policyName, tableName } });
|
|
774
|
+
return result.rows && result.rows.length > 0;
|
|
775
|
+
}
|
|
776
|
+
async commentTable(tableName, comment) {
|
|
777
|
+
const sql = `EXEC sp_addextendedproperty 'MS_Description', @comment, 'TABLE', null, null, null, null, null, null, null, null, 'SCHEMA', 'dbo', 'TABLE', @tableName`;
|
|
778
|
+
await this.query(sql, { bindings: { comment, tableName } });
|
|
779
|
+
}
|
|
780
|
+
async commentColumn(tableName, columnName, comment) {
|
|
781
|
+
const sql = `EXEC sp_addextendedproperty 'MS_Description', @comment, 'COLUMN', null, null, null, null, null, null, null, null, 'SCHEMA', 'dbo', 'TABLE', @tableName, 'COLUMN', @columnName`;
|
|
782
|
+
await this.query(sql, { bindings: { comment, tableName, columnName } });
|
|
783
|
+
}
|
|
784
|
+
async createPartialIndex(tableName, indexName, fields, where, options) {
|
|
785
|
+
// MSSQL supports filtered indexes which are equivalent to partial indexes
|
|
786
|
+
let sql = 'CREATE';
|
|
787
|
+
if (options?.unique)
|
|
788
|
+
sql += ' UNIQUE';
|
|
789
|
+
sql += ` INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)}`;
|
|
790
|
+
sql += ` (${fields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
791
|
+
sql += ` WHERE ${where}`;
|
|
792
|
+
if (options?.using) {
|
|
793
|
+
sql += ` USING ${options.using}`;
|
|
794
|
+
}
|
|
795
|
+
await this.query(sql);
|
|
796
|
+
}
|
|
797
|
+
async createExpressionIndex(tableName, indexName, expression, options) {
|
|
798
|
+
// SQL Server does not support indexing an arbitrary expression directly
|
|
799
|
+
// (unlike Postgres). The only way to index an expression is to first add
|
|
800
|
+
// a computed column for it, then build a normal index on that column.
|
|
801
|
+
// We default the computed column to PERSISTED so it's materialized on
|
|
802
|
+
// disk and indexable without SQL Server re-evaluating the expression
|
|
803
|
+
// (and re-checking determinism) on every index seek/scan.
|
|
804
|
+
const computedColumnName = options?.computedColumnName || `${indexName}_expr`;
|
|
805
|
+
const persisted = options?.persisted !== false;
|
|
806
|
+
await this.createComputedColumn(tableName, computedColumnName, expression, {
|
|
807
|
+
persisted,
|
|
808
|
+
});
|
|
809
|
+
let sql = 'CREATE';
|
|
810
|
+
if (options?.unique)
|
|
811
|
+
sql += ' UNIQUE';
|
|
812
|
+
sql += ` INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)}`;
|
|
813
|
+
sql += ` (${this.escapeId(computedColumnName)})`;
|
|
814
|
+
if (options?.using) {
|
|
815
|
+
sql += ` USING ${options.using}`;
|
|
816
|
+
}
|
|
817
|
+
await this.query(sql);
|
|
818
|
+
}
|
|
819
|
+
async createIdentityColumn(tableName, columnName, options) {
|
|
820
|
+
const startWith = options?.startWith || 1;
|
|
821
|
+
const incrementBy = options?.incrementBy || 1;
|
|
822
|
+
const sql = `ALTER TABLE ${this.escapeId(tableName)} ADD ${this.escapeId(columnName)} INT IDENTITY(${startWith},${incrementBy})`;
|
|
823
|
+
await this.query(sql);
|
|
824
|
+
}
|
|
825
|
+
async createComputedColumn(tableName, columnName, expression, options) {
|
|
826
|
+
const persisted = options?.persisted ? 'PERSISTED' : '';
|
|
827
|
+
const type = options?.type || 'AS';
|
|
828
|
+
const sql = `ALTER TABLE ${this.escapeId(tableName)} ADD ${this.escapeId(columnName)} ${type} (${expression}) ${persisted}`;
|
|
829
|
+
await this.query(sql);
|
|
830
|
+
}
|
|
831
|
+
async addColumn(tableName, columnName, definition) {
|
|
832
|
+
const sql = `ALTER TABLE ${this.escapeId(tableName)} ADD ${this.getColumnDefinitionSql(columnName, definition)}`;
|
|
833
|
+
await this.query(sql);
|
|
834
|
+
}
|
|
835
|
+
async removeColumn(tableName, columnName) {
|
|
836
|
+
const sql = `ALTER TABLE ${this.escapeId(tableName)} DROP COLUMN ${this.escapeId(columnName)}`;
|
|
837
|
+
await this.query(sql);
|
|
838
|
+
}
|
|
839
|
+
async changeColumn(tableName, columnName, definition) {
|
|
840
|
+
const sql = `ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.getColumnDefinitionSql(columnName, definition)}`;
|
|
841
|
+
await this.query(sql);
|
|
842
|
+
}
|
|
843
|
+
async showTables() {
|
|
844
|
+
const result = await this.query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'");
|
|
845
|
+
return result.rows.map((row) => row.TABLE_NAME);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Get table status (MSSQL implementation)
|
|
849
|
+
*/
|
|
850
|
+
async getTableStatus(tableName) {
|
|
851
|
+
const sql = tableName
|
|
852
|
+
? `SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @tableName`
|
|
853
|
+
: `SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'`;
|
|
854
|
+
const result = tableName
|
|
855
|
+
? await this.query(sql, { bindings: { tableName } })
|
|
856
|
+
: await this.query(sql);
|
|
857
|
+
return result.rows;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Get table create statement (MSSQL implementation)
|
|
861
|
+
*/
|
|
862
|
+
async getCreateTable(tableName) {
|
|
863
|
+
const sql = `
|
|
864
|
+
SELECT OBJECT_DEFINITION(OBJECT_ID(@tableName)) AS CreateStatement
|
|
865
|
+
`;
|
|
866
|
+
const result = await this.query(sql, { bindings: { tableName } });
|
|
867
|
+
return result.rows[0]?.CreateStatement || '';
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Check if a table has partitions (MSSQL implementation)
|
|
871
|
+
*/
|
|
872
|
+
async hasPartition(tableName) {
|
|
873
|
+
const sql = `
|
|
874
|
+
SELECT 1 FROM sys.partitions p
|
|
875
|
+
JOIN sys.tables t ON p.object_id = t.object_id
|
|
876
|
+
WHERE t.name = @tableName AND p.index_id IN (0, 1)
|
|
877
|
+
`;
|
|
878
|
+
const result = await this.query(sql, { bindings: { tableName } });
|
|
879
|
+
// Check if there are multiple partitions (base table has 1 partition by default)
|
|
880
|
+
return result.rows.length > 1;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Show constraints for a table
|
|
884
|
+
*/
|
|
885
|
+
async showConstraints(tableName) {
|
|
886
|
+
const sql = `
|
|
887
|
+
SELECT
|
|
888
|
+
tc.CONSTRAINT_NAME AS name,
|
|
889
|
+
tc.TABLE_NAME AS tableName,
|
|
890
|
+
tc.CONSTRAINT_TYPE AS type,
|
|
891
|
+
kcu.COLUMN_NAME AS columnName
|
|
892
|
+
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc
|
|
893
|
+
LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu
|
|
894
|
+
ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
|
|
895
|
+
AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA
|
|
896
|
+
WHERE tc.TABLE_NAME = @tableName
|
|
897
|
+
ORDER BY tc.CONSTRAINT_NAME
|
|
898
|
+
`;
|
|
899
|
+
const result = await this.query(sql, { bindings: { tableName } });
|
|
900
|
+
return result.rows;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Show indexes for a table
|
|
904
|
+
*/
|
|
905
|
+
async showIndexes(tableName) {
|
|
906
|
+
const sql = `
|
|
907
|
+
SELECT
|
|
908
|
+
i.name AS name,
|
|
909
|
+
OBJECT_NAME(i.object_id) AS tableName,
|
|
910
|
+
i.is_unique AS isUnique,
|
|
911
|
+
i.type AS type,
|
|
912
|
+
ic.column_id AS columnId,
|
|
913
|
+
COL_NAME(ic.object_id, ic.column_id) AS columnName
|
|
914
|
+
FROM sys.indexes i
|
|
915
|
+
LEFT JOIN sys.index_columns ic
|
|
916
|
+
ON i.object_id = ic.object_id
|
|
917
|
+
AND i.index_id = ic.index_id
|
|
918
|
+
WHERE OBJECT_NAME(i.object_id) = @tableName
|
|
919
|
+
AND i.is_primary_key = 0
|
|
920
|
+
ORDER BY i.name, ic.key_ordinal
|
|
921
|
+
`;
|
|
922
|
+
const result = await this.query(sql, { bindings: { tableName } });
|
|
923
|
+
return result.rows;
|
|
924
|
+
}
|
|
925
|
+
async showViews() {
|
|
926
|
+
const result = await this.query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_SCHEMA = 'dbo'");
|
|
927
|
+
return result.rows.map((row) => row.TABLE_NAME);
|
|
928
|
+
}
|
|
929
|
+
async showMaterializedViews() {
|
|
930
|
+
// In MSSQL, indexed views are the equivalent of materialized views
|
|
931
|
+
// Return all views that have a unique clustered index
|
|
932
|
+
const result = await this.query(`
|
|
933
|
+
SELECT v.name AS view_name, SCHEMA_NAME(v.schema_id) AS schema_name
|
|
934
|
+
FROM sys.views v
|
|
935
|
+
INNER JOIN sys.indexes i ON v.object_id = i.object_id
|
|
936
|
+
WHERE i.type = 1 AND i.is_unique = 1
|
|
937
|
+
ORDER BY schema_name, view_name
|
|
938
|
+
`);
|
|
939
|
+
return result.rows.map((row) => `${row.schema_name}.${row.view_name}`);
|
|
940
|
+
}
|
|
941
|
+
async describeTable(tableName) {
|
|
942
|
+
const result = await this.query(`
|
|
943
|
+
SELECT c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT
|
|
944
|
+
FROM INFORMATION_SCHEMA.COLUMNS c
|
|
945
|
+
WHERE c.TABLE_NAME = @tableName
|
|
946
|
+
ORDER BY c.ORDINAL_POSITION
|
|
947
|
+
`, { bindings: { tableName } });
|
|
948
|
+
const description = {};
|
|
949
|
+
for (const row of result.rows) {
|
|
950
|
+
description[row.COLUMN_NAME] = {
|
|
951
|
+
type: row.DATA_TYPE,
|
|
952
|
+
allowNull: row.IS_NULLABLE === 'YES',
|
|
953
|
+
defaultValue: row.COLUMN_DEFAULT,
|
|
954
|
+
primaryKey: false,
|
|
955
|
+
autoIncrement: false,
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
return description;
|
|
959
|
+
}
|
|
960
|
+
async renameTable(oldName, newName) {
|
|
961
|
+
const sql = `EXEC sp_rename @objname = @oldName, @newname = @newName`;
|
|
962
|
+
await this.query(sql, { bindings: { oldName, newName } });
|
|
963
|
+
}
|
|
964
|
+
async addIndex(tableName, indexName, fields, options) {
|
|
965
|
+
const fieldsArr = fields || [];
|
|
966
|
+
let sql = 'CREATE';
|
|
967
|
+
if (options?.unique)
|
|
968
|
+
sql += ' UNIQUE';
|
|
969
|
+
if (options?.type)
|
|
970
|
+
sql += ` ${options.type}`;
|
|
971
|
+
sql += ' INDEX';
|
|
972
|
+
sql += ` ${this.escapeId(indexName)} ON ${this.escapeId(tableName)}`;
|
|
973
|
+
sql += ` (${fieldsArr.map((f) => this.escapeId(f)).join(', ')})`;
|
|
974
|
+
if (options?.using)
|
|
975
|
+
sql += ` USING ${options.using}`;
|
|
976
|
+
// Filtered index predicate (a real, commonly-used MSSQL feature) - was
|
|
977
|
+
// previously accepted by createIndex() but silently dropped here.
|
|
978
|
+
if (options?.where) {
|
|
979
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
980
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
981
|
+
}
|
|
982
|
+
await this.query(sql);
|
|
983
|
+
}
|
|
984
|
+
async removeIndex(tableName, indexName) {
|
|
985
|
+
const sql = `DROP INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)}`;
|
|
986
|
+
await this.query(sql);
|
|
987
|
+
}
|
|
988
|
+
async createIndex(tableName, indexDef) {
|
|
989
|
+
await this.addIndex(tableName, indexDef.name, indexDef.fields, {
|
|
990
|
+
unique: indexDef.unique,
|
|
991
|
+
type: indexDef.type,
|
|
992
|
+
using: indexDef.using,
|
|
993
|
+
where: indexDef.where,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
async dropIndex(tableName, indexName, _options) {
|
|
997
|
+
await this.removeIndex(tableName, indexName);
|
|
998
|
+
}
|
|
999
|
+
async createConstraint(tableName, constraintDef) {
|
|
1000
|
+
const sql = `ALTER TABLE ${this.escapeId(tableName)} ADD ${this.buildConstraintSql(constraintDef)}`;
|
|
1001
|
+
await this.query(sql);
|
|
1002
|
+
}
|
|
1003
|
+
async dropConstraint(tableName, constraintName, _options) {
|
|
1004
|
+
const sql = `ALTER TABLE ${this.escapeId(tableName)} DROP CONSTRAINT ${this.escapeId(constraintName)}`;
|
|
1005
|
+
await this.query(sql);
|
|
1006
|
+
}
|
|
1007
|
+
async startTransaction(_options) {
|
|
1008
|
+
await this.query('BEGIN TRANSACTION');
|
|
1009
|
+
return new types_1.Transaction({ autocommit: false });
|
|
1010
|
+
}
|
|
1011
|
+
async commitTransaction(_transaction) {
|
|
1012
|
+
await this.query('COMMIT TRANSACTION');
|
|
1013
|
+
}
|
|
1014
|
+
async rollbackTransaction(_transaction) {
|
|
1015
|
+
await this.query('ROLLBACK TRANSACTION');
|
|
1016
|
+
}
|
|
1017
|
+
async createSchema(schema) {
|
|
1018
|
+
const sql = `CREATE SCHEMA ${this.escapeId(schema)}`;
|
|
1019
|
+
await this.query(sql);
|
|
1020
|
+
}
|
|
1021
|
+
async dropSchema(schema, options) {
|
|
1022
|
+
if (options?.cascade) {
|
|
1023
|
+
throw new Error('MSSQL DROP SCHEMA does not support CASCADE. T-SQL requires a schema to be empty before ' +
|
|
1024
|
+
'it can be dropped; drop or move all objects out of the schema manually first.');
|
|
1025
|
+
}
|
|
1026
|
+
let sql = 'DROP SCHEMA';
|
|
1027
|
+
if (options?.ifExists)
|
|
1028
|
+
sql += ' IF EXISTS';
|
|
1029
|
+
sql += ` ${this.escapeId(schema)}`;
|
|
1030
|
+
await this.query(sql);
|
|
1031
|
+
}
|
|
1032
|
+
async showAllSchemas() {
|
|
1033
|
+
const result = await this.query("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('INFORMATION_SCHEMA', 'sys')");
|
|
1034
|
+
return result.rows.map((row) => row.SCHEMA_NAME);
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* List all schemas
|
|
1038
|
+
*/
|
|
1039
|
+
async listSchemas() {
|
|
1040
|
+
return this.showAllSchemas();
|
|
1041
|
+
}
|
|
1042
|
+
getDataTypeSql(dataType) {
|
|
1043
|
+
if (typeof dataType === 'string')
|
|
1044
|
+
return dataType;
|
|
1045
|
+
if (!dataType || typeof dataType !== 'object')
|
|
1046
|
+
return 'NVARCHAR(255)';
|
|
1047
|
+
const dt = dataType;
|
|
1048
|
+
switch (dt.key) {
|
|
1049
|
+
case 'STRING':
|
|
1050
|
+
if (dt.length === 'max')
|
|
1051
|
+
return 'NVARCHAR(MAX)';
|
|
1052
|
+
return `NVARCHAR(${String(dt.length || 255)})`;
|
|
1053
|
+
case 'CHAR':
|
|
1054
|
+
return `CHAR(${String(dt.length || 1)})`;
|
|
1055
|
+
case 'TEXT': {
|
|
1056
|
+
// MSSQL has no MySQL-style TEXT/MEDIUMTEXT/LONGTEXT length tiers, and
|
|
1057
|
+
// the legacy TEXT/NTEXT types are deprecated since SQL Server 2005 in
|
|
1058
|
+
// favor of VARCHAR(MAX)/NVARCHAR(MAX). Map an explicit, reasonable
|
|
1059
|
+
// length to NVARCHAR(n) (max non-MAX length is 4000 for NVARCHAR),
|
|
1060
|
+
// and anything unbounded/larger (including legacy MySQL length
|
|
1061
|
+
// sentinels like 65535/16777215/4294967295 that used to be
|
|
1062
|
+
// misinterpreted here) to NVARCHAR(MAX).
|
|
1063
|
+
if (dt.length === undefined || dt.length === null || dt.length === 'max') {
|
|
1064
|
+
return 'NVARCHAR(MAX)';
|
|
1065
|
+
}
|
|
1066
|
+
const len = Number(dt.length);
|
|
1067
|
+
if (!Number.isFinite(len) || len <= 0 || len > 4000)
|
|
1068
|
+
return 'NVARCHAR(MAX)';
|
|
1069
|
+
return `NVARCHAR(${len})`;
|
|
1070
|
+
}
|
|
1071
|
+
case 'INTEGER': {
|
|
1072
|
+
let intType = 'INT';
|
|
1073
|
+
if (dt.length === 1)
|
|
1074
|
+
intType = 'TINYINT';
|
|
1075
|
+
else if (dt.length === 2)
|
|
1076
|
+
intType = 'SMALLINT';
|
|
1077
|
+
else if (dt.length === 8)
|
|
1078
|
+
intType = 'BIGINT';
|
|
1079
|
+
return intType;
|
|
1080
|
+
}
|
|
1081
|
+
case 'BIGINT':
|
|
1082
|
+
return 'BIGINT';
|
|
1083
|
+
case 'FLOAT':
|
|
1084
|
+
return dt.length ? `FLOAT(${String(dt.length)})` : 'FLOAT';
|
|
1085
|
+
case 'DOUBLE':
|
|
1086
|
+
return 'FLOAT';
|
|
1087
|
+
case 'DECIMAL':
|
|
1088
|
+
return `DECIMAL(${String(dt.precision || 10)},${String(dt.scale || 0)})`;
|
|
1089
|
+
case 'BOOLEAN':
|
|
1090
|
+
return 'BIT';
|
|
1091
|
+
case 'DATE':
|
|
1092
|
+
return dt.precision ? `DATETIME2(${String(dt.precision)})` : 'DATETIME2';
|
|
1093
|
+
case 'DATEONLY':
|
|
1094
|
+
return 'DATE';
|
|
1095
|
+
case 'TIME':
|
|
1096
|
+
return dt.precision ? `TIME(${String(dt.precision)})` : 'TIME';
|
|
1097
|
+
case 'BLOB':
|
|
1098
|
+
return 'VARBINARY(MAX)';
|
|
1099
|
+
case 'ENUM':
|
|
1100
|
+
return 'NVARCHAR(255)';
|
|
1101
|
+
case 'JSON':
|
|
1102
|
+
return 'NVARCHAR(MAX)';
|
|
1103
|
+
case 'JSONB':
|
|
1104
|
+
return 'NVARCHAR(MAX)';
|
|
1105
|
+
case 'UUID':
|
|
1106
|
+
return 'UNIQUEIDENTIFIER';
|
|
1107
|
+
case 'GEOMETRY':
|
|
1108
|
+
return 'GEOMETRY';
|
|
1109
|
+
case 'VIRTUAL':
|
|
1110
|
+
return '';
|
|
1111
|
+
default:
|
|
1112
|
+
return 'NVARCHAR(255)';
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
buildWhereClause(where, _options) {
|
|
1116
|
+
const values = [];
|
|
1117
|
+
if (!where || Object.keys(where).length === 0) {
|
|
1118
|
+
return { sql: '', values };
|
|
1119
|
+
}
|
|
1120
|
+
// Operator objects (e.g. { age: { $gt: 18 } }) and top-level logical
|
|
1121
|
+
// operators ($and/$or/$not) used to be silently dropped here (any key
|
|
1122
|
+
// starting with `$` hit `continue`), which meant filters like $or/$gt
|
|
1123
|
+
// vanished from the generated SQL and queries silently returned more
|
|
1124
|
+
// rows than intended. This now translates the operators supported
|
|
1125
|
+
// across the ORM (mirroring the Postgres dialect's buildWhereClause)
|
|
1126
|
+
// and throws for anything it doesn't recognize instead of dropping it.
|
|
1127
|
+
const buildCondition = (condition) => {
|
|
1128
|
+
if (!condition || typeof condition !== 'object') {
|
|
1129
|
+
return '';
|
|
1130
|
+
}
|
|
1131
|
+
const cond = condition;
|
|
1132
|
+
const logicalParts = [];
|
|
1133
|
+
if (cond.$and !== undefined) {
|
|
1134
|
+
const subConditions = cond.$and
|
|
1135
|
+
.map((c) => buildCondition(c))
|
|
1136
|
+
.filter(Boolean);
|
|
1137
|
+
if (subConditions.length > 0) {
|
|
1138
|
+
logicalParts.push(`(${subConditions.join(' AND ')})`);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (cond.$or !== undefined) {
|
|
1142
|
+
const subConditions = cond.$or
|
|
1143
|
+
.map((c) => buildCondition(c))
|
|
1144
|
+
.filter(Boolean);
|
|
1145
|
+
if (subConditions.length > 0) {
|
|
1146
|
+
logicalParts.push(`(${subConditions.join(' OR ')})`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (cond.$not !== undefined) {
|
|
1150
|
+
const subCondition = buildCondition(cond.$not);
|
|
1151
|
+
if (subCondition) {
|
|
1152
|
+
logicalParts.push(`NOT (${subCondition})`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
const fieldParts = [];
|
|
1156
|
+
for (const [key, value] of Object.entries(cond)) {
|
|
1157
|
+
if (key.startsWith('$'))
|
|
1158
|
+
continue;
|
|
1159
|
+
if (value === null) {
|
|
1160
|
+
fieldParts.push(`${this.escapeId(key)} IS NULL`);
|
|
1161
|
+
}
|
|
1162
|
+
else if (Array.isArray(value)) {
|
|
1163
|
+
if (value.length === 0) {
|
|
1164
|
+
// An empty IN-list matches nothing.
|
|
1165
|
+
fieldParts.push('1 = 0');
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
const placeholders = value
|
|
1169
|
+
.map((v) => {
|
|
1170
|
+
values.push(v);
|
|
1171
|
+
return `@p${values.length}`;
|
|
1172
|
+
})
|
|
1173
|
+
.join(', ');
|
|
1174
|
+
fieldParts.push(`${this.escapeId(key)} IN (${placeholders})`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
else if (value instanceof Date || value instanceof prorm_1.Literal) {
|
|
1178
|
+
values.push(value);
|
|
1179
|
+
fieldParts.push(`${this.escapeId(key)} = @p${values.length}`);
|
|
1180
|
+
}
|
|
1181
|
+
else if (value && typeof value === 'object') {
|
|
1182
|
+
const valueObj = value;
|
|
1183
|
+
const operatorKeys = Object.keys(valueObj).filter((k) => k.startsWith('$'));
|
|
1184
|
+
if (operatorKeys.length === 0) {
|
|
1185
|
+
values.push(value);
|
|
1186
|
+
fieldParts.push(`${this.escapeId(key)} = @p${values.length}`);
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
for (const op of operatorKeys) {
|
|
1190
|
+
const opValue = valueObj[op];
|
|
1191
|
+
switch (op) {
|
|
1192
|
+
case '$eq':
|
|
1193
|
+
values.push(opValue);
|
|
1194
|
+
fieldParts.push(`${this.escapeId(key)} = @p${values.length}`);
|
|
1195
|
+
break;
|
|
1196
|
+
case '$ne':
|
|
1197
|
+
values.push(opValue);
|
|
1198
|
+
fieldParts.push(`${this.escapeId(key)} <> @p${values.length}`);
|
|
1199
|
+
break;
|
|
1200
|
+
case '$gt':
|
|
1201
|
+
values.push(opValue);
|
|
1202
|
+
fieldParts.push(`${this.escapeId(key)} > @p${values.length}`);
|
|
1203
|
+
break;
|
|
1204
|
+
case '$gte':
|
|
1205
|
+
values.push(opValue);
|
|
1206
|
+
fieldParts.push(`${this.escapeId(key)} >= @p${values.length}`);
|
|
1207
|
+
break;
|
|
1208
|
+
case '$lt':
|
|
1209
|
+
values.push(opValue);
|
|
1210
|
+
fieldParts.push(`${this.escapeId(key)} < @p${values.length}`);
|
|
1211
|
+
break;
|
|
1212
|
+
case '$lte':
|
|
1213
|
+
values.push(opValue);
|
|
1214
|
+
fieldParts.push(`${this.escapeId(key)} <= @p${values.length}`);
|
|
1215
|
+
break;
|
|
1216
|
+
case '$like':
|
|
1217
|
+
values.push(opValue);
|
|
1218
|
+
fieldParts.push(`${this.escapeId(key)} LIKE @p${values.length}`);
|
|
1219
|
+
break;
|
|
1220
|
+
case '$notLike':
|
|
1221
|
+
values.push(opValue);
|
|
1222
|
+
fieldParts.push(`${this.escapeId(key)} NOT LIKE @p${values.length}`);
|
|
1223
|
+
break;
|
|
1224
|
+
case '$in': {
|
|
1225
|
+
const inValues = opValue || [];
|
|
1226
|
+
if (inValues.length === 0) {
|
|
1227
|
+
fieldParts.push('1 = 0');
|
|
1228
|
+
}
|
|
1229
|
+
else {
|
|
1230
|
+
const placeholders = inValues
|
|
1231
|
+
.map((v) => {
|
|
1232
|
+
values.push(v);
|
|
1233
|
+
return `@p${values.length}`;
|
|
1234
|
+
})
|
|
1235
|
+
.join(', ');
|
|
1236
|
+
fieldParts.push(`${this.escapeId(key)} IN (${placeholders})`);
|
|
1237
|
+
}
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
case '$notIn': {
|
|
1241
|
+
const notInValues = opValue || [];
|
|
1242
|
+
if (notInValues.length === 0) {
|
|
1243
|
+
fieldParts.push('1 = 1');
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
const placeholders = notInValues
|
|
1247
|
+
.map((v) => {
|
|
1248
|
+
values.push(v);
|
|
1249
|
+
return `@p${values.length}`;
|
|
1250
|
+
})
|
|
1251
|
+
.join(', ');
|
|
1252
|
+
fieldParts.push(`${this.escapeId(key)} NOT IN (${placeholders})`);
|
|
1253
|
+
}
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
case '$between': {
|
|
1257
|
+
const [lo, hi] = opValue;
|
|
1258
|
+
values.push(lo, hi);
|
|
1259
|
+
fieldParts.push(`${this.escapeId(key)} BETWEEN @p${values.length - 1} AND @p${values.length}`);
|
|
1260
|
+
break;
|
|
1261
|
+
}
|
|
1262
|
+
case '$notBetween': {
|
|
1263
|
+
const [lo, hi] = opValue;
|
|
1264
|
+
values.push(lo, hi);
|
|
1265
|
+
fieldParts.push(`${this.escapeId(key)} NOT BETWEEN @p${values.length - 1} AND @p${values.length}`);
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
case '$isNull':
|
|
1269
|
+
fieldParts.push(opValue ? `${this.escapeId(key)} IS NULL` : `${this.escapeId(key)} IS NOT NULL`);
|
|
1270
|
+
break;
|
|
1271
|
+
default:
|
|
1272
|
+
throw new Error(`Unsupported WHERE operator "${op}" for the MSSQL dialect. ` +
|
|
1273
|
+
'Supported operators: $eq, $ne, $gt, $gte, $lt, $lte, $like, $notLike, ' +
|
|
1274
|
+
'$in, $notIn, $between, $notBetween, $isNull, $and, $or, $not.');
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
values.push(value);
|
|
1280
|
+
fieldParts.push(`${this.escapeId(key)} = @p${values.length}`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
return [...logicalParts, ...fieldParts].join(' AND ');
|
|
1284
|
+
};
|
|
1285
|
+
const sql = buildCondition(where);
|
|
1286
|
+
return { sql, values };
|
|
1287
|
+
}
|
|
1288
|
+
buildOrderClause(order, _options) {
|
|
1289
|
+
if (!order || (Array.isArray(order) && order.length === 0))
|
|
1290
|
+
return '';
|
|
1291
|
+
const orderParts = [];
|
|
1292
|
+
const processOrder = (orderItem) => {
|
|
1293
|
+
if (typeof orderItem === 'string') {
|
|
1294
|
+
const parts = orderItem.split(' ');
|
|
1295
|
+
if (parts.length === 1) {
|
|
1296
|
+
orderParts.push(this.escapeId(parts[0]));
|
|
1297
|
+
}
|
|
1298
|
+
else {
|
|
1299
|
+
orderParts.push(`${this.escapeId(parts[0])} ${parts[1].toUpperCase()}`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
else if (typeof orderItem === 'object' && orderItem !== null) {
|
|
1303
|
+
for (const [key, value] of Object.entries(orderItem)) {
|
|
1304
|
+
if (key === '$raw') {
|
|
1305
|
+
orderParts.push(String(value));
|
|
1306
|
+
}
|
|
1307
|
+
else {
|
|
1308
|
+
const direction = value?.toUpperCase() || 'ASC';
|
|
1309
|
+
orderParts.push(`${this.escapeId(key)} ${direction}`);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
if (Array.isArray(order)) {
|
|
1315
|
+
order.forEach(processOrder);
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
processOrder(order);
|
|
1319
|
+
}
|
|
1320
|
+
return orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
|
|
1321
|
+
}
|
|
1322
|
+
buildLimitOffset(limit, offset, hasOrderBy = false) {
|
|
1323
|
+
let sql = '';
|
|
1324
|
+
if (offset !== undefined || limit !== undefined) {
|
|
1325
|
+
// SQL Server requires an ORDER BY clause to precede OFFSET/FETCH — without
|
|
1326
|
+
// one, SQL Server raises "The OFFSET clause is not allowed... unless
|
|
1327
|
+
// ORDER BY is also specified." Callers that know they already emitted an
|
|
1328
|
+
// ORDER BY should pass hasOrderBy=true; otherwise we inject a harmless
|
|
1329
|
+
// no-op ORDER BY so pagination still works instead of failing at runtime.
|
|
1330
|
+
if (!hasOrderBy) {
|
|
1331
|
+
sql += ' ORDER BY (SELECT NULL)';
|
|
1332
|
+
}
|
|
1333
|
+
// SQL Server requires OFFSET to precede FETCH; default to 0 rows skipped
|
|
1334
|
+
// when only a limit is supplied.
|
|
1335
|
+
sql += ` OFFSET ${Number(offset ?? 0)} ROWS`;
|
|
1336
|
+
}
|
|
1337
|
+
if (limit !== undefined) {
|
|
1338
|
+
sql += ` FETCH NEXT ${Number(limit)} ROWS ONLY`;
|
|
1339
|
+
}
|
|
1340
|
+
return sql;
|
|
1341
|
+
}
|
|
1342
|
+
buildInsertQuery(tableName, values, options) {
|
|
1343
|
+
const columns = Object.keys(values);
|
|
1344
|
+
const processedValues = [];
|
|
1345
|
+
const placeholders = [];
|
|
1346
|
+
for (const value of Object.values(values)) {
|
|
1347
|
+
if (value instanceof prorm_1.Literal) {
|
|
1348
|
+
placeholders.push(value.val);
|
|
1349
|
+
}
|
|
1350
|
+
else {
|
|
1351
|
+
processedValues.push(value);
|
|
1352
|
+
placeholders.push(`@p${processedValues.length}`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
let sql = `INSERT INTO ${this.escapeId(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${placeholders.join(', ')})`;
|
|
1356
|
+
if (options?.returning) {
|
|
1357
|
+
if (options.returning === true) {
|
|
1358
|
+
sql += ' OUTPUT INSERTED.*';
|
|
1359
|
+
}
|
|
1360
|
+
else if (Array.isArray(options.returning)) {
|
|
1361
|
+
sql += ` OUTPUT INSERTED.${options.returning.map((c) => this.escapeId(c)).join(', INSERTED.')}`;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return { sql, values: processedValues };
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Execute a single-row INSERT, optionally toggling `IDENTITY_INSERT` for
|
|
1368
|
+
* tables where an explicit value is being supplied for an IDENTITY column
|
|
1369
|
+
* (see `MSSQLInsertOptions.identityInsert`). Unlike `buildInsertQuery`
|
|
1370
|
+
* (which only builds SQL text), this method actually runs the query, which
|
|
1371
|
+
* is required to bracket it with `SET IDENTITY_INSERT ... ON/OFF`.
|
|
1372
|
+
*/
|
|
1373
|
+
async insert(tableName, values, options) {
|
|
1374
|
+
const { sql, values: queryValues } = this.buildInsertQuery(tableName, values, options);
|
|
1375
|
+
if (!options?.identityInsert) {
|
|
1376
|
+
return this.query(sql, { replacements: queryValues });
|
|
1377
|
+
}
|
|
1378
|
+
const table = this.quoteTable(tableName, options?.schema);
|
|
1379
|
+
await this.query(`SET IDENTITY_INSERT ${table} ON`);
|
|
1380
|
+
try {
|
|
1381
|
+
return await this.query(sql, { replacements: queryValues });
|
|
1382
|
+
}
|
|
1383
|
+
finally {
|
|
1384
|
+
// Always turn IDENTITY_INSERT back off, even on failure, since it's a
|
|
1385
|
+
// session-level setting and only one table per session may have it on.
|
|
1386
|
+
await this.query(`SET IDENTITY_INSERT ${table} OFF`);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
buildUpdateQuery(tableName, values, where, options) {
|
|
1390
|
+
const processedValues = [];
|
|
1391
|
+
const setClauses = [];
|
|
1392
|
+
for (const [key, value] of Object.entries(values)) {
|
|
1393
|
+
if (value instanceof prorm_1.Literal) {
|
|
1394
|
+
setClauses.push(`${this.escapeId(key)} = ${value.val}`);
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
processedValues.push(value);
|
|
1398
|
+
setClauses.push(`${this.escapeId(key)} = @p${processedValues.length}`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
let sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')}`;
|
|
1402
|
+
if (where && Object.keys(where).length > 0) {
|
|
1403
|
+
const whereClause = this.buildWhereClause(where);
|
|
1404
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1405
|
+
processedValues.push(...whereClause.values);
|
|
1406
|
+
}
|
|
1407
|
+
if (options?.limit) {
|
|
1408
|
+
sql = sql.replace('UPDATE', `UPDATE TOP(${options.limit})`);
|
|
1409
|
+
}
|
|
1410
|
+
if (options?.returning) {
|
|
1411
|
+
if (options.returning === true) {
|
|
1412
|
+
sql += ' OUTPUT INSERTED.*';
|
|
1413
|
+
}
|
|
1414
|
+
else if (Array.isArray(options.returning)) {
|
|
1415
|
+
sql += ` OUTPUT INSERTED.${options.returning.map((c) => this.escapeId(c)).join(', INSERTED.')}`;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return { sql, values: processedValues };
|
|
1419
|
+
}
|
|
1420
|
+
buildDeleteQuery(tableName, where, options) {
|
|
1421
|
+
let sql = `DELETE FROM ${this.escapeId(tableName)}`;
|
|
1422
|
+
const processedValues = [];
|
|
1423
|
+
if (where && Object.keys(where).length > 0) {
|
|
1424
|
+
const whereClause = this.buildWhereClause(where);
|
|
1425
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1426
|
+
processedValues.push(...whereClause.values);
|
|
1427
|
+
}
|
|
1428
|
+
if (options?.limit) {
|
|
1429
|
+
sql = sql.replace('DELETE', `DELETE TOP(${options.limit})`);
|
|
1430
|
+
}
|
|
1431
|
+
if (options?.returning) {
|
|
1432
|
+
if (options.returning === true) {
|
|
1433
|
+
sql += ' OUTPUT DELETED.*';
|
|
1434
|
+
}
|
|
1435
|
+
else if (Array.isArray(options.returning)) {
|
|
1436
|
+
sql += ` OUTPUT DELETED.${options.returning.map((c) => this.escapeId(c)).join(', DELETED.')}`;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return { sql, values: processedValues };
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Render one CTE definition as `name [(cols)] AS (query)`.
|
|
1443
|
+
*/
|
|
1444
|
+
buildCteDefinition(cte) {
|
|
1445
|
+
const nameSql = this.escapeId(cte.name);
|
|
1446
|
+
const columnsSql = cte.columns && cte.columns.length > 0
|
|
1447
|
+
? ` (${cte.columns.map((c) => this.escapeId(c)).join(', ')})`
|
|
1448
|
+
: '';
|
|
1449
|
+
return `${nameSql}${columnsSql} AS (${cte.query})`;
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Build the `WITH cte1 AS (...), cte2 AS (...)` prefix for a SELECT.
|
|
1453
|
+
* T-SQL (unlike Postgres/SQLite) never uses a `RECURSIVE` keyword — even
|
|
1454
|
+
* recursive CTEs are introduced with plain `WITH`. The `recursive` flag on
|
|
1455
|
+
* each definition is accepted for API parity with other dialects but does
|
|
1456
|
+
* not change the emitted SQL.
|
|
1457
|
+
*/
|
|
1458
|
+
buildCteClause(ctes) {
|
|
1459
|
+
if (!ctes || ctes.length === 0)
|
|
1460
|
+
return '';
|
|
1461
|
+
return `WITH ${ctes.map((c) => this.buildCteDefinition(c)).join(', ')} `;
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Format a single SELECT-list attribute, handling plain column names as
|
|
1465
|
+
* well as the `[expr, alias]` tuple / `col()` / `fn()` / `literal()`
|
|
1466
|
+
* expression shapes produced by helpers such as the window-function
|
|
1467
|
+
* builders in `src/query-builders/functions/window.ts` (`.as('alias')`
|
|
1468
|
+
* returns `[{ __type: 'literal', sql }, alias]`).
|
|
1469
|
+
*/
|
|
1470
|
+
formatAttribute(attr) {
|
|
1471
|
+
if (attr === '*')
|
|
1472
|
+
return '*';
|
|
1473
|
+
if (Array.isArray(attr)) {
|
|
1474
|
+
const [expr, alias] = attr;
|
|
1475
|
+
const exprSql = this.formatAttributeExpr(expr);
|
|
1476
|
+
return alias ? `${exprSql} AS ${this.escapeId(String(alias))}` : exprSql;
|
|
1477
|
+
}
|
|
1478
|
+
return this.formatAttributeExpr(attr);
|
|
1479
|
+
}
|
|
1480
|
+
formatAttributeExpr(expr) {
|
|
1481
|
+
if (expr !== null && typeof expr === 'object') {
|
|
1482
|
+
const e = expr;
|
|
1483
|
+
if (e.__type === 'literal' || '$literal' in e) {
|
|
1484
|
+
return (e.sql ?? e.$literal);
|
|
1485
|
+
}
|
|
1486
|
+
if (e.__type === 'col' && e.col) {
|
|
1487
|
+
return this.escapeId(e.col);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (typeof expr === 'string') {
|
|
1491
|
+
return this.escapeId(expr);
|
|
1492
|
+
}
|
|
1493
|
+
return String(expr);
|
|
1494
|
+
}
|
|
1495
|
+
buildSelectQuery(options) {
|
|
1496
|
+
// Captured so the TOP-clause insertion below can be scoped to the outer
|
|
1497
|
+
// query only — a CTE body is itself a SELECT statement, so a blind
|
|
1498
|
+
// `sql.replace('SELECT', ...)` over the whole accumulated string would
|
|
1499
|
+
// otherwise splice TOP(n) into the CTE's inner SELECT instead of the
|
|
1500
|
+
// outer one whenever a `cte`/`with` option is present.
|
|
1501
|
+
const cteClause = this.buildCteClause(options.cte || options.with);
|
|
1502
|
+
let sql = cteClause + 'SELECT';
|
|
1503
|
+
const queryValues = [];
|
|
1504
|
+
if (options.distinct)
|
|
1505
|
+
sql += ' DISTINCT';
|
|
1506
|
+
if (options.attributes) {
|
|
1507
|
+
if (Array.isArray(options.attributes)) {
|
|
1508
|
+
sql += ` ${options.attributes.map((c) => this.formatAttribute(c)).join(', ')}`;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
else {
|
|
1512
|
+
sql += ' *';
|
|
1513
|
+
}
|
|
1514
|
+
const tableName = this.quoteTable(options.tableName, options.schema);
|
|
1515
|
+
sql += ` FROM ${tableName}`;
|
|
1516
|
+
// SQL Server system-versioned temporal table clause. Must immediately
|
|
1517
|
+
// follow the table reference, before any JOINs/WHERE/etc.
|
|
1518
|
+
if (options.temporalAsOf) {
|
|
1519
|
+
sql += ` FOR SYSTEM_TIME AS OF ${this.escape(options.temporalAsOf)}`;
|
|
1520
|
+
}
|
|
1521
|
+
else if (options.temporalBetween) {
|
|
1522
|
+
const [start, end] = options.temporalBetween;
|
|
1523
|
+
sql += ` FOR SYSTEM_TIME BETWEEN ${this.escape(start)} AND ${this.escape(end)}`;
|
|
1524
|
+
}
|
|
1525
|
+
else if (options.temporalAll) {
|
|
1526
|
+
sql += ' FOR SYSTEM_TIME ALL';
|
|
1527
|
+
}
|
|
1528
|
+
if (options.include && options.include.length > 0) {
|
|
1529
|
+
for (const include of options.include) {
|
|
1530
|
+
// CROSS APPLY / OUTER APPLY — a lateral join, needed for anything an
|
|
1531
|
+
// ordinary JOIN can't express: invoking a table-valued function (or a
|
|
1532
|
+
// correlated subquery that references outer columns) once per outer
|
|
1533
|
+
// row. This is also how OPENJSON's row-shredding output gets joined
|
|
1534
|
+
// back to the driving table on a per-row basis.
|
|
1535
|
+
const includeAny = include;
|
|
1536
|
+
if (includeAny.apply) {
|
|
1537
|
+
const applyType = String(includeAny.apply).toUpperCase() === 'OUTER' ? 'OUTER APPLY' : 'CROSS APPLY';
|
|
1538
|
+
const applySource = includeAny.tableFunction || includeAny.subquery;
|
|
1539
|
+
if (!applySource) {
|
|
1540
|
+
throw new Error('APPLY join requires a "tableFunction" or "subquery" expression on the include (e.g. include.tableFunction = "OPENJSON(t.data)")');
|
|
1541
|
+
}
|
|
1542
|
+
const aliasSql = include.as ? ` AS ${this.escapeId(include.as)}` : '';
|
|
1543
|
+
sql += ` ${applyType} ${applySource}${aliasSql}`;
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
const joinType = include.required ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1547
|
+
const joinModel = include.model;
|
|
1548
|
+
const joinTableName = joinModel.tableName || joinModel.name || '';
|
|
1549
|
+
const joinAlias = include.as || joinTableName;
|
|
1550
|
+
sql += ` ${joinType} ${this.quoteTable(joinTableName)} AS ${this.escapeId(joinAlias)}`;
|
|
1551
|
+
if (include.on) {
|
|
1552
|
+
const onClause = this.buildWhereClause(include.on);
|
|
1553
|
+
sql += ` ON ${onClause.sql}`;
|
|
1554
|
+
queryValues.push(...onClause.values);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (options.where && Object.keys(options.where).length > 0) {
|
|
1559
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
1560
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1561
|
+
queryValues.push(...whereClause.values);
|
|
1562
|
+
}
|
|
1563
|
+
if (options.group) {
|
|
1564
|
+
const groupBy = Array.isArray(options.group) ? options.group : [options.group];
|
|
1565
|
+
sql += ` GROUP BY ${groupBy.map((g) => this.escapeId(g)).join(', ')}`;
|
|
1566
|
+
if (options.having && Object.keys(options.having).length > 0) {
|
|
1567
|
+
const havingClause = this.buildWhereClause(options.having);
|
|
1568
|
+
sql += ` HAVING ${havingClause.sql}`;
|
|
1569
|
+
queryValues.push(...havingClause.values);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
let orderClause = '';
|
|
1573
|
+
if (options.order) {
|
|
1574
|
+
orderClause = this.buildOrderClause(options.order);
|
|
1575
|
+
if (orderClause)
|
|
1576
|
+
sql += ` ${orderClause}`;
|
|
1577
|
+
}
|
|
1578
|
+
// SQL Server does not allow TOP and OFFSET/FETCH in the same SELECT statement.
|
|
1579
|
+
// Use TOP for a simple limit with no offset; use OFFSET/FETCH for pagination.
|
|
1580
|
+
if (options.offset !== undefined) {
|
|
1581
|
+
sql += this.buildLimitOffset(options.limit, options.offset, !!orderClause);
|
|
1582
|
+
}
|
|
1583
|
+
else if (options.limit !== undefined) {
|
|
1584
|
+
// T-SQL requires TOP to come after DISTINCT: `SELECT DISTINCT TOP(n) ...`,
|
|
1585
|
+
// not `SELECT TOP(n) DISTINCT ...`. A blind `sql.replace('SELECT', ...)`
|
|
1586
|
+
// would insert TOP before DISTINCT since 'SELECT' is matched first.
|
|
1587
|
+
const topClause = `TOP(${Number(options.limit)})`;
|
|
1588
|
+
// Scope the replace to everything after the CTE prefix (see comment
|
|
1589
|
+
// above `cteClause`) so a `SELECT` inside a CTE body never gets matched
|
|
1590
|
+
// instead of the outer query's own SELECT.
|
|
1591
|
+
const head = sql.slice(0, cteClause.length);
|
|
1592
|
+
const rest = sql.slice(cteClause.length);
|
|
1593
|
+
const newRest = options.distinct
|
|
1594
|
+
? rest.replace('SELECT DISTINCT', `SELECT DISTINCT ${topClause}`)
|
|
1595
|
+
: rest.replace('SELECT', `SELECT ${topClause}`);
|
|
1596
|
+
sql = head + newRest;
|
|
1597
|
+
}
|
|
1598
|
+
// `FOR JSON` must be the last clause in the statement — always append it
|
|
1599
|
+
// after TOP/OFFSET-FETCH have already been spliced in above.
|
|
1600
|
+
if (options.forJson) {
|
|
1601
|
+
const { mode, root, includeNullValues, withoutArrayWrapper } = options.forJson;
|
|
1602
|
+
let forJsonSql = `FOR JSON ${mode}`;
|
|
1603
|
+
const jsonOptions = [];
|
|
1604
|
+
if (root)
|
|
1605
|
+
jsonOptions.push(`ROOT('${root.replace(/'/g, "''")}')`);
|
|
1606
|
+
if (includeNullValues)
|
|
1607
|
+
jsonOptions.push('INCLUDE_NULL_VALUES');
|
|
1608
|
+
if (withoutArrayWrapper)
|
|
1609
|
+
jsonOptions.push('WITHOUT_ARRAY_WRAPPER');
|
|
1610
|
+
if (jsonOptions.length > 0)
|
|
1611
|
+
forJsonSql += `, ${jsonOptions.join(', ')}`;
|
|
1612
|
+
sql += ` ${forJsonSql}`;
|
|
1613
|
+
}
|
|
1614
|
+
return { sql, values: queryValues };
|
|
1615
|
+
}
|
|
1616
|
+
buildUpsertQuery(tableName, values, options) {
|
|
1617
|
+
const columns = Object.keys(values);
|
|
1618
|
+
const queryValues = Object.values(values);
|
|
1619
|
+
const updateFields = options?.updateOnDuplicate && options.updateOnDuplicate.length > 0
|
|
1620
|
+
? options.updateOnDuplicate
|
|
1621
|
+
: columns;
|
|
1622
|
+
const conflictFields = options?.conflictFields || columns;
|
|
1623
|
+
const sourceValues = columns.map((c, i) => `@p${i + 1} AS ${this.escapeId(c)}`).join(', ');
|
|
1624
|
+
const onClauses = conflictFields
|
|
1625
|
+
.map((c) => `target.${this.escapeId(c)} = source.${this.escapeId(c)}`)
|
|
1626
|
+
.join(' AND ');
|
|
1627
|
+
const updateSet = updateFields
|
|
1628
|
+
.map((c) => `${this.escapeId(c)} = source.${this.escapeId(c)}`)
|
|
1629
|
+
.join(', ');
|
|
1630
|
+
const insertColumns = columns.map((c) => this.escapeId(c)).join(', ');
|
|
1631
|
+
const insertValues = columns.map((c) => `source.${this.escapeId(c)}`).join(', ');
|
|
1632
|
+
let sql = `MERGE INTO ${this.escapeId(tableName)} AS target USING (SELECT ${sourceValues}) AS source ON ${onClauses} WHEN MATCHED THEN UPDATE SET ${updateSet} WHEN NOT MATCHED THEN INSERT (${insertColumns}) VALUES (${insertValues})`;
|
|
1633
|
+
if (options?.returning) {
|
|
1634
|
+
if (options.returning === true) {
|
|
1635
|
+
sql += ' OUTPUT INSERTED.*';
|
|
1636
|
+
}
|
|
1637
|
+
else if (Array.isArray(options.returning)) {
|
|
1638
|
+
sql += ` OUTPUT INSERTED.${options.returning.map((c) => this.escapeId(c)).join(', INSERTED.')}`;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
// SQL Server requires MERGE statements to be terminated with a semicolon.
|
|
1642
|
+
sql += ';';
|
|
1643
|
+
return { sql, values: queryValues };
|
|
1644
|
+
}
|
|
1645
|
+
buildIncrementQuery(tableName, fields, where, options) {
|
|
1646
|
+
const by = options?.by ?? 1;
|
|
1647
|
+
const queryValues = [];
|
|
1648
|
+
let setClauses;
|
|
1649
|
+
if (typeof fields === 'string') {
|
|
1650
|
+
queryValues.push(by);
|
|
1651
|
+
setClauses = [`${this.escapeId(fields)} = ${this.escapeId(fields)} + @p1`];
|
|
1652
|
+
}
|
|
1653
|
+
else if (Array.isArray(fields)) {
|
|
1654
|
+
setClauses = fields.map((field, index) => {
|
|
1655
|
+
queryValues.push(by);
|
|
1656
|
+
return `${this.escapeId(field)} = ${this.escapeId(field)} + @p${index + 1}`;
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
else {
|
|
1660
|
+
setClauses = Object.entries(fields).map(([field, value], index) => {
|
|
1661
|
+
queryValues.push(value);
|
|
1662
|
+
return `${this.escapeId(field)} = ${this.escapeId(field)} + @p${index + 1}`;
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
let sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')}`;
|
|
1666
|
+
if (where && Object.keys(where).length > 0) {
|
|
1667
|
+
const whereClause = this.buildWhereClause(where);
|
|
1668
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1669
|
+
queryValues.push(...whereClause.values);
|
|
1670
|
+
}
|
|
1671
|
+
return { sql, values: queryValues };
|
|
1672
|
+
}
|
|
1673
|
+
buildJsonExtract(column, path, asText = true) {
|
|
1674
|
+
const columnRef = this.escapeId(column);
|
|
1675
|
+
const normalizedPath = path.startsWith('$.') ? path : `$.${path}`;
|
|
1676
|
+
if (asText)
|
|
1677
|
+
return `JSON_VALUE(${columnRef}, '${normalizedPath}')`;
|
|
1678
|
+
return `JSON_QUERY(${columnRef}, '${normalizedPath}')`;
|
|
1679
|
+
}
|
|
1680
|
+
// Foreign Data Wrapper stubs - not supported in MSSQL
|
|
1681
|
+
buildCreateServerQuery(_name, _opts) {
|
|
1682
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1683
|
+
}
|
|
1684
|
+
buildAlterServerQuery(_name, _opts) {
|
|
1685
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1686
|
+
}
|
|
1687
|
+
buildDropServerQuery(_name, _opts) {
|
|
1688
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1689
|
+
}
|
|
1690
|
+
buildCreateUserMappingQuery(_opts) {
|
|
1691
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1692
|
+
}
|
|
1693
|
+
buildAlterUserMappingQuery(_opts) {
|
|
1694
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1695
|
+
}
|
|
1696
|
+
buildDropUserMappingQuery(_serverName, _user, _opts) {
|
|
1697
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1698
|
+
}
|
|
1699
|
+
buildCreateForeignTableQuery(_tableName, _opts) {
|
|
1700
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1701
|
+
}
|
|
1702
|
+
buildDropForeignTableQuery(_tableName, _opts) {
|
|
1703
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1704
|
+
}
|
|
1705
|
+
buildImportForeignSchemaQuery(_remoteSchema, _serverName, _opts) {
|
|
1706
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1707
|
+
}
|
|
1708
|
+
getServersQuery() {
|
|
1709
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1710
|
+
}
|
|
1711
|
+
// Foreign Data Wrapper stubs - not supported in MSSQL (async methods)
|
|
1712
|
+
async createForeignDataWrapper(_fdwName, _options) {
|
|
1713
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1714
|
+
}
|
|
1715
|
+
async dropForeignDataWrapper(_fdwName, _options) {
|
|
1716
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1717
|
+
}
|
|
1718
|
+
async createForeignServer(_serverName, _fdwName, _options) {
|
|
1719
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1720
|
+
}
|
|
1721
|
+
async dropForeignServer(_serverName, _options) {
|
|
1722
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1723
|
+
}
|
|
1724
|
+
async createForeignTable(_tableName, _columns, _options) {
|
|
1725
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1726
|
+
}
|
|
1727
|
+
async createUserMapping(_serverName, _userName, _options) {
|
|
1728
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1729
|
+
}
|
|
1730
|
+
async dropUserMapping(_serverName, _userName, _options) {
|
|
1731
|
+
throw new Error('Foreign Data Wrappers are not supported by MSSQL. This is a PostgreSQL-specific feature. Consider using MSSQL linked servers (sp_addlinkedserver) instead.');
|
|
1732
|
+
}
|
|
1733
|
+
async changeOwner(_newOwner, _tableName) {
|
|
1734
|
+
throw new Error('changeOwner is not supported in MSSQL');
|
|
1735
|
+
}
|
|
1736
|
+
async addConstraint(_tableName, _options) {
|
|
1737
|
+
throw new Error('addConstraint is not supported in MSSQL');
|
|
1738
|
+
}
|
|
1739
|
+
async removeConstraint(_tableName, _constraintName) {
|
|
1740
|
+
throw new Error('removeConstraint is not supported in MSSQL');
|
|
1741
|
+
}
|
|
1742
|
+
async createSecurityPolicy(policyName, tableName, options) {
|
|
1743
|
+
const schema = options?.schema || 'dbo';
|
|
1744
|
+
// A security predicate is a function-call expression (e.g.
|
|
1745
|
+
// "dbo.fn_security_predicate(col)"), not an identifier — bracket-escaping
|
|
1746
|
+
// it with escapeId() would corrupt the SQL (e.g. turn it into the
|
|
1747
|
+
// meaningless identifier "[dbo.fn_security_predicate(col)]"). It must be
|
|
1748
|
+
// spliced in raw, the same way createPolicy() handles predicateFunction.
|
|
1749
|
+
const predicate = options?.predicate || '(1=1)';
|
|
1750
|
+
const sql = `CREATE SECURITY POLICY ${this.escapeId(policyName)} ADD FILTER PREDICATE ${predicate} ON ${this.escapeId(schema)}.${this.escapeId(tableName)}`;
|
|
1751
|
+
await this.query(sql);
|
|
1752
|
+
}
|
|
1753
|
+
async dropSecurityPolicy(policyName, tableName) {
|
|
1754
|
+
const sql = `DROP SECURITY POLICY ${this.escapeId(policyName)}`;
|
|
1755
|
+
await this.query(sql);
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Compute the maximum number of rows per multi-row `INSERT ... VALUES`
|
|
1759
|
+
* statement for a table with `columnsPerRow` columns, respecting two
|
|
1760
|
+
* distinct T-SQL limits:
|
|
1761
|
+
* - A table-value constructor (`VALUES (...), (...), ...`) is capped at
|
|
1762
|
+
* 1,000 rows.
|
|
1763
|
+
* - A single parameterized batch/RPC is capped at 2,100 parameters.
|
|
1764
|
+
* The smaller of the two (per row) wins.
|
|
1765
|
+
*/
|
|
1766
|
+
maxBulkInsertRowsPerBatch(columnsPerRow) {
|
|
1767
|
+
const paramLimited = Math.floor(2100 / Math.max(1, columnsPerRow));
|
|
1768
|
+
return Math.max(1, Math.min(1000, paramLimited));
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Bulk insert records into a table.
|
|
1772
|
+
*
|
|
1773
|
+
* T-SQL caps a single `INSERT ... VALUES (...), (...), ...` statement at
|
|
1774
|
+
* 1,000 rows (table-value constructor limit) and 2,100 parameters per
|
|
1775
|
+
* batch. For batches that would exceed either limit, this splits the
|
|
1776
|
+
* records into multiple INSERT statements, executed inside a single
|
|
1777
|
+
* `BEGIN TRANSACTION` / `COMMIT TRANSACTION` so the whole call stays
|
|
1778
|
+
* atomic (SQL Server nests transactions via `@@TRANCOUNT`, so this is safe
|
|
1779
|
+
* even if the caller already has an outer transaction open — only the
|
|
1780
|
+
* outermost COMMIT actually commits, and a ROLLBACK at any nesting level
|
|
1781
|
+
* rolls back the entire transaction, mirroring the BEGIN/COMMIT/ROLLBACK
|
|
1782
|
+
* TRANSACTION pattern already used by `startTransaction`/`commitTransaction`
|
|
1783
|
+
* /`rollbackTransaction` on this dialect).
|
|
1784
|
+
*/
|
|
1785
|
+
async bulkInsert(tableName, records, options) {
|
|
1786
|
+
if (records.length === 0) {
|
|
1787
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
1788
|
+
}
|
|
1789
|
+
const columns = Object.keys(records[0]);
|
|
1790
|
+
const table = this.quoteTable(tableName, options?.schema);
|
|
1791
|
+
const columnList = columns.map((c) => this.escapeId(c)).join(', ');
|
|
1792
|
+
const safeMaxRows = this.maxBulkInsertRowsPerBatch(columns.length);
|
|
1793
|
+
const rowsPerBatch = options?.batchSize && options.batchSize > 0
|
|
1794
|
+
? Math.min(options.batchSize, safeMaxRows)
|
|
1795
|
+
: safeMaxRows;
|
|
1796
|
+
const chunks = [];
|
|
1797
|
+
for (let i = 0; i < records.length; i += rowsPerBatch) {
|
|
1798
|
+
chunks.push(records.slice(i, i + rowsPerBatch));
|
|
1799
|
+
}
|
|
1800
|
+
const buildChunkSql = (chunk) => {
|
|
1801
|
+
const values = [];
|
|
1802
|
+
const placeholders = [];
|
|
1803
|
+
for (const record of chunk) {
|
|
1804
|
+
const rowPlaceholders = [];
|
|
1805
|
+
for (const column of columns) {
|
|
1806
|
+
rowPlaceholders.push(`@p${values.length + 1}`);
|
|
1807
|
+
values.push(record[column]);
|
|
1808
|
+
}
|
|
1809
|
+
placeholders.push(`(${rowPlaceholders.join(', ')})`);
|
|
1810
|
+
}
|
|
1811
|
+
let sql = `INSERT INTO ${table} (${columnList}) VALUES ${placeholders.join(', ')}`;
|
|
1812
|
+
if (options?.returning) {
|
|
1813
|
+
if (options.returning === true) {
|
|
1814
|
+
sql += ' OUTPUT INSERTED.*';
|
|
1815
|
+
}
|
|
1816
|
+
else if (Array.isArray(options.returning)) {
|
|
1817
|
+
sql += ` OUTPUT INSERTED.${options.returning.map((c) => this.escapeId(c)).join(', INSERTED.')}`;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return { sql, values };
|
|
1821
|
+
};
|
|
1822
|
+
const aggregated = { rows: [], rowCount: 0, fields: [] };
|
|
1823
|
+
const needsTransaction = chunks.length > 1;
|
|
1824
|
+
const identityInsert = options?.identityInsert === true;
|
|
1825
|
+
if (identityInsert) {
|
|
1826
|
+
await this.query(`SET IDENTITY_INSERT ${table} ON`);
|
|
1827
|
+
}
|
|
1828
|
+
try {
|
|
1829
|
+
if (needsTransaction) {
|
|
1830
|
+
await this.query('BEGIN TRANSACTION');
|
|
1831
|
+
}
|
|
1832
|
+
try {
|
|
1833
|
+
for (const chunk of chunks) {
|
|
1834
|
+
const { sql, values } = buildChunkSql(chunk);
|
|
1835
|
+
const result = await this.query(sql, { replacements: values });
|
|
1836
|
+
aggregated.rows.push(...result.rows);
|
|
1837
|
+
aggregated.rowCount += result.rowCount;
|
|
1838
|
+
if (result.fields.length > 0 && aggregated.fields.length === 0) {
|
|
1839
|
+
aggregated.fields = result.fields;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
if (needsTransaction) {
|
|
1843
|
+
await this.query('COMMIT TRANSACTION');
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
catch (error) {
|
|
1847
|
+
if (needsTransaction) {
|
|
1848
|
+
await this.query('ROLLBACK TRANSACTION');
|
|
1849
|
+
}
|
|
1850
|
+
throw error;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
finally {
|
|
1854
|
+
if (identityInsert) {
|
|
1855
|
+
// Always turn IDENTITY_INSERT back off, even on failure, since it's
|
|
1856
|
+
// a session-level setting and only one table per session may have
|
|
1857
|
+
// it on at a time.
|
|
1858
|
+
await this.query(`SET IDENTITY_INSERT ${table} OFF`);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
return aggregated;
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Add a foreign key to a table
|
|
1865
|
+
*/
|
|
1866
|
+
async addForeignKey(tableName, columnName, referencedTableName, referencedColumnName, options) {
|
|
1867
|
+
const constraintName = options?.name || `${tableName}_${columnName}_fkey`;
|
|
1868
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} FOREIGN KEY (${this.escapeId(columnName)}) REFERENCES ${this.quoteTable(referencedTableName)}(${this.escapeId(referencedColumnName)})`;
|
|
1869
|
+
const clauses = [];
|
|
1870
|
+
if (options?.onDelete) {
|
|
1871
|
+
clauses.push(`ON DELETE ${options.onDelete}`);
|
|
1872
|
+
}
|
|
1873
|
+
if (options?.onUpdate) {
|
|
1874
|
+
clauses.push(`ON UPDATE ${options.onUpdate}`);
|
|
1875
|
+
}
|
|
1876
|
+
if (clauses.length > 0) {
|
|
1877
|
+
sql += ' ' + clauses.join(' ');
|
|
1878
|
+
}
|
|
1879
|
+
await this.query(sql);
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Rename a column
|
|
1883
|
+
*/
|
|
1884
|
+
async renameColumn(tableName, oldColumnName, newColumnName) {
|
|
1885
|
+
const sql = `EXEC sp_rename @objname = @objName, @newname = @newColumnName, @objtype = 'COLUMN'`;
|
|
1886
|
+
await this.query(sql, {
|
|
1887
|
+
bindings: { objName: `${tableName}.${oldColumnName}`, newColumnName },
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Create a fulltext index (MSSQL supports fulltext search).
|
|
1892
|
+
*
|
|
1893
|
+
* SQL Server fulltext indexes are always keyed off a pre-existing UNIQUE
|
|
1894
|
+
* (usually the primary key) index on the table — `keyIndexName` must name
|
|
1895
|
+
* that existing index, not the fulltext index itself (fulltext indexes
|
|
1896
|
+
* don't have their own name in T-SQL). Fulltext indexes also always belong
|
|
1897
|
+
* to a fulltext catalog; this method creates the catalog if it doesn't
|
|
1898
|
+
* already exist before issuing `CREATE FULLTEXT INDEX`.
|
|
1899
|
+
*
|
|
1900
|
+
* Real syntax produced:
|
|
1901
|
+
* CREATE FULLTEXT INDEX ON table (col1, col2) KEY INDEX <keyIndexName> ON <catalogName>
|
|
1902
|
+
*/
|
|
1903
|
+
async createFulltextIndex(tableName, keyIndexName, fields, options) {
|
|
1904
|
+
const catalogName = options?.catalogName || 'default_fulltext_catalog';
|
|
1905
|
+
if (options?.createCatalog !== false) {
|
|
1906
|
+
await this.query(`IF NOT EXISTS (SELECT 1 FROM sys.fulltext_catalogs WHERE name = '${catalogName}') CREATE FULLTEXT CATALOG ${this.escapeId(catalogName)}`);
|
|
1907
|
+
}
|
|
1908
|
+
const languageClause = options?.parser ? ` LANGUAGE ${options.parser}` : '';
|
|
1909
|
+
const columnList = fields.map((f) => `${this.escapeId(f)}${languageClause}`).join(', ');
|
|
1910
|
+
const sql = `CREATE FULLTEXT INDEX ON ${this.quoteTable(tableName)} (${columnList}) KEY INDEX ${this.escapeId(keyIndexName)} ON ${this.escapeId(catalogName)}`;
|
|
1911
|
+
await this.query(sql);
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Create a spatial index (MSSQL supports spatial indexes)
|
|
1915
|
+
*/
|
|
1916
|
+
async createSpatialIndex(tableName, indexName, fields, options) {
|
|
1917
|
+
const sql = `CREATE SPATIAL INDEX ${this.escapeId(indexName)} ON ${this.quoteTable(tableName)} (${fields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
1918
|
+
await this.query(sql);
|
|
1919
|
+
}
|
|
1920
|
+
// ---- User Management (MSSQL) ----
|
|
1921
|
+
/**
|
|
1922
|
+
* Build CREATE LOGIN and CREATE USER statements for MSSQL.
|
|
1923
|
+
* MSSQL uses LOGIN for server-level authentication and USER for database-level access.
|
|
1924
|
+
*/
|
|
1925
|
+
buildCreateUserQuery(username, options = {}) {
|
|
1926
|
+
const loginName = this.escapeId(username);
|
|
1927
|
+
const password = options.password || '';
|
|
1928
|
+
// CREATE LOGIN statement (server-level)
|
|
1929
|
+
let loginSql = 'CREATE LOGIN';
|
|
1930
|
+
if (options.ifNotExists) {
|
|
1931
|
+
loginSql = `IF NOT EXISTS (SELECT name FROM sys.server_principals WHERE name = '${username}') CREATE LOGIN`;
|
|
1932
|
+
}
|
|
1933
|
+
loginSql += ` ${loginName} WITH PASSWORD = '${this.escapePassword(password)}'`;
|
|
1934
|
+
// CREATE USER statement (database-level)
|
|
1935
|
+
const userSql = `CREATE USER ${this.escapeId(username)} FOR LOGIN ${loginName}`;
|
|
1936
|
+
return `${loginSql}; ${userSql}`;
|
|
1937
|
+
}
|
|
1938
|
+
escapePassword(password) {
|
|
1939
|
+
return password.replace(/'/g, "''");
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Build ALTER LOGIN and ALTER USER statements for MSSQL.
|
|
1943
|
+
*/
|
|
1944
|
+
buildAlterUserQuery(username, options) {
|
|
1945
|
+
const loginName = this.escapeId(username);
|
|
1946
|
+
let sql = `ALTER LOGIN ${loginName}`;
|
|
1947
|
+
if (options.password) {
|
|
1948
|
+
sql += ` WITH PASSWORD = '${this.escapePassword(options.password)}'`;
|
|
1949
|
+
}
|
|
1950
|
+
if (options.unlockAccount) {
|
|
1951
|
+
sql += ' UNLOCK';
|
|
1952
|
+
}
|
|
1953
|
+
else if (options.accountLocked) {
|
|
1954
|
+
sql += ' DISABLE';
|
|
1955
|
+
}
|
|
1956
|
+
return sql;
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Build DROP USER and DROP LOGIN statements for MSSQL.
|
|
1960
|
+
* Must drop USER first, then LOGIN (due to dependencies).
|
|
1961
|
+
*/
|
|
1962
|
+
buildDropUserQuery(username, options) {
|
|
1963
|
+
const userName = this.escapeId(username);
|
|
1964
|
+
const loginName = this.escapeId(username);
|
|
1965
|
+
let userSql = 'DROP USER';
|
|
1966
|
+
let loginSql = 'DROP LOGIN';
|
|
1967
|
+
if (options?.ifExists) {
|
|
1968
|
+
userSql = `IF EXISTS (SELECT name FROM sys.database_principals WHERE name = '${username}' AND type = 'U') DROP USER`;
|
|
1969
|
+
loginSql = `IF EXISTS (SELECT name FROM sys.server_principals WHERE name = '${username}') DROP LOGIN`;
|
|
1970
|
+
}
|
|
1971
|
+
// Must drop USER before LOGIN (database-level depends on server-level)
|
|
1972
|
+
return `${userSql} ${userName}; ${loginSql} ${loginName}`;
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Get list of users from MSSQL.
|
|
1976
|
+
*/
|
|
1977
|
+
getUsersQuery() {
|
|
1978
|
+
return `SELECT name AS user, type_desc AS type, create_date, modify_date
|
|
1979
|
+
FROM sys.database_principals
|
|
1980
|
+
WHERE type IN ('U', 'S')
|
|
1981
|
+
AND name NOT LIKE '#%'`;
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Build GRANT statement for MSSQL.
|
|
1985
|
+
*/
|
|
1986
|
+
buildGrantQuery(options) {
|
|
1987
|
+
const privileges = options.privileges.join(', ');
|
|
1988
|
+
let scopeSql;
|
|
1989
|
+
switch (options.on.level) {
|
|
1990
|
+
case 'global':
|
|
1991
|
+
scopeSql = '*.*';
|
|
1992
|
+
break;
|
|
1993
|
+
case 'database':
|
|
1994
|
+
scopeSql = `${this.escapeId(options.on.database)}.*`;
|
|
1995
|
+
break;
|
|
1996
|
+
case 'table': {
|
|
1997
|
+
const db = options.on.database ? `${this.escapeId(options.on.database)}.` : '';
|
|
1998
|
+
scopeSql = `${db}${this.escapeId(options.on.table)}`;
|
|
1999
|
+
break;
|
|
2000
|
+
}
|
|
2001
|
+
case 'column': {
|
|
2002
|
+
const dbCol = options.on.database ? `${this.escapeId(options.on.database)}.` : '';
|
|
2003
|
+
const cols = options.on.columns.map((c) => this.escapeId(c)).join(', ');
|
|
2004
|
+
scopeSql = `${dbCol}${this.escapeId(options.on.table)} (${cols})`;
|
|
2005
|
+
break;
|
|
2006
|
+
}
|
|
2007
|
+
case 'routine': {
|
|
2008
|
+
const dbRoutine = options.on.database ? `${this.escapeId(options.on.database)}.` : '';
|
|
2009
|
+
const routineType = options.on.routineType || 'PROCEDURE';
|
|
2010
|
+
scopeSql = `${routineType} ${dbRoutine}${this.escapeId(options.on.routine)}`;
|
|
2011
|
+
break;
|
|
2012
|
+
}
|
|
2013
|
+
default:
|
|
2014
|
+
scopeSql = '*.*';
|
|
2015
|
+
}
|
|
2016
|
+
const recipients = (Array.isArray(options.to) ? options.to : [options.to])
|
|
2017
|
+
.map((r) => this.escapeId(r))
|
|
2018
|
+
.join(', ');
|
|
2019
|
+
let sql = `GRANT ${privileges} ON ${scopeSql} TO ${recipients}`;
|
|
2020
|
+
if (options.withGrantOption) {
|
|
2021
|
+
sql += ' WITH GRANT OPTION';
|
|
2022
|
+
}
|
|
2023
|
+
return sql;
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Build REVOKE statement for MSSQL.
|
|
2027
|
+
*/
|
|
2028
|
+
buildRevokeQuery(options) {
|
|
2029
|
+
const privileges = options.privileges.join(', ');
|
|
2030
|
+
let scopeSql;
|
|
2031
|
+
switch (options.on.level) {
|
|
2032
|
+
case 'global':
|
|
2033
|
+
scopeSql = '*.*';
|
|
2034
|
+
break;
|
|
2035
|
+
case 'database':
|
|
2036
|
+
scopeSql = `${this.escapeId(options.on.database)}.*`;
|
|
2037
|
+
break;
|
|
2038
|
+
case 'table': {
|
|
2039
|
+
const db = options.on.database ? `${this.escapeId(options.on.database)}.` : '';
|
|
2040
|
+
scopeSql = `${db}${this.escapeId(options.on.table)}`;
|
|
2041
|
+
break;
|
|
2042
|
+
}
|
|
2043
|
+
case 'column': {
|
|
2044
|
+
const dbCol = options.on.database ? `${this.escapeId(options.on.database)}.` : '';
|
|
2045
|
+
const cols = options.on.columns.map((c) => this.escapeId(c)).join(', ');
|
|
2046
|
+
scopeSql = `${dbCol}${this.escapeId(options.on.table)} (${cols})`;
|
|
2047
|
+
break;
|
|
2048
|
+
}
|
|
2049
|
+
case 'routine': {
|
|
2050
|
+
const dbRoutine = options.on.database ? `${this.escapeId(options.on.database)}.` : '';
|
|
2051
|
+
const routineType = options.on.routineType || 'PROCEDURE';
|
|
2052
|
+
scopeSql = `${routineType} ${dbRoutine}${this.escapeId(options.on.routine)}`;
|
|
2053
|
+
break;
|
|
2054
|
+
}
|
|
2055
|
+
default:
|
|
2056
|
+
scopeSql = '*.*';
|
|
2057
|
+
}
|
|
2058
|
+
const targets = (Array.isArray(options.from) ? options.from : [options.from])
|
|
2059
|
+
.map((r) => this.escapeId(r))
|
|
2060
|
+
.join(', ');
|
|
2061
|
+
let sql;
|
|
2062
|
+
if (options.grantOptionFor) {
|
|
2063
|
+
sql = `REVOKE GRANT OPTION FOR ${privileges} ON ${scopeSql} FROM ${targets}`;
|
|
2064
|
+
}
|
|
2065
|
+
else {
|
|
2066
|
+
sql = `REVOKE ${privileges} ON ${scopeSql} FROM ${targets}`;
|
|
2067
|
+
}
|
|
2068
|
+
if (options.cascade) {
|
|
2069
|
+
sql += ' CASCADE';
|
|
2070
|
+
}
|
|
2071
|
+
if (options.restrict) {
|
|
2072
|
+
sql += ' RESTRICT';
|
|
2073
|
+
}
|
|
2074
|
+
return sql;
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Build SHOW GRANTS query for MSSQL.
|
|
2078
|
+
* Queries sys.database_permissions for database-level permissions.
|
|
2079
|
+
*/
|
|
2080
|
+
buildShowGrantsQuery(username, _host) {
|
|
2081
|
+
return `SELECT
|
|
2082
|
+
pr.name AS grantee,
|
|
2083
|
+
pr.type_desc AS grantee_type,
|
|
2084
|
+
perm.permission_name,
|
|
2085
|
+
perm.state_desc AS permission_state,
|
|
2086
|
+
OBJECT_NAME(perm.major_id) AS object_name,
|
|
2087
|
+
col_name(perm.major_id, perm.minor_id) AS column_name
|
|
2088
|
+
FROM sys.database_principals pr
|
|
2089
|
+
LEFT JOIN sys.database_permissions perm ON pr.principal_id = perm.grantee_principal_id
|
|
2090
|
+
WHERE pr.name = '${username}'
|
|
2091
|
+
ORDER BY perm.permission_name`;
|
|
2092
|
+
}
|
|
2093
|
+
buildFlushPrivilegesQuery() {
|
|
2094
|
+
// MSSQL does not have FLUSH PRIVILEGES - permissions are automatic
|
|
2095
|
+
return '-- MSSQL does not require FLUSH PRIVILEGES - permissions are automatically synced';
|
|
2096
|
+
}
|
|
2097
|
+
// ---- Roles (MSSQL) ----
|
|
2098
|
+
/**
|
|
2099
|
+
* Build CREATE ROLE statement for MSSQL.
|
|
2100
|
+
*/
|
|
2101
|
+
buildCreateRoleQuery(roleName, options = {}) {
|
|
2102
|
+
let sql = 'CREATE ROLE';
|
|
2103
|
+
if (options.ifNotExists) {
|
|
2104
|
+
sql = `IF NOT EXISTS (SELECT name FROM sys.database_principals WHERE name = '${roleName}' AND type = 'R') CREATE ROLE`;
|
|
2105
|
+
}
|
|
2106
|
+
sql += ` ${this.escapeId(roleName)}`;
|
|
2107
|
+
return sql;
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Build DROP ROLE statement for MSSQL.
|
|
2111
|
+
*/
|
|
2112
|
+
buildDropRoleQuery(roleName, options) {
|
|
2113
|
+
let sql = 'DROP ROLE';
|
|
2114
|
+
if (options?.ifExists) {
|
|
2115
|
+
sql = `IF EXISTS (SELECT name FROM sys.database_principals WHERE name = '${roleName}' AND type = 'R') DROP ROLE`;
|
|
2116
|
+
}
|
|
2117
|
+
sql += ` ${this.escapeId(roleName)}`;
|
|
2118
|
+
if (options?.cascade) {
|
|
2119
|
+
sql += ' CASCADE';
|
|
2120
|
+
}
|
|
2121
|
+
return sql;
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Build GRANT role statement for MSSQL.
|
|
2125
|
+
* Uses ALTER ROLE ADD MEMBER (MSSQL syntax).
|
|
2126
|
+
*/
|
|
2127
|
+
buildGrantRoleQuery(role, to, options) {
|
|
2128
|
+
const recipients = (Array.isArray(to) ? to : [to]).map((r) => this.escapeId(r)).join(', ');
|
|
2129
|
+
let sql = `ALTER ROLE ${this.escapeId(role)} ADD MEMBER ${recipients}`;
|
|
2130
|
+
if (options?.withAdminOption) {
|
|
2131
|
+
sql += ' WITH GRANT OPTION';
|
|
2132
|
+
}
|
|
2133
|
+
return sql;
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Build REVOKE role statement for MSSQL.
|
|
2137
|
+
* Uses ALTER ROLE DROP MEMBER (MSSQL syntax).
|
|
2138
|
+
*/
|
|
2139
|
+
buildRevokeRoleQuery(role, from, options) {
|
|
2140
|
+
const targets = (Array.isArray(from) ? from : [from]).map((r) => this.escapeId(r)).join(', ');
|
|
2141
|
+
let sql = `ALTER ROLE ${this.escapeId(role)} DROP MEMBER ${targets}`;
|
|
2142
|
+
if (options?.cascade) {
|
|
2143
|
+
sql += ' CASCADE';
|
|
2144
|
+
}
|
|
2145
|
+
return sql;
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Get roles query for MSSQL.
|
|
2149
|
+
*/
|
|
2150
|
+
getRolesQuery() {
|
|
2151
|
+
return `SELECT name AS role FROM sys.database_principals WHERE type = 'R' AND name NOT IN ('public', 'db_owner', 'db_accessadmin', 'db_securityadmin', 'db_ddladmin', 'db_backupoperator', 'db_datareader', 'db_datawriter', 'db_denydatareader', 'db_denydatawriter')`;
|
|
2152
|
+
}
|
|
2153
|
+
// Database creation/drop stubs
|
|
2154
|
+
createDatabaseSQL(options) {
|
|
2155
|
+
const parts = [];
|
|
2156
|
+
parts.push(`CREATE DATABASE ${this.quoteIdentifier(options.name)}`);
|
|
2157
|
+
if (options.collate)
|
|
2158
|
+
parts.push(`COLLATE = '${options.collate}'`);
|
|
2159
|
+
return parts.join(' ');
|
|
2160
|
+
}
|
|
2161
|
+
dropDatabaseSQL(name) {
|
|
2162
|
+
return `DROP DATABASE IF EXISTS ${this.quoteIdentifier(name)}`;
|
|
2163
|
+
}
|
|
2164
|
+
// Savepoint stubs
|
|
2165
|
+
// T-SQL has no `SAVEPOINT` keyword — the equivalent statement is
|
|
2166
|
+
// `SAVE TRANSACTION <name>` / `ROLLBACK TRANSACTION <name>`. There is also
|
|
2167
|
+
// no `RELEASE SAVEPOINT` concept in T-SQL: a savepoint created with
|
|
2168
|
+
// SAVE TRANSACTION is implicitly released when the outer transaction
|
|
2169
|
+
// commits or rolls back, so releaseSavepointSQL() is a documented no-op.
|
|
2170
|
+
createSavepointSQL(name) {
|
|
2171
|
+
const savepointName = name || `sp_${Date.now()}`;
|
|
2172
|
+
return `SAVE TRANSACTION ${savepointName}`;
|
|
2173
|
+
}
|
|
2174
|
+
releaseSavepointSQL(_name) {
|
|
2175
|
+
// T-SQL has no RELEASE SAVEPOINT statement; savepoints created via
|
|
2176
|
+
// SAVE TRANSACTION are automatically released on COMMIT/ROLLBACK.
|
|
2177
|
+
// Return a harmless no-op statement so callers that unconditionally
|
|
2178
|
+
// execute the returned SQL don't send an empty batch.
|
|
2179
|
+
return '-- RELEASE SAVEPOINT is a no-op in T-SQL (SQL Server has no equivalent statement)';
|
|
2180
|
+
}
|
|
2181
|
+
rollbackToSavepointSQL(name) {
|
|
2182
|
+
return `ROLLBACK TRANSACTION ${name}`;
|
|
2183
|
+
}
|
|
2184
|
+
// Extension stubs - not supported in MSSQL
|
|
2185
|
+
createExtension(_extensionName, _options) {
|
|
2186
|
+
throw new Error('Extensions are not supported by MSSQL. This is a PostgreSQL-specific feature.');
|
|
2187
|
+
}
|
|
2188
|
+
dropExtension(_extensionName, _options) {
|
|
2189
|
+
throw new Error('Extensions are not supported by MSSQL. This is a PostgreSQL-specific feature.');
|
|
2190
|
+
}
|
|
2191
|
+
getExtensions() {
|
|
2192
|
+
throw new Error('Extensions are not supported by MSSQL. This is a PostgreSQL-specific feature.');
|
|
2193
|
+
}
|
|
2194
|
+
hasExtension(_extensionName) {
|
|
2195
|
+
throw new Error('Extensions are not supported by MSSQL. This is a PostgreSQL-specific feature.');
|
|
2196
|
+
}
|
|
2197
|
+
// JSON query builder
|
|
2198
|
+
buildJsonQuery(column, path, value, operator = '=') {
|
|
2199
|
+
const values = [];
|
|
2200
|
+
const jsonExtract = this.buildJsonExtract(column, path, true);
|
|
2201
|
+
if (value !== undefined) {
|
|
2202
|
+
values.push(value);
|
|
2203
|
+
return { sql: `${jsonExtract} ${operator} @p1`, values };
|
|
2204
|
+
}
|
|
2205
|
+
return { sql: jsonExtract, values };
|
|
2206
|
+
}
|
|
2207
|
+
// ReplaceReplacements method
|
|
2208
|
+
replaceReplacements(sql, _replacements) {
|
|
2209
|
+
// Basic implementation - for full implementation would need more complex logic
|
|
2210
|
+
return sql;
|
|
2211
|
+
}
|
|
2212
|
+
formatValue(value) {
|
|
2213
|
+
if (value === null || value === undefined)
|
|
2214
|
+
return 'NULL';
|
|
2215
|
+
if (typeof value === 'boolean')
|
|
2216
|
+
return value ? '1' : '0';
|
|
2217
|
+
if (typeof value === 'number')
|
|
2218
|
+
return String(value);
|
|
2219
|
+
if (value instanceof Date)
|
|
2220
|
+
return this.formatDate(value);
|
|
2221
|
+
if (Buffer.isBuffer(value))
|
|
2222
|
+
return `0x${value.toString('hex')}`;
|
|
2223
|
+
if (Array.isArray(value) || typeof value === 'object')
|
|
2224
|
+
return this.escape(JSON.stringify(value));
|
|
2225
|
+
return this.escape(String(value));
|
|
2226
|
+
}
|
|
2227
|
+
// ==================== Spatial Functions ====================
|
|
2228
|
+
/**
|
|
2229
|
+
* ST_Distance - calculate distance between two geometries (MSSQL)
|
|
2230
|
+
*/
|
|
2231
|
+
stDistance(geom1, geom2, srid) {
|
|
2232
|
+
const sridStr = srid ? `, ${srid}` : '';
|
|
2233
|
+
return `geometry::STGeomFromText(${geom1},${sridStr}).STDistance(geometry::STGeomFromText(${geom2}${sridStr}))`;
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* ST_Within - check if geometry A is within geometry B (MSSQL)
|
|
2237
|
+
*/
|
|
2238
|
+
stWithin(geom1, geom2, srid) {
|
|
2239
|
+
const sridStr = srid ? `, ${srid}` : '';
|
|
2240
|
+
return `geometry::STGeomFromText(${geom1},${sridStr}).STWithin(geometry::STGeomFromText(${geom2}${sridStr}))`;
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* ST_Contains - check if geometry A contains geometry B (MSSQL)
|
|
2244
|
+
*/
|
|
2245
|
+
stContains(geom1, geom2, srid) {
|
|
2246
|
+
const sridStr = srid ? `, ${srid}` : '';
|
|
2247
|
+
return `geometry::STGeomFromText(${geom1},${sridStr}).STContains(geometry::STGeomFromText(${geom2}${sridStr}))`;
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* ST_Intersects - check if geometries intersect (MSSQL)
|
|
2251
|
+
*/
|
|
2252
|
+
stIntersects(geom1, geom2, srid) {
|
|
2253
|
+
const sridStr = srid ? `, ${srid}` : '';
|
|
2254
|
+
return `geometry::STGeomFromText(${geom1},${sridStr}).STIntersects(geometry::STGeomFromText(${geom2}${sridStr}))`;
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* ST_DWithin - check if geometries are within a given distance (MSSQL)
|
|
2258
|
+
*/
|
|
2259
|
+
stDWithin(geom1, geom2, distance, srid) {
|
|
2260
|
+
const sridStr = srid ? `, ${srid}` : '';
|
|
2261
|
+
return `geometry::STGeomFromText(${geom1},${sridStr}).STDistance(geometry::STGeomFromText(${geom2}${sridStr})) <= ${distance}`;
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* ST_AsText - convert geometry to text representation (MSSQL)
|
|
2265
|
+
*/
|
|
2266
|
+
stAsText(geom) {
|
|
2267
|
+
return `${geom}.STAsText()`;
|
|
2268
|
+
}
|
|
2269
|
+
/**
|
|
2270
|
+
* ST_GeomFromText - create geometry from text (MSSQL)
|
|
2271
|
+
*/
|
|
2272
|
+
stGeomFromText(wkt, srid) {
|
|
2273
|
+
return srid ? `geometry::STGeomFromText('${wkt}', ${srid})` : `geometry::STGeomFromText('${wkt}', 0)`;
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Build an OPENJSON expression to shred a JSON document/array into relational rows.
|
|
2277
|
+
* MSSQL has no `JSON_TABLE` function; `OPENJSON(expr, path) WITH (...)` is the real
|
|
2278
|
+
* equivalent, using explicit JSON-path-per-column instead of Oracle/MySQL's COLUMNS(...)
|
|
2279
|
+
* clause. Note: unlike JSON_TABLE, OPENJSON has no `FOR ORDINALITY` column concept, so
|
|
2280
|
+
* columns flagged `forOrdinality` are not supported here.
|
|
2281
|
+
* MSSQL: OPENJSON(expr, '$.path') WITH (name type '$.path', ...) AS alias
|
|
2282
|
+
*/
|
|
2283
|
+
buildJsonTable(jsonExpression, rowPath, columns, alias) {
|
|
2284
|
+
if (columns.some((col) => col.forOrdinality)) {
|
|
2285
|
+
throw new Error('MSSQL OPENJSON does not support FOR ORDINALITY columns; omit forOrdinality and use a JSON path instead');
|
|
2286
|
+
}
|
|
2287
|
+
const columnDefs = columns
|
|
2288
|
+
.map((col) => `${this.escapeId(col.name)} ${col.type} '${col.path}'`)
|
|
2289
|
+
.join(', ');
|
|
2290
|
+
return `OPENJSON(${jsonExpression}, '${rowPath}') WITH (${columnDefs}) AS ${this.escapeId(alias)}`;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
exports.MSSQLDialect = MSSQLDialect;
|
|
2294
|
+
function createMSSQLDialect(config) {
|
|
2295
|
+
return new MSSQLDialect(config);
|
|
2296
|
+
}
|