ts-prorm-orm 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/CHANGELOG.md +1111 -0
  2. package/LICENSE +21 -0
  3. package/README.md +573 -0
  4. package/dist/audit/history-query.js +180 -0
  5. package/dist/audit/index.js +23 -0
  6. package/dist/audit/logger.js +236 -0
  7. package/dist/cache/cache-manager.js +84 -0
  8. package/dist/cache/index.js +16 -0
  9. package/dist/cache/redis-cluster-cache.js +557 -0
  10. package/dist/cli.js +2200 -0
  11. package/dist/compliance/audit-trail.js +68 -0
  12. package/dist/compliance/backup-verification.js +685 -0
  13. package/dist/compliance/breach-detector.js +505 -0
  14. package/dist/compliance/consent-record.js +227 -0
  15. package/dist/compliance/consent-versioning.js +331 -0
  16. package/dist/compliance/cross-border-log.js +530 -0
  17. package/dist/compliance/data-classifier.js +258 -0
  18. package/dist/compliance/data-lineage.js +565 -0
  19. package/dist/compliance/data-masker.js +303 -0
  20. package/dist/compliance/data-portability.js +265 -0
  21. package/dist/compliance/data-retention.js +195 -0
  22. package/dist/compliance/dsar-workflow.js +342 -0
  23. package/dist/compliance/field-encryption.js +223 -0
  24. package/dist/compliance/immutable-record.js +148 -0
  25. package/dist/compliance/index.js +260 -0
  26. package/dist/compliance/privacy-impact-assessment.js +364 -0
  27. package/dist/compliance/pseudonymization.js +269 -0
  28. package/dist/compliance/query-firewall.js +1182 -0
  29. package/dist/compliance/rate-limiter.js +580 -0
  30. package/dist/compliance/right-to-erasure.js +117 -0
  31. package/dist/compliance/row-level-security.js +246 -0
  32. package/dist/compliance/security-decorator.js +574 -0
  33. package/dist/compliance/security-monitor.js +525 -0
  34. package/dist/compliance/sensitive-data-discovery.js +476 -0
  35. package/dist/compliance/session-isolation.js +472 -0
  36. package/dist/compliance/tls-enforcer.js +108 -0
  37. package/dist/compliance/worm-storage.js +712 -0
  38. package/dist/connection-manager.js +198 -0
  39. package/dist/connection-pool.js +519 -0
  40. package/dist/decorators/audit.js +136 -0
  41. package/dist/decorators/belongs-to-many.js +115 -0
  42. package/dist/decorators/belongs-to.js +115 -0
  43. package/dist/decorators/check.js +435 -0
  44. package/dist/decorators/collate.js +329 -0
  45. package/dist/decorators/comment.js +205 -0
  46. package/dist/decorators/database-settings.js +236 -0
  47. package/dist/decorators/default.js +244 -0
  48. package/dist/decorators/encryption.js +235 -0
  49. package/dist/decorators/engine.js +97 -0
  50. package/dist/decorators/fk-constraints.js +594 -0
  51. package/dist/decorators/foreign-table.js +136 -0
  52. package/dist/decorators/generated.js +274 -0
  53. package/dist/decorators/has-many.js +127 -0
  54. package/dist/decorators/has-one.js +116 -0
  55. package/dist/decorators/hstore.js +129 -0
  56. package/dist/decorators/index.js +355 -0
  57. package/dist/decorators/json-column.js +83 -0
  58. package/dist/decorators/jsonb.js +101 -0
  59. package/dist/decorators/orm-decorators.js +425 -0
  60. package/dist/decorators/permissions.js +294 -0
  61. package/dist/decorators/procedure.js +210 -0
  62. package/dist/decorators/query-options.js +556 -0
  63. package/dist/decorators/range.js +167 -0
  64. package/dist/decorators/set-column.js +83 -0
  65. package/dist/decorators/spatial.js +114 -0
  66. package/dist/decorators/storage.js +580 -0
  67. package/dist/decorators/timezone.js +512 -0
  68. package/dist/decorators/trigger.js +90 -0
  69. package/dist/decorators/uuid.js +135 -0
  70. package/dist/decorators/view.js +258 -0
  71. package/dist/diagrams/chen-diagram.js +354 -0
  72. package/dist/diagrams/class-diagram.js +384 -0
  73. package/dist/diagrams/dependency-diagram.js +432 -0
  74. package/dist/diagrams/er-diagram.js +605 -0
  75. package/dist/diagrams/flow-diagram.js +394 -0
  76. package/dist/diagrams/gantt-diagram.js +411 -0
  77. package/dist/diagrams/index-diagram.js +353 -0
  78. package/dist/diagrams/index.js +184 -0
  79. package/dist/diagrams/migration-diagram.js +316 -0
  80. package/dist/diagrams/model-diagram.js +616 -0
  81. package/dist/diagrams/package-diagram.js +376 -0
  82. package/dist/diagrams/palette.js +85 -0
  83. package/dist/diagrams/relational-diagram.js +455 -0
  84. package/dist/diagrams/schemadoc-diagram.js +309 -0
  85. package/dist/diagrams/sequence-diagram.js +307 -0
  86. package/dist/diagrams/state-diagram.js +344 -0
  87. package/dist/diagrams/svg-dom.js +111 -0
  88. package/dist/diagrams/tree-diagram.js +250 -0
  89. package/dist/dialects/clickhouse/index.js +1742 -0
  90. package/dist/dialects/cockroachdb/index.js +4677 -0
  91. package/dist/dialects/cratedb/index.js +441 -0
  92. package/dist/dialects/databricks/index.js +517 -0
  93. package/dist/dialects/db2/index.js +2225 -0
  94. package/dist/dialects/dialect.js +720 -0
  95. package/dist/dialects/duckdb/index.js +2014 -0
  96. package/dist/dialects/exasol/index.js +357 -0
  97. package/dist/dialects/firebird/index.js +495 -0
  98. package/dist/dialects/greenplum/index.js +457 -0
  99. package/dist/dialects/hana/index.js +1741 -0
  100. package/dist/dialects/mariadb/index.js +3590 -0
  101. package/dist/dialects/mssql/index.js +2296 -0
  102. package/dist/dialects/mysql/index.js +4058 -0
  103. package/dist/dialects/oracle/index.js +2999 -0
  104. package/dist/dialects/postgres/index.js +5319 -0
  105. package/dist/dialects/query-stream-helper.js +152 -0
  106. package/dist/dialects/questdb/index.js +433 -0
  107. package/dist/dialects/redshift/index.js +2277 -0
  108. package/dist/dialects/singlestore/index.js +354 -0
  109. package/dist/dialects/snowflake/index.js +2082 -0
  110. package/dist/dialects/spanner/index.js +1768 -0
  111. package/dist/dialects/sqlite/index.js +3382 -0
  112. package/dist/dialects/tidb/index.js +377 -0
  113. package/dist/dialects/timescaledb/index.js +164 -0
  114. package/dist/dialects/trino/index.js +420 -0
  115. package/dist/dialects/turso/index.js +227 -0
  116. package/dist/dialects/vertica/index.js +317 -0
  117. package/dist/dialects/yugabytedb/index.js +424 -0
  118. package/dist/errors/index.js +472 -0
  119. package/dist/errors/utils.js +340 -0
  120. package/dist/errors.js +21 -0
  121. package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
  122. package/dist/extensions/catalog/mssql-features.js +147 -0
  123. package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
  124. package/dist/extensions/catalog/oracle-db2-features.js +196 -0
  125. package/dist/extensions/catalog/postgres-extensions.js +516 -0
  126. package/dist/extensions/index.js +71 -0
  127. package/dist/extensions/types.js +13 -0
  128. package/dist/foreign-data.js +173 -0
  129. package/dist/hooks/hooks-manager.js +350 -0
  130. package/dist/hooks/index.js +37 -0
  131. package/dist/index.js +463 -0
  132. package/dist/logging.js +335 -0
  133. package/dist/migrations/index.js +40 -0
  134. package/dist/migrations/migration.js +196 -0
  135. package/dist/migrations/migrator.js +411 -0
  136. package/dist/migrations/prormmigration.js +275 -0
  137. package/dist/migrations/query-interface.js +435 -0
  138. package/dist/migrations/seeder.js +353 -0
  139. package/dist/models/associations.js +852 -0
  140. package/dist/models/constraints.js +288 -0
  141. package/dist/models/data-types.js +2518 -0
  142. package/dist/models/decorators.js +445 -0
  143. package/dist/models/index.js +33 -0
  144. package/dist/models/indexes.js +531 -0
  145. package/dist/models/methods.js +382 -0
  146. package/dist/models/model-manager.js +103 -0
  147. package/dist/models/model.js +5349 -0
  148. package/dist/models/operators.js +67 -0
  149. package/dist/models/scopes.js +189 -0
  150. package/dist/models/typescript-types.js +26 -0
  151. package/dist/nosql/aerospike/index.js +205 -0
  152. package/dist/nosql/allegrograph/index.js +169 -0
  153. package/dist/nosql/arangodb/index.js +364 -0
  154. package/dist/nosql/azure-blob/index.js +206 -0
  155. package/dist/nosql/beanstalkd/index.js +231 -0
  156. package/dist/nosql/beequeue/index.js +210 -0
  157. package/dist/nosql/bigchaindb/index.js +195 -0
  158. package/dist/nosql/bigtable/index.js +224 -0
  159. package/dist/nosql/blazegraph/index.js +173 -0
  160. package/dist/nosql/bullmq/index.js +191 -0
  161. package/dist/nosql/cassandra/index.js +174 -0
  162. package/dist/nosql/chroma/index.js +190 -0
  163. package/dist/nosql/cloudflare-kv/index.js +220 -0
  164. package/dist/nosql/coherence/index.js +200 -0
  165. package/dist/nosql/cosmosdb/index.js +157 -0
  166. package/dist/nosql/couchbase/index.js +213 -0
  167. package/dist/nosql/dax/index.js +212 -0
  168. package/dist/nosql/deno-kv/index.js +206 -0
  169. package/dist/nosql/dgraph/index.js +171 -0
  170. package/dist/nosql/doris/index.js +169 -0
  171. package/dist/nosql/druid/index.js +162 -0
  172. package/dist/nosql/dynamodb/index.js +937 -0
  173. package/dist/nosql/elasticsearch/index.js +377 -0
  174. package/dist/nosql/etcd/index.js +502 -0
  175. package/dist/nosql/eventhubs/index.js +213 -0
  176. package/dist/nosql/eventstore/index.js +254 -0
  177. package/dist/nosql/faunadb/index.js +188 -0
  178. package/dist/nosql/firestore/index.js +177 -0
  179. package/dist/nosql/fluree/index.js +148 -0
  180. package/dist/nosql/fuseki/index.js +170 -0
  181. package/dist/nosql/gcs/index.js +172 -0
  182. package/dist/nosql/gearman/index.js +160 -0
  183. package/dist/nosql/geode/index.js +196 -0
  184. package/dist/nosql/graphdb/index.js +169 -0
  185. package/dist/nosql/graylog/index.js +188 -0
  186. package/dist/nosql/gridgain/index.js +171 -0
  187. package/dist/nosql/hazelcast/index.js +162 -0
  188. package/dist/nosql/hbase/index.js +230 -0
  189. package/dist/nosql/ignite/index.js +173 -0
  190. package/dist/nosql/immudb/index.js +184 -0
  191. package/dist/nosql/index.js +232 -0
  192. package/dist/nosql/infinispan/index.js +200 -0
  193. package/dist/nosql/influxdb/index.js +0 -0
  194. package/dist/nosql/kafka/index.js +234 -0
  195. package/dist/nosql/keyspaces/index.js +189 -0
  196. package/dist/nosql/kinesis/index.js +253 -0
  197. package/dist/nosql/leveldb/index.js +153 -0
  198. package/dist/nosql/lmdb/index.js +160 -0
  199. package/dist/nosql/loki/index.js +201 -0
  200. package/dist/nosql/marklogic/index.js +204 -0
  201. package/dist/nosql/materialize/index.js +145 -0
  202. package/dist/nosql/meilisearch/index.js +154 -0
  203. package/dist/nosql/memcached/index.js +223 -0
  204. package/dist/nosql/milvus/index.js +410 -0
  205. package/dist/nosql/minio/index.js +265 -0
  206. package/dist/nosql/momento/index.js +179 -0
  207. package/dist/nosql/mongodb/index.js +461 -0
  208. package/dist/nosql/nats/index.js +247 -0
  209. package/dist/nosql/nedb/index.js +164 -0
  210. package/dist/nosql/neo4j/index.js +450 -0
  211. package/dist/nosql/neptune/index.js +470 -0
  212. package/dist/nosql/nsq/index.js +200 -0
  213. package/dist/nosql/opensearch/index.js +186 -0
  214. package/dist/nosql/orientdb/index.js +175 -0
  215. package/dist/nosql/papertrail/index.js +200 -0
  216. package/dist/nosql/pinecone/index.js +0 -0
  217. package/dist/nosql/pinot/index.js +133 -0
  218. package/dist/nosql/pouchdb/index.js +172 -0
  219. package/dist/nosql/prometheus/index.js +174 -0
  220. package/dist/nosql/provendb/index.js +147 -0
  221. package/dist/nosql/pubsub/index.js +187 -0
  222. package/dist/nosql/pulsar/index.js +232 -0
  223. package/dist/nosql/qdrant/index.js +295 -0
  224. package/dist/nosql/qldb/index.js +192 -0
  225. package/dist/nosql/r2/index.js +281 -0
  226. package/dist/nosql/rabbitmq/index.js +237 -0
  227. package/dist/nosql/ravendb/index.js +175 -0
  228. package/dist/nosql/redis/index.js +607 -0
  229. package/dist/nosql/redpanda/index.js +237 -0
  230. package/dist/nosql/resque/index.js +203 -0
  231. package/dist/nosql/rethinkdb/index.js +232 -0
  232. package/dist/nosql/rocksdb/index.js +152 -0
  233. package/dist/nosql/rockset/index.js +126 -0
  234. package/dist/nosql/s3/index.js +298 -0
  235. package/dist/nosql/scylladb/index.js +178 -0
  236. package/dist/nosql/signoz/index.js +227 -0
  237. package/dist/nosql/sns/index.js +201 -0
  238. package/dist/nosql/solr/index.js +231 -0
  239. package/dist/nosql/splunk/index.js +227 -0
  240. package/dist/nosql/sqs/index.js +246 -0
  241. package/dist/nosql/stardog/index.js +169 -0
  242. package/dist/nosql/starrocks/index.js +170 -0
  243. package/dist/nosql/store.js +2 -0
  244. package/dist/nosql/sumologic/index.js +213 -0
  245. package/dist/nosql/surrealdb/index.js +179 -0
  246. package/dist/nosql/terminusdb/index.js +151 -0
  247. package/dist/nosql/tigergraph/index.js +469 -0
  248. package/dist/nosql/typesense/index.js +148 -0
  249. package/dist/nosql/unqlite/index.js +157 -0
  250. package/dist/nosql/upstash/index.js +167 -0
  251. package/dist/nosql/vercel-kv/index.js +166 -0
  252. package/dist/nosql/victoriametrics/index.js +213 -0
  253. package/dist/nosql/virtuoso/index.js +169 -0
  254. package/dist/nosql/weaviate/index.js +276 -0
  255. package/dist/operators/index.js +108 -0
  256. package/dist/operators.js +2690 -0
  257. package/dist/prisma-migrate/index.js +38 -0
  258. package/dist/prisma-migrate/migration-generator.js +125 -0
  259. package/dist/prisma-migrate/model-generator.js +172 -0
  260. package/dist/prisma-migrate/relations.js +100 -0
  261. package/dist/prisma-migrate/schema-parser.js +167 -0
  262. package/dist/prisma-migrate/type-mapper.js +40 -0
  263. package/dist/prorm.js +6832 -0
  264. package/dist/query-builders/cte-builder.js +80 -0
  265. package/dist/query-builders/functions/aggregate.js +390 -0
  266. package/dist/query-builders/functions/conditional.js +503 -0
  267. package/dist/query-builders/functions/datetime.js +695 -0
  268. package/dist/query-builders/functions/fulltext.js +439 -0
  269. package/dist/query-builders/functions/index.js +93 -0
  270. package/dist/query-builders/functions/json.js +427 -0
  271. package/dist/query-builders/functions/math.js +399 -0
  272. package/dist/query-builders/functions/string.js +518 -0
  273. package/dist/query-builders/functions/window.js +328 -0
  274. package/dist/query-builders/include-builder.js +323 -0
  275. package/dist/query-builders/index-expression-builder.js +242 -0
  276. package/dist/query-builders/index.js +161 -0
  277. package/dist/query-builders/insert-builder.js +164 -0
  278. package/dist/query-builders/model-helpers.js +38 -0
  279. package/dist/query-builders/order-limit-builder.js +239 -0
  280. package/dist/query-builders/sql-compiler.js +2040 -0
  281. package/dist/query-builders/subquery-builder.js +152 -0
  282. package/dist/query-builders/update-builder.js +182 -0
  283. package/dist/query-builders/view-builder.js +218 -0
  284. package/dist/query-builders/where-builder.js +1532 -0
  285. package/dist/query-interface.js +622 -0
  286. package/dist/query-optimizers/batch-optimizer.js +426 -0
  287. package/dist/query-optimizers/explain-plans.js +415 -0
  288. package/dist/query-optimizers/index.js +51 -0
  289. package/dist/query-optimizers/prepared-statement-cache.js +423 -0
  290. package/dist/query-optimizers/query-hints.js +438 -0
  291. package/dist/query-optimizers/query-optimizer.js +278 -0
  292. package/dist/query-optimizers/slow-query-logger.js +271 -0
  293. package/dist/replica-manager.js +507 -0
  294. package/dist/schema/index.js +15 -0
  295. package/dist/schema/migration-generator.js +374 -0
  296. package/dist/schema/schema-differ.js +549 -0
  297. package/dist/schema/types.js +6 -0
  298. package/dist/sql-constants.js +300 -0
  299. package/dist/sqlite-advanced.js +1047 -0
  300. package/dist/streams/index.js +12 -0
  301. package/dist/streams/transforms.js +178 -0
  302. package/dist/transaction.js +456 -0
  303. package/dist/types/index.js +180 -0
  304. package/dist/user-management.js +127 -0
  305. package/dist/utils/date.js +300 -0
  306. package/dist/utils/index.js +1079 -0
  307. package/dist/utils/string.js +161 -0
  308. package/dist/validators/index.js +19 -0
  309. package/dist/validators/validator.js +911 -0
  310. package/package.json +190 -0
@@ -0,0 +1,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
+ }