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,2040 @@
1
+ "use strict";
2
+ /**
3
+ * SQL Compiler
4
+ * Compiles query objects to SQL strings with parameter substitution
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.SqlCompiler = void 0;
8
+ exports.jsonExtract = jsonExtract;
9
+ exports.jsonExtractUnquote = jsonExtractUnquote;
10
+ exports.jsonSet = jsonSet;
11
+ exports.jsonInsert = jsonInsert;
12
+ exports.jsonReplace = jsonReplace;
13
+ exports.jsonRemove = jsonRemove;
14
+ exports.jsonKeys = jsonKeys;
15
+ exports.jsonLength = jsonLength;
16
+ exports.jsonContains = jsonContains;
17
+ exports.jsonSearch = jsonSearch;
18
+ const operators_1 = require("../operators");
19
+ /**
20
+ * Default compiler options
21
+ */
22
+ const defaultOptions = {
23
+ parameterized: true,
24
+ paramChar: '?',
25
+ inlineValues: false,
26
+ pretty: false,
27
+ };
28
+ /**
29
+ * SQL Compiler class
30
+ * Compiles query objects to SQL strings with proper escaping
31
+ */
32
+ class SqlCompiler {
33
+ constructor(dialect, options) {
34
+ this.dialect = dialect;
35
+ this.options = { ...defaultOptions, ...options };
36
+ this.fieldMapping = options?.fieldMapping;
37
+ }
38
+ /**
39
+ * Set the field mapping
40
+ */
41
+ setFieldMapping(fieldMapping) {
42
+ this.fieldMapping = fieldMapping;
43
+ }
44
+ /**
45
+ * Get the DB column name for an attribute
46
+ */
47
+ getColumnName(attributeName) {
48
+ if (!this.fieldMapping) {
49
+ return attributeName;
50
+ }
51
+ return this.fieldMapping.attributeToColumn[attributeName] || attributeName;
52
+ }
53
+ /**
54
+ * Get the attribute name for a DB column
55
+ */
56
+ getAttributeName(columnName) {
57
+ if (!this.fieldMapping) {
58
+ return columnName;
59
+ }
60
+ return this.fieldMapping.columnToAttribute[columnName] || columnName;
61
+ }
62
+ /**
63
+ * Set the dialect
64
+ */
65
+ setDialect(dialect) {
66
+ this.dialect = dialect;
67
+ }
68
+ /**
69
+ * Set compiler options
70
+ */
71
+ setOptions(options) {
72
+ this.options = { ...this.options, ...options };
73
+ }
74
+ /**
75
+ * Escape a value for use in SQL
76
+ */
77
+ escape(value) {
78
+ if (this.dialect) {
79
+ return this.dialect.escape(value);
80
+ }
81
+ // Default escaping if no dialect
82
+ if (value === null || value === undefined) {
83
+ return 'NULL';
84
+ }
85
+ if (typeof value === 'boolean') {
86
+ return value ? '1' : '0';
87
+ }
88
+ if (typeof value === 'number') {
89
+ return String(value);
90
+ }
91
+ if (value instanceof Date) {
92
+ return `'${value.toISOString()}'`;
93
+ }
94
+ if (Buffer.isBuffer(value)) {
95
+ return `X'${value.toString('hex')}'`;
96
+ }
97
+ if (typeof value === 'object') {
98
+ return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
99
+ }
100
+ return `'${String(value).replace(/'/g, "''")}'`;
101
+ }
102
+ /**
103
+ * Escape an identifier (column, table name)
104
+ */
105
+ escapeId(identifier) {
106
+ const id = String(identifier ?? '');
107
+ if (this.dialect) {
108
+ return this.dialect.quoteIdentifier(id);
109
+ }
110
+ // Default quoting if no dialect
111
+ return `"${id.replace(/"/g, '""')}"`;
112
+ }
113
+ /**
114
+ * Convert an attribute expression to SQL
115
+ * Handles fn(), col(), literal(), and plain strings
116
+ * @param attr - The attribute value (string, FnExpression, ColExpression, LiteralExpression, or array)
117
+ * @param tableAlias - Optional table alias to prepend to column names
118
+ * @returns The SQL representation of the attribute
119
+ */
120
+ attributeToSql(attr, tableAlias) {
121
+ // Handle array format: [expression, alias] or [fn, args, alias] or [col, alias]
122
+ if (Array.isArray(attr)) {
123
+ const expr = attr[0];
124
+ const alias = attr[1];
125
+ const sqlExpr = this.attributeToSql(expr, tableAlias);
126
+ return alias ? `${sqlExpr} AS ${this.escapeId(alias)}` : sqlExpr;
127
+ }
128
+ // Handle FnExpression: { __type: 'fn', fn: 'COUNT', args: [...] }
129
+ if ((0, operators_1.isFnExpression)(attr)) {
130
+ const args = attr.args.map((arg) => this.attributeToSql(arg, tableAlias)).join(', ');
131
+ return `${attr.fn}(${args})`;
132
+ }
133
+ // Handle ColExpression: { __type: 'col', col: 'field' }
134
+ if ((0, operators_1.isColExpression)(attr)) {
135
+ const col = attr.col;
136
+ if (col.includes('.')) {
137
+ // Already has table prefix
138
+ return col
139
+ .split('.')
140
+ .map((part) => this.escapeId(part))
141
+ .join('.');
142
+ }
143
+ return tableAlias ? `${this.escapeId(tableAlias)}.${this.escapeId(col)}` : this.escapeId(col);
144
+ }
145
+ // Handle LiteralExpression: { __type: 'literal', sql: '...' }
146
+ if ((0, operators_1.isLiteralExpression)(attr)) {
147
+ return attr.sql;
148
+ }
149
+ // Handle CastExpression: { __type: 'cast', value: ..., type: ... }
150
+ if ((0, operators_1.isCastExpression)(attr)) {
151
+ const value = this.attributeToSql(attr.value, tableAlias);
152
+ const castType = attr.type || 'TEXT';
153
+ return `CAST(${value} AS ${castType})`;
154
+ }
155
+ // Handle ExtractExpression: { __type: 'extract', field: ..., part: ... }
156
+ if ((0, operators_1.isExtractExpression)(attr)) {
157
+ const field = this.attributeToSql(attr.field, tableAlias);
158
+ const part = (attr.part || 'YEAR').toUpperCase();
159
+ return `EXTRACT(${part} FROM ${field})`;
160
+ }
161
+ // Handle ConvExpression: { __type: 'conv', value: ..., from: ..., to: ... }
162
+ if ((0, operators_1.isConvExpression)(attr)) {
163
+ const value = this.attributeToSql(attr.value, tableAlias);
164
+ const to = attr.to || 10;
165
+ if (attr.from) {
166
+ return `CONVERT(${value} USING ${to})`;
167
+ }
168
+ return `CONVERT(${value}, ${to})`;
169
+ }
170
+ // Handle plain string (column name or SQL expression)
171
+ if (typeof attr === 'string') {
172
+ // Check if it looks like a SQL function or expression
173
+ if (attr.includes('(') || attr.includes(' ')) {
174
+ // It's likely a SQL expression - check if it's a simple column name with table prefix
175
+ if (attr.includes('.')) {
176
+ // Check if it's just a table.column reference
177
+ const parts = attr.split('.');
178
+ if (parts.length === 2 && !parts[1].includes('(')) {
179
+ return `${this.escapeId(parts[0])}.${this.escapeId(parts[1])}`;
180
+ }
181
+ }
182
+ // It's a complex expression - pass through as-is (literal)
183
+ return attr;
184
+ }
185
+ // Simple column name
186
+ return tableAlias
187
+ ? `${this.escapeId(tableAlias)}.${this.escapeId(attr)}`
188
+ : this.escapeId(attr);
189
+ }
190
+ // Fallback: convert to string
191
+ return String(attr);
192
+ }
193
+ /**
194
+ * Process an attribute value (which could be a column name, fn expression, col expression, or literal)
195
+ * into a SQL expression string
196
+ *
197
+ * @param attr - The attribute value to process
198
+ * @param tableAlias - Optional table alias to prefix column names with
199
+ * @returns The SQL expression string
200
+ */
201
+ processAttribute(attr, tableAlias) {
202
+ // Handle CAST expressions: CAST(value AS type)
203
+ if ((0, operators_1.isCastExpression)(attr)) {
204
+ const value = this.processAttribute(attr.value, tableAlias);
205
+ return `CAST(${value} AS ${attr.type})`;
206
+ }
207
+ // Handle EXTRACT expressions: EXTRACT(part FROM field)
208
+ if ((0, operators_1.isExtractExpression)(attr)) {
209
+ const field = this.processAttribute(attr.field, tableAlias);
210
+ // Uppercase the part for SQL standard
211
+ const part = attr.part.toUpperCase();
212
+ return `EXTRACT(${part} FROM ${field})`;
213
+ }
214
+ // Handle CONVERT expressions: CONVERT(value, type) or CONVERT(value USING charset)
215
+ if ((0, operators_1.isConvExpression)(attr)) {
216
+ const value = this.processAttribute(attr.value, tableAlias);
217
+ if (attr.from) {
218
+ // MySQL: CONVERT(value USING charset)
219
+ return `CONVERT(${value} USING ${attr.to})`;
220
+ }
221
+ else {
222
+ // MySQL: CONVERT(value, type)
223
+ return `CONVERT(${value}, ${attr.to})`;
224
+ }
225
+ }
226
+ // Handle function expressions: fn('COUNT', col('id'))
227
+ if ((0, operators_1.isFnExpression)(attr)) {
228
+ const args = attr.args.map((arg) => this.processAttribute(arg, tableAlias));
229
+ return `${attr.fn}(${args.join(', ')})`;
230
+ }
231
+ // Handle column expressions: col('id') or col('table.column')
232
+ if ((0, operators_1.isColExpression)(attr)) {
233
+ const col = attr.col;
234
+ if (col.includes('.')) {
235
+ // Already has table prefix, use as-is with escaping
236
+ return col
237
+ .split('.')
238
+ .map((part) => this.escapeId(part))
239
+ .join('.');
240
+ }
241
+ // Add table alias if provided
242
+ return tableAlias ? `${this.escapeId(tableAlias)}.${this.escapeId(col)}` : this.escapeId(col);
243
+ }
244
+ // Handle literal expressions: literal('COUNT(*)')
245
+ if ((0, operators_1.isLiteralExpression)(attr)) {
246
+ return attr.sql;
247
+ }
248
+ // Handle plain string - could be a column name or a literal SQL expression
249
+ if (typeof attr === 'string') {
250
+ // Check if it looks like a literal SQL expression (contains parentheses or aggregate functions)
251
+ if (attr.includes('(') || attr.includes('*') || attr.includes(' ')) {
252
+ // Likely a literal SQL expression, use as-is
253
+ return attr;
254
+ }
255
+ // Treat as column name
256
+ return tableAlias
257
+ ? `${this.escapeId(tableAlias)}.${this.escapeId(attr)}`
258
+ : this.escapeId(attr);
259
+ }
260
+ // Handle other types (numbers, etc.)
261
+ if (typeof attr === 'number') {
262
+ return String(attr);
263
+ }
264
+ // Fallback: stringify
265
+ return String(attr);
266
+ }
267
+ /**
268
+ * Convert an attribute to SQL with optional alias
269
+ * Handles formats like:
270
+ * - 'columnName' -> "columnName"
271
+ * - ['columnName', 'alias'] -> "columnName" AS "alias"
272
+ * - [fn('COUNT', col('id')), 'count'] -> COUNT("id") AS "count"
273
+ * - ['COUNT(*)', 'count'] -> COUNT(*) AS "count"
274
+ *
275
+ * @param attr - The attribute (string or array with alias)
276
+ * @param tableAlias - Optional table alias
277
+ * @returns SQL fragment with optional alias
278
+ */
279
+ attributeToSqlWithAlias(attr, tableAlias) {
280
+ if (Array.isArray(attr)) {
281
+ // Array format: [expression, alias] or [fn, args, alias] (legacy)
282
+ if (attr.length === 2) {
283
+ const [expr, alias] = attr;
284
+ const sqlExpr = this.processAttribute(expr, tableAlias);
285
+ return `${sqlExpr} AS ${this.escapeId(alias)}`;
286
+ }
287
+ else if (attr.length === 3) {
288
+ // Legacy format: [fn, args, alias]
289
+ const [fnName, args, alias] = attr;
290
+ const processedArgs = Array.isArray(args)
291
+ ? args.map((a) => this.processAttribute(a, tableAlias)).join(', ')
292
+ : this.processAttribute(args, tableAlias);
293
+ return `${fnName.toUpperCase()}(${processedArgs}) AS ${this.escapeId(alias)}`;
294
+ }
295
+ }
296
+ // Simple string or expression
297
+ return this.processAttribute(attr, tableAlias);
298
+ }
299
+ /**
300
+ * Compile WHERE clause
301
+ */
302
+ compileWhere(where, options) {
303
+ const values = [];
304
+ const paramChar = options?.paramChar || this.options.paramChar || '?';
305
+ if (!where || Object.keys(where).length === 0) {
306
+ return { sql: '', values };
307
+ }
308
+ const sql = this.buildWhereClause(where, values, paramChar);
309
+ return { sql, values };
310
+ }
311
+ /**
312
+ * Build WHERE clause from WhereOptions
313
+ */
314
+ buildWhereClause(where, values, paramChar) {
315
+ if (!where)
316
+ return '';
317
+ // Handle logical operators ($and, $or, $not)
318
+ if ('$and' in where || '$or' in where || '$not' in where) {
319
+ return this.buildLogicalWhereClause(where, values, paramChar);
320
+ }
321
+ // Handle regular field conditions
322
+ const conditions = [];
323
+ for (const [key, value] of Object.entries(where)) {
324
+ if (key.startsWith('$'))
325
+ continue; // Skip logical operators
326
+ const condition = this.buildFieldCondition(key, value, values, paramChar);
327
+ if (condition) {
328
+ conditions.push(condition);
329
+ }
330
+ }
331
+ return conditions.join(' AND ');
332
+ }
333
+ /**
334
+ * Build logical WHERE clause (AND, OR, NOT)
335
+ */
336
+ buildLogicalWhereClause(where, values, paramChar) {
337
+ const parts = [];
338
+ if (where.$and && Array.isArray(where.$and)) {
339
+ const andParts = where.$and.map((cond) => {
340
+ if (typeof cond === 'object' && cond !== null) {
341
+ return `(${this.buildWhereClause(cond, values, paramChar)})`;
342
+ }
343
+ return `${paramChar}`;
344
+ });
345
+ if (andParts.length > 0) {
346
+ parts.push(`(${andParts.join(' AND ')})`);
347
+ }
348
+ }
349
+ if (where.$or && Array.isArray(where.$or)) {
350
+ const orParts = where.$or.map((cond) => {
351
+ if (typeof cond === 'object' && cond !== null) {
352
+ return `(${this.buildWhereClause(cond, values, paramChar)})`;
353
+ }
354
+ return `${paramChar}`;
355
+ });
356
+ if (orParts.length > 0) {
357
+ parts.push(`(${orParts.join(' OR ')})`);
358
+ }
359
+ }
360
+ if (where.$not) {
361
+ const notPart = this.buildWhereClause(where.$not, values, paramChar);
362
+ if (notPart) {
363
+ parts.push(`NOT (${notPart})`);
364
+ }
365
+ }
366
+ return parts.join(' AND ');
367
+ }
368
+ /**
369
+ * Build a single field condition
370
+ */
371
+ buildFieldCondition(key, value, values, paramChar) {
372
+ // Direct value (simple equality)
373
+ if (!value || typeof value !== 'object') {
374
+ values.push(value);
375
+ return `${this.escapeId(key)} = ${paramChar}`;
376
+ }
377
+ // Object with operators
378
+ const opValue = value;
379
+ // Handle $eq
380
+ if (opValue.$eq !== undefined) {
381
+ values.push(opValue.$eq);
382
+ return `${this.escapeId(key)} = ${paramChar}`;
383
+ }
384
+ // Handle $ne
385
+ if (opValue.$ne !== undefined) {
386
+ values.push(opValue.$ne);
387
+ return `${this.escapeId(key)} != ${paramChar}`;
388
+ }
389
+ // Handle $gt
390
+ if (opValue.$gt !== undefined) {
391
+ values.push(opValue.$gt);
392
+ return `${this.escapeId(key)} > ${paramChar}`;
393
+ }
394
+ // Handle $gte
395
+ if (opValue.$gte !== undefined) {
396
+ values.push(opValue.$gte);
397
+ return `${this.escapeId(key)} >= ${paramChar}`;
398
+ }
399
+ // Handle $lt
400
+ if (opValue.$lt !== undefined) {
401
+ values.push(opValue.$lt);
402
+ return `${this.escapeId(key)} < ${paramChar}`;
403
+ }
404
+ // Handle $lte
405
+ if (opValue.$lte !== undefined) {
406
+ values.push(opValue.$lte);
407
+ return `${this.escapeId(key)} <= ${paramChar}`;
408
+ }
409
+ // Handle $like
410
+ if (opValue.$like !== undefined) {
411
+ values.push(opValue.$like);
412
+ return `${this.escapeId(key)} LIKE ${paramChar}`;
413
+ }
414
+ // Handle $notLike
415
+ if (opValue.$notLike !== undefined) {
416
+ values.push(opValue.$notLike);
417
+ return `${this.escapeId(key)} NOT LIKE ${paramChar}`;
418
+ }
419
+ // Handle $iLike (case-insensitive like)
420
+ if (opValue.$iLike !== undefined) {
421
+ values.push(opValue.$iLike);
422
+ return `${this.escapeId(key)} LIKE ${paramChar}`; // Use dialect-specific for case-insensitive
423
+ }
424
+ // Handle $notILike
425
+ if (opValue.$notILike !== undefined) {
426
+ values.push(opValue.$notILike);
427
+ return `${this.escapeId(key)} NOT LIKE ${paramChar}`;
428
+ }
429
+ // Handle $startsWith - LIKE 'value%'
430
+ if (opValue.$startsWith !== undefined) {
431
+ values.push(opValue.$startsWith + '%');
432
+ return `${this.escapeId(key)} LIKE ${paramChar}`;
433
+ }
434
+ // Handle $notStartsWith - NOT LIKE 'value%'
435
+ if (opValue.$notStartsWith !== undefined) {
436
+ values.push(opValue.$notStartsWith + '%');
437
+ return `${this.escapeId(key)} NOT LIKE ${paramChar}`;
438
+ }
439
+ // Handle $endsWith - LIKE '%value'
440
+ if (opValue.$endsWith !== undefined) {
441
+ values.push('%' + opValue.$endsWith);
442
+ return `${this.escapeId(key)} LIKE ${paramChar}`;
443
+ }
444
+ // Handle $notEndsWith - NOT LIKE '%value'
445
+ if (opValue.$notEndsWith !== undefined) {
446
+ values.push('%' + opValue.$notEndsWith);
447
+ return `${this.escapeId(key)} NOT LIKE ${paramChar}`;
448
+ }
449
+ // Handle $substring - LIKE '%value%'
450
+ if (opValue.$substring !== undefined) {
451
+ values.push('%' + opValue.$substring + '%');
452
+ return `${this.escapeId(key)} LIKE ${paramChar}`;
453
+ }
454
+ // Handle $notSubstring - NOT LIKE '%value%'
455
+ if (opValue.$notSubstring !== undefined) {
456
+ values.push('%' + opValue.$notSubstring + '%');
457
+ return `${this.escapeId(key)} NOT LIKE ${paramChar}`;
458
+ }
459
+ // Handle $any - for array ANY queries (PostgreSQL)
460
+ if (opValue.$any !== undefined) {
461
+ const anyValues = Array.isArray(opValue.$any) ? opValue.$any : [opValue.$any];
462
+ values.push(anyValues);
463
+ return `${this.escapeId(key)} = ANY(${paramChar})`;
464
+ }
465
+ // Handle $all - for array ALL queries (PostgreSQL)
466
+ if (opValue.$all !== undefined) {
467
+ const allValues = Array.isArray(opValue.$all) ? opValue.$all : [opValue.$all];
468
+ values.push(allValues);
469
+ return `${this.escapeId(key)} = ALL(${paramChar})`;
470
+ }
471
+ // Handle $in
472
+ if (opValue.$in !== undefined) {
473
+ const inValues = Array.isArray(opValue.$in) ? opValue.$in : [opValue.$in];
474
+ values.push(...inValues);
475
+ const placeholders = inValues.map(() => paramChar).join(', ');
476
+ return `${this.escapeId(key)} IN (${placeholders})`;
477
+ }
478
+ // Handle $notIn
479
+ if (opValue.$notIn !== undefined) {
480
+ const notInValues = Array.isArray(opValue.$notIn) ? opValue.$notIn : [opValue.$notIn];
481
+ values.push(...notInValues);
482
+ const placeholders = notInValues.map(() => paramChar).join(', ');
483
+ return `${this.escapeId(key)} NOT IN (${placeholders})`;
484
+ }
485
+ // Handle $between
486
+ if (opValue.$between !== undefined) {
487
+ const between = opValue.$between;
488
+ if (Array.isArray(between) && between.length >= 2) {
489
+ values.push(between[0], between[1]);
490
+ return `${this.escapeId(key)} BETWEEN ${paramChar} AND ${paramChar}`;
491
+ }
492
+ }
493
+ // Handle $notBetween
494
+ if (opValue.$notBetween !== undefined) {
495
+ const notBetween = opValue.$notBetween;
496
+ if (Array.isArray(notBetween) && notBetween.length >= 2) {
497
+ values.push(notBetween[0], notBetween[1]);
498
+ return `${this.escapeId(key)} NOT BETWEEN ${paramChar} AND ${paramChar}`;
499
+ }
500
+ }
501
+ // Handle $isNull
502
+ if (opValue.$isNull !== undefined) {
503
+ if (opValue.$isNull) {
504
+ return `${this.escapeId(key)} IS NULL`;
505
+ }
506
+ return `${this.escapeId(key)} IS NOT NULL`;
507
+ }
508
+ // Handle $isNotNull (alias for $isNull: false)
509
+ if (opValue.$isNotNull !== undefined) {
510
+ if (opValue.$isNotNull) {
511
+ return `${this.escapeId(key)} IS NOT NULL`;
512
+ }
513
+ return `${this.escapeId(key)} IS NULL`;
514
+ }
515
+ // Handle $regexp
516
+ if (opValue.$regexp !== undefined) {
517
+ values.push(opValue.$regexp);
518
+ return `${this.escapeId(key)} REGEXP ${paramChar}`;
519
+ }
520
+ // Handle $notRegexp
521
+ if (opValue.$notRegexp !== undefined) {
522
+ values.push(opValue.$notRegexp);
523
+ return `${this.escapeId(key)} NOT REGEXP ${paramChar}`;
524
+ }
525
+ // Handle $iRegexp (case-insensitive regexp)
526
+ if (opValue.$iRegexp !== undefined) {
527
+ values.push(opValue.$iRegexp);
528
+ return `${this.escapeId(key)} REGEXP ${paramChar}`;
529
+ }
530
+ // Default: equality
531
+ values.push(value);
532
+ return `${this.escapeId(key)} = ${paramChar}`;
533
+ }
534
+ /**
535
+ * Compile ORDER BY clause
536
+ */
537
+ compileOrder(order) {
538
+ if (!order || (Array.isArray(order) && order.length === 0)) {
539
+ return '';
540
+ }
541
+ const orderArray = Array.isArray(order) ? order : [order];
542
+ const orderParts = [];
543
+ for (const item of orderArray) {
544
+ if (typeof item === 'string') {
545
+ orderParts.push(item);
546
+ }
547
+ else if (Array.isArray(item)) {
548
+ const [modelOrField, direction] = item;
549
+ if (typeof modelOrField === 'string') {
550
+ const field = modelOrField.includes('.')
551
+ ? modelOrField
552
+ .split('.')
553
+ .map((part) => this.escapeId(part))
554
+ .join('.')
555
+ : this.escapeId(modelOrField);
556
+ orderParts.push(direction ? `${field} ${direction}` : field);
557
+ }
558
+ else if (modelOrField && typeof modelOrField === 'object') {
559
+ // Handle object format: [{ model: ModelStatic, as?: string }, field]
560
+ const modelInfo = modelOrField;
561
+ const tableName = modelInfo.tableName || modelInfo.name || '';
562
+ const fieldStr = direction || '';
563
+ if (tableName && fieldStr) {
564
+ orderParts.push(`${this.escapeId(tableName)}.${this.escapeId(fieldStr)}`);
565
+ }
566
+ else if (fieldStr) {
567
+ orderParts.push(this.escapeId(fieldStr));
568
+ }
569
+ }
570
+ }
571
+ else if (item && typeof item === 'object') {
572
+ // Handle object like { tableName: 'users', fieldName: 'name' }
573
+ const itemObj = item;
574
+ if (itemObj.tableName && itemObj.fieldName) {
575
+ orderParts.push(`${this.escapeId(itemObj.tableName)}.${this.escapeId(itemObj.fieldName)}`);
576
+ }
577
+ else if (itemObj.fieldName) {
578
+ orderParts.push(this.escapeId(itemObj.fieldName));
579
+ }
580
+ }
581
+ }
582
+ return orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
583
+ }
584
+ /**
585
+ * Compile LIMIT/OFFSET clause
586
+ * Handles SQLite which requires LIMIT when using OFFSET
587
+ */
588
+ compileLimitOffset(limit, offset) {
589
+ let sql = '';
590
+ // Check if we're using SQLite
591
+ const isSQLite = this.dialect?.name === 'sqlite';
592
+ if (limit !== undefined && limit !== null) {
593
+ const limitValue = Number(limit);
594
+ if (!isNaN(limitValue) && limitValue >= 0) {
595
+ sql += ` LIMIT ${limitValue}`;
596
+ if (offset !== undefined && offset !== null) {
597
+ const offsetValue = Number(offset);
598
+ if (!isNaN(offsetValue) && offsetValue >= 0) {
599
+ sql += ` OFFSET ${offsetValue}`;
600
+ }
601
+ }
602
+ }
603
+ }
604
+ else if (offset !== undefined && offset !== null) {
605
+ // If only offset is provided without limit
606
+ const offsetValue = Number(offset);
607
+ if (!isNaN(offsetValue) && offsetValue >= 0) {
608
+ if (isSQLite) {
609
+ // SQLite requires LIMIT when using OFFSET
610
+ // Use a very large limit to get all remaining rows
611
+ sql += ` LIMIT 9223372036854775807 OFFSET ${offsetValue}`;
612
+ }
613
+ else {
614
+ // Other databases allow OFFSET without LIMIT
615
+ sql += ` OFFSET ${offsetValue}`;
616
+ }
617
+ }
618
+ }
619
+ return sql;
620
+ }
621
+ /**
622
+ * Compile GROUP BY clause
623
+ * Supports:
624
+ * - Plain strings: 'role', 'userId'
625
+ * - Arrays of strings: ['role', 'status']
626
+ * - col() expressions: col('role'), col('table.column')
627
+ * - fn() expressions: fn('UPPER', col('name'))
628
+ * - literal expressions: literal('YEAR(created_at)')
629
+ * - Array of mixed expressions
630
+ *
631
+ * @example
632
+ * group: 'role'
633
+ * group: ['role', 'status']
634
+ * group: col('role')
635
+ * group: [col('role'), fn('COUNT', col('id'))]
636
+ */
637
+ compileGroupBy(group, groupType, groupingSets) {
638
+ const hasGroupType = groupType && groupType !== 'none';
639
+ const hasGroupingSets = groupingSets && groupingSets.length > 0;
640
+ // If no group and no advanced features, return empty
641
+ if (!group && !hasGroupType && !hasGroupingSets)
642
+ return '';
643
+ // Build the base GROUP BY clause from the group parameter
644
+ let groupFields = [];
645
+ if (group) {
646
+ const groupArray = Array.isArray(group) ? group : [group];
647
+ groupFields = groupArray.map((field) => {
648
+ // Handle col() expressions: { __type: 'col', col: 'field' }
649
+ if ((0, operators_1.isColExpression)(field)) {
650
+ const col = field.col;
651
+ if (col.includes('.')) {
652
+ return col
653
+ .split('.')
654
+ .map((part) => this.escapeId(part))
655
+ .join('.');
656
+ }
657
+ return this.escapeId(col);
658
+ }
659
+ // Handle fn() expressions: { __type: 'fn', fn: 'UPPER', args: [...] }
660
+ if ((0, operators_1.isFnExpression)(field)) {
661
+ const args = field.args.map((arg) => this.processAttribute(arg)).join(', ');
662
+ return `${field.fn}(${args})`;
663
+ }
664
+ // Handle literal expressions: { __type: 'literal', sql: '...' }
665
+ if ((0, operators_1.isLiteralExpression)(field)) {
666
+ return field.sql;
667
+ }
668
+ // Handle plain string - could be a column name or a literal SQL expression
669
+ if (typeof field === 'string') {
670
+ // Check if it's a literal SQL expression (contains parentheses, aggregate functions, etc.)
671
+ if (field.includes('(') || field.includes('*') || field.includes(' ')) {
672
+ // It's likely a literal SQL expression, use as-is
673
+ return field;
674
+ }
675
+ // Check for dot notation: table.column
676
+ if (field.includes('.')) {
677
+ return field
678
+ .split('.')
679
+ .map((part) => this.escapeId(part))
680
+ .join('.');
681
+ }
682
+ // Simple column name - escape it
683
+ return this.escapeId(field);
684
+ }
685
+ // Fallback: convert to string
686
+ return String(field);
687
+ });
688
+ }
689
+ // Handle GROUPING SETS
690
+ if (hasGroupingSets) {
691
+ const setsSql = groupingSets
692
+ .map((set) => {
693
+ if (set.length === 0) {
694
+ return '()'; // Grand total
695
+ }
696
+ return `(${set.map((col) => this.escapeId(col)).join(', ')})`;
697
+ })
698
+ .join(', ');
699
+ return `GROUP BY GROUPING SETS(${setsSql})`;
700
+ }
701
+ // Handle ROLLUP
702
+ if (groupType === 'rollup') {
703
+ if (groupFields.length === 0) {
704
+ return 'GROUP BY ROLLUP()';
705
+ }
706
+ return `GROUP BY ROLLUP(${groupFields.join(', ')})`;
707
+ }
708
+ // Handle CUBE
709
+ if (groupType === 'cube') {
710
+ if (groupFields.length === 0) {
711
+ return 'GROUP BY CUBE()';
712
+ }
713
+ return `GROUP BY CUBE(${groupFields.join(', ')})`;
714
+ }
715
+ // Standard GROUP BY (no advanced features)
716
+ if (groupFields.length > 0) {
717
+ return `GROUP BY ${groupFields.join(', ')}`;
718
+ }
719
+ return '';
720
+ }
721
+ /**
722
+ * Compile Common Table Expression (CTE) clause
723
+ * Supports both regular WITH and WITH RECURSIVE
724
+ */
725
+ compileCTE(ctes) {
726
+ if (!ctes || ctes.length === 0) {
727
+ return '';
728
+ }
729
+ // Check if any CTE is recursive
730
+ const hasRecursive = ctes.some((cte) => cte.recursive);
731
+ const keyword = hasRecursive ? 'WITH RECURSIVE' : 'WITH';
732
+ const cteDefinitions = ctes
733
+ .map((cte) => {
734
+ const columnsPart = cte.columns && cte.columns.length > 0 ? `(${cte.columns.join(', ')})` : '';
735
+ return `${cte.name}${columnsPart} AS (${cte.query})`;
736
+ })
737
+ .join(', ');
738
+ return `${keyword} ${cteDefinitions}`;
739
+ }
740
+ /**
741
+ * Compile HAVING clause for aggregation filtering
742
+ * Similar to WHERE but specifically for aggregated results
743
+ * Supports all operators: Op.gt, Op.eq, Op.between, etc.
744
+ * Also supports direct aggregation functions: COUNT(), SUM(), AVG(), MAX(), MIN()
745
+ */
746
+ compileHaving(having, options) {
747
+ const values = [];
748
+ const paramChar = options?.paramChar || this.options.paramChar || '?';
749
+ if (!having || (typeof having === 'object' && Object.keys(having).length === 0)) {
750
+ return { sql: '', values };
751
+ }
752
+ // Handle special having formats
753
+ // Format: { $where: { $col: 'count', $gt: 5 } }
754
+ if (having.$where && having.$where.$col) {
755
+ const wherePart = having.$where;
756
+ const col = wherePart.$col;
757
+ const opKey = Object.keys(wherePart).find((k) => k.startsWith('$') && k !== '$col' && k !== '$where');
758
+ if (opKey) {
759
+ values.push(wherePart[opKey]);
760
+ const op = opKey.replace('$', '');
761
+ const opSql = this.getOperatorSql(op);
762
+ return { sql: `${this.escapeId(col)} ${opSql} ${paramChar}`, values };
763
+ }
764
+ }
765
+ // Standard having format: { count: { $gt: 5 } }
766
+ const sql = this.buildHavingClause(having, values, paramChar);
767
+ return { sql, values };
768
+ }
769
+ /**
770
+ * Get SQL operator from operator key
771
+ */
772
+ getOperatorSql(op) {
773
+ const operatorMap = {
774
+ eq: '=',
775
+ ne: '!=',
776
+ gt: '>',
777
+ gte: '>=',
778
+ lt: '<',
779
+ lte: '<=',
780
+ like: 'LIKE',
781
+ notLike: 'NOT LIKE',
782
+ iLike: 'ILIKE',
783
+ notILike: 'NOT ILIKE',
784
+ in: 'IN',
785
+ notIn: 'NOT IN',
786
+ between: 'BETWEEN',
787
+ notBetween: 'NOT BETWEEN',
788
+ isNull: 'IS NULL',
789
+ isNotNull: 'IS NOT NULL',
790
+ regexp: 'REGEXP',
791
+ notRegexp: 'NOT REGEXP',
792
+ startsWith: 'LIKE',
793
+ endsWith: 'LIKE',
794
+ substring: 'LIKE',
795
+ };
796
+ return operatorMap[op] || '=';
797
+ }
798
+ /**
799
+ * Build HAVING clause from having options
800
+ */
801
+ buildHavingClause(having, values, paramChar) {
802
+ if (!having)
803
+ return '';
804
+ // Handle logical operators ($and, $or, $not)
805
+ if ('$and' in having || '$or' in having || '$not' in having) {
806
+ return this.buildLogicalHavingClause(having, values, paramChar);
807
+ }
808
+ // Handle regular field conditions
809
+ const conditions = [];
810
+ for (const [key, value] of Object.entries(having)) {
811
+ if (key.startsWith('$'))
812
+ continue; // Skip logical operators
813
+ const condition = this.buildHavingCondition(key, value, values, paramChar);
814
+ if (condition) {
815
+ conditions.push(condition);
816
+ }
817
+ }
818
+ return conditions.join(' AND ');
819
+ }
820
+ /**
821
+ * Build logical HAVING clause (AND, OR, NOT)
822
+ */
823
+ buildLogicalHavingClause(having, values, paramChar) {
824
+ const parts = [];
825
+ if (having.$and && Array.isArray(having.$and)) {
826
+ const andParts = having.$and.map((cond) => {
827
+ if (typeof cond === 'object' && cond !== null) {
828
+ return `(${this.buildHavingClause(cond, values, paramChar)})`;
829
+ }
830
+ return `${paramChar}`;
831
+ });
832
+ if (andParts.length > 0) {
833
+ parts.push(`(${andParts.join(' AND ')})`);
834
+ }
835
+ }
836
+ if (having.$or && Array.isArray(having.$or)) {
837
+ const orParts = having.$or.map((cond) => {
838
+ if (typeof cond === 'object' && cond !== null) {
839
+ return `(${this.buildHavingClause(cond, values, paramChar)})`;
840
+ }
841
+ return `${paramChar}`;
842
+ });
843
+ if (orParts.length > 0) {
844
+ parts.push(`(${orParts.join(' OR ')})`);
845
+ }
846
+ }
847
+ if (having.$not) {
848
+ const notPart = this.buildHavingClause(having.$not, values, paramChar);
849
+ if (notPart) {
850
+ parts.push(`NOT (${notPart})`);
851
+ }
852
+ }
853
+ return parts.join(' AND ');
854
+ }
855
+ /**
856
+ * Build a single HAVING condition
857
+ */
858
+ buildHavingCondition(key, value, values, paramChar) {
859
+ // Direct value (simple equality)
860
+ if (!value || typeof value !== 'object') {
861
+ values.push(value);
862
+ return `${this.escapeId(key)} = ${paramChar}`;
863
+ }
864
+ // Object with operators
865
+ const opValue = value;
866
+ // Handle $eq
867
+ if (opValue.$eq !== undefined) {
868
+ values.push(opValue.$eq);
869
+ return `${this.escapeId(key)} = ${paramChar}`;
870
+ }
871
+ // Handle $ne
872
+ if (opValue.$ne !== undefined) {
873
+ values.push(opValue.$ne);
874
+ return `${this.escapeId(key)} != ${paramChar}`;
875
+ }
876
+ // Handle $gt
877
+ if (opValue.$gt !== undefined) {
878
+ values.push(opValue.$gt);
879
+ return `${this.escapeId(key)} > ${paramChar}`;
880
+ }
881
+ // Handle $gte
882
+ if (opValue.$gte !== undefined) {
883
+ values.push(opValue.$gte);
884
+ return `${this.escapeId(key)} >= ${paramChar}`;
885
+ }
886
+ // Handle $lt
887
+ if (opValue.$lt !== undefined) {
888
+ values.push(opValue.$lt);
889
+ return `${this.escapeId(key)} < ${paramChar}`;
890
+ }
891
+ // Handle $lte
892
+ if (opValue.$lte !== undefined) {
893
+ values.push(opValue.$lte);
894
+ return `${this.escapeId(key)} <= ${paramChar}`;
895
+ }
896
+ // Handle $like
897
+ if (opValue.$like !== undefined) {
898
+ values.push(opValue.$like);
899
+ return `${this.escapeId(key)} LIKE ${paramChar}`;
900
+ }
901
+ // Handle $notLike
902
+ if (opValue.$notLike !== undefined) {
903
+ values.push(opValue.$notLike);
904
+ return `${this.escapeId(key)} NOT LIKE ${paramChar}`;
905
+ }
906
+ // Handle $iLike (case-insensitive like)
907
+ if (opValue.$iLike !== undefined) {
908
+ values.push(opValue.$iLike);
909
+ return `${this.escapeId(key)} ILIKE ${paramChar}`;
910
+ }
911
+ // Handle $notILike
912
+ if (opValue.$notILike !== undefined) {
913
+ values.push(opValue.$notILike);
914
+ return `${this.escapeId(key)} NOT ILIKE ${paramChar}`;
915
+ }
916
+ // Handle $startsWith - LIKE 'value%'
917
+ if (opValue.$startsWith !== undefined) {
918
+ values.push(opValue.$startsWith + '%');
919
+ return `${this.escapeId(key)} LIKE ${paramChar}`;
920
+ }
921
+ // Handle $notStartsWith - NOT LIKE 'value%'
922
+ if (opValue.$notStartsWith !== undefined) {
923
+ values.push(opValue.$notStartsWith + '%');
924
+ return `${this.escapeId(key)} NOT LIKE ${paramChar}`;
925
+ }
926
+ // Handle $endsWith - LIKE '%value'
927
+ if (opValue.$endsWith !== undefined) {
928
+ values.push('%' + opValue.$endsWith);
929
+ return `${this.escapeId(key)} LIKE ${paramChar}`;
930
+ }
931
+ // Handle $notEndsWith - NOT LIKE '%value'
932
+ if (opValue.$notEndsWith !== undefined) {
933
+ values.push('%' + opValue.$notEndsWith);
934
+ return `${this.escapeId(key)} NOT LIKE ${paramChar}`;
935
+ }
936
+ // Handle $substring - LIKE '%value%'
937
+ if (opValue.$substring !== undefined) {
938
+ values.push('%' + opValue.$substring + '%');
939
+ return `${this.escapeId(key)} LIKE ${paramChar}`;
940
+ }
941
+ // Handle $notSubstring - NOT LIKE '%value%'
942
+ if (opValue.$notSubstring !== undefined) {
943
+ values.push('%' + opValue.$notSubstring + '%');
944
+ return `${this.escapeId(key)} NOT LIKE ${paramChar}`;
945
+ }
946
+ // Handle $in
947
+ if (opValue.$in !== undefined) {
948
+ const inValues = Array.isArray(opValue.$in) ? opValue.$in : [opValue.$in];
949
+ values.push(...inValues);
950
+ const placeholders = inValues.map(() => paramChar).join(', ');
951
+ return `${this.escapeId(key)} IN (${placeholders})`;
952
+ }
953
+ // Handle $notIn
954
+ if (opValue.$notIn !== undefined) {
955
+ const notInValues = Array.isArray(opValue.$notIn) ? opValue.$notIn : [opValue.$notIn];
956
+ values.push(...notInValues);
957
+ const placeholders = notInValues.map(() => paramChar).join(', ');
958
+ return `${this.escapeId(key)} NOT IN (${placeholders})`;
959
+ }
960
+ // Handle $between
961
+ if (opValue.$between !== undefined) {
962
+ const between = opValue.$between;
963
+ if (Array.isArray(between) && between.length >= 2) {
964
+ values.push(between[0], between[1]);
965
+ return `${this.escapeId(key)} BETWEEN ${paramChar} AND ${paramChar}`;
966
+ }
967
+ }
968
+ // Handle $notBetween
969
+ if (opValue.$notBetween !== undefined) {
970
+ const notBetween = opValue.$notBetween;
971
+ if (Array.isArray(notBetween) && notBetween.length >= 2) {
972
+ values.push(notBetween[0], notBetween[1]);
973
+ return `${this.escapeId(key)} NOT BETWEEN ${paramChar} AND ${paramChar}`;
974
+ }
975
+ }
976
+ // Handle $isNull
977
+ if (opValue.$isNull !== undefined) {
978
+ if (opValue.$isNull) {
979
+ return `${this.escapeId(key)} IS NULL`;
980
+ }
981
+ return `${this.escapeId(key)} IS NOT NULL`;
982
+ }
983
+ // Handle $isNotNull (alias for $isNull: false)
984
+ if (opValue.$isNotNull !== undefined) {
985
+ if (opValue.$isNotNull) {
986
+ return `${this.escapeId(key)} IS NOT NULL`;
987
+ }
988
+ return `${this.escapeId(key)} IS NULL`;
989
+ }
990
+ // Handle $regexp
991
+ if (opValue.$regexp !== undefined) {
992
+ values.push(opValue.$regexp);
993
+ return `${this.escapeId(key)} REGEXP ${paramChar}`;
994
+ }
995
+ // Handle $notRegexp
996
+ if (opValue.$notRegexp !== undefined) {
997
+ values.push(opValue.$notRegexp);
998
+ return `${this.escapeId(key)} NOT REGEXP ${paramChar}`;
999
+ }
1000
+ // Default: equality
1001
+ values.push(value);
1002
+ return `${this.escapeId(key)} = ${paramChar}`;
1003
+ }
1004
+ /**
1005
+ * Compile a full SELECT query
1006
+ */
1007
+ compileSelect(options) {
1008
+ const values = [];
1009
+ // Build JOIN clause with support for nested includes first (to get includes info)
1010
+ let flattenedIncludes = [];
1011
+ if (options.include && options.include.length > 0) {
1012
+ flattenedIncludes = this.flattenIncludesForJoins(options.include, 'main');
1013
+ }
1014
+ // Build SELECT clause with proper column handling to avoid duplicates
1015
+ let selectSql;
1016
+ if (options.attributes) {
1017
+ // Use explicit attributes from main table only (to avoid duplicates)
1018
+ if (Array.isArray(options.attributes)) {
1019
+ // Handle array of attributes with support for fn(), col(), literal() and aliases
1020
+ selectSql = options.attributes.map((a) => this.attributeToSql(a, 'main')).join(', ');
1021
+ }
1022
+ else if (options.attributes.include) {
1023
+ // Handle include/exclude format with function expression support
1024
+ selectSql = options.attributes.include
1025
+ .map((a) => this.attributeToSql(a, 'main'))
1026
+ .join(', ');
1027
+ }
1028
+ else if (options.attributes.exclude) {
1029
+ // For exclude, we need all columns from main table except excluded ones
1030
+ selectSql = `* EXCEPT (${options.attributes.exclude.map((a) => this.escapeId(a)).join(', ')})`;
1031
+ }
1032
+ else {
1033
+ selectSql = this.generateSelectColumns(options.tableName, 'main', options.attributes, flattenedIncludes);
1034
+ }
1035
+ }
1036
+ else if (flattenedIncludes.length > 0) {
1037
+ // When there are includes, use table aliases to avoid duplicate columns
1038
+ selectSql = this.generateSelectColumns(options.tableName, 'main', undefined, flattenedIncludes);
1039
+ }
1040
+ else {
1041
+ // Simple case: just select all from main table
1042
+ selectSql = `${this.escapeId('main')}.*`;
1043
+ }
1044
+ // Build CTE clause if present
1045
+ const cteClause = this.compileCTE(options.cte);
1046
+ const ctePrefix = cteClause ? `${cteClause} ` : '';
1047
+ let sql = `${ctePrefix}SELECT ${selectSql} FROM ${this.escapeId(options.tableName)} AS ${this.escapeId('main')}`;
1048
+ // Collect anti-join subqueries for WHERE clause
1049
+ const antiJoinSubqueries = [];
1050
+ const antiJoinValues = [];
1051
+ // Build JOIN clause with support for nested includes
1052
+ for (const item of flattenedIncludes) {
1053
+ // Skip anti-joins that use notExists or notIn patterns (they'll be handled in WHERE)
1054
+ if (item.antiJoin && (item.antiJoinType === 'notExists' || item.antiJoinType === 'notIn')) {
1055
+ // Build subquery for anti-join
1056
+ const subquery = this.buildAntiJoinSubquery(item, options.tableName);
1057
+ if (item.antiJoinType === 'notExists') {
1058
+ antiJoinSubqueries.push(`NOT EXISTS (${subquery.sql})`);
1059
+ }
1060
+ else {
1061
+ antiJoinSubqueries.push(`NOT IN (${subquery.sql})`);
1062
+ }
1063
+ antiJoinValues.push(...subquery.values);
1064
+ continue;
1065
+ }
1066
+ let joinType;
1067
+ if (item.joinType) {
1068
+ switch (item.joinType) {
1069
+ case 'INNER':
1070
+ joinType = 'INNER JOIN';
1071
+ break;
1072
+ case 'LEFT':
1073
+ joinType = 'LEFT JOIN';
1074
+ break;
1075
+ case 'RIGHT':
1076
+ joinType = 'RIGHT JOIN';
1077
+ break;
1078
+ case 'FULL':
1079
+ joinType = 'FULL OUTER JOIN';
1080
+ break;
1081
+ case 'CROSS':
1082
+ joinType = 'CROSS JOIN';
1083
+ break;
1084
+ default:
1085
+ joinType = item.required ? 'INNER JOIN' : 'LEFT JOIN';
1086
+ }
1087
+ }
1088
+ else {
1089
+ joinType = item.required ? 'INNER JOIN' : 'LEFT JOIN';
1090
+ }
1091
+ // For anti-joins with leftJoinIsNull, always use LEFT JOIN
1092
+ if (item.antiJoin && item.antiJoinType === 'leftJoinIsNull') {
1093
+ joinType = 'LEFT JOIN';
1094
+ }
1095
+ // Handle LATERAL keyword
1096
+ const lateralKeyword = item.lateral ? 'LATERAL ' : '';
1097
+ sql += ` ${joinType} ${lateralKeyword}${this.escapeId(item.tableName)} ${this.escapeId(item.alias)}`;
1098
+ // Add ON clause
1099
+ if (item.on) {
1100
+ const onClause = this.buildWhereClause(item.on, values, this.options.paramChar || '?');
1101
+ if (onClause) {
1102
+ sql += ` ON ${onClause}`;
1103
+ }
1104
+ }
1105
+ else if (item.parentAlias && item.foreignKey && item.targetKey) {
1106
+ // Build implicit ON clause from foreign key
1107
+ sql += ` ON ${this.escapeId(item.parentAlias)}.${this.escapeId(item.targetKey)} = ${this.escapeId(item.alias)}.${this.escapeId(item.foreignKey)}`;
1108
+ }
1109
+ // Add WHERE clause for the joined table if specified
1110
+ if (item.where) {
1111
+ const whereClause = this.compileWhere(item.where);
1112
+ if (whereClause.sql) {
1113
+ sql += ` AND (${whereClause.sql})`;
1114
+ values.push(...whereClause.values);
1115
+ }
1116
+ }
1117
+ // For leftJoinIsNull anti-join, add WHERE clause to check for NULL
1118
+ if (item.antiJoin && item.antiJoinType === 'leftJoinIsNull') {
1119
+ sql += ` AND ${this.escapeId(item.alias)}.${this.escapeId(item.targetKey || 'id')} IS NULL`;
1120
+ }
1121
+ }
1122
+ // Build WHERE clause
1123
+ const whereConditions = [];
1124
+ const whereValues = [];
1125
+ // Add user-provided where conditions
1126
+ if (options.where) {
1127
+ const whereClause = this.compileWhere(options.where);
1128
+ if (whereClause.sql) {
1129
+ whereConditions.push(whereClause.sql);
1130
+ whereValues.push(...whereClause.values);
1131
+ }
1132
+ }
1133
+ // Add anti-join subqueries (NOT EXISTS or NOT IN)
1134
+ if (antiJoinSubqueries.length > 0) {
1135
+ whereConditions.push(...antiJoinSubqueries);
1136
+ whereValues.push(...antiJoinValues);
1137
+ }
1138
+ if (whereConditions.length > 0) {
1139
+ sql += ` WHERE ${whereConditions.join(' AND ')}`;
1140
+ values.push(...whereValues);
1141
+ }
1142
+ // Build GROUP BY clause
1143
+ if (options.group || options.groupType || options.groupingSets) {
1144
+ sql += ` ${this.compileGroupBy(options.group, options.groupType, options.groupingSets)}`;
1145
+ }
1146
+ // Handle PIVOT clause
1147
+ if (options.pivot) {
1148
+ const pivot = options.pivot;
1149
+ const groupByColumns = options.group
1150
+ ? Array.isArray(options.group)
1151
+ ? options.group
1152
+ : [options.group]
1153
+ : [];
1154
+ // Get unique pivot values if not provided
1155
+ const pivotValues = options.pivotValues || [];
1156
+ if (pivotValues.length > 0) {
1157
+ // Build CASE WHEN expressions for each pivot value and aggregated column
1158
+ const aggregatedColumns = [];
1159
+ const pivotColumn = this.escapeId(pivot.on);
1160
+ for (const [column, aggregateFn] of Object.entries(pivot.using)) {
1161
+ const aggFn = aggregateFn.toUpperCase();
1162
+ const escapedColumn = this.escapeId(column);
1163
+ for (const pivotValue of pivotValues) {
1164
+ const columnAlias = `${column}_${pivotValue}`.replace(/[^a-zA-Z0-9_]/g, '_');
1165
+ aggregatedColumns.push(`${aggFn}(CASE WHEN ${pivotColumn} = ${this.escape(pivotValue)} THEN ${escapedColumn} END) AS ${this.escapeId(columnAlias)}`);
1166
+ }
1167
+ }
1168
+ // Replace the select clause with pivot columns
1169
+ const groupByCols = groupByColumns.map((g) => this.escapeId(g)).join(', ');
1170
+ selectSql = groupByCols
1171
+ ? `${groupByCols}, ${aggregatedColumns.join(', ')}`
1172
+ : aggregatedColumns.join(', ');
1173
+ }
1174
+ }
1175
+ // Handle UNPIVOT clause
1176
+ if (options.unpivot) {
1177
+ const unpivot = options.unpivot;
1178
+ const unpivotColumns = unpivot.in.map((col) => this.escapeId(col));
1179
+ const valueColumn = this.escapeId(unpivot.valueColumn);
1180
+ const nameColumn = this.escapeId(unpivot.nameColumn);
1181
+ // Build the VALUES clause for CROSS JOIN
1182
+ const valuesClauses = [];
1183
+ for (const col of unpivot.in) {
1184
+ const escapedCol = this.escapeId(col);
1185
+ valuesClauses.push(`(${this.escape(col)}, ${escapedCol})`);
1186
+ }
1187
+ // Replace the select clause with unpivot columns
1188
+ const groupByCols = options.group
1189
+ ? (Array.isArray(options.group) ? options.group : [options.group])
1190
+ .map((g) => this.escapeId(g))
1191
+ .join(', ')
1192
+ : '';
1193
+ selectSql = groupByCols
1194
+ ? `${groupByCols}, t.${nameColumn}, t.${valueColumn}`
1195
+ : `t.${nameColumn}, t.${valueColumn}`;
1196
+ // Add CROSS JOIN with VALUES after the main table
1197
+ sql = sql.replace(`FROM ${this.escapeId(options.tableName)} AS ${this.escapeId('main')}`, `FROM ${this.escapeId(options.tableName)} AS ${this.escapeId('main')} CROSS JOIN (VALUES ${valuesClauses.join(', ')}) AS t(${nameColumn}, ${valueColumn})`);
1198
+ }
1199
+ // Build HAVING clause
1200
+ if (options.having) {
1201
+ const havingClause = this.compileHaving(options.having);
1202
+ if (havingClause.sql) {
1203
+ sql += ` HAVING ${havingClause.sql}`;
1204
+ values.push(...havingClause.values);
1205
+ }
1206
+ }
1207
+ // Build ORDER BY clause
1208
+ if (options.order) {
1209
+ const orderClause = this.compileOrder(options.order);
1210
+ if (orderClause) {
1211
+ sql += ` ${orderClause}`;
1212
+ }
1213
+ }
1214
+ // Build LIMIT/OFFSET clause
1215
+ sql += this.compileLimitOffset(options.limit, options.offset);
1216
+ return { sql, values };
1217
+ }
1218
+ /**
1219
+ * Compile a PIVOT query - transform rows to columns
1220
+ */
1221
+ compilePivot(options) {
1222
+ const values = [];
1223
+ const { pivot, groupBy } = options;
1224
+ // Get unique values from the pivot column
1225
+ const pivotColumn = this.escapeId(pivot.on);
1226
+ // Build the SELECT clause with pivot columns
1227
+ const groupByColumns = Array.isArray(groupBy)
1228
+ ? groupBy.map((g) => this.escapeId(g)).join(', ')
1229
+ : this.escapeId(groupBy);
1230
+ // Build CASE WHEN expressions for each aggregated column
1231
+ const pivotColumns = [];
1232
+ const pivotValues = new Set();
1233
+ // We need to get unique values from the pivot column at query time
1234
+ // This will be handled by the query-generator which will first query unique values
1235
+ // Build aggregated columns using CASE WHEN
1236
+ const aggregatedColumns = [];
1237
+ for (const [column, aggregateFn] of Object.entries(pivot.using)) {
1238
+ const aggFn = aggregateFn.toUpperCase();
1239
+ const escapedColumn = this.escapeId(column);
1240
+ // Generate column alias from the pivot column value
1241
+ // The actual pivot values will be provided by the query-generator
1242
+ aggregatedColumns.push(`${aggFn}(CASE WHEN ${pivotColumn} = ? THEN ${escapedColumn} END) AS ${this.escapeId(column)}`);
1243
+ values.push(pivot.on); // Placeholder for the value - will be replaced
1244
+ }
1245
+ let sql = `SELECT ${groupByColumns}, ${aggregatedColumns.join(', ')} FROM ${this.escapeId(options.tableName)}`;
1246
+ // Add WHERE clause
1247
+ if (options.where) {
1248
+ const whereClause = this.compileWhere(options.where);
1249
+ if (whereClause.sql) {
1250
+ sql += ` WHERE ${whereClause.sql}`;
1251
+ values.push(...whereClause.values);
1252
+ }
1253
+ }
1254
+ // Add GROUP BY clause
1255
+ sql += ` GROUP BY ${groupByColumns}`;
1256
+ // Add ORDER BY clause
1257
+ if (options.order) {
1258
+ const orderClause = this.compileOrder(options.order);
1259
+ if (orderClause) {
1260
+ sql += ` ORDER BY ${orderClause}`;
1261
+ }
1262
+ }
1263
+ // Add LIMIT/OFFSET clause
1264
+ sql += this.compileLimitOffset(options.limit, options.offset);
1265
+ return { sql, values };
1266
+ }
1267
+ /**
1268
+ * Compile a PIVOT query with specific pivot values (after they're resolved)
1269
+ */
1270
+ compilePivotWithValues(options) {
1271
+ const values = [];
1272
+ const { pivot, pivotValues: valuesToPivot, groupBy } = options;
1273
+ // Get the pivot column
1274
+ const pivotColumn = this.escapeId(pivot.on);
1275
+ // Build GROUP BY clause
1276
+ const groupByColumns = Array.isArray(groupBy)
1277
+ ? groupBy.map((g) => this.escapeId(g)).join(', ')
1278
+ : this.escapeId(groupBy);
1279
+ // Build CASE WHEN expressions for each pivot value and aggregated column
1280
+ const aggregatedColumns = [];
1281
+ for (const [column, aggregateFn] of Object.entries(pivot.using)) {
1282
+ const aggFn = aggregateFn.toUpperCase();
1283
+ const escapedColumn = this.escapeId(column);
1284
+ for (const pivotValue of valuesToPivot) {
1285
+ const columnAlias = `${column}_${pivotValue}`.replace(/[^a-zA-Z0-9_]/g, '_');
1286
+ aggregatedColumns.push(`${aggFn}(CASE WHEN ${pivotColumn} = ${this.escape(pivotValue)} THEN ${escapedColumn} END) AS ${this.escapeId(columnAlias)}`);
1287
+ }
1288
+ }
1289
+ let sql = `SELECT ${groupByColumns}, ${aggregatedColumns.join(', ')} FROM ${this.escapeId(options.tableName)}`;
1290
+ // Add WHERE clause
1291
+ if (options.where) {
1292
+ const whereClause = this.compileWhere(options.where);
1293
+ if (whereClause.sql) {
1294
+ sql += ` WHERE ${whereClause.sql}`;
1295
+ values.push(...whereClause.values);
1296
+ }
1297
+ }
1298
+ // Add GROUP BY clause
1299
+ sql += ` GROUP BY ${groupByColumns}`;
1300
+ // Add ORDER BY clause
1301
+ if (options.order) {
1302
+ const orderClause = this.compileOrder(options.order);
1303
+ if (orderClause) {
1304
+ sql += ` ORDER BY ${orderClause}`;
1305
+ }
1306
+ }
1307
+ // Add LIMIT/OFFSET clause
1308
+ sql += this.compileLimitOffset(options.limit, options.offset);
1309
+ return { sql, values };
1310
+ }
1311
+ /**
1312
+ * Compile an UNPIVOT query - transform columns to rows
1313
+ */
1314
+ compileUnpivot(options) {
1315
+ const values = [];
1316
+ const { unpivot } = options;
1317
+ // Columns to keep (not unpivoted) - we'll assume all non-unpivot columns are kept
1318
+ // For simplicity, we'll include all columns and let the user specify via attributes
1319
+ const unpivotColumns = unpivot.in.map((col) => this.escapeId(col));
1320
+ const valueColumn = this.escapeId(unpivot.valueColumn);
1321
+ const nameColumn = this.escapeId(unpivot.nameColumn);
1322
+ // Build the VALUES clause for CROSS JOIN
1323
+ const valuesClauses = [];
1324
+ for (const col of unpivot.in) {
1325
+ const escapedCol = this.escapeId(col);
1326
+ valuesClauses.push(`(${this.escape(col)}, ${escapedCol})`);
1327
+ }
1328
+ let sql = `SELECT t.${nameColumn}, t.${valueColumn} FROM ${this.escapeId(options.tableName)}`;
1329
+ // Add WHERE clause
1330
+ if (options.where) {
1331
+ const whereClause = this.compileWhere(options.where);
1332
+ if (whereClause.sql) {
1333
+ sql += ` WHERE ${whereClause.sql}`;
1334
+ values.push(...whereClause.values);
1335
+ }
1336
+ }
1337
+ // Add CROSS JOIN with VALUES
1338
+ sql += ` CROSS JOIN (VALUES ${valuesClauses.join(', ')}) AS t(${nameColumn}, ${valueColumn})`;
1339
+ // Add ORDER BY clause
1340
+ if (options.order) {
1341
+ const orderClause = this.compileOrder(options.order);
1342
+ if (orderClause) {
1343
+ sql += ` ORDER BY ${orderClause}`;
1344
+ }
1345
+ }
1346
+ // Add LIMIT/OFFSET clause
1347
+ sql += this.compileLimitOffset(options.limit, options.offset);
1348
+ return { sql, values };
1349
+ }
1350
+ /**
1351
+ * Compile an INSERT query
1352
+ */
1353
+ compileInsert(options) {
1354
+ // Map attribute names to column names using field mapping
1355
+ const columns = Object.keys(options.values).map((attrName) => this.getColumnName(attrName));
1356
+ const values = Object.values(options.values);
1357
+ const placeholders = columns.map(() => '?').join(', ');
1358
+ let sql = `INSERT INTO ${this.escapeId(options.tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${placeholders})`;
1359
+ // Add RETURNING clause for supported databases
1360
+ if (options.returning) {
1361
+ if (Array.isArray(options.returning)) {
1362
+ // Map attribute names to column names for RETURNING
1363
+ sql += ` RETURNING ${options.returning.map((r) => this.escapeId(this.getColumnName(r))).join(', ')}`;
1364
+ }
1365
+ else {
1366
+ sql += ' RETURNING *';
1367
+ }
1368
+ }
1369
+ return { sql, values };
1370
+ }
1371
+ /**
1372
+ * Compile an UPDATE query
1373
+ */
1374
+ compileUpdate(options) {
1375
+ const setClauses = [];
1376
+ const values = [];
1377
+ for (const [key, value] of Object.entries(options.values)) {
1378
+ // Map attribute name to column name using field mapping
1379
+ const columnName = this.getColumnName(key);
1380
+ setClauses.push(`${this.escapeId(columnName)} = ?`);
1381
+ values.push(value);
1382
+ }
1383
+ const whereClause = this.compileWhere(options.where);
1384
+ let sql = `UPDATE ${this.escapeId(options.tableName)} SET ${setClauses.join(', ')}`;
1385
+ if (whereClause.sql) {
1386
+ sql += ` WHERE ${whereClause.sql}`;
1387
+ values.push(...whereClause.values);
1388
+ }
1389
+ // Add RETURNING clause
1390
+ if (options.returning) {
1391
+ if (Array.isArray(options.returning)) {
1392
+ // Map attribute names to column names for RETURNING
1393
+ sql += ` RETURNING ${options.returning.map((r) => this.escapeId(this.getColumnName(r))).join(', ')}`;
1394
+ }
1395
+ else {
1396
+ sql += ' RETURNING *';
1397
+ }
1398
+ }
1399
+ return { sql, values };
1400
+ }
1401
+ /**
1402
+ * Compile a DELETE query
1403
+ */
1404
+ compileDelete(options) {
1405
+ const whereClause = this.compileWhere(options.where);
1406
+ let sql = `DELETE FROM ${this.escapeId(options.tableName)}`;
1407
+ if (whereClause.sql) {
1408
+ sql += ` WHERE ${whereClause.sql}`;
1409
+ }
1410
+ if (options.limit !== undefined) {
1411
+ sql += ` LIMIT ${options.limit}`;
1412
+ }
1413
+ // Add RETURNING clause
1414
+ if (options.returning) {
1415
+ if (Array.isArray(options.returning)) {
1416
+ sql += ` RETURNING ${options.returning.map((r) => this.escapeId(r)).join(', ')}`;
1417
+ }
1418
+ else {
1419
+ sql += ' RETURNING *';
1420
+ }
1421
+ }
1422
+ return { sql, values: whereClause.values };
1423
+ }
1424
+ /**
1425
+ * Compile a UNION/EXCEPT/INTERSECT query
1426
+ * Supports combining multiple queries with UNION, UNION ALL, EXCEPT, or INTERSECT
1427
+ */
1428
+ compileUnion(options) {
1429
+ const values = [];
1430
+ // Compile main query
1431
+ const mainResult = this.compileSelect({
1432
+ tableName: options.main.tableName,
1433
+ attributes: options.main.attributes,
1434
+ where: options.main.where,
1435
+ include: options.main.include,
1436
+ order: options.main.order,
1437
+ limit: options.main.limit,
1438
+ offset: options.main.offset,
1439
+ });
1440
+ values.push(...mainResult.values);
1441
+ // Compile union queries
1442
+ const unionParts = [mainResult.sql];
1443
+ for (const u of options.union) {
1444
+ let unionSql = '';
1445
+ // Determine the table to query
1446
+ const tableName = u.tableName ||
1447
+ (u.model
1448
+ ? u.model.tableName || u.model.name
1449
+ : 'unknown');
1450
+ // Compile the union query
1451
+ const unionResult = this.compileSelect({
1452
+ tableName,
1453
+ attributes: u.attributes,
1454
+ where: u.where,
1455
+ include: u.include,
1456
+ order: u.order,
1457
+ limit: u.limit,
1458
+ offset: u.offset,
1459
+ });
1460
+ values.push(...unionResult.values);
1461
+ // Add the union type
1462
+ unionSql = `${u.type} ${unionResult.sql}`;
1463
+ unionParts.push(unionSql);
1464
+ }
1465
+ // Combine all parts
1466
+ let sql = unionParts.join(' ');
1467
+ // Add ORDER BY for the combined result
1468
+ if (options.order) {
1469
+ const orderClause = this.compileOrder(options.order);
1470
+ if (orderClause) {
1471
+ sql += ` ${orderClause}`;
1472
+ }
1473
+ }
1474
+ // Add LIMIT/OFFSET for the combined result
1475
+ sql += this.compileLimitOffset(options.limit, options.offset);
1476
+ return { sql, values };
1477
+ }
1478
+ /**
1479
+ * Compile an EXCEPT query - returns rows from main query that don't exist in except query
1480
+ */
1481
+ compileExcept(mainQuery, exceptQuery, options) {
1482
+ return this.compileUnion({
1483
+ main: mainQuery,
1484
+ union: [
1485
+ {
1486
+ type: 'EXCEPT',
1487
+ tableName: exceptQuery.tableName,
1488
+ attributes: exceptQuery.attributes,
1489
+ where: exceptQuery.where,
1490
+ include: exceptQuery.include,
1491
+ },
1492
+ ],
1493
+ order: options?.order,
1494
+ limit: options?.limit,
1495
+ offset: options?.offset,
1496
+ });
1497
+ }
1498
+ /**
1499
+ * Compile an INTERSECT query - returns rows that exist in both queries
1500
+ */
1501
+ compileIntersect(mainQuery, intersectQuery, options) {
1502
+ return this.compileUnion({
1503
+ main: mainQuery,
1504
+ union: [
1505
+ {
1506
+ type: 'INTERSECT',
1507
+ tableName: intersectQuery.tableName,
1508
+ attributes: intersectQuery.attributes,
1509
+ where: intersectQuery.where,
1510
+ include: intersectQuery.include,
1511
+ },
1512
+ ],
1513
+ order: options?.order,
1514
+ limit: options?.limit,
1515
+ offset: options?.offset,
1516
+ });
1517
+ }
1518
+ /**
1519
+ * Format SQL for different dialects
1520
+ */
1521
+ formatForDialect(sql, dialect) {
1522
+ switch (dialect) {
1523
+ case 'mysql':
1524
+ return this.formatForMySQL(sql);
1525
+ case 'postgres':
1526
+ return this.formatForPostgres(sql);
1527
+ case 'sqlite':
1528
+ return this.formatForSQLite(sql);
1529
+ case 'mssql':
1530
+ return this.formatForMSSQL(sql);
1531
+ default:
1532
+ return sql;
1533
+ }
1534
+ }
1535
+ /**
1536
+ * Format SQL for MySQL
1537
+ */
1538
+ formatForMySQL(sql) {
1539
+ // MySQL uses backticks for identifiers
1540
+ return sql.replace(/"([^"]+)"/g, '`$1`');
1541
+ }
1542
+ /**
1543
+ * Format SQL for PostgreSQL
1544
+ */
1545
+ formatForPostgres(sql) {
1546
+ // PostgreSQL uses double quotes
1547
+ return sql;
1548
+ }
1549
+ /**
1550
+ * Format SQL for SQLite
1551
+ */
1552
+ formatForSQLite(sql) {
1553
+ // SQLite uses double quotes
1554
+ return sql;
1555
+ }
1556
+ /**
1557
+ * Format SQL for MSSQL
1558
+ */
1559
+ formatForMSSQL(sql) {
1560
+ // MSSQL uses square brackets
1561
+ return sql.replace(/"([^"]+)"/g, '[$1]');
1562
+ }
1563
+ /**
1564
+ * Resolve the 'all' include option to actual include configurations
1565
+ */
1566
+ resolveAllInclude(include, parentAlias, depth) {
1567
+ const result = [];
1568
+ const model = include.model;
1569
+ const associations = model?.associations || {};
1570
+ for (const [assocName, association] of Object.entries(associations)) {
1571
+ // If 'all' is a string, only include that specific association
1572
+ if (typeof include.all === 'string' && include.all !== assocName) {
1573
+ continue;
1574
+ }
1575
+ const assocModel = association.target;
1576
+ const assocTableName = assocModel?.tableName || assocModel?.name || assocName;
1577
+ // Build the include config for this association
1578
+ const assocInclude = {
1579
+ model: assocModel,
1580
+ as: assocName,
1581
+ where: include.where,
1582
+ required: include.required,
1583
+ attributes: include.attributes,
1584
+ order: include.order,
1585
+ limit: include.limit,
1586
+ offset: include.offset,
1587
+ duplicating: include.duplicating,
1588
+ };
1589
+ // Handle nested includes within 'all' - recursively process them
1590
+ if (include.include && include.include.length > 0) {
1591
+ assocInclude.include = this.resolveAllInclude({ all: true, include: include.include }, assocName, depth + 1);
1592
+ }
1593
+ result.push(assocInclude);
1594
+ }
1595
+ return result;
1596
+ }
1597
+ /**
1598
+ * Flatten nested includes for proper JOIN ordering
1599
+ * Returns a flat array of include objects with parent alias information
1600
+ */
1601
+ flattenIncludesForJoins(includes, parentAlias = 'main', depth = 0) {
1602
+ const flattened = [];
1603
+ for (const include of includes) {
1604
+ // Handle 'all' option - include all associations or filter by name
1605
+ if (include.all) {
1606
+ const resolvedIncludes = this.resolveAllInclude(include, parentAlias, depth);
1607
+ for (const resolved of resolvedIncludes) {
1608
+ const nested = this.flattenIncludesForJoins([resolved], parentAlias, depth);
1609
+ flattened.push(...nested);
1610
+ }
1611
+ continue;
1612
+ }
1613
+ const model = include.model;
1614
+ const tableName = model?.tableName || model?.name || '';
1615
+ const alias = include.as || tableName;
1616
+ // Guess foreign key if not specified
1617
+ let foreignKey = include.foreignKey;
1618
+ if (!foreignKey) {
1619
+ const modelName = model?.name || tableName;
1620
+ foreignKey = `${modelName.charAt(0).toLowerCase() + modelName.slice(1)}Id`;
1621
+ }
1622
+ const targetKey = (include.targetKey || 'id');
1623
+ // Check for anti-join
1624
+ const isAntiJoin = include.antiJoin === true || include.exclude === true;
1625
+ const antiJoinType = include.antiJoinType || 'leftJoinIsNull';
1626
+ flattened.push({
1627
+ tableName,
1628
+ alias,
1629
+ parentAlias,
1630
+ required: include.required || false,
1631
+ where: include.where,
1632
+ on: include.on,
1633
+ foreignKey,
1634
+ targetKey,
1635
+ depth,
1636
+ joinType: include.joinType,
1637
+ lateral: include.lateral,
1638
+ antiJoin: isAntiJoin,
1639
+ antiJoinType,
1640
+ apply: include.apply,
1641
+ lateralSubquery: include.lateralSubquery,
1642
+ });
1643
+ // Recursively process nested includes
1644
+ if (include.include && include.include.length > 0) {
1645
+ const nested = this.flattenIncludesForJoins(include.include, alias, depth + 1);
1646
+ flattened.push(...nested);
1647
+ }
1648
+ }
1649
+ return flattened;
1650
+ }
1651
+ /**
1652
+ * Build anti-join subquery for NOT EXISTS or NOT IN patterns
1653
+ */
1654
+ buildAntiJoinSubquery(item, mainTableName) {
1655
+ const values = [];
1656
+ // Build the subquery
1657
+ let subquery = `SELECT 1 FROM ${this.escapeId(item.tableName)} ${this.escapeId(item.alias)}`;
1658
+ // Build ON clause to connect to main table
1659
+ const onConditions = [];
1660
+ if (item.on) {
1661
+ const onClause = this.buildWhereClause(item.on, values, this.options.paramChar || '?');
1662
+ if (onClause) {
1663
+ onConditions.push(onClause);
1664
+ }
1665
+ }
1666
+ else if (item.parentAlias && item.foreignKey && item.targetKey) {
1667
+ onConditions.push(`${this.escapeId(item.parentAlias)}.${this.escapeId(item.targetKey)} = ${this.escapeId(item.alias)}.${this.escapeId(item.foreignKey)}`);
1668
+ }
1669
+ if (onConditions.length > 0) {
1670
+ subquery += ` WHERE ${onConditions.join(' AND ')}`;
1671
+ }
1672
+ // Add WHERE clause for the joined table if specified
1673
+ if (item.where) {
1674
+ const whereClause = this.compileWhere(item.where);
1675
+ if (whereClause.sql) {
1676
+ subquery += ` AND (${whereClause.sql})`;
1677
+ values.push(...whereClause.values);
1678
+ }
1679
+ }
1680
+ return { sql: subquery, values };
1681
+ }
1682
+ /**
1683
+ * Build lateral subquery for inline subqueries with LATERAL keyword
1684
+ */
1685
+ buildLateralSubquery(item, values) {
1686
+ const subqueryModel = item.lateralSubquery.model;
1687
+ const tableName = subqueryModel?.tableName || subqueryModel?.name || 'subquery';
1688
+ const subqueryAlias = item.alias;
1689
+ // Build SELECT clause for the subquery
1690
+ let selectColumns = '*';
1691
+ if (item.lateralSubquery.attributes) {
1692
+ if (Array.isArray(item.lateralSubquery.attributes)) {
1693
+ selectColumns = item.lateralSubquery.attributes
1694
+ .map((c) => this.escapeId(c))
1695
+ .join(', ');
1696
+ }
1697
+ else if (item.lateralSubquery.attributes.include) {
1698
+ selectColumns = item.lateralSubquery.attributes.include
1699
+ .map((c) => this.escapeId(c))
1700
+ .join(', ');
1701
+ }
1702
+ }
1703
+ // Build the subquery
1704
+ let subquery = `SELECT ${selectColumns} FROM ${this.escapeId(tableName)}`;
1705
+ // If there's a foreign key relationship, add ON clause to connect to parent
1706
+ if (item.parentAlias && item.foreignKey && item.targetKey) {
1707
+ subquery += ` ON ${this.escapeId(item.parentAlias)}.${this.escapeId(item.targetKey)} = ${this.escapeId(subqueryAlias)}.${this.escapeId(item.foreignKey)}`;
1708
+ }
1709
+ // Add WHERE clause if specified
1710
+ if (item.lateralSubquery.where) {
1711
+ const whereClause = this.compileWhere(item.lateralSubquery.where);
1712
+ if (whereClause.sql) {
1713
+ subquery += ` WHERE ${whereClause.sql}`;
1714
+ values.push(...whereClause.values);
1715
+ }
1716
+ }
1717
+ return subquery;
1718
+ }
1719
+ /**
1720
+ * Generate column list with proper aliasing to avoid duplicates
1721
+ */
1722
+ generateSelectColumns(mainTableName, mainTableAlias, attributes, includes) {
1723
+ // If explicit attributes are provided, use those
1724
+ if (attributes) {
1725
+ if (Array.isArray(attributes)) {
1726
+ // Add table alias prefix to avoid duplicates
1727
+ return attributes
1728
+ .map((a) => `${this.escapeId(mainTableAlias)}.${this.escapeId(a)}`)
1729
+ .join(', ');
1730
+ }
1731
+ else if (attributes.include) {
1732
+ return attributes.include
1733
+ .map((a) => `${this.escapeId(mainTableAlias)}.${this.escapeId(a)}`)
1734
+ .join(', ');
1735
+ }
1736
+ else if (attributes.exclude) {
1737
+ // For exclude, we need to get all columns and exclude specific ones
1738
+ // This is database-specific; for now, return * with EXCEPT
1739
+ return `* EXCEPT (${attributes.exclude.map((a) => this.escapeId(a)).join(', ')})`;
1740
+ }
1741
+ }
1742
+ // Default: select all columns from main table with alias prefix
1743
+ // and all columns from included tables with their aliases
1744
+ const mainColumns = [`${this.escapeId(mainTableAlias)}.*`];
1745
+ // Add columns from included tables to avoid duplicates
1746
+ if (includes && includes.length > 0) {
1747
+ for (const inc of includes) {
1748
+ mainColumns.push(`${this.escapeId(inc.alias)}.*`);
1749
+ }
1750
+ }
1751
+ return mainColumns.join(', ');
1752
+ }
1753
+ // ==================== Window Function Compilation ====================
1754
+ /**
1755
+ * Compile PARTITION BY clause
1756
+ */
1757
+ compilePartitionBy(partitionBy) {
1758
+ if (!partitionBy) {
1759
+ return '';
1760
+ }
1761
+ const partitions = Array.isArray(partitionBy) ? partitionBy : [partitionBy];
1762
+ const partitionParts = partitions.map((part) => {
1763
+ if (part.includes('.')) {
1764
+ return part
1765
+ .split('.')
1766
+ .map((p) => this.escapeId(p))
1767
+ .join('.');
1768
+ }
1769
+ return this.escapeId(part);
1770
+ });
1771
+ return `PARTITION BY ${partitionParts.join(', ')}`;
1772
+ }
1773
+ /**
1774
+ * Compile window frame specification
1775
+ * Examples:
1776
+ * - ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
1777
+ * - RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
1778
+ * - GROUPS 2 PRECEDING AND CURRENT ROW
1779
+ */
1780
+ compileWindowFrame(windowFrame) {
1781
+ if (!windowFrame) {
1782
+ return '';
1783
+ }
1784
+ const { type, start, end } = windowFrame;
1785
+ const frameType = type || 'ROWS';
1786
+ let frameStart = '';
1787
+ let frameEnd = '';
1788
+ // Handle start bound
1789
+ if (start !== undefined) {
1790
+ frameStart = this.compileFrameBound(start);
1791
+ }
1792
+ else {
1793
+ frameStart = 'UNBOUNDED PRECEDING';
1794
+ }
1795
+ // Handle end bound
1796
+ if (end !== undefined) {
1797
+ frameEnd = this.compileFrameBound(end);
1798
+ }
1799
+ else {
1800
+ frameEnd = 'CURRENT ROW';
1801
+ }
1802
+ return `${frameType} BETWEEN ${frameStart} AND ${frameEnd}`;
1803
+ }
1804
+ /**
1805
+ * Compile a single frame bound (start or end)
1806
+ */
1807
+ compileFrameBound(bound) {
1808
+ if (bound === 0) {
1809
+ return 'CURRENT ROW';
1810
+ }
1811
+ if (bound < 0) {
1812
+ const absBound = Math.abs(bound);
1813
+ return absBound === 1 ? '1 PRECEDING' : `${absBound} PRECEDING`;
1814
+ }
1815
+ // bound > 0
1816
+ return bound === 1 ? '1 FOLLOWING' : `${bound} FOLLOWING`;
1817
+ }
1818
+ /**
1819
+ * Compile lock options for row-level locking
1820
+ * Supports NOWAIT and SKIP LOCKED for PostgreSQL and MySQL 8.0+
1821
+ * @param lock - Lock options
1822
+ * @param dialect - The database dialect
1823
+ * @returns The compiled lock SQL clause
1824
+ */
1825
+ compileLockOptions(lock, dialect = 'postgres') {
1826
+ if (!lock) {
1827
+ return '';
1828
+ }
1829
+ let lockType;
1830
+ let nowait = false;
1831
+ let skipLocked = false;
1832
+ // Extract lock configuration
1833
+ if (lock === true) {
1834
+ lockType = 'UPDATE';
1835
+ }
1836
+ else if (typeof lock === 'string') {
1837
+ lockType = lock;
1838
+ }
1839
+ else if (typeof lock === 'object') {
1840
+ lockType = lock.level || lock;
1841
+ nowait = lock.nowait === true;
1842
+ skipLocked = lock.skipLocked === true;
1843
+ }
1844
+ // Build base lock clause
1845
+ let lockClause = '';
1846
+ if (lockType === 'UPDATE' || lockType === 'UPDATE') {
1847
+ lockClause = 'FOR UPDATE';
1848
+ }
1849
+ else if (lockType === 'SHARE') {
1850
+ if (dialect === 'mysql') {
1851
+ lockClause = 'LOCK IN SHARE MODE';
1852
+ }
1853
+ else {
1854
+ lockClause = 'FOR SHARE';
1855
+ }
1856
+ }
1857
+ else if (lockType === 'KEY SHARE') {
1858
+ lockClause = 'FOR KEY SHARE';
1859
+ }
1860
+ // Add NOWAIT or SKIP LOCKED (PostgreSQL and MySQL 8.0+)
1861
+ if (nowait && (dialect === 'postgres' || dialect === 'mysql')) {
1862
+ lockClause += ' NOWAIT';
1863
+ }
1864
+ else if (skipLocked && (dialect === 'postgres' || dialect === 'mysql')) {
1865
+ lockClause += ' SKIP LOCKED';
1866
+ }
1867
+ return lockClause;
1868
+ }
1869
+ /**
1870
+ * Compile OVER clause with partition, order, and window frame
1871
+ */
1872
+ compileOverClause(options) {
1873
+ const partitionClause = this.compilePartitionBy(options.partitionBy);
1874
+ const orderClause = options.orderBy ? this.compileOrder(options.orderBy) : '';
1875
+ const frameClause = this.compileWindowFrame(options.windowFrame);
1876
+ const clauses = [];
1877
+ if (partitionClause)
1878
+ clauses.push(partitionClause);
1879
+ if (orderClause)
1880
+ clauses.push(orderClause.replace('ORDER BY ', ''));
1881
+ if (frameClause)
1882
+ clauses.push(frameClause);
1883
+ if (clauses.length > 0) {
1884
+ return `OVER (${clauses.join(' ')})`;
1885
+ }
1886
+ return 'OVER ()';
1887
+ }
1888
+ /**
1889
+ * Compile a window function expression
1890
+ * @param functionName - The window function name (ROW_NUMBER, RANK, etc.)
1891
+ * @param args - Arguments to the window function
1892
+ * @param options - Window function options
1893
+ * @returns The compiled SQL expression
1894
+ */
1895
+ compileWindowFunction(functionName, args = [], options = {}) {
1896
+ const argsSql = args.map((arg) => this.attributeToSql(arg)).join(', ');
1897
+ const overClause = this.compileOverClause(options);
1898
+ if (argsSql) {
1899
+ return `${functionName}(${argsSql}) ${overClause}`;
1900
+ }
1901
+ return `${functionName}() ${overClause}`;
1902
+ }
1903
+ /**
1904
+ * Pretty print SQL
1905
+ */
1906
+ prettyPrint(sql) {
1907
+ const keywords = [
1908
+ 'SELECT',
1909
+ 'FROM',
1910
+ 'WHERE',
1911
+ 'AND',
1912
+ 'OR',
1913
+ 'JOIN',
1914
+ 'LEFT JOIN',
1915
+ 'RIGHT JOIN',
1916
+ 'INNER JOIN',
1917
+ 'ON',
1918
+ 'GROUP BY',
1919
+ 'HAVING',
1920
+ 'ORDER BY',
1921
+ 'LIMIT',
1922
+ 'OFFSET',
1923
+ 'INSERT INTO',
1924
+ 'VALUES',
1925
+ 'UPDATE',
1926
+ 'SET',
1927
+ 'DELETE FROM',
1928
+ 'RETURNING',
1929
+ ];
1930
+ let result = sql;
1931
+ for (const keyword of keywords) {
1932
+ const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
1933
+ result = result.replace(regex, keyword);
1934
+ }
1935
+ return result;
1936
+ }
1937
+ }
1938
+ exports.SqlCompiler = SqlCompiler;
1939
+ // ==================== MySQL JSON Helper Functions ====================
1940
+ /**
1941
+ * Generate MySQL JSON_EXTRACT function
1942
+ * @param columnName - The column containing JSON data
1943
+ * @param path - The JSON path (e.g., '$.key' or '$.array[0]')
1944
+ */
1945
+ function jsonExtract(columnName, path) {
1946
+ return `JSON_EXTRACT(\`${columnName}\`, '${path}')`;
1947
+ }
1948
+ /**
1949
+ * Generate MySQL JSON_UNQUOTE(JSON_EXTRACT(...)) for unquoted values
1950
+ * @param columnName - The column containing JSON data
1951
+ * @param path - The JSON path (e.g., '$.key' or '$.array[0]')
1952
+ */
1953
+ function jsonExtractUnquote(columnName, path) {
1954
+ return `JSON_UNQUOTE(JSON_EXTRACT(\`${columnName}\`, '${path}'))`;
1955
+ }
1956
+ /**
1957
+ * Generate MySQL JSON_SET function to set a value in JSON
1958
+ * @param columnName - The column containing JSON data
1959
+ * @param path - The JSON path
1960
+ * @param value - The value to set (can be a column reference or literal)
1961
+ * @param extraPaths - Additional paths and values to set
1962
+ */
1963
+ function jsonSet(columnName, path, value, ...extraPaths) {
1964
+ let sql = `JSON_SET(\`${columnName}\`, '${path}', ${value})`;
1965
+ for (const [extraPath, extraValue] of extraPaths) {
1966
+ sql = `JSON_SET(${sql}, '${extraPath}', ${extraValue})`;
1967
+ }
1968
+ return sql;
1969
+ }
1970
+ /**
1971
+ * Generate MySQL JSON_INSERT function to insert a value in JSON
1972
+ * @param columnName - The column containing JSON data
1973
+ * @param path - The JSON path
1974
+ * @param value - The value to insert
1975
+ */
1976
+ function jsonInsert(columnName, path, value) {
1977
+ return `JSON_INSERT(\`${columnName}\`, '${path}', ${value})`;
1978
+ }
1979
+ /**
1980
+ * Generate MySQL JSON_REPLACE function to replace a value in JSON
1981
+ * @param columnName - The column containing JSON data
1982
+ * @param path - The JSON path
1983
+ * @param value - The value to replace
1984
+ */
1985
+ function jsonReplace(columnName, path, value) {
1986
+ return `JSON_REPLACE(\`${columnName}\`, '${path}', ${value})`;
1987
+ }
1988
+ /**
1989
+ * Generate MySQL JSON_REMOVE function to remove a value from JSON
1990
+ * @param columnName - The column containing JSON data
1991
+ * @param path - The JSON path to remove
1992
+ */
1993
+ function jsonRemove(columnName, path) {
1994
+ return `JSON_REMOVE(\`${columnName}\`, '${path}')`;
1995
+ }
1996
+ /**
1997
+ * Generate MySQL JSON_KEYS function to get keys from JSON object
1998
+ * @param columnName - The column containing JSON data
1999
+ * @param path - Optional path to get keys from specific object
2000
+ */
2001
+ function jsonKeys(columnName, path) {
2002
+ if (path) {
2003
+ return `JSON_KEYS(\`${columnName}\`, '${path}')`;
2004
+ }
2005
+ return `JSON_KEYS(\`${columnName}\`)`;
2006
+ }
2007
+ /**
2008
+ * Generate MySQL JSON_LENGTH function to get length of JSON array/object
2009
+ * @param columnName - The column containing JSON data
2010
+ * @param path - Optional path to get length of specific array/object
2011
+ */
2012
+ function jsonLength(columnName, path) {
2013
+ if (path) {
2014
+ return `JSON_LENGTH(\`${columnName}\`, '${path}')`;
2015
+ }
2016
+ return `JSON_LENGTH(\`${columnName}\`)`;
2017
+ }
2018
+ /**
2019
+ * Generate MySQL JSON_CONTAINS function to check if JSON contains a value
2020
+ * @param columnName - The column containing JSON data
2021
+ * @param value - The value to check for
2022
+ * @param path - The path to check at
2023
+ */
2024
+ function jsonContains(columnName, value, path) {
2025
+ return `JSON_CONTAINS(\`${columnName}\`, ${value}, '${path}')`;
2026
+ }
2027
+ /**
2028
+ * Generate MySQL JSON_SEARCH function for full-text search in JSON
2029
+ * @param columnName - The column containing JSON data
2030
+ * @param searchString - The string to search for
2031
+ * @param escapeChar - Optional escape character
2032
+ * @param path - Optional path to search within
2033
+ */
2034
+ function jsonSearch(columnName, searchString, escapeChar, path) {
2035
+ const searchArg = `'${searchString}'`;
2036
+ const escapeArg = escapeChar ? `, '${escapeChar}'` : '';
2037
+ const pathArg = path ? `, '${path}'` : '';
2038
+ return `JSON_SEARCH(\`${columnName}\`${escapeArg}, 'one', ${searchArg}${pathArg})`;
2039
+ }
2040
+ exports.default = SqlCompiler;