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