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,3382 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SQLite dialect implementation
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SQLiteTransaction = exports.SQLiteDialect = void 0;
|
|
10
|
+
exports.createSQLiteDialect = createSQLiteDialect;
|
|
11
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
12
|
+
const stream_1 = require("stream");
|
|
13
|
+
const prorm_1 = require("../../prorm");
|
|
14
|
+
const operators_1 = require("../../operators");
|
|
15
|
+
const sqlite_advanced_1 = require("../../sqlite-advanced");
|
|
16
|
+
/**
|
|
17
|
+
* SQLite dialect class that implements the Dialect interface
|
|
18
|
+
*/
|
|
19
|
+
class SQLiteDialect {
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.name = 'sqlite';
|
|
22
|
+
this.library = 'better-sqlite3';
|
|
23
|
+
this.connection = null;
|
|
24
|
+
this._isConnected = false;
|
|
25
|
+
this.transactionDepth = 0;
|
|
26
|
+
this._advanced = null;
|
|
27
|
+
this.config = {
|
|
28
|
+
storage: ':memory:',
|
|
29
|
+
...config,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Connect to the SQLite database
|
|
34
|
+
*/
|
|
35
|
+
async connect() {
|
|
36
|
+
try {
|
|
37
|
+
const options = {
|
|
38
|
+
readonly: this.config.readonly || false,
|
|
39
|
+
fileMustExist: this.config.fileMustExist || false,
|
|
40
|
+
timeout: this.config.timeout || 5000,
|
|
41
|
+
verbose: this.config.verbose || undefined,
|
|
42
|
+
};
|
|
43
|
+
this.connection = new better_sqlite3_1.default(this.config.storage, options);
|
|
44
|
+
this._isConnected = true;
|
|
45
|
+
// Enable foreign keys by default
|
|
46
|
+
this.connection.pragma('foreign_keys = ON');
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new Error(`Failed to connect to SQLite database: ${error}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Disconnect from the SQLite database
|
|
54
|
+
*/
|
|
55
|
+
async disconnect() {
|
|
56
|
+
if (this.connection) {
|
|
57
|
+
this.connection.close();
|
|
58
|
+
this.connection = null;
|
|
59
|
+
this._isConnected = false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get the current database connection
|
|
64
|
+
*/
|
|
65
|
+
getConnection() {
|
|
66
|
+
return this.connection;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if connected
|
|
70
|
+
*/
|
|
71
|
+
isConnected() {
|
|
72
|
+
return this._isConnected && this.connection !== null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get SQLite advanced features instance
|
|
76
|
+
*/
|
|
77
|
+
get advanced() {
|
|
78
|
+
if (!this._advanced) {
|
|
79
|
+
this._advanced = (0, sqlite_advanced_1.createSQLiteAdvanced)(this);
|
|
80
|
+
}
|
|
81
|
+
return this._advanced;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Replace placeholders in SQL with actual values
|
|
85
|
+
* Supports both named (:param) and positional (?) placeholders
|
|
86
|
+
*/
|
|
87
|
+
replaceReplacements(sql, replacements) {
|
|
88
|
+
if (!replacements) {
|
|
89
|
+
return sql;
|
|
90
|
+
}
|
|
91
|
+
// Handle positional replacements (?)
|
|
92
|
+
if (Array.isArray(replacements)) {
|
|
93
|
+
let result = sql;
|
|
94
|
+
for (const value of replacements) {
|
|
95
|
+
result = result.replace(/\?/, () => this.escape(value));
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
// Handle named replacements (:param)
|
|
100
|
+
let result = sql;
|
|
101
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
102
|
+
// Handle array values for IN clauses
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
const escapedValues = value.map((v) => this.escape(v)).join(', ');
|
|
105
|
+
// Replace :paramName (word boundary) with the array values
|
|
106
|
+
const regex = new RegExp(`:${key}(?![\\w])`, 'g');
|
|
107
|
+
result = result.replace(regex, escapedValues);
|
|
108
|
+
}
|
|
109
|
+
else if (value === null) {
|
|
110
|
+
// Handle null values
|
|
111
|
+
const regex = new RegExp(`:${key}(?![\\w])`, 'g');
|
|
112
|
+
result = result.replace(regex, 'NULL');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Handle scalar values
|
|
116
|
+
const regex = new RegExp(`:${key}(?![\\w])`, 'g');
|
|
117
|
+
result = result.replace(regex, () => this.escape(value));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Execute a raw SQL query with retry support
|
|
124
|
+
*/
|
|
125
|
+
async query(sql, options) {
|
|
126
|
+
// Default retry options for queries
|
|
127
|
+
const defaultQueryRetryOptions = {
|
|
128
|
+
max: 3,
|
|
129
|
+
timeout: 1000,
|
|
130
|
+
match: ['SQLITE_BUSY', 'SQLITE_LOCKED', 'database is locked', 'database table is locked'],
|
|
131
|
+
backoff: false,
|
|
132
|
+
backoffMultiplier: 2,
|
|
133
|
+
backoffMax: 10000,
|
|
134
|
+
};
|
|
135
|
+
const retryOptions = options?.retry;
|
|
136
|
+
const effectiveRetry = retryOptions
|
|
137
|
+
? {
|
|
138
|
+
...defaultQueryRetryOptions,
|
|
139
|
+
...retryOptions,
|
|
140
|
+
match: retryOptions.match || defaultQueryRetryOptions.match,
|
|
141
|
+
}
|
|
142
|
+
: null;
|
|
143
|
+
const executeQuery = async () => {
|
|
144
|
+
if (!this.connection) {
|
|
145
|
+
throw new Error('Not connected to database');
|
|
146
|
+
}
|
|
147
|
+
const sqlUpper = sql.trim().toUpperCase();
|
|
148
|
+
// EXPLAIN / EXPLAIN QUERY PLAN and WITH (CTE) statements also return rows.
|
|
149
|
+
const isSelect = sqlUpper.startsWith('SELECT') ||
|
|
150
|
+
sqlUpper.startsWith('PRAGMA') ||
|
|
151
|
+
sqlUpper.startsWith('EXPLAIN') ||
|
|
152
|
+
sqlUpper.startsWith('WITH');
|
|
153
|
+
const stmt = this.connection.prepare(sql);
|
|
154
|
+
// Bind parameters for `?` / named placeholders. Several call sites in this
|
|
155
|
+
// dialect (bulkInsert, getCreateTable, getTableStatus, sqlite-advanced.ts
|
|
156
|
+
// listViews/getViewDefinition/etc.) build parameterized SQL and pass the
|
|
157
|
+
// values via `replacements` (array) or `bind` (array or named object) -
|
|
158
|
+
// previously these were silently dropped and better-sqlite3 would throw
|
|
159
|
+
// "Too few parameter values were provided" whenever placeholders were used.
|
|
160
|
+
const rawParams = options?.replacements ?? options?.bind ?? options?.bindings;
|
|
161
|
+
// better-sqlite3 only binds numbers/strings/bigints/buffers/null. Coerce
|
|
162
|
+
// the JS types the query/where builders commonly produce (boolean, Date,
|
|
163
|
+
// plain object/array) so those bindings don't throw at execution time.
|
|
164
|
+
const coerceBind = (v) => {
|
|
165
|
+
if (typeof v === 'boolean')
|
|
166
|
+
return v ? 1 : 0;
|
|
167
|
+
if (v instanceof Date)
|
|
168
|
+
return v.toISOString();
|
|
169
|
+
if (v !== null && v !== undefined && typeof v === 'object' && !Buffer.isBuffer(v)) {
|
|
170
|
+
return JSON.stringify(v);
|
|
171
|
+
}
|
|
172
|
+
return v;
|
|
173
|
+
};
|
|
174
|
+
const params = Array.isArray(rawParams)
|
|
175
|
+
? rawParams.map(coerceBind)
|
|
176
|
+
: rawParams && typeof rawParams === 'object'
|
|
177
|
+
? Object.fromEntries(Object.entries(rawParams).map(([k, v]) => [
|
|
178
|
+
k,
|
|
179
|
+
coerceBind(v),
|
|
180
|
+
]))
|
|
181
|
+
: rawParams;
|
|
182
|
+
try {
|
|
183
|
+
if (isSelect) {
|
|
184
|
+
const rows = params === undefined
|
|
185
|
+
? stmt.all()
|
|
186
|
+
: Array.isArray(params)
|
|
187
|
+
? stmt.all(...params)
|
|
188
|
+
: stmt.all(params);
|
|
189
|
+
return {
|
|
190
|
+
rows,
|
|
191
|
+
rowCount: rows.length,
|
|
192
|
+
fields: [],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const result = params === undefined
|
|
197
|
+
? stmt.run()
|
|
198
|
+
: Array.isArray(params)
|
|
199
|
+
? stmt.run(...params)
|
|
200
|
+
: stmt.run(params);
|
|
201
|
+
return {
|
|
202
|
+
rows: [],
|
|
203
|
+
rowCount: result.changes,
|
|
204
|
+
fields: [],
|
|
205
|
+
lastInsertRowid: result.lastInsertRowid !== undefined ? Number(result.lastInsertRowid) : undefined,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
throw new Error(`Query error: ${error}`);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
// If retry options are provided, use retry logic
|
|
214
|
+
if (effectiveRetry && effectiveRetry.max > 0) {
|
|
215
|
+
return this.executeWithRetry(executeQuery, effectiveRetry);
|
|
216
|
+
}
|
|
217
|
+
return executeQuery();
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Stream query results using better-sqlite3's native `Statement#iterate()`
|
|
221
|
+
* — a real in-process cursor (no round trips at all, since sqlite is
|
|
222
|
+
* embedded), so this is more efficient than the generic paginated
|
|
223
|
+
* LIMIT/OFFSET fallback other dialects use.
|
|
224
|
+
* @param sql - The SELECT statement to stream
|
|
225
|
+
* @param options - Streaming options (batch size only affects backpressure batching, not fetch round trips)
|
|
226
|
+
*/
|
|
227
|
+
queryStream(sql, options) {
|
|
228
|
+
const stream = new stream_1.Readable({ objectMode: true, highWaterMark: options?.highWaterMark || 1000, read() { } });
|
|
229
|
+
if (!this.connection) {
|
|
230
|
+
process.nextTick(() => stream.destroy(new Error('Not connected to database')));
|
|
231
|
+
return stream;
|
|
232
|
+
}
|
|
233
|
+
let iterator;
|
|
234
|
+
try {
|
|
235
|
+
iterator = this.connection.prepare(sql).iterate();
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
process.nextTick(() => stream.destroy(error instanceof Error ? error : new Error(String(error))));
|
|
239
|
+
return stream;
|
|
240
|
+
}
|
|
241
|
+
// Pull rows out of the synchronous iterator on a microtask loop so we
|
|
242
|
+
// still respect Readable backpressure (stop pulling once push() returns
|
|
243
|
+
// false, resume from _read()).
|
|
244
|
+
const pump = () => {
|
|
245
|
+
let canContinue = true;
|
|
246
|
+
while (canContinue) {
|
|
247
|
+
const next = iterator.next();
|
|
248
|
+
if (next.done) {
|
|
249
|
+
stream.push(null);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const row = next.value;
|
|
253
|
+
if (options?.mapToModel && options?.model) {
|
|
254
|
+
const instance = new options.model();
|
|
255
|
+
Object.assign(instance, row);
|
|
256
|
+
canContinue = stream.push(instance);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
canContinue = stream.push(row);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
stream._read = pump;
|
|
264
|
+
return stream;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Execute a function with retry logic for query execution
|
|
268
|
+
*/
|
|
269
|
+
async executeWithRetry(fn, retryOptions) {
|
|
270
|
+
let lastError;
|
|
271
|
+
const { backoff, backoffMultiplier, backoffMax } = retryOptions;
|
|
272
|
+
for (let attempt = 0; attempt <= retryOptions.max; attempt++) {
|
|
273
|
+
try {
|
|
274
|
+
return await fn();
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
lastError = error;
|
|
278
|
+
const errorMessage = lastError.message;
|
|
279
|
+
// Check if this error should be retried
|
|
280
|
+
const shouldRetry = retryOptions.match.some((pattern) => errorMessage.toLowerCase().includes(pattern.toLowerCase()));
|
|
281
|
+
if (!shouldRetry || attempt === retryOptions.max) {
|
|
282
|
+
throw lastError;
|
|
283
|
+
}
|
|
284
|
+
// Calculate delay - use exponential backoff if enabled
|
|
285
|
+
let delay;
|
|
286
|
+
if (backoff) {
|
|
287
|
+
delay = retryOptions.timeout * Math.pow(backoffMultiplier || 2, attempt);
|
|
288
|
+
delay = Math.min(delay, backoffMax || 10000);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
delay = retryOptions.timeout;
|
|
292
|
+
}
|
|
293
|
+
// Add some jitter to avoid thundering herd
|
|
294
|
+
const jitter = Math.random() * 100;
|
|
295
|
+
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
throw lastError;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Escape a value for use in a query
|
|
302
|
+
*/
|
|
303
|
+
escape(value) {
|
|
304
|
+
if (value === null) {
|
|
305
|
+
return 'NULL';
|
|
306
|
+
}
|
|
307
|
+
// Handle undefined - return NULL (similar to null)
|
|
308
|
+
if (value === undefined) {
|
|
309
|
+
return 'NULL';
|
|
310
|
+
}
|
|
311
|
+
// Handle Literal (raw SQL) values
|
|
312
|
+
if (value instanceof prorm_1.Literal) {
|
|
313
|
+
return value.val;
|
|
314
|
+
}
|
|
315
|
+
if (typeof value === 'string') {
|
|
316
|
+
return `'${this.escapeString(value)}'`;
|
|
317
|
+
}
|
|
318
|
+
if (typeof value === 'number') {
|
|
319
|
+
return String(value);
|
|
320
|
+
}
|
|
321
|
+
if (typeof value === 'bigint') {
|
|
322
|
+
return value.toString();
|
|
323
|
+
}
|
|
324
|
+
if (typeof value === 'boolean') {
|
|
325
|
+
return value ? '1' : '0';
|
|
326
|
+
}
|
|
327
|
+
if (value instanceof Date) {
|
|
328
|
+
return `'${value.toISOString()}'`;
|
|
329
|
+
}
|
|
330
|
+
if (Buffer.isBuffer(value)) {
|
|
331
|
+
return `X'${value.toString('hex')}'`;
|
|
332
|
+
}
|
|
333
|
+
// JSON stringify objects
|
|
334
|
+
return `'${this.escapeString(JSON.stringify(value))}'`;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Build a JSON extraction query for SQLite
|
|
338
|
+
* SQLite: json_extract(column, '$.path')
|
|
339
|
+
*/
|
|
340
|
+
buildJsonExtract(column, path, asText = true) {
|
|
341
|
+
const columnRef = this.escapeId(column);
|
|
342
|
+
const normalizedPath = this.escapeString(path.startsWith('$.') ? path : `$.${path}`);
|
|
343
|
+
// SQLite's json_extract returns JSON by default. When comparing to text values,
|
|
344
|
+
// we need to CAST to TEXT to ensure proper comparison (equivalent to PostgreSQL's ->> operator)
|
|
345
|
+
if (asText) {
|
|
346
|
+
return `CAST(json_extract(${columnRef}, '${normalizedPath}') AS TEXT)`;
|
|
347
|
+
}
|
|
348
|
+
return `json_extract(${columnRef}, '${normalizedPath}')`;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Build a JSON contains query for SQLite
|
|
352
|
+
* SQLite: uses json_each and json_extract for containment checks
|
|
353
|
+
*/
|
|
354
|
+
buildJsonContains(column, value, path) {
|
|
355
|
+
const columnRef = this.escapeId(column);
|
|
356
|
+
// json_extract() returns scalars (string/number/boolean) unwrapped, but returns
|
|
357
|
+
// objects/arrays as compact JSON text. JSON.stringify()-ing a scalar (e.g. the
|
|
358
|
+
// string "foo" -> '"foo"') would never match the unwrapped value SQLite returns,
|
|
359
|
+
// so only stringify non-scalar values here.
|
|
360
|
+
const isScalar = value === null || (typeof value !== 'object' && typeof value !== 'function');
|
|
361
|
+
const comparisonValue = isScalar ? value : JSON.stringify(value);
|
|
362
|
+
if (path) {
|
|
363
|
+
const normalizedPath = path.startsWith('$.') ? path : `$.${path}`;
|
|
364
|
+
// SQLite: check if the path contains the value
|
|
365
|
+
return {
|
|
366
|
+
sql: `json_extract(${columnRef}, '${this.escapeString(normalizedPath)}') = ?`,
|
|
367
|
+
values: [comparisonValue],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
// For JSON objects, compare as stringified JSON
|
|
371
|
+
return {
|
|
372
|
+
sql: `${columnRef} = ?`,
|
|
373
|
+
values: [JSON.stringify(value)],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Build a JSON has key query for SQLite
|
|
378
|
+
* SQLite: json_each or json_extract for key existence
|
|
379
|
+
*/
|
|
380
|
+
buildJsonHasKey(column, key, type = 'one') {
|
|
381
|
+
const columnRef = this.escapeId(column);
|
|
382
|
+
const normalizedKey = this.escapeString(key.startsWith('$.') ? key.substring(2) : key);
|
|
383
|
+
// SQLite: check if key exists by checking if json_extract returns non-null
|
|
384
|
+
return `json_extract(${columnRef}, '$.${normalizedKey}') IS NOT NULL`;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Build a JSON path query for SQLite
|
|
388
|
+
*/
|
|
389
|
+
buildJsonPathQuery(column, path, value, operator = '=') {
|
|
390
|
+
const values = [];
|
|
391
|
+
const columnRef = this.escapeId(column);
|
|
392
|
+
const normalizedPath = this.escapeString(path.startsWith('$.') ? path : `$.${path}`);
|
|
393
|
+
const jsonExtract = `json_extract(${columnRef}, '${normalizedPath}')`;
|
|
394
|
+
if (value !== undefined) {
|
|
395
|
+
values.push(value);
|
|
396
|
+
return { sql: `${jsonExtract} ${operator} ?`, values };
|
|
397
|
+
}
|
|
398
|
+
return { sql: jsonExtract, values };
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Build a `json_group_array()` aggregate expression for SQLite.
|
|
402
|
+
*
|
|
403
|
+
* `json_group_array()` is SQLite's JSON1 aggregate for collecting the
|
|
404
|
+
* values of `valueExpr` (evaluated once per row in the current group) into
|
|
405
|
+
* a single JSON array. Commonly used for "hasMany as nested JSON" style
|
|
406
|
+
* queries, e.g. rolling up child rows into a JSON array column of a
|
|
407
|
+
* grouped parent query.
|
|
408
|
+
*
|
|
409
|
+
* @param valueExpr - a column name or SQL expression to collect per row.
|
|
410
|
+
* Bare identifiers are escaped as columns; anything containing SQL
|
|
411
|
+
* syntax (parens, function calls, `.`) is passed through as-is so
|
|
412
|
+
* callers can pass expressions like `json_object('id', id)`.
|
|
413
|
+
* @param options.distinct - emit `json_group_array(DISTINCT ...)`
|
|
414
|
+
*/
|
|
415
|
+
buildJsonGroupArray(valueExpr, options) {
|
|
416
|
+
const expr = this.escapeAggregateExpr(valueExpr);
|
|
417
|
+
const distinct = options?.distinct ? 'DISTINCT ' : '';
|
|
418
|
+
return `json_group_array(${distinct}${expr})`;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Build a `json_group_object()` aggregate expression for SQLite.
|
|
422
|
+
*
|
|
423
|
+
* `json_group_object()` collects `(keyExpr, valueExpr)` pairs from the
|
|
424
|
+
* rows of the current group into a single JSON object, keyed by
|
|
425
|
+
* `keyExpr`. Useful for building a JSON map of `{ [key]: value }` from
|
|
426
|
+
* grouped rows (e.g. nested-relation queries keyed by id).
|
|
427
|
+
*/
|
|
428
|
+
buildJsonGroupObject(keyExpr, valueExpr) {
|
|
429
|
+
const key = this.escapeAggregateExpr(keyExpr);
|
|
430
|
+
const value = this.escapeAggregateExpr(valueExpr);
|
|
431
|
+
return `json_group_object(${key}, ${value})`;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Escape a column/expression argument passed to an aggregate builder.
|
|
435
|
+
* Bare identifiers (letters, digits, underscore, dot for table.column)
|
|
436
|
+
* are quoted as an identifier; anything else (function calls, literals,
|
|
437
|
+
* `*`, etc.) is passed through untouched so callers can supply arbitrary
|
|
438
|
+
* SQL expressions.
|
|
439
|
+
*/
|
|
440
|
+
escapeAggregateExpr(expr) {
|
|
441
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(expr)) {
|
|
442
|
+
return expr
|
|
443
|
+
.split('.')
|
|
444
|
+
.map((part) => this.escapeId(part))
|
|
445
|
+
.join('.');
|
|
446
|
+
}
|
|
447
|
+
return expr;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Build a `json_patch()` expression for SQLite (RFC 7396 JSON Merge Patch).
|
|
451
|
+
*
|
|
452
|
+
* `json_patch(target, patch)` applies an RFC 7396 merge patch to the JSON
|
|
453
|
+
* stored in `column`, returning the patched JSON document. Unlike
|
|
454
|
+
* `json_set()` (which sets a single path) this can add, replace, and
|
|
455
|
+
* remove multiple keys in one call - keys in `patch` set to `null` are
|
|
456
|
+
* removed from the result.
|
|
457
|
+
*
|
|
458
|
+
* @param column - the JSON column to patch
|
|
459
|
+
* @param patch - a JS object/value to be JSON-stringified and applied as
|
|
460
|
+
* the merge patch; passed as a bound parameter, not interpolated.
|
|
461
|
+
*/
|
|
462
|
+
buildJsonPatch(column, patch) {
|
|
463
|
+
const columnRef = this.escapeId(column);
|
|
464
|
+
return {
|
|
465
|
+
sql: `json_patch(${columnRef}, json(?))`,
|
|
466
|
+
values: [JSON.stringify(patch)],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Escape a string for SQL
|
|
471
|
+
*/
|
|
472
|
+
escapeString(str) {
|
|
473
|
+
return str.replace(/'/g, "''");
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Escape an identifier (table name, column name, etc.)
|
|
477
|
+
*/
|
|
478
|
+
escapeId(identifier) {
|
|
479
|
+
const id = String(identifier ?? '');
|
|
480
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Quote an identifier (column name, table name)
|
|
484
|
+
* Same as escapeId for SQLite
|
|
485
|
+
*/
|
|
486
|
+
quoteIdentifier(identifier) {
|
|
487
|
+
return this.escapeId(identifier);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Quote a table name
|
|
491
|
+
* Same as escapeId for SQLite
|
|
492
|
+
* If schema is provided, returns schema.tableName (SQLite treats schemas as prefixes)
|
|
493
|
+
*/
|
|
494
|
+
quoteTable(tableName, schema) {
|
|
495
|
+
if (schema) {
|
|
496
|
+
return `${this.escapeId(schema)}.${this.escapeId(tableName)}`;
|
|
497
|
+
}
|
|
498
|
+
return this.escapeId(tableName);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Get the database version
|
|
502
|
+
*/
|
|
503
|
+
async getDatabaseVersion() {
|
|
504
|
+
if (!this.connection) {
|
|
505
|
+
throw new Error('Not connected to database');
|
|
506
|
+
}
|
|
507
|
+
const result = this.connection.pragma('user_version', { simple: true });
|
|
508
|
+
return String(result);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Generate SQL for creating a database
|
|
512
|
+
* SQLite uses ATTACH DATABASE for creating new database files
|
|
513
|
+
*/
|
|
514
|
+
createDatabaseSQL(options) {
|
|
515
|
+
const sql = `CREATE DATABASE "${options.name}"`;
|
|
516
|
+
const optionsList = [];
|
|
517
|
+
if (options.encoding)
|
|
518
|
+
optionsList.push(`ENCODING = '${options.encoding}'`);
|
|
519
|
+
if (options.lcCollate)
|
|
520
|
+
optionsList.push(`LC_COLLATE = '${options.lcCollate}'`);
|
|
521
|
+
if (options.lcCtype)
|
|
522
|
+
optionsList.push(`LC_CTYPE = '${options.lcCtype}'`);
|
|
523
|
+
if (options.collate)
|
|
524
|
+
optionsList.push(`COLLATE = '${options.collate}'`);
|
|
525
|
+
if (optionsList.length > 0) {
|
|
526
|
+
return sql + ' ' + optionsList.join(' ');
|
|
527
|
+
}
|
|
528
|
+
return sql;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Generate SQL for dropping a database
|
|
532
|
+
* SQLite uses DETACH DATABASE for dropping database files
|
|
533
|
+
*/
|
|
534
|
+
dropDatabaseSQL(name) {
|
|
535
|
+
return `DROP DATABASE "${name}"`;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Attach an external SQLite database file to the current connection under
|
|
539
|
+
* `alias`, making its tables reachable as `alias.tableName` in subsequent
|
|
540
|
+
* queries (cross-database queries/joins). Executes a real
|
|
541
|
+
* `ATTACH DATABASE ? AS <alias>` statement rather than just documenting the
|
|
542
|
+
* capability.
|
|
543
|
+
*
|
|
544
|
+
* @param path - filesystem path to the database file to attach (or
|
|
545
|
+
* `:memory:` for an attached in-memory database)
|
|
546
|
+
* @param alias - schema name subsequent SQL will use to refer to the
|
|
547
|
+
* attached database (e.g. `alias.users`)
|
|
548
|
+
*/
|
|
549
|
+
async attachDatabase(path, alias) {
|
|
550
|
+
if (!this.connection) {
|
|
551
|
+
throw new Error('Not connected to database');
|
|
552
|
+
}
|
|
553
|
+
if (!alias || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(alias)) {
|
|
554
|
+
throw new Error(`Invalid database alias: ${alias}`);
|
|
555
|
+
}
|
|
556
|
+
const aliasId = this.escapeId(alias);
|
|
557
|
+
this.connection.prepare(`ATTACH DATABASE ? AS ${aliasId}`).run(path);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Detach a previously-attached database from the current connection.
|
|
561
|
+
* Executes a real `DETACH DATABASE <alias>` statement.
|
|
562
|
+
*
|
|
563
|
+
* @param alias - the schema name the database was attached under via
|
|
564
|
+
* `attachDatabase()`
|
|
565
|
+
*/
|
|
566
|
+
async detachDatabase(alias) {
|
|
567
|
+
if (!this.connection) {
|
|
568
|
+
throw new Error('Not connected to database');
|
|
569
|
+
}
|
|
570
|
+
if (!alias || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(alias)) {
|
|
571
|
+
throw new Error(`Invalid database alias: ${alias}`);
|
|
572
|
+
}
|
|
573
|
+
const aliasId = this.escapeId(alias);
|
|
574
|
+
this.connection.exec(`DETACH DATABASE ${aliasId}`);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Generate SQL for creating a savepoint
|
|
578
|
+
*/
|
|
579
|
+
createSavepointSQL(name) {
|
|
580
|
+
const savepointName = name || `sp_${Date.now()}`;
|
|
581
|
+
return `SAVEPOINT ${savepointName}`;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Generate SQL for releasing a savepoint
|
|
585
|
+
*/
|
|
586
|
+
releaseSavepointSQL(name) {
|
|
587
|
+
return `RELEASE SAVEPOINT ${name}`;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Generate SQL for rolling back to a savepoint
|
|
591
|
+
*/
|
|
592
|
+
rollbackToSavepointSQL(name) {
|
|
593
|
+
return `ROLLBACK TO SAVEPOINT ${name}`;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Create a new table
|
|
597
|
+
*/
|
|
598
|
+
async createTable(tableName, columns, options) {
|
|
599
|
+
const columnDefs = [];
|
|
600
|
+
for (const [columnName, definition] of Object.entries(columns)) {
|
|
601
|
+
// Skip virtual fields - they don't create database columns
|
|
602
|
+
const typeObj = definition.type;
|
|
603
|
+
if (typeObj && typeObj.key === 'VIRTUAL') {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
columnDefs.push(this.getColumnDefinitionSql(columnName, definition));
|
|
607
|
+
}
|
|
608
|
+
// Add constraints from options
|
|
609
|
+
if (options?.constraints) {
|
|
610
|
+
for (const constraint of options.constraints) {
|
|
611
|
+
const constraintSql = this.buildConstraintSql(constraint);
|
|
612
|
+
if (constraintSql) {
|
|
613
|
+
columnDefs.push(constraintSql);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
let sql = `CREATE TABLE`;
|
|
618
|
+
if (options?.ifNotExists) {
|
|
619
|
+
sql += ' IF NOT EXISTS';
|
|
620
|
+
}
|
|
621
|
+
sql += ` ${this.escapeId(tableName)} (${columnDefs.join(', ')})`;
|
|
622
|
+
// SQLite 3.37+ STRICT tables and WITHOUT ROWID tables are both table-level
|
|
623
|
+
// modifiers that go after the closing paren of the column-def list, and
|
|
624
|
+
// can be combined as `) STRICT, WITHOUT ROWID`.
|
|
625
|
+
const tableModifiers = [];
|
|
626
|
+
if (options?.strict) {
|
|
627
|
+
tableModifiers.push('STRICT');
|
|
628
|
+
}
|
|
629
|
+
if (options?.withoutRowid) {
|
|
630
|
+
tableModifiers.push('WITHOUT ROWID');
|
|
631
|
+
}
|
|
632
|
+
if (tableModifiers.length > 0) {
|
|
633
|
+
sql += ` ${tableModifiers.join(', ')}`;
|
|
634
|
+
}
|
|
635
|
+
// SQLite does not support ENGINE, CHARSET, COLLATE in CREATE TABLE
|
|
636
|
+
// These options are ignored for SQLite but can be logged for debugging
|
|
637
|
+
if (options?.engine) {
|
|
638
|
+
console.warn(`SQLite: engine option '${options.engine}' is not supported and will be ignored`);
|
|
639
|
+
}
|
|
640
|
+
if (options?.charset) {
|
|
641
|
+
console.warn(`SQLite: charset option '${options.charset}' is not supported and will be ignored`);
|
|
642
|
+
}
|
|
643
|
+
if (options?.collate) {
|
|
644
|
+
// SQLite supports COLLATE in column definitions, not table-level
|
|
645
|
+
// Add COLLATE to text columns instead
|
|
646
|
+
console.warn(`SQLite: table-level collate option is not supported, apply collate to text columns instead`);
|
|
647
|
+
}
|
|
648
|
+
if (options?.rowFormat) {
|
|
649
|
+
console.warn(`SQLite: rowFormat option '${options.rowFormat}' is not supported and will be ignored`);
|
|
650
|
+
}
|
|
651
|
+
// Handle uniqueKeys for SQLite by creating unique indexes after table creation
|
|
652
|
+
let uniqueKeysToCreate = [];
|
|
653
|
+
if (options?.uniqueKeys) {
|
|
654
|
+
if (Array.isArray(options.uniqueKeys)) {
|
|
655
|
+
// New format: UniqueKeyOptions[]
|
|
656
|
+
uniqueKeysToCreate = options.uniqueKeys.map((uk) => ({
|
|
657
|
+
name: uk.name || `${tableName}_${uk.fields.join('_')}_key`,
|
|
658
|
+
fields: uk.fields,
|
|
659
|
+
}));
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
// Legacy format: Record<string, string[]>
|
|
663
|
+
uniqueKeysToCreate = Object.entries(options.uniqueKeys).map(([keyName, fields]) => ({
|
|
664
|
+
name: keyName || `uq_${tableName}_${fields.join('_')}`,
|
|
665
|
+
fields,
|
|
666
|
+
}));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (options?.initialAutoIncrement) {
|
|
670
|
+
console.warn(`SQLite: initialAutoIncrement option '${options.initialAutoIncrement}' is not supported and will be ignored`);
|
|
671
|
+
}
|
|
672
|
+
// Handle comment - SQLite doesn't support table comments natively
|
|
673
|
+
// Store in sqlite_master as a comment (extended feature)
|
|
674
|
+
let tableComment;
|
|
675
|
+
if (options?.comment) {
|
|
676
|
+
tableComment = options.comment;
|
|
677
|
+
}
|
|
678
|
+
await this.query(sql);
|
|
679
|
+
// Store table comment as extended property if provided
|
|
680
|
+
// SQLite has no native mechanism to attach a comment to a table, so this
|
|
681
|
+
// is a documented no-op kept for interface compatibility with other dialects.
|
|
682
|
+
if (tableComment) {
|
|
683
|
+
try {
|
|
684
|
+
// no-op: SQLite does not support table comments
|
|
685
|
+
}
|
|
686
|
+
catch (e) {
|
|
687
|
+
// Ignore if extended comments aren't supported
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// SQLite doesn't support initialAutoIncrement in CREATE TABLE
|
|
691
|
+
// It's controlled by AUTOINCREMENT keyword on column definitions
|
|
692
|
+
if (options?.initialAutoIncrement) {
|
|
693
|
+
console.warn(`SQLite: initialAutoIncrement option is not supported in CREATE TABLE`);
|
|
694
|
+
}
|
|
695
|
+
// Create indexes after table creation
|
|
696
|
+
if (options?.indexes) {
|
|
697
|
+
for (const index of options.indexes) {
|
|
698
|
+
await this.addIndex(tableName, index.name || `idx_${tableName}_${index.fields.join('_')}`, index.fields, {
|
|
699
|
+
unique: index.unique,
|
|
700
|
+
type: index.type,
|
|
701
|
+
using: index.using,
|
|
702
|
+
where: index.where,
|
|
703
|
+
expression: index.expression,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Build constraint SQL for table creation
|
|
710
|
+
*/
|
|
711
|
+
buildConstraintSql(constraint) {
|
|
712
|
+
const name = constraint.name ? `CONSTRAINT "${constraint.name}" ` : '';
|
|
713
|
+
const fields = constraint.fields?.map((f) => this.escapeId(f)).join(', ') || '';
|
|
714
|
+
switch (constraint.type) {
|
|
715
|
+
case 'PRIMARY KEY':
|
|
716
|
+
return `${name}PRIMARY KEY (${fields})`;
|
|
717
|
+
case 'UNIQUE':
|
|
718
|
+
return `${name}UNIQUE (${fields})`;
|
|
719
|
+
case 'FOREIGN KEY':
|
|
720
|
+
if (!constraint.references)
|
|
721
|
+
return null;
|
|
722
|
+
let fkSql = `${name}FOREIGN KEY (${fields}) `;
|
|
723
|
+
// Handle composite foreign keys (string | string[])
|
|
724
|
+
const refField = constraint.references.field;
|
|
725
|
+
const refFieldSql = Array.isArray(refField)
|
|
726
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
727
|
+
: this.escapeId(refField);
|
|
728
|
+
fkSql += `REFERENCES ${this.escapeId(constraint.references.table)}${refFieldSql}`;
|
|
729
|
+
if (constraint.references.onDelete) {
|
|
730
|
+
fkSql += ` ON DELETE ${constraint.references.onDelete}`;
|
|
731
|
+
}
|
|
732
|
+
if (constraint.references.onUpdate) {
|
|
733
|
+
fkSql += ` ON UPDATE ${constraint.references.onUpdate}`;
|
|
734
|
+
}
|
|
735
|
+
return fkSql;
|
|
736
|
+
case 'CHECK':
|
|
737
|
+
if (!constraint.check)
|
|
738
|
+
return null;
|
|
739
|
+
return `${name}CHECK (${constraint.check})`;
|
|
740
|
+
default:
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Generate column definition SQL
|
|
746
|
+
*/
|
|
747
|
+
getColumnDefinitionSql(columnName, definition) {
|
|
748
|
+
let sql = `${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type, columnName)}`;
|
|
749
|
+
if (definition.allowNull === false) {
|
|
750
|
+
sql += ' NOT NULL';
|
|
751
|
+
}
|
|
752
|
+
if (definition.defaultValue !== undefined) {
|
|
753
|
+
sql += ` DEFAULT ${this.getDefaultValue(definition.defaultValue)}`;
|
|
754
|
+
}
|
|
755
|
+
if (definition.primaryKey) {
|
|
756
|
+
sql += ' PRIMARY KEY';
|
|
757
|
+
// SQLite AUTOINCREMENT is only valid on INTEGER PRIMARY KEY columns
|
|
758
|
+
if (definition.autoIncrement) {
|
|
759
|
+
const resolvedType = this.getDataTypeSql(definition.type).toUpperCase();
|
|
760
|
+
if (resolvedType === 'INTEGER') {
|
|
761
|
+
sql += ' AUTOINCREMENT';
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (definition.unique) {
|
|
766
|
+
sql += ' UNIQUE';
|
|
767
|
+
}
|
|
768
|
+
if (definition.references) {
|
|
769
|
+
// Handle composite foreign keys (string | string[])
|
|
770
|
+
const refField5 = definition.references.field;
|
|
771
|
+
const refFieldSql5 = Array.isArray(refField5)
|
|
772
|
+
? `(${refField5.map((f) => this.escapeId(f)).join(', ')})`
|
|
773
|
+
: `(${this.escapeId(refField5)})`;
|
|
774
|
+
sql += ` REFERENCES ${this.escapeId(definition.references.table)}${refFieldSql5}`;
|
|
775
|
+
if (definition.references.onDelete) {
|
|
776
|
+
sql += ` ON DELETE ${definition.references.onDelete}`;
|
|
777
|
+
}
|
|
778
|
+
if (definition.references.onUpdate) {
|
|
779
|
+
sql += ` ON UPDATE ${definition.references.onUpdate}`;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// SQLite does not support column-level comments in CREATE TABLE
|
|
783
|
+
// Comments are stored separately and not part of the column definition
|
|
784
|
+
return sql;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Get default value SQL
|
|
788
|
+
*/
|
|
789
|
+
getDefaultValue(value) {
|
|
790
|
+
// Handle Literal (raw SQL) values - insert directly without escaping
|
|
791
|
+
if (value instanceof prorm_1.Literal) {
|
|
792
|
+
return value.val;
|
|
793
|
+
}
|
|
794
|
+
if (value === null) {
|
|
795
|
+
return 'NULL';
|
|
796
|
+
}
|
|
797
|
+
if (typeof value === 'string') {
|
|
798
|
+
return `'${this.escapeString(value)}'`;
|
|
799
|
+
}
|
|
800
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
801
|
+
return String(value);
|
|
802
|
+
}
|
|
803
|
+
if (value instanceof Date) {
|
|
804
|
+
return `'${value.toISOString()}'`;
|
|
805
|
+
}
|
|
806
|
+
// For functions like CURRENT_TIMESTAMP
|
|
807
|
+
if (typeof value === 'string' && value.toUpperCase().includes('CURRENT_')) {
|
|
808
|
+
return value;
|
|
809
|
+
}
|
|
810
|
+
// Handle DataType instances with toDefaultValue method (e.g., DataTypes.NOW)
|
|
811
|
+
if (typeof value === 'object' &&
|
|
812
|
+
value !== null &&
|
|
813
|
+
typeof value.toDefaultValue === 'function') {
|
|
814
|
+
return value.toDefaultValue();
|
|
815
|
+
}
|
|
816
|
+
return `'${this.escapeString(String(value))}'`;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Drop a table
|
|
820
|
+
*/
|
|
821
|
+
async dropTable(tableName, options) {
|
|
822
|
+
let sql = 'DROP TABLE';
|
|
823
|
+
if (options?.ifExists) {
|
|
824
|
+
sql += ' IF EXISTS';
|
|
825
|
+
}
|
|
826
|
+
sql += ` ${this.escapeId(tableName)}`;
|
|
827
|
+
// SQLite has no `DROP TABLE ... CASCADE` syntax - appending it produces a
|
|
828
|
+
// syntax error, which made every `cascade: true` drop fail (silently
|
|
829
|
+
// swallowed by callers like `Prorm.syncForce()`, which only logs a
|
|
830
|
+
// warning and moves on). That meant `sync({ force: true })` never
|
|
831
|
+
// actually dropped/recreated tables that already existed, since
|
|
832
|
+
// `CREATE TABLE IF NOT EXISTS` is then a no-op against the stale table.
|
|
833
|
+
// SQLite doesn't need an explicit CASCADE: dropping a table just removes
|
|
834
|
+
// its schema entry - it doesn't cascade-delete rows in other tables the
|
|
835
|
+
// way `ON DELETE CASCADE` does, so `cascade` is a no-op here rather than
|
|
836
|
+
// an error.
|
|
837
|
+
await this.query(sql);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Create a partitioned table (SQLite emulation)
|
|
841
|
+
* Creates base table + child partition tables + union view
|
|
842
|
+
*/
|
|
843
|
+
async createPartitionedTable(tableName, columns, options) {
|
|
844
|
+
const partitionConfig = options?.partitionBy;
|
|
845
|
+
// Handle partitions being a number (e.g., partitions: 2 for KEY partitioning)
|
|
846
|
+
// or undefined, or an array
|
|
847
|
+
const partitions = Array.isArray(options?.partitions)
|
|
848
|
+
? options.partitions
|
|
849
|
+
: typeof options?.partitions === 'number'
|
|
850
|
+
? []
|
|
851
|
+
: [];
|
|
852
|
+
// Create the base table (used as template)
|
|
853
|
+
await this.createTable(tableName, columns, {
|
|
854
|
+
...options,
|
|
855
|
+
ifNotExists: options?.ifNotExists ?? true,
|
|
856
|
+
});
|
|
857
|
+
// Store partition metadata
|
|
858
|
+
const metadataTable = `${tableName}_partitions`;
|
|
859
|
+
await this.query(`CREATE TABLE IF NOT EXISTS ${this.escapeId(metadataTable)} (
|
|
860
|
+
partition_name TEXT PRIMARY KEY,
|
|
861
|
+
parent_table TEXT NOT NULL,
|
|
862
|
+
bound TEXT,
|
|
863
|
+
is_detached INTEGER DEFAULT 0,
|
|
864
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
865
|
+
)`);
|
|
866
|
+
// Create partitions if specified
|
|
867
|
+
for (const partition of partitions) {
|
|
868
|
+
await this.createPartition({
|
|
869
|
+
parentTable: tableName,
|
|
870
|
+
name: partition.name,
|
|
871
|
+
bound: partition.bound,
|
|
872
|
+
tablespace: partition.tablespace,
|
|
873
|
+
storageParameters: partition.storageParameters,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
// Create a UNION ALL view to provide a unified interface
|
|
877
|
+
if (partitions.length > 0) {
|
|
878
|
+
const partitionSelects = partitions.map((p) => `SELECT * FROM ${this.escapeId(p.name)}`).join('\n UNION ALL\n ');
|
|
879
|
+
const viewName = `${tableName}_union`;
|
|
880
|
+
await this.createView(viewName, partitionSelects, { replace: true });
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Create a partition for an existing partitioned table (SQLite emulation)
|
|
885
|
+
*/
|
|
886
|
+
async createPartition(options) {
|
|
887
|
+
const { parentTable, name, bound } = options;
|
|
888
|
+
// Get the column structure from the parent table
|
|
889
|
+
const parentColumns = await this.describeTable(parentTable);
|
|
890
|
+
// Create the partition table with the same structure
|
|
891
|
+
const partitionColumns = {};
|
|
892
|
+
for (const [colName, colDef] of Object.entries(parentColumns)) {
|
|
893
|
+
partitionColumns[colName] = {
|
|
894
|
+
type: colDef.type,
|
|
895
|
+
allowNull: colDef.allowNull,
|
|
896
|
+
defaultValue: colDef.defaultValue,
|
|
897
|
+
autoIncrement: colDef.autoIncrement,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
// Create the partition table
|
|
901
|
+
await this.createTable(name, partitionColumns, { ifNotExists: true });
|
|
902
|
+
// Insert partition metadata
|
|
903
|
+
const metadataTable = `${parentTable}_partitions`;
|
|
904
|
+
const boundStr = bound ? JSON.stringify(bound) : 'NULL';
|
|
905
|
+
await this.query(`INSERT OR IGNORE INTO ${this.escapeId(metadataTable)} (partition_name, parent_table, bound) VALUES (${this.escape(name)}, ${this.escape(parentTable)}, ${boundStr})`);
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Attach a partition to a partitioned table (SQLite emulation)
|
|
909
|
+
*/
|
|
910
|
+
async attachPartition(options) {
|
|
911
|
+
const { parentTable, partitionName } = options;
|
|
912
|
+
// Verify the partition table exists
|
|
913
|
+
const tables = await this.showTables();
|
|
914
|
+
if (!tables.includes(partitionName)) {
|
|
915
|
+
throw new Error(`Partition table '${partitionName}' does not exist`);
|
|
916
|
+
}
|
|
917
|
+
// Update partition metadata to mark as attached
|
|
918
|
+
const metadataTable = `${parentTable}_partitions`;
|
|
919
|
+
await this.query(`INSERT OR REPLACE INTO ${this.escapeId(metadataTable)} (partition_name, parent_table, is_detached) VALUES (${this.escape(partitionName)}, ${this.escape(parentTable)}, 0)`);
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Detach a partition from a partitioned table (SQLite emulation)
|
|
923
|
+
* Marks the partition as detached but keeps the table
|
|
924
|
+
*/
|
|
925
|
+
async detachPartition(options) {
|
|
926
|
+
const { partitionName } = options;
|
|
927
|
+
// Find the parent table from metadata
|
|
928
|
+
const partitions = await this.listPartitions(partitionName);
|
|
929
|
+
if (partitions.length === 0) {
|
|
930
|
+
throw new Error(`Partition '${partitionName}' not found`);
|
|
931
|
+
}
|
|
932
|
+
const partitionInfo = partitions[0];
|
|
933
|
+
const metadataTable = `${partitionInfo.parent_table}_partitions`;
|
|
934
|
+
// Mark partition as detached
|
|
935
|
+
await this.query(`UPDATE ${this.escapeId(metadataTable)} SET is_detached = 1 WHERE partition_name = ${this.escape(partitionName)}`);
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Drop a partition (SQLite emulation)
|
|
939
|
+
*/
|
|
940
|
+
async dropPartition(partitionName, options) {
|
|
941
|
+
const ifExists = options?.ifExists ?? false;
|
|
942
|
+
const cascade = options?.cascade ?? false;
|
|
943
|
+
// Find the parent table from metadata
|
|
944
|
+
const partitions = await this.listPartitions(partitionName);
|
|
945
|
+
let parentTable = null;
|
|
946
|
+
if (partitions.length > 0) {
|
|
947
|
+
parentTable = partitions[0].parent_table;
|
|
948
|
+
}
|
|
949
|
+
// Drop the partition table
|
|
950
|
+
let sql = 'DROP TABLE';
|
|
951
|
+
if (ifExists) {
|
|
952
|
+
sql += ' IF EXISTS';
|
|
953
|
+
}
|
|
954
|
+
sql += ` ${this.escapeId(partitionName)}`;
|
|
955
|
+
if (cascade) {
|
|
956
|
+
sql += ' CASCADE';
|
|
957
|
+
}
|
|
958
|
+
await this.query(sql);
|
|
959
|
+
// Remove partition metadata if parent table exists
|
|
960
|
+
if (parentTable) {
|
|
961
|
+
const metadataTable = `${parentTable}_partitions`;
|
|
962
|
+
try {
|
|
963
|
+
await this.query(`DELETE FROM ${this.escapeId(metadataTable)} WHERE partition_name = ${this.escape(partitionName)}`);
|
|
964
|
+
}
|
|
965
|
+
catch {
|
|
966
|
+
// Ignore if metadata table doesn't exist
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Add a partition to an existing partitioned table (SQLite emulation)
|
|
972
|
+
* @param tableName - Name of the partitioned table
|
|
973
|
+
* @param partitionName - Name for the new partition
|
|
974
|
+
* @param partitionSpec - Partition specification
|
|
975
|
+
*/
|
|
976
|
+
async addPartition(tableName, partitionName, partitionSpec) {
|
|
977
|
+
// SQLite does not support table partitioning
|
|
978
|
+
throw new Error(`Table partitioning is not supported by SQLite. ` +
|
|
979
|
+
`Please use PostgreSQL, MySQL, or another database that supports partitioning.`);
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* List partitions for a table (SQLite emulation)
|
|
983
|
+
*/
|
|
984
|
+
async listPartitions(tableName) {
|
|
985
|
+
const metadataTable = `${tableName}_partitions`;
|
|
986
|
+
try {
|
|
987
|
+
const result = await this.query(`SELECT * FROM ${this.escapeId(metadataTable)}`);
|
|
988
|
+
return result.rows.map((row) => ({
|
|
989
|
+
name: row.partition_name,
|
|
990
|
+
parent_table: row.parent_table,
|
|
991
|
+
bound: row.bound,
|
|
992
|
+
is_detached: row.is_detached,
|
|
993
|
+
}));
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
// If metadata table doesn't exist, return empty array
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Check if a table has partitions (SQLite emulation)
|
|
1002
|
+
*/
|
|
1003
|
+
async hasPartition(tableName) {
|
|
1004
|
+
const partitions = await this.listPartitions(tableName);
|
|
1005
|
+
return partitions.length > 0;
|
|
1006
|
+
}
|
|
1007
|
+
// ==================== User Management Stubs ====================
|
|
1008
|
+
/**
|
|
1009
|
+
* Create a user (stub - not supported by SQLite)
|
|
1010
|
+
*/
|
|
1011
|
+
async createUser(_username, _options) {
|
|
1012
|
+
throw new Error('User management is not supported by SQLite');
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Drop a user (stub - not supported by SQLite)
|
|
1016
|
+
*/
|
|
1017
|
+
async dropUser(_username, _options) {
|
|
1018
|
+
throw new Error('User management is not supported by SQLite');
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* List users (stub - not supported by SQLite)
|
|
1022
|
+
*/
|
|
1023
|
+
async listUsers() {
|
|
1024
|
+
throw new Error('User management is not supported by SQLite');
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Create a role (stub - not supported by SQLite)
|
|
1028
|
+
*/
|
|
1029
|
+
async createRole(_roleName, _options) {
|
|
1030
|
+
throw new Error('Role management is not supported by SQLite');
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Drop a role (stub - not supported by SQLite)
|
|
1034
|
+
*/
|
|
1035
|
+
async dropRole(_roleName, _options) {
|
|
1036
|
+
throw new Error('Role management is not supported by SQLite');
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* List roles (stub - not supported by SQLite)
|
|
1040
|
+
*/
|
|
1041
|
+
async listRoles() {
|
|
1042
|
+
throw new Error('Role management is not supported by SQLite');
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Grant privileges (stub - not supported by SQLite)
|
|
1046
|
+
*/
|
|
1047
|
+
async grant(_privilege, _on, _to) {
|
|
1048
|
+
throw new Error('User management is not supported by SQLite');
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Revoke privileges (stub - not supported by SQLite)
|
|
1052
|
+
*/
|
|
1053
|
+
async revoke(_privilege, _on, _from) {
|
|
1054
|
+
throw new Error('User management is not supported by SQLite');
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Add a column to a table
|
|
1058
|
+
*/
|
|
1059
|
+
async addColumn(tableName, columnName, definition) {
|
|
1060
|
+
const sql = `ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${this.getColumnDefinitionSql(columnName, definition)}`;
|
|
1061
|
+
await this.query(sql);
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Remove a column from a table
|
|
1065
|
+
*/
|
|
1066
|
+
async removeColumn(tableName, columnName) {
|
|
1067
|
+
// SQLite doesn't support DROP COLUMN directly, need to recreate table
|
|
1068
|
+
await this.recreateTableWithoutColumn(tableName, columnName);
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Change a column definition
|
|
1072
|
+
*/
|
|
1073
|
+
async changeColumn(tableName, columnName, definition) {
|
|
1074
|
+
// SQLite doesn't support ALTER COLUMN, need to recreate table
|
|
1075
|
+
await this.recreateTableModifyColumn(tableName, columnName, definition);
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Show all tables in the database
|
|
1079
|
+
*/
|
|
1080
|
+
async showTables() {
|
|
1081
|
+
const sql = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'";
|
|
1082
|
+
const result = await this.query(sql);
|
|
1083
|
+
return result.rows.map((row) => row.name);
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Get table status (SQLite implementation)
|
|
1087
|
+
*/
|
|
1088
|
+
async getTableStatus(tableName) {
|
|
1089
|
+
const sql = tableName
|
|
1090
|
+
? "SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name = ?"
|
|
1091
|
+
: "SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'";
|
|
1092
|
+
const result = tableName
|
|
1093
|
+
? await this.query(sql, { replacements: [tableName] })
|
|
1094
|
+
: await this.query(sql);
|
|
1095
|
+
return result.rows;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Get table create statement (SQLite implementation)
|
|
1099
|
+
*/
|
|
1100
|
+
async getCreateTable(tableName) {
|
|
1101
|
+
const sql = "SELECT sql FROM sqlite_master WHERE type='table' AND name = ?";
|
|
1102
|
+
const result = await this.query(sql, { replacements: [tableName] });
|
|
1103
|
+
return result.rows[0]?.sql || '';
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Get MariaDB-specific version info (not supported in SQLite)
|
|
1107
|
+
* This method exists for compatibility when tests fall back to SQLite
|
|
1108
|
+
*/
|
|
1109
|
+
async getMariaDBVersionInfo() {
|
|
1110
|
+
throw new Error('getMariaDBVersionInfo is not supported in SQLite. Use MariaDB dialect for this feature.');
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Show constraints for a table (SQLite)
|
|
1114
|
+
*/
|
|
1115
|
+
async showConstraints(tableName) {
|
|
1116
|
+
const sql = `PRAGMA table_info(${this.escapeId(tableName)})`;
|
|
1117
|
+
const result = await this.query(sql);
|
|
1118
|
+
return result.rows.map((row) => ({
|
|
1119
|
+
name: row.name,
|
|
1120
|
+
type: row.type,
|
|
1121
|
+
notnull: row.notnull === 1,
|
|
1122
|
+
dflt_value: row.dflt_value,
|
|
1123
|
+
pk: row.pk === 1,
|
|
1124
|
+
}));
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Show indexes for a table
|
|
1128
|
+
*/
|
|
1129
|
+
async showIndexes(tableName) {
|
|
1130
|
+
const sql = `PRAGMA index_list(${this.escapeId(tableName)})`;
|
|
1131
|
+
const result = await this.query(sql);
|
|
1132
|
+
return result.rows.map((row) => ({
|
|
1133
|
+
name: row.name,
|
|
1134
|
+
unique: row.unique === 1,
|
|
1135
|
+
origin: row.origin,
|
|
1136
|
+
}));
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Create a schema
|
|
1140
|
+
* SQLite doesn't have native schema support, but we can use ATTACH DATABASE
|
|
1141
|
+
* For now, this is a no-op for compatibility with other dialects
|
|
1142
|
+
* @param schema - The schema name to create
|
|
1143
|
+
*/
|
|
1144
|
+
async createSchema(_schema) {
|
|
1145
|
+
// SQLite doesn't have native schema support
|
|
1146
|
+
// Schemas are typically simulated using database files or naming conventions
|
|
1147
|
+
// For compatibility, this is a no-op - there is no SQL statement to execute
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Drop a schema
|
|
1152
|
+
* SQLite doesn't have native schema support
|
|
1153
|
+
* For now, this is a no-op for compatibility with other dialects
|
|
1154
|
+
* @param schema - The schema name to drop
|
|
1155
|
+
* @param options - Drop options
|
|
1156
|
+
*/
|
|
1157
|
+
async dropSchema(_schema, _options) {
|
|
1158
|
+
// SQLite doesn't have native schema support
|
|
1159
|
+
// For compatibility, this is a no-op - there is no SQL statement to execute
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Show all schemas in the database
|
|
1164
|
+
* SQLite doesn't have native schema support, returns default schema
|
|
1165
|
+
*/
|
|
1166
|
+
async showAllSchemas() {
|
|
1167
|
+
// SQLite only has the main schema
|
|
1168
|
+
return ['main'];
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* List all schemas (SQLite only has main)
|
|
1172
|
+
*/
|
|
1173
|
+
async listSchemas() {
|
|
1174
|
+
return this.showAllSchemas();
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* VACUUM the database - reclaims disk space and defragments
|
|
1178
|
+
* @param options - VACUUM options
|
|
1179
|
+
*/
|
|
1180
|
+
async vacuum(options) {
|
|
1181
|
+
return this.advanced.vacuum(options);
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* ANALYZE the database - updates statistics for query optimizer
|
|
1185
|
+
* @param target - Optional table or index name
|
|
1186
|
+
*/
|
|
1187
|
+
async analyze(target) {
|
|
1188
|
+
return this.advanced.analyze(target);
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Get database information using PRAGMA
|
|
1192
|
+
*/
|
|
1193
|
+
async getDatabaseInfo() {
|
|
1194
|
+
return this.advanced.getDatabaseInfo();
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Create an FTS5 virtual table for full-text search
|
|
1198
|
+
*/
|
|
1199
|
+
async createFTS5Table(tableName, columns, options) {
|
|
1200
|
+
return this.advanced.createFTS5Table(tableName, columns, options);
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Perform full-text search
|
|
1204
|
+
*/
|
|
1205
|
+
async fullTextSearch(ftsTable, searchQuery, options) {
|
|
1206
|
+
return this.advanced.fullTextSearch(ftsTable, searchQuery, options);
|
|
1207
|
+
}
|
|
1208
|
+
// ==================== Stored Procedures ====================
|
|
1209
|
+
// SQLite does not support stored procedures natively
|
|
1210
|
+
// These methods provide helpful messages explaining limitations and workarounds
|
|
1211
|
+
/**
|
|
1212
|
+
* Create a stored procedure (SQLite - Not Supported)
|
|
1213
|
+
*
|
|
1214
|
+
* SQLite does not support stored procedures natively. However, you can achieve
|
|
1215
|
+
* similar functionality using:
|
|
1216
|
+
*
|
|
1217
|
+
* 1. Application-level code: Move business logic into your application code
|
|
1218
|
+
* 2. Views: For read-only operations
|
|
1219
|
+
* 3. Triggers: For automated responses to data changes
|
|
1220
|
+
* 4. FTS5 virtual tables: For full-text search operations
|
|
1221
|
+
* 5. SQLite ATTACH: Connect to another database that supports stored procedures
|
|
1222
|
+
*
|
|
1223
|
+
* Example workaround using application code:
|
|
1224
|
+
* ```typescript
|
|
1225
|
+
* // Instead of:
|
|
1226
|
+
* await db.executeStoredProcedure({ procedureName: 'calculateTotal', params: { orderId: 1 } });
|
|
1227
|
+
*
|
|
1228
|
+
* // Use application-level code:
|
|
1229
|
+
* const total = await calculateTotal(orderId);
|
|
1230
|
+
* ```
|
|
1231
|
+
*/
|
|
1232
|
+
async createStoredProcedure(options) {
|
|
1233
|
+
const procName = options.name;
|
|
1234
|
+
throw new Error(`SQLite does not support stored procedures. Cannot create '${procName}'.\n\n` +
|
|
1235
|
+
`Workarounds:\n` +
|
|
1236
|
+
`- Move logic to application code\n` +
|
|
1237
|
+
`- Use views for read-only operations\n` +
|
|
1238
|
+
`- Use triggers for automated data changes\n` +
|
|
1239
|
+
`- Use FTS5 for full-text search\n` +
|
|
1240
|
+
`- Use ATTACH DATABASE to connect to PostgreSQL/MySQL for stored procedures\n\n` +
|
|
1241
|
+
`Example: If using this in an app, create a TypeScript function that executes ` +
|
|
1242
|
+
`the same SQL logic and call that instead.`);
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Drop a stored procedure (SQLite - Not Supported)
|
|
1246
|
+
*
|
|
1247
|
+
* Since SQLite does not support stored procedures, there is nothing to drop.
|
|
1248
|
+
* If you previously created a stored procedure in an attached database,
|
|
1249
|
+
* drop it from that database instead.
|
|
1250
|
+
*/
|
|
1251
|
+
async dropStoredProcedure(procedureName, _options) {
|
|
1252
|
+
throw new Error(`SQLite does not support stored procedures. Cannot drop '${procedureName}'.\n\n` +
|
|
1253
|
+
`If you created this procedure in an attached database using ATTACH, ` +
|
|
1254
|
+
`drop it from that database instead:\n` +
|
|
1255
|
+
` await db.query("DETACH DATABASE alias");\n` +
|
|
1256
|
+
`Then recreate the procedure in the target database.`);
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Drop a stored procedure (alias for dropStoredProcedure)
|
|
1260
|
+
* SQLite does not support stored procedures - use application code instead.
|
|
1261
|
+
*/
|
|
1262
|
+
async dropProcedure(procedureName, options) {
|
|
1263
|
+
return this.dropStoredProcedure(procedureName, options);
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Execute a stored procedure (SQLite - Not Supported)
|
|
1267
|
+
*
|
|
1268
|
+
* SQLite does not support stored procedures. To execute the equivalent logic,
|
|
1269
|
+
* move the procedure's SQL statements into your application code.
|
|
1270
|
+
*
|
|
1271
|
+
* Example:
|
|
1272
|
+
* ```typescript
|
|
1273
|
+
* // Instead of:
|
|
1274
|
+
* const result = await db.executeStoredProcedure({
|
|
1275
|
+
* procedureName: 'getUserOrders',
|
|
1276
|
+
* params: { userId: 123 }
|
|
1277
|
+
* });
|
|
1278
|
+
*
|
|
1279
|
+
* // Use a query directly:
|
|
1280
|
+
* const result = await db.query(
|
|
1281
|
+
* 'SELECT * FROM orders WHERE user_id = ?',
|
|
1282
|
+
* [123]
|
|
1283
|
+
* );
|
|
1284
|
+
* ```
|
|
1285
|
+
*/
|
|
1286
|
+
async executeStoredProcedure(options) {
|
|
1287
|
+
const procName = options.procedureName;
|
|
1288
|
+
throw new Error(`SQLite does not support stored procedures. Cannot execute '${procName}'.\n\n` +
|
|
1289
|
+
`Workaround: Execute the SQL logic directly in your application:\n` +
|
|
1290
|
+
` const result = await db.query('YOUR_SQL_HERE', [params]);\n\n` +
|
|
1291
|
+
`If you need to reuse this logic, create a TypeScript/JavaScript function ` +
|
|
1292
|
+
`that wraps the SQL query.`);
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Check if a stored procedure exists (SQLite - Always Returns False)
|
|
1296
|
+
*
|
|
1297
|
+
* Since SQLite does not support stored procedures, this always returns false.
|
|
1298
|
+
* If you are checking for a procedure in an attached database, use that
|
|
1299
|
+
* database's native method instead.
|
|
1300
|
+
*/
|
|
1301
|
+
async hasStoredProcedure(_procedureName, _schema) {
|
|
1302
|
+
// SQLite does not support stored procedures, so none can exist
|
|
1303
|
+
return false;
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Create a stored procedure (alias for createStoredProcedure)
|
|
1307
|
+
* SQLite does not support stored procedures - use application code instead.
|
|
1308
|
+
*/
|
|
1309
|
+
async createProcedure(options) {
|
|
1310
|
+
return this.createStoredProcedure(options);
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Create a foreign data wrapper (SQLite - not supported)
|
|
1314
|
+
* @param fdwName - Foreign data wrapper name
|
|
1315
|
+
* @param options - Options including handler function
|
|
1316
|
+
*/
|
|
1317
|
+
async createForeignDataWrapper(_fdwName, _options) {
|
|
1318
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1319
|
+
}
|
|
1320
|
+
async dropForeignDataWrapper(_fdwName, _options) {
|
|
1321
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1322
|
+
}
|
|
1323
|
+
async createForeignServer(_serverName, _fdwName, _options) {
|
|
1324
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1325
|
+
}
|
|
1326
|
+
async dropForeignServer(_serverName, _options) {
|
|
1327
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1328
|
+
}
|
|
1329
|
+
async createForeignTable(_tableName, _columns, _options) {
|
|
1330
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1331
|
+
}
|
|
1332
|
+
async changeOwner(_newOwner, _tableName) {
|
|
1333
|
+
throw new Error('changeOwner is not supported in SQLite');
|
|
1334
|
+
}
|
|
1335
|
+
async addConstraint(_tableName, _options) {
|
|
1336
|
+
// SQLite cannot ALTER TABLE ADD CONSTRAINT, but a UNIQUE constraint is
|
|
1337
|
+
// equivalent to a UNIQUE INDEX, which we can create on an existing table.
|
|
1338
|
+
const type = String(_options.type || '').toUpperCase();
|
|
1339
|
+
const name = _options.name ||
|
|
1340
|
+
`${_tableName}_${(_options.fields || []).join('_')}_${type.toLowerCase().replace(/\s+/g, '_')}`;
|
|
1341
|
+
if (type === 'UNIQUE') {
|
|
1342
|
+
await this.addIndex(_tableName, name, _options.fields, { unique: true });
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
throw new Error(`addConstraint type "${type}" cannot be added to an existing SQLite table; ` +
|
|
1346
|
+
`define it when the table is created (SQLite only supports UNIQUE via a unique index post-hoc).`);
|
|
1347
|
+
}
|
|
1348
|
+
async removeConstraint(_tableName, _constraintName) {
|
|
1349
|
+
// The only post-hoc constraint we create is a unique index; drop it if present.
|
|
1350
|
+
await this.query(`DROP INDEX IF EXISTS ${this.escapeId(_constraintName)}`);
|
|
1351
|
+
}
|
|
1352
|
+
async createSecurityPolicy(_policyName, _tableName, _options) {
|
|
1353
|
+
throw new Error('Security policies are not supported in SQLite');
|
|
1354
|
+
}
|
|
1355
|
+
async dropSecurityPolicy(_policyName, _tableName) {
|
|
1356
|
+
throw new Error('Security policies are not supported in SQLite');
|
|
1357
|
+
}
|
|
1358
|
+
// ==================== Triggers ====================
|
|
1359
|
+
/**
|
|
1360
|
+
* Create a trigger
|
|
1361
|
+
*/
|
|
1362
|
+
async createTrigger(triggerName, timing, events, tableName, body, options) {
|
|
1363
|
+
return this.advanced.createTrigger(triggerName, timing, events, tableName, body, options);
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Drop a trigger
|
|
1367
|
+
*/
|
|
1368
|
+
async dropTrigger(triggerName, options) {
|
|
1369
|
+
return this.advanced.dropTrigger(triggerName, options);
|
|
1370
|
+
}
|
|
1371
|
+
async hasTrigger(_triggerName, _tableName) {
|
|
1372
|
+
// Check if trigger exists
|
|
1373
|
+
const sql = `SELECT name FROM sqlite_master WHERE type='trigger' AND name=?`;
|
|
1374
|
+
const result = await this.query(sql, { plain: true });
|
|
1375
|
+
return !!result;
|
|
1376
|
+
}
|
|
1377
|
+
// ==================== Sequences ====================
|
|
1378
|
+
// SQLite does not have native sequences, but they can be simulated
|
|
1379
|
+
// using INTEGER PRIMARY KEY AUTOINCREMENT tables
|
|
1380
|
+
/**
|
|
1381
|
+
* Create a sequence (simulated using AUTOINCREMENT table)
|
|
1382
|
+
*/
|
|
1383
|
+
async createSequence(options) {
|
|
1384
|
+
const seqName = options.name;
|
|
1385
|
+
const schema = options.schema;
|
|
1386
|
+
// Use a special prefix to distinguish sequence tables
|
|
1387
|
+
const tableName = this.quoteTable(this._getSequenceTableName(seqName, schema));
|
|
1388
|
+
// Check if sequence already exists
|
|
1389
|
+
if (options.ifNotExists) {
|
|
1390
|
+
const exists = await this.hasSequence(seqName, schema);
|
|
1391
|
+
if (exists) {
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
// Build the CREATE TABLE statement
|
|
1396
|
+
let sql = `CREATE TABLE ${tableName} (value INTEGER PRIMARY KEY AUTOINCREMENT)`;
|
|
1397
|
+
// Handle startWith - SQLite AUTOINCREMENT starts at 1 by default
|
|
1398
|
+
// To set a different starting value, we need to insert a row first
|
|
1399
|
+
if (options.startWith !== undefined && options.startWith > 1) {
|
|
1400
|
+
// Create the table first
|
|
1401
|
+
await this.query(sql);
|
|
1402
|
+
// Insert a row with value = startWith - 1 so next value will be startWith
|
|
1403
|
+
const insertSql = `INSERT INTO ${tableName} (value) VALUES (?)`;
|
|
1404
|
+
await this.query(insertSql, { bind: [options.startWith - 1] });
|
|
1405
|
+
// Delete that row so the sequence starts at the correct value
|
|
1406
|
+
const deleteSql = `DELETE FROM ${tableName}`;
|
|
1407
|
+
await this.query(deleteSql);
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
await this.query(sql);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Drop a sequence (simulated by dropping the sequence table)
|
|
1415
|
+
*/
|
|
1416
|
+
async dropSequence(sequenceName, options) {
|
|
1417
|
+
const schema = options?.schema;
|
|
1418
|
+
const tableName = this._getSequenceTableName(sequenceName, schema);
|
|
1419
|
+
const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
|
|
1420
|
+
const sql = `DROP TABLE ${ifExists}${this.quoteTable(tableName)}`;
|
|
1421
|
+
await this.query(sql);
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Get next value from a sequence
|
|
1425
|
+
* (simulated by inserting and getting last_insert_rowid)
|
|
1426
|
+
*/
|
|
1427
|
+
async nextSequenceValue(sequenceName, schema) {
|
|
1428
|
+
const tableName = this._getSequenceTableName(sequenceName, schema);
|
|
1429
|
+
const sql = `INSERT INTO ${this.quoteTable(tableName)} (value) VALUES (NULL)`;
|
|
1430
|
+
await this.query(sql);
|
|
1431
|
+
// Get the last inserted rowid (the auto-generated value)
|
|
1432
|
+
const result = await this.query('SELECT last_insert_rowid() as value', { raw: true });
|
|
1433
|
+
return result.rows?.[0]?.value;
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Check if a sequence exists
|
|
1437
|
+
* (simulated by checking if the sequence table exists)
|
|
1438
|
+
*/
|
|
1439
|
+
async hasSequence(sequenceName, schema) {
|
|
1440
|
+
const tableName = this._getSequenceTableName(sequenceName, schema);
|
|
1441
|
+
const sql = `SELECT name FROM sqlite_master WHERE type='table' AND name=?`;
|
|
1442
|
+
const result = await this.query(sql, { bind: [tableName], plain: true });
|
|
1443
|
+
return !!result;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* List all sequences in the database (SQLite - simulated via tables)
|
|
1447
|
+
* @returns Array of sequence names
|
|
1448
|
+
*/
|
|
1449
|
+
async listSequences() {
|
|
1450
|
+
const sql = `SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '_sequence_%'`;
|
|
1451
|
+
const result = await this.query(sql);
|
|
1452
|
+
return result.rows.map((row) => row.name.replace(/^_sequence_/, ''));
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Get the internal table name for a sequence
|
|
1456
|
+
*/
|
|
1457
|
+
_getSequenceTableName(sequenceName, schema) {
|
|
1458
|
+
// Prefix with _sequence_ to avoid name collisions with regular tables
|
|
1459
|
+
const prefix = '_sequence_';
|
|
1460
|
+
const schemaPrefix = schema ? `${schema}_` : '';
|
|
1461
|
+
return `${prefix}${schemaPrefix}${sequenceName}`;
|
|
1462
|
+
}
|
|
1463
|
+
// ==================== Row-Level Security (RLS) ====================
|
|
1464
|
+
async createPolicy(_options) {
|
|
1465
|
+
throw new Error('Row-level security is not supported by SQLite');
|
|
1466
|
+
}
|
|
1467
|
+
async dropPolicy(_policyName, _tableName, _options) {
|
|
1468
|
+
throw new Error('Row-level security is not supported by SQLite');
|
|
1469
|
+
}
|
|
1470
|
+
async enableRLS(_tableName, _schema) {
|
|
1471
|
+
throw new Error('Row-level security is not supported by SQLite');
|
|
1472
|
+
}
|
|
1473
|
+
async enableRowLevelSecurity(_tableName, _schema) {
|
|
1474
|
+
throw new Error('Row-level security is not supported by SQLite');
|
|
1475
|
+
}
|
|
1476
|
+
async disableRLS(_tableName, _schema) {
|
|
1477
|
+
throw new Error('Row-level security is not supported by SQLite');
|
|
1478
|
+
}
|
|
1479
|
+
async hasPolicy(_policyName, _tableName) {
|
|
1480
|
+
return false;
|
|
1481
|
+
}
|
|
1482
|
+
// ==================== Comments ====================
|
|
1483
|
+
async commentTable(_tableName, _comment) {
|
|
1484
|
+
// SQLite does not support table comments natively
|
|
1485
|
+
// Could use SQLITE_SQLITE_MASTER comment extension or pragma
|
|
1486
|
+
}
|
|
1487
|
+
async commentColumn(_tableName, _columnName, _comment) {
|
|
1488
|
+
// SQLite does not support column comments natively
|
|
1489
|
+
}
|
|
1490
|
+
// ==================== Advanced Indexes ====================
|
|
1491
|
+
async createPartialIndex(tableName, indexName, fields, where, options) {
|
|
1492
|
+
// SQLite supports partial indexes via WHERE clause
|
|
1493
|
+
const fieldsSql = fields.map((f) => this.quoteIdentifier(f)).join(', ');
|
|
1494
|
+
let sql = `CREATE INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteTable(tableName)} (${fieldsSql}) WHERE ${where}`;
|
|
1495
|
+
if (options?.unique)
|
|
1496
|
+
sql = sql.replace('CREATE INDEX', 'CREATE UNIQUE INDEX');
|
|
1497
|
+
if (options?.ifNotExists)
|
|
1498
|
+
sql = sql.replace('CREATE INDEX', 'CREATE INDEX IF NOT EXISTS');
|
|
1499
|
+
await this.query(sql);
|
|
1500
|
+
}
|
|
1501
|
+
async createExpressionIndex(tableName, indexName, expression, options) {
|
|
1502
|
+
// SQLite supports expression indexes
|
|
1503
|
+
let sql = `CREATE INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteTable(tableName)} (${expression})`;
|
|
1504
|
+
if (options?.unique)
|
|
1505
|
+
sql = sql.replace('CREATE INDEX', 'CREATE UNIQUE INDEX');
|
|
1506
|
+
if (options?.ifNotExists)
|
|
1507
|
+
sql = sql.replace('CREATE INDEX', 'CREATE INDEX IF NOT EXISTS');
|
|
1508
|
+
await this.query(sql);
|
|
1509
|
+
}
|
|
1510
|
+
// ==================== Identity & Computed Columns ====================
|
|
1511
|
+
async createIdentityColumn(_tableName, _columnName, _options) {
|
|
1512
|
+
throw new Error('Identity columns are not supported by SQLite. Use AUTOINCREMENT instead.');
|
|
1513
|
+
}
|
|
1514
|
+
async createComputedColumn(tableName, columnName, expression, options) {
|
|
1515
|
+
// SQLite supports generated columns (computed)
|
|
1516
|
+
const persistedSql = options?.persisted ? 'STORED' : 'VIRTUAL';
|
|
1517
|
+
const typeSql = options?.type || 'ANY';
|
|
1518
|
+
const sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD COLUMN ${this.quoteIdentifier(columnName)} ${typeSql} GENERATED ALWAYS AS (${expression}) ${persistedSql}`;
|
|
1519
|
+
await this.query(sql);
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Create a PostgreSQL extension (not supported in SQLite)
|
|
1523
|
+
*/
|
|
1524
|
+
async createExtension(_extensionName, _options) {
|
|
1525
|
+
throw new Error('Extensions are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Drop a PostgreSQL extension (not supported in SQLite)
|
|
1529
|
+
*/
|
|
1530
|
+
async dropExtension(_extensionName, _options) {
|
|
1531
|
+
throw new Error('Extensions are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Get all installed PostgreSQL extensions (not supported in SQLite)
|
|
1535
|
+
*/
|
|
1536
|
+
async getExtensions() {
|
|
1537
|
+
throw new Error('Extensions are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Check if a PostgreSQL extension is installed (not supported in SQLite)
|
|
1541
|
+
*/
|
|
1542
|
+
async hasExtension(_extensionName) {
|
|
1543
|
+
throw new Error('Extensions are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1544
|
+
}
|
|
1545
|
+
// ---------------------------------------------------------------------------
|
|
1546
|
+
// Foreign Data Wrapper – not supported in SQLite
|
|
1547
|
+
// ---------------------------------------------------------------------------
|
|
1548
|
+
buildCreateServerQuery(_name, _opts) {
|
|
1549
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1550
|
+
}
|
|
1551
|
+
buildAlterServerQuery(_name, _opts) {
|
|
1552
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1553
|
+
}
|
|
1554
|
+
buildDropServerQuery(_name, _opts) {
|
|
1555
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1556
|
+
}
|
|
1557
|
+
buildCreateUserMappingQuery(_opts) {
|
|
1558
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1559
|
+
}
|
|
1560
|
+
buildAlterUserMappingQuery(_opts) {
|
|
1561
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1562
|
+
}
|
|
1563
|
+
buildDropUserMappingQuery(_serverName, _user, _opts) {
|
|
1564
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1565
|
+
}
|
|
1566
|
+
buildCreateForeignTableQuery(_tableName, _opts) {
|
|
1567
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1568
|
+
}
|
|
1569
|
+
buildDropForeignTableQuery(_tableName, _opts) {
|
|
1570
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1571
|
+
}
|
|
1572
|
+
buildImportForeignSchemaQuery(_remoteSchema, _serverName, _opts) {
|
|
1573
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1574
|
+
}
|
|
1575
|
+
getServersQuery() {
|
|
1576
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Create a database view (SQLite uses CREATE VIEW)
|
|
1580
|
+
* @param viewName - Name of the view to create
|
|
1581
|
+
* @param query - The SELECT query for the view
|
|
1582
|
+
* @param options - View options
|
|
1583
|
+
*/
|
|
1584
|
+
async createView(viewName, query, options) {
|
|
1585
|
+
let sql = 'CREATE VIEW';
|
|
1586
|
+
if (options?.replace) {
|
|
1587
|
+
sql = 'CREATE OR REPLACE VIEW';
|
|
1588
|
+
}
|
|
1589
|
+
sql += ` ${this.escapeId(viewName)} AS ${query}`;
|
|
1590
|
+
await this.query(sql);
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Drop a database view
|
|
1594
|
+
* @param viewName - Name of the view to drop
|
|
1595
|
+
* @param options - Drop options
|
|
1596
|
+
*/
|
|
1597
|
+
async dropView(viewName, options) {
|
|
1598
|
+
let sql = 'DROP VIEW';
|
|
1599
|
+
if (options?.ifExists) {
|
|
1600
|
+
sql += ' IF EXISTS';
|
|
1601
|
+
}
|
|
1602
|
+
sql += ` ${this.escapeId(viewName)}`;
|
|
1603
|
+
await this.query(sql);
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Show all views in the database
|
|
1607
|
+
*/
|
|
1608
|
+
async showViews() {
|
|
1609
|
+
const sql = "SELECT name FROM sqlite_master WHERE type='view' AND name NOT LIKE 'sqlite_%'";
|
|
1610
|
+
const result = await this.query(sql);
|
|
1611
|
+
return result.rows.map((row) => row.name);
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Create a materialized view - Not supported by SQLite
|
|
1615
|
+
*/
|
|
1616
|
+
async createMaterializedView(_options) {
|
|
1617
|
+
throw new Error('Materialized views are not supported in SQLite. This feature is only available in PostgreSQL.');
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Refresh a materialized view - Not supported by SQLite
|
|
1621
|
+
*/
|
|
1622
|
+
async refreshMaterializedView(_viewName, _options) {
|
|
1623
|
+
throw new Error('Materialized views are not supported in SQLite. This feature is only available in PostgreSQL.');
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Drop a materialized view - Not supported by SQLite
|
|
1627
|
+
*/
|
|
1628
|
+
async dropMaterializedView(_viewName, _options) {
|
|
1629
|
+
throw new Error('Materialized views are not supported in SQLite. This feature is only available in PostgreSQL.');
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Check if a materialized view exists - Not supported by SQLite
|
|
1633
|
+
*/
|
|
1634
|
+
async hasMaterializedView(_viewName) {
|
|
1635
|
+
throw new Error('Materialized views are not supported in SQLite. This feature is only available in PostgreSQL.');
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Show all materialized views - Not supported by SQLite
|
|
1639
|
+
*/
|
|
1640
|
+
async showMaterializedViews() {
|
|
1641
|
+
throw new Error('Materialized views are not supported in SQLite. This feature is only available in PostgreSQL.');
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Describe a table (get column information)
|
|
1645
|
+
*/
|
|
1646
|
+
async describeTable(tableName) {
|
|
1647
|
+
const sql = `PRAGMA table_info(${this.escapeId(tableName)})`;
|
|
1648
|
+
const result = await this.query(sql);
|
|
1649
|
+
// PRAGMA table_info doesn't expose AUTOINCREMENT directly, but SQLite only
|
|
1650
|
+
// allows a single AUTOINCREMENT column per table and it must be the INTEGER
|
|
1651
|
+
// PRIMARY KEY, so we can detect it by checking whether the table's own
|
|
1652
|
+
// CREATE TABLE statement contains the AUTOINCREMENT keyword and attribute it
|
|
1653
|
+
// to the (single) primary key column. Without this, table-rebuild operations
|
|
1654
|
+
// (removeColumn/changeColumn/createConstraint) would silently drop
|
|
1655
|
+
// AUTOINCREMENT from the primary key every time they run.
|
|
1656
|
+
let tableHasAutoIncrement = false;
|
|
1657
|
+
try {
|
|
1658
|
+
const createTableSql = await this.getCreateTable(tableName);
|
|
1659
|
+
tableHasAutoIncrement = /\bAUTOINCREMENT\b/i.test(createTableSql);
|
|
1660
|
+
}
|
|
1661
|
+
catch {
|
|
1662
|
+
// If we can't determine this (e.g. table doesn't exist yet), default to false
|
|
1663
|
+
}
|
|
1664
|
+
const description = {};
|
|
1665
|
+
for (const row of result.rows) {
|
|
1666
|
+
const rowData = row;
|
|
1667
|
+
const isPrimaryKey = rowData.pk === 1;
|
|
1668
|
+
description[rowData.name] = {
|
|
1669
|
+
type: rowData.type || 'TEXT',
|
|
1670
|
+
allowNull: rowData.notnull === 0,
|
|
1671
|
+
defaultValue: rowData.dflt_value,
|
|
1672
|
+
primaryKey: isPrimaryKey,
|
|
1673
|
+
autoIncrement: isPrimaryKey && tableHasAutoIncrement,
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
return description;
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Convert a column description (as returned by describeTable, sourced from
|
|
1680
|
+
* PRAGMA table_info) into a ColumnDefinition suitable for getColumnDefinitionSql.
|
|
1681
|
+
*
|
|
1682
|
+
* PRAGMA table_info's `dflt_value` is already a raw SQL literal exactly as SQLite
|
|
1683
|
+
* stored it (e.g. the string column default `'foo'` comes back as the 3-character
|
|
1684
|
+
* string `'foo'`, quotes included). It must be wrapped in a Literal so it is
|
|
1685
|
+
* emitted verbatim instead of being re-escaped and quoted a second time.
|
|
1686
|
+
*/
|
|
1687
|
+
toColumnDefinition(colInfo) {
|
|
1688
|
+
const rawDefault = colInfo.defaultValue;
|
|
1689
|
+
return {
|
|
1690
|
+
...colInfo,
|
|
1691
|
+
defaultValue: typeof rawDefault === 'string' ? new prorm_1.Literal(rawDefault) : rawDefault,
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Rename a table
|
|
1696
|
+
*/
|
|
1697
|
+
async renameTable(oldName, newName) {
|
|
1698
|
+
const sql = `ALTER TABLE ${this.escapeId(oldName)} RENAME TO ${this.escapeId(newName)}`;
|
|
1699
|
+
await this.query(sql);
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Add an index to a table
|
|
1703
|
+
* Supports partial indexes (WHERE) and expression indexes (SQLite 3.9.0+)
|
|
1704
|
+
*/
|
|
1705
|
+
async addIndex(tableName, indexName, fields = [], options) {
|
|
1706
|
+
// Support the `(tableName, fields, options)` shorthand: when the second
|
|
1707
|
+
// argument is an array, it holds the fields and the index name is derived.
|
|
1708
|
+
if (Array.isArray(indexName)) {
|
|
1709
|
+
if (fields && !Array.isArray(fields)) {
|
|
1710
|
+
options = fields;
|
|
1711
|
+
}
|
|
1712
|
+
fields = indexName;
|
|
1713
|
+
indexName = `${tableName}_${fields.join('_')}_idx`;
|
|
1714
|
+
}
|
|
1715
|
+
let sql = 'CREATE';
|
|
1716
|
+
if (options?.unique) {
|
|
1717
|
+
sql += ' UNIQUE';
|
|
1718
|
+
}
|
|
1719
|
+
sql += ' INDEX';
|
|
1720
|
+
// Use IF NOT EXISTS to avoid errors when recreating indexes
|
|
1721
|
+
sql += ' IF NOT EXISTS';
|
|
1722
|
+
if (options?.type) {
|
|
1723
|
+
sql += ` ${options.type}`;
|
|
1724
|
+
}
|
|
1725
|
+
sql += ` ${this.escapeId(indexName)} ON ${this.escapeId(tableName)}`;
|
|
1726
|
+
// Handle expression index (SQLite 3.9.0+)
|
|
1727
|
+
if (options?.expression) {
|
|
1728
|
+
sql += ` ((${options.expression}))`;
|
|
1729
|
+
}
|
|
1730
|
+
else {
|
|
1731
|
+
sql += ` (${fields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
1732
|
+
}
|
|
1733
|
+
// Add partial index WHERE clause (SQLite 3.8.0+)
|
|
1734
|
+
if (options?.where) {
|
|
1735
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
1736
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1737
|
+
}
|
|
1738
|
+
await this.query(sql);
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Remove an index from a table
|
|
1742
|
+
*/
|
|
1743
|
+
async removeIndex(tableName, indexName) {
|
|
1744
|
+
// Drop by the exact name if present, otherwise fall back to the name the
|
|
1745
|
+
// `(tableName, fields)` addIndex shorthand generates (`${table}_${col}_idx`),
|
|
1746
|
+
// so callers can remove an index by either the explicit or derived name.
|
|
1747
|
+
await this.query(`DROP INDEX IF EXISTS ${this.escapeId(indexName)}`);
|
|
1748
|
+
const derived = `${tableName}_${indexName}_idx`;
|
|
1749
|
+
await this.query(`DROP INDEX IF EXISTS ${this.escapeId(derived)}`);
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Create an index on a table with full options support
|
|
1753
|
+
* Supports partial indexes (WHERE) and expression indexes (SQLite 3.9.0+)
|
|
1754
|
+
*/
|
|
1755
|
+
async createIndex(tableName, indexDef) {
|
|
1756
|
+
const fields = indexDef.fields || [];
|
|
1757
|
+
let sql = 'CREATE';
|
|
1758
|
+
if (indexDef.unique) {
|
|
1759
|
+
sql += ' UNIQUE';
|
|
1760
|
+
}
|
|
1761
|
+
sql += ' INDEX IF NOT EXISTS';
|
|
1762
|
+
if (indexDef.type) {
|
|
1763
|
+
sql += ` ${indexDef.type}`;
|
|
1764
|
+
}
|
|
1765
|
+
sql += ` ${this.escapeId(indexDef.name)} ON ${this.escapeId(tableName)}`;
|
|
1766
|
+
// Handle expression index (SQLite 3.9.0+)
|
|
1767
|
+
if (indexDef.expression) {
|
|
1768
|
+
sql += ` ((${indexDef.expression}))`;
|
|
1769
|
+
}
|
|
1770
|
+
else {
|
|
1771
|
+
sql += ` (${fields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
1772
|
+
}
|
|
1773
|
+
if (indexDef.using) {
|
|
1774
|
+
sql += ` USING ${indexDef.using}`;
|
|
1775
|
+
}
|
|
1776
|
+
if (indexDef.where && Object.keys(indexDef.where).length > 0) {
|
|
1777
|
+
const whereClause = this.buildWhereClause(indexDef.where);
|
|
1778
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1779
|
+
}
|
|
1780
|
+
await this.query(sql);
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Drop an index from a table
|
|
1784
|
+
*/
|
|
1785
|
+
async dropIndex(tableName, indexName, _options) {
|
|
1786
|
+
const sql = `DROP INDEX ${this.escapeId(indexName)}`;
|
|
1787
|
+
await this.query(sql);
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Create a constraint on a table
|
|
1791
|
+
* Note: SQLite has limited ALTER TABLE support for constraints
|
|
1792
|
+
*/
|
|
1793
|
+
async createConstraint(tableName, constraintDef) {
|
|
1794
|
+
// SQLite doesn't support ADD CONSTRAINT via ALTER TABLE in most versions
|
|
1795
|
+
// We need to recreate the table with the new constraint
|
|
1796
|
+
await this.recreateTableWithConstraint(tableName, constraintDef);
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Drop a constraint from a table
|
|
1800
|
+
* Note: SQLite has limited ALTER TABLE support for constraints
|
|
1801
|
+
*/
|
|
1802
|
+
async dropConstraint(tableName, constraintName, _options) {
|
|
1803
|
+
// SQLite doesn't support DROP CONSTRAINT via ALTER TABLE
|
|
1804
|
+
// We would need to recreate the table without the constraint
|
|
1805
|
+
throw new Error('SQLite does not support dropping constraints with ALTER TABLE. Recreate the table instead.');
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Recreate table with a new constraint (for createConstraint)
|
|
1809
|
+
*/
|
|
1810
|
+
async recreateTableWithConstraint(tableName, constraintDef) {
|
|
1811
|
+
const tableInfo = await this.describeTable(tableName);
|
|
1812
|
+
// Build column definitions, preserving existing NOT NULL / DEFAULT / PRIMARY KEY /
|
|
1813
|
+
// AUTOINCREMENT / UNIQUE constraints (not just the bare data type) since SQLite
|
|
1814
|
+
// requires a full table rebuild rather than in-place ALTER for constraint changes.
|
|
1815
|
+
const columnDefs = [];
|
|
1816
|
+
for (const [colName, colInfo] of Object.entries(tableInfo)) {
|
|
1817
|
+
columnDefs.push(this.getColumnDefinitionSql(colName, this.toColumnDefinition(colInfo)));
|
|
1818
|
+
}
|
|
1819
|
+
// Add constraint to column definitions
|
|
1820
|
+
if (constraintDef.fields && constraintDef.fields.length > 0) {
|
|
1821
|
+
const escapedFields = constraintDef.fields.map((f) => this.escapeId(f)).join(', ');
|
|
1822
|
+
switch (constraintDef.type) {
|
|
1823
|
+
case 'PRIMARY KEY':
|
|
1824
|
+
columnDefs.push(`PRIMARY KEY (${escapedFields})`);
|
|
1825
|
+
break;
|
|
1826
|
+
case 'UNIQUE':
|
|
1827
|
+
columnDefs.push(`UNIQUE (${escapedFields})`);
|
|
1828
|
+
break;
|
|
1829
|
+
case 'FOREIGN KEY':
|
|
1830
|
+
if (constraintDef.references) {
|
|
1831
|
+
// Handle composite foreign keys (string | string[])
|
|
1832
|
+
const refField2 = constraintDef.references.field;
|
|
1833
|
+
const refFieldSql2 = Array.isArray(refField2)
|
|
1834
|
+
? `(${refField2.map((f) => this.escapeId(f)).join(', ')})`
|
|
1835
|
+
: this.escapeId(refField2);
|
|
1836
|
+
let fkDef = `FOREIGN KEY (${escapedFields}) REFERENCES ${this.escapeId(constraintDef.references.table)}${refFieldSql2}`;
|
|
1837
|
+
if (constraintDef.references.onDelete) {
|
|
1838
|
+
fkDef += ` ON DELETE ${constraintDef.references.onDelete}`;
|
|
1839
|
+
}
|
|
1840
|
+
if (constraintDef.references.onUpdate) {
|
|
1841
|
+
fkDef += ` ON UPDATE ${constraintDef.references.onUpdate}`;
|
|
1842
|
+
}
|
|
1843
|
+
columnDefs.push(fkDef);
|
|
1844
|
+
}
|
|
1845
|
+
break;
|
|
1846
|
+
case 'CHECK':
|
|
1847
|
+
if (constraintDef.check) {
|
|
1848
|
+
columnDefs.push(`CHECK (${constraintDef.check})`);
|
|
1849
|
+
}
|
|
1850
|
+
break;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
const tempTable = `${tableName}_temp_${Date.now()}`;
|
|
1854
|
+
// Rename old table
|
|
1855
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} RENAME TO ${this.escapeId(tempTable)}`);
|
|
1856
|
+
// Create new table with constraint
|
|
1857
|
+
await this.query(`CREATE TABLE ${this.escapeId(tableName)} (${columnDefs.join(', ')})`);
|
|
1858
|
+
// Copy data
|
|
1859
|
+
const colNames = Object.keys(tableInfo)
|
|
1860
|
+
.map((c) => this.escapeId(c))
|
|
1861
|
+
.join(', ');
|
|
1862
|
+
await this.query(`INSERT INTO ${this.escapeId(tableName)} SELECT ${colNames} FROM ${this.escapeId(tempTable)}`);
|
|
1863
|
+
// Drop old table
|
|
1864
|
+
await this.query(`DROP TABLE ${this.escapeId(tempTable)}`);
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Begin a new transaction
|
|
1868
|
+
*/
|
|
1869
|
+
async startTransaction(options) {
|
|
1870
|
+
if (!this.connection) {
|
|
1871
|
+
throw new Error('Not connected to database');
|
|
1872
|
+
}
|
|
1873
|
+
const transaction = new SQLiteTransaction(this.connection, this.transactionDepth++, options, this);
|
|
1874
|
+
if (transaction.isOuter) {
|
|
1875
|
+
this.connection.exec('BEGIN TRANSACTION');
|
|
1876
|
+
}
|
|
1877
|
+
else {
|
|
1878
|
+
this.connection.exec(`SAVEPOINT ${transaction.id}`);
|
|
1879
|
+
}
|
|
1880
|
+
return transaction;
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Commit a transaction
|
|
1884
|
+
*/
|
|
1885
|
+
async commitTransaction(transaction) {
|
|
1886
|
+
if (!this.connection) {
|
|
1887
|
+
throw new Error('Not connected to database');
|
|
1888
|
+
}
|
|
1889
|
+
const sqliteTx = transaction;
|
|
1890
|
+
if (sqliteTx.isOuter) {
|
|
1891
|
+
this.connection.exec('COMMIT');
|
|
1892
|
+
}
|
|
1893
|
+
else {
|
|
1894
|
+
this.connection.exec(`RELEASE SAVEPOINT ${sqliteTx.id}`);
|
|
1895
|
+
}
|
|
1896
|
+
this.transactionDepth--;
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Rollback a transaction
|
|
1900
|
+
*/
|
|
1901
|
+
async rollbackTransaction(transaction) {
|
|
1902
|
+
if (!this.connection) {
|
|
1903
|
+
throw new Error('Not connected to database');
|
|
1904
|
+
}
|
|
1905
|
+
const sqliteTx = transaction;
|
|
1906
|
+
if (sqliteTx.isOuter) {
|
|
1907
|
+
this.connection.exec('ROLLBACK');
|
|
1908
|
+
}
|
|
1909
|
+
else {
|
|
1910
|
+
this.connection.exec(`ROLLBACK TO SAVEPOINT ${sqliteTx.id}`);
|
|
1911
|
+
}
|
|
1912
|
+
this.transactionDepth--;
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Get the SQL for a data type
|
|
1916
|
+
*/
|
|
1917
|
+
getDataTypeSql(dataType, columnName) {
|
|
1918
|
+
if (typeof dataType === 'string') {
|
|
1919
|
+
// SQLite has no native ENUM; emulate a string ENUM(...) type as TEXT with
|
|
1920
|
+
// a CHECK on the allowed values (matching the object-form ENUM below).
|
|
1921
|
+
const enumMatch = dataType.match(/^\s*ENUM\s*\((.*)\)\s*$/i);
|
|
1922
|
+
if (enumMatch) {
|
|
1923
|
+
const colRef = columnName ? this.escapeId(columnName) : 'value';
|
|
1924
|
+
return `TEXT CHECK(${colRef} IN (${enumMatch[1]}))`;
|
|
1925
|
+
}
|
|
1926
|
+
return dataType;
|
|
1927
|
+
}
|
|
1928
|
+
// If passed a factory function (e.g. DataTypes.INTEGER without calling it), call it to get an instance
|
|
1929
|
+
if (typeof dataType === 'function') {
|
|
1930
|
+
try {
|
|
1931
|
+
dataType = dataType();
|
|
1932
|
+
}
|
|
1933
|
+
catch {
|
|
1934
|
+
return 'TEXT';
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
if (!dataType || typeof dataType !== 'object') {
|
|
1938
|
+
return 'TEXT';
|
|
1939
|
+
}
|
|
1940
|
+
const dt = dataType;
|
|
1941
|
+
switch (dt.key) {
|
|
1942
|
+
case 'STRING':
|
|
1943
|
+
return `VARCHAR(${dt.length || 255})`;
|
|
1944
|
+
case 'CHAR':
|
|
1945
|
+
return `CHAR(${dt.length || 1})`;
|
|
1946
|
+
case 'TEXT':
|
|
1947
|
+
return dt.length ? `TEXT(${dt.length})` : 'TEXT';
|
|
1948
|
+
case 'INTEGER':
|
|
1949
|
+
return dt.unsigned ? 'INTEGER UNSIGNED' : 'INTEGER';
|
|
1950
|
+
case 'BIGINT':
|
|
1951
|
+
return dt.unsigned ? 'BIGINT UNSIGNED' : 'BIGINT';
|
|
1952
|
+
case 'FLOAT':
|
|
1953
|
+
return `FLOAT(${dt.length || 10},${dt.decimals || 2})`;
|
|
1954
|
+
case 'DOUBLE':
|
|
1955
|
+
return `DOUBLE(${dt.length || 10},${dt.decimals || 2})`;
|
|
1956
|
+
case 'DECIMAL':
|
|
1957
|
+
return `DECIMAL(${dt.precision || 10},${dt.scale || 0})`;
|
|
1958
|
+
case 'BOOLEAN':
|
|
1959
|
+
return 'INTEGER'; // SQLite uses INTEGER for booleans
|
|
1960
|
+
case 'DATE':
|
|
1961
|
+
return 'DATETIME';
|
|
1962
|
+
case 'DATEONLY':
|
|
1963
|
+
return 'DATE';
|
|
1964
|
+
case 'TIME':
|
|
1965
|
+
return 'TIME';
|
|
1966
|
+
case 'BLOB':
|
|
1967
|
+
return 'BLOB';
|
|
1968
|
+
case 'ENUM': {
|
|
1969
|
+
// Reference the actual column being defined, not a hardcoded literal "col"
|
|
1970
|
+
// (SQLite would otherwise fail with "no such column: col", or worse silently
|
|
1971
|
+
// reference an unrelated column that happens to be named "col").
|
|
1972
|
+
const colRef = columnName ? this.escapeId(columnName) : 'value';
|
|
1973
|
+
return `TEXT CHECK(${dt.values ? `${colRef} IN (${dt.values.map((v) => `'${this.escapeString(String(v))}'`).join(',')})` : '1=1'})`;
|
|
1974
|
+
}
|
|
1975
|
+
case 'JSON':
|
|
1976
|
+
return 'TEXT'; // SQLite stores JSON as text
|
|
1977
|
+
case 'JSONB':
|
|
1978
|
+
return 'TEXT';
|
|
1979
|
+
case 'UUID':
|
|
1980
|
+
return 'TEXT';
|
|
1981
|
+
case 'GEOMETRY':
|
|
1982
|
+
return 'BLOB';
|
|
1983
|
+
case 'VIRTUAL':
|
|
1984
|
+
// Virtual fields don't create a database column - return empty string
|
|
1985
|
+
return '';
|
|
1986
|
+
default:
|
|
1987
|
+
return 'TEXT';
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Build a WHERE clause from a WhereOptions object
|
|
1992
|
+
*/
|
|
1993
|
+
buildWhereClause(where, options) {
|
|
1994
|
+
const values = [];
|
|
1995
|
+
if (!where ||
|
|
1996
|
+
(Object.keys(where).length === 0 && Object.getOwnPropertySymbols(where).length === 0)) {
|
|
1997
|
+
return { sql: '', values };
|
|
1998
|
+
}
|
|
1999
|
+
const buildCondition = (condition, parentKey) => {
|
|
2000
|
+
if (!condition) {
|
|
2001
|
+
return { sql: '', values: [] };
|
|
2002
|
+
}
|
|
2003
|
+
if (typeof condition !== 'object') {
|
|
2004
|
+
values.push(condition);
|
|
2005
|
+
return { sql: '?', values: [condition] };
|
|
2006
|
+
}
|
|
2007
|
+
// Normalize Symbol operator keys at the top level too
|
|
2008
|
+
const rawCond = condition;
|
|
2009
|
+
const topSymbols = Object.getOwnPropertySymbols(rawCond);
|
|
2010
|
+
const normalizedCond = { ...rawCond };
|
|
2011
|
+
for (const sym of topSymbols) {
|
|
2012
|
+
const strKey = (0, operators_1.operatorToWhereKey)(sym);
|
|
2013
|
+
if (strKey)
|
|
2014
|
+
normalizedCond[strKey] = rawCond[sym];
|
|
2015
|
+
}
|
|
2016
|
+
const cond = normalizedCond;
|
|
2017
|
+
// Handle logical operators. These are collected (NOT returned early) so
|
|
2018
|
+
// that sibling field conditions on the same object — e.g.
|
|
2019
|
+
// { active: 1, $or: [...] } — are still AND-ed in rather than dropped.
|
|
2020
|
+
const logicalConditions = [];
|
|
2021
|
+
if (cond.$and || cond.$or || cond.$not) {
|
|
2022
|
+
if (cond.$and) {
|
|
2023
|
+
const andConditions = cond.$and
|
|
2024
|
+
.map((c) => buildCondition(c).sql)
|
|
2025
|
+
.filter(Boolean);
|
|
2026
|
+
if (andConditions.length)
|
|
2027
|
+
logicalConditions.push(`(${andConditions.join(' AND ')})`);
|
|
2028
|
+
}
|
|
2029
|
+
if (cond.$or) {
|
|
2030
|
+
const orConditions = cond.$or
|
|
2031
|
+
.map((c) => buildCondition(c).sql)
|
|
2032
|
+
.filter(Boolean);
|
|
2033
|
+
if (orConditions.length)
|
|
2034
|
+
logicalConditions.push(`(${orConditions.join(' OR ')})`);
|
|
2035
|
+
}
|
|
2036
|
+
if (cond.$not) {
|
|
2037
|
+
const result = buildCondition(cond.$not);
|
|
2038
|
+
if (result.sql)
|
|
2039
|
+
logicalConditions.push(`NOT (${result.sql})`);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
// Handle field conditions
|
|
2043
|
+
const fieldConditions = [...logicalConditions];
|
|
2044
|
+
for (const [key, value] of Object.entries(cond)) {
|
|
2045
|
+
if (key.startsWith('$'))
|
|
2046
|
+
continue;
|
|
2047
|
+
// A bare array value (e.g. `where: { id: [1, 2, 3] }`, the common
|
|
2048
|
+
// Sequelize-style shorthand for "IN") used to fall through to the
|
|
2049
|
+
// generic object-condition branch below, which spreads the array
|
|
2050
|
+
// into a plain object (`{0: 1, 1: 2, 2: 3}`), matches none of the
|
|
2051
|
+
// `$eq`/`$in`/etc. operator keys, and ends up binding the whole
|
|
2052
|
+
// array as a single `= ?` parameter - better-sqlite3 silently
|
|
2053
|
+
// matches nothing for that (no rows have an id equal to an array),
|
|
2054
|
+
// so `where: { id: [...] }` matched zero rows instead of the
|
|
2055
|
+
// intended "any of these ids".
|
|
2056
|
+
if (Array.isArray(value)) {
|
|
2057
|
+
values.push(...value);
|
|
2058
|
+
fieldConditions.push(`${this.escapeId(key)} IN (${value.map(() => '?').join(',') || 'NULL'})`);
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
if (value && typeof value === 'object') {
|
|
2062
|
+
// Normalize Symbol operator keys to string keys ($gt, $eq, etc.)
|
|
2063
|
+
const rawValueObj = value;
|
|
2064
|
+
const symbolKeys = Object.getOwnPropertySymbols(rawValueObj);
|
|
2065
|
+
const normalizedObj = { ...rawValueObj };
|
|
2066
|
+
for (const sym of symbolKeys) {
|
|
2067
|
+
const strKey = (0, operators_1.operatorToWhereKey)(sym);
|
|
2068
|
+
if (strKey)
|
|
2069
|
+
normalizedObj[strKey] = rawValueObj[sym];
|
|
2070
|
+
}
|
|
2071
|
+
const valueObj = normalizedObj;
|
|
2072
|
+
// When several operators target the same field (e.g. { age: { $gte, $lte } }),
|
|
2073
|
+
// the single-operator dispatch below would only emit the first one. Split
|
|
2074
|
+
// into one condition per operator and AND them together.
|
|
2075
|
+
const opKeys = Object.keys(valueObj).filter((k) => k.startsWith('$'));
|
|
2076
|
+
if (opKeys.length > 1) {
|
|
2077
|
+
const parts = opKeys
|
|
2078
|
+
.map((ok) => buildCondition({ [key]: { [ok]: valueObj[ok] } }).sql)
|
|
2079
|
+
.filter(Boolean);
|
|
2080
|
+
if (parts.length > 0) {
|
|
2081
|
+
fieldConditions.push(parts.length > 1 ? `(${parts.join(' AND ')})` : parts[0]);
|
|
2082
|
+
}
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
if (valueObj.$eq !== undefined) {
|
|
2086
|
+
values.push(valueObj.$eq);
|
|
2087
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
2088
|
+
}
|
|
2089
|
+
else if (valueObj.$ne !== undefined) {
|
|
2090
|
+
values.push(valueObj.$ne);
|
|
2091
|
+
fieldConditions.push(`${this.escapeId(key)} != ?`);
|
|
2092
|
+
}
|
|
2093
|
+
else if (valueObj.$gt !== undefined) {
|
|
2094
|
+
values.push(valueObj.$gt);
|
|
2095
|
+
fieldConditions.push(`${this.escapeId(key)} > ?`);
|
|
2096
|
+
}
|
|
2097
|
+
else if (valueObj.$gte !== undefined) {
|
|
2098
|
+
values.push(valueObj.$gte);
|
|
2099
|
+
fieldConditions.push(`${this.escapeId(key)} >= ?`);
|
|
2100
|
+
}
|
|
2101
|
+
else if (valueObj.$lt !== undefined) {
|
|
2102
|
+
values.push(valueObj.$lt);
|
|
2103
|
+
fieldConditions.push(`${this.escapeId(key)} < ?`);
|
|
2104
|
+
}
|
|
2105
|
+
else if (valueObj.$lte !== undefined) {
|
|
2106
|
+
values.push(valueObj.$lte);
|
|
2107
|
+
fieldConditions.push(`${this.escapeId(key)} <= ?`);
|
|
2108
|
+
}
|
|
2109
|
+
else if (valueObj.$like !== undefined) {
|
|
2110
|
+
values.push(valueObj.$like);
|
|
2111
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
2112
|
+
}
|
|
2113
|
+
else if (valueObj.$notLike !== undefined) {
|
|
2114
|
+
values.push(valueObj.$notLike);
|
|
2115
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
2116
|
+
}
|
|
2117
|
+
else if (valueObj.$startsWith !== undefined) {
|
|
2118
|
+
values.push(valueObj.$startsWith + '%');
|
|
2119
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
2120
|
+
}
|
|
2121
|
+
else if (valueObj.$notStartsWith !== undefined) {
|
|
2122
|
+
values.push(valueObj.$notStartsWith + '%');
|
|
2123
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
2124
|
+
}
|
|
2125
|
+
else if (valueObj.$endsWith !== undefined) {
|
|
2126
|
+
values.push('%' + valueObj.$endsWith);
|
|
2127
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
2128
|
+
}
|
|
2129
|
+
else if (valueObj.$notEndsWith !== undefined) {
|
|
2130
|
+
values.push('%' + valueObj.$notEndsWith);
|
|
2131
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
2132
|
+
}
|
|
2133
|
+
else if (valueObj.$substring !== undefined) {
|
|
2134
|
+
values.push('%' + valueObj.$substring + '%');
|
|
2135
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
2136
|
+
}
|
|
2137
|
+
else if (valueObj.$notSubstring !== undefined) {
|
|
2138
|
+
values.push('%' + valueObj.$notSubstring + '%');
|
|
2139
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
2140
|
+
}
|
|
2141
|
+
else if (valueObj.$in) {
|
|
2142
|
+
const inValues = valueObj.$in;
|
|
2143
|
+
values.push(...inValues);
|
|
2144
|
+
fieldConditions.push(`${this.escapeId(key)} IN (${inValues.map(() => '?').join(',')})`);
|
|
2145
|
+
}
|
|
2146
|
+
else if (valueObj.$notIn) {
|
|
2147
|
+
const notInValues = valueObj.$notIn;
|
|
2148
|
+
values.push(...notInValues);
|
|
2149
|
+
fieldConditions.push(`${this.escapeId(key)} NOT IN (${notInValues.map(() => '?').join(',')})`);
|
|
2150
|
+
}
|
|
2151
|
+
else if (valueObj.$between) {
|
|
2152
|
+
const between = valueObj.$between;
|
|
2153
|
+
values.push(between[0], between[1]);
|
|
2154
|
+
fieldConditions.push(`${this.escapeId(key)} BETWEEN ? AND ?`);
|
|
2155
|
+
}
|
|
2156
|
+
else if (valueObj.$notBetween) {
|
|
2157
|
+
const notBetween = valueObj.$notBetween;
|
|
2158
|
+
values.push(notBetween[0], notBetween[1]);
|
|
2159
|
+
fieldConditions.push(`${this.escapeId(key)} NOT BETWEEN ? AND ?`);
|
|
2160
|
+
}
|
|
2161
|
+
else if (valueObj.$isNull !== undefined) {
|
|
2162
|
+
if (valueObj.$isNull) {
|
|
2163
|
+
fieldConditions.push(`${this.escapeId(key)} IS NULL`);
|
|
2164
|
+
}
|
|
2165
|
+
else {
|
|
2166
|
+
fieldConditions.push(`${this.escapeId(key)} IS NOT NULL`);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
else if (valueObj.$isNotNull !== undefined) {
|
|
2170
|
+
if (valueObj.$isNotNull) {
|
|
2171
|
+
fieldConditions.push(`${this.escapeId(key)} IS NOT NULL`);
|
|
2172
|
+
}
|
|
2173
|
+
else {
|
|
2174
|
+
fieldConditions.push(`${this.escapeId(key)} IS NULL`);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
else if (valueObj.$iLike !== undefined) {
|
|
2178
|
+
// SQLite's LIKE is case-insensitive for ASCII, so it serves as ILIKE.
|
|
2179
|
+
values.push(valueObj.$iLike);
|
|
2180
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
2181
|
+
}
|
|
2182
|
+
else if (valueObj.$notILike !== undefined) {
|
|
2183
|
+
values.push(valueObj.$notILike);
|
|
2184
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
2185
|
+
}
|
|
2186
|
+
else if (valueObj.$is !== undefined) {
|
|
2187
|
+
if (valueObj.$is === null) {
|
|
2188
|
+
fieldConditions.push(`${this.escapeId(key)} IS NULL`);
|
|
2189
|
+
}
|
|
2190
|
+
else {
|
|
2191
|
+
values.push(valueObj.$is);
|
|
2192
|
+
fieldConditions.push(`${this.escapeId(key)} IS ?`);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
else if (valueObj.$not !== undefined) {
|
|
2196
|
+
if (valueObj.$not === null) {
|
|
2197
|
+
fieldConditions.push(`${this.escapeId(key)} IS NOT NULL`);
|
|
2198
|
+
}
|
|
2199
|
+
else {
|
|
2200
|
+
values.push(valueObj.$not);
|
|
2201
|
+
fieldConditions.push(`${this.escapeId(key)} != ?`);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
else if (valueObj.$contains !== undefined) {
|
|
2205
|
+
// Array containment: the JSON-array column must contain every given
|
|
2206
|
+
// element. Emulated with json_each since SQLite has no array type.
|
|
2207
|
+
const contained = Array.isArray(valueObj.$contains)
|
|
2208
|
+
? valueObj.$contains
|
|
2209
|
+
: [valueObj.$contains];
|
|
2210
|
+
const placeholders = contained.map(() => '?').join(', ');
|
|
2211
|
+
values.push(...contained);
|
|
2212
|
+
fieldConditions.push(`(SELECT COUNT(DISTINCT value) FROM json_each(${this.escapeId(key)}) WHERE value IN (${placeholders})) = ${contained.length}`);
|
|
2213
|
+
}
|
|
2214
|
+
else if (valueObj.$col !== undefined) {
|
|
2215
|
+
fieldConditions.push(`${this.escapeId(key)} = ${this.escapeId(String(valueObj.$col))}`);
|
|
2216
|
+
}
|
|
2217
|
+
else {
|
|
2218
|
+
values.push(value);
|
|
2219
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
else {
|
|
2223
|
+
values.push(value);
|
|
2224
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
return { sql: fieldConditions.join(' AND '), values };
|
|
2228
|
+
};
|
|
2229
|
+
const result = buildCondition(where);
|
|
2230
|
+
return { sql: result.sql, values: result.values };
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Build an ORDER BY clause
|
|
2234
|
+
*/
|
|
2235
|
+
buildOrderClause(order, options) {
|
|
2236
|
+
const orderArray = order;
|
|
2237
|
+
if (!orderArray || !Array.isArray(orderArray) || orderArray.length === 0) {
|
|
2238
|
+
return '';
|
|
2239
|
+
}
|
|
2240
|
+
const orderParts = [];
|
|
2241
|
+
for (const item of orderArray) {
|
|
2242
|
+
if (Array.isArray(item)) {
|
|
2243
|
+
const field = typeof item[0] === 'string' ? this.escapeId(item[0]) : item[0];
|
|
2244
|
+
const direction = item[1] ? ` ${item[1]}` : '';
|
|
2245
|
+
orderParts.push(`${field}${direction}`);
|
|
2246
|
+
}
|
|
2247
|
+
else if (typeof item === 'string') {
|
|
2248
|
+
orderParts.push(item);
|
|
2249
|
+
}
|
|
2250
|
+
else if (item && typeof item === 'object') {
|
|
2251
|
+
const itemArr = item;
|
|
2252
|
+
const [modelOrString, field] = itemArr;
|
|
2253
|
+
const model = typeof modelOrString === 'string'
|
|
2254
|
+
? modelOrString
|
|
2255
|
+
: modelOrString.tableName || '';
|
|
2256
|
+
const fieldStr = typeof field === 'string' ? field : '';
|
|
2257
|
+
orderParts.push(`${model ? `${this.escapeId(model)}.` : ''}${this.escapeId(fieldStr)}`);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Build a LIMIT/OFFSET clause
|
|
2264
|
+
*/
|
|
2265
|
+
buildLimitOffset(limit, offset) {
|
|
2266
|
+
let sql = '';
|
|
2267
|
+
if (limit !== undefined) {
|
|
2268
|
+
sql += ` LIMIT ${Number(limit)}`;
|
|
2269
|
+
if (offset !== undefined) {
|
|
2270
|
+
sql += ` OFFSET ${Number(offset)}`;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
else if (offset !== undefined) {
|
|
2274
|
+
// SQLite requires LIMIT to use OFFSET; use -1 for "all rows"
|
|
2275
|
+
sql += ` LIMIT -1 OFFSET ${Number(offset)}`;
|
|
2276
|
+
}
|
|
2277
|
+
return sql;
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Build an INSERT query
|
|
2281
|
+
*/
|
|
2282
|
+
buildInsertQuery(tableName, values, options) {
|
|
2283
|
+
const columns = Object.keys(values);
|
|
2284
|
+
// If no values provided, return a minimal valid INSERT (for defaults only)
|
|
2285
|
+
if (columns.length === 0) {
|
|
2286
|
+
return {
|
|
2287
|
+
sql: `INSERT INTO ${this.quoteTable(tableName, options?.schema)} DEFAULT VALUES`,
|
|
2288
|
+
values: [],
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
const processedValues = [];
|
|
2292
|
+
const placeholders = [];
|
|
2293
|
+
// Process each value - handle Literal differently
|
|
2294
|
+
for (const value of Object.values(values)) {
|
|
2295
|
+
if (value instanceof prorm_1.Literal) {
|
|
2296
|
+
// For Literal values, inline the SQL directly
|
|
2297
|
+
placeholders.push(value.val);
|
|
2298
|
+
}
|
|
2299
|
+
else {
|
|
2300
|
+
// For regular values, use parameterized placeholder
|
|
2301
|
+
placeholders.push('?');
|
|
2302
|
+
processedValues.push(value);
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
let sql = `INSERT INTO ${this.quoteTable(tableName, options?.schema)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${placeholders.join(', ')})`;
|
|
2306
|
+
if (options?.upsert) {
|
|
2307
|
+
if (options.conflictTargets && options.conflictTargets.length > 0) {
|
|
2308
|
+
sql += this.buildConflictClausesSql(options.conflictTargets, columns);
|
|
2309
|
+
}
|
|
2310
|
+
else if (options.doNothing) {
|
|
2311
|
+
// `DO NOTHING` is the one form of `ON CONFLICT` SQLite allows without an
|
|
2312
|
+
// explicit conflict target - it applies to any constraint violation.
|
|
2313
|
+
sql +=
|
|
2314
|
+
options.conflictFields && options.conflictFields.length > 0
|
|
2315
|
+
? ` ON CONFLICT (${options.conflictFields.map((c) => this.escapeId(c)).join(', ')}) DO NOTHING`
|
|
2316
|
+
: ' ON CONFLICT DO NOTHING';
|
|
2317
|
+
}
|
|
2318
|
+
else {
|
|
2319
|
+
const updateColumns = columns
|
|
2320
|
+
.map((c) => `${this.escapeId(c)} = excluded.${this.escapeId(c)}`)
|
|
2321
|
+
.join(', ');
|
|
2322
|
+
// SQLite's `ON CONFLICT (...) DO UPDATE` requires an explicit conflict target
|
|
2323
|
+
// that matches a real PRIMARY KEY/UNIQUE index on the table - unlike
|
|
2324
|
+
// `DO NOTHING`, it cannot be omitted. Defaulting to "all inserted columns"
|
|
2325
|
+
// (the previous behavior) almost never matches a real constraint and fails
|
|
2326
|
+
// at runtime with a confusing "ON CONFLICT clause does not match any
|
|
2327
|
+
// PRIMARY KEY or UNIQUE constraint" error, so fail fast with a clear message.
|
|
2328
|
+
if (!options.conflictFields || options.conflictFields.length === 0) {
|
|
2329
|
+
throw new Error('SQLite upsert requires `conflictFields` naming the PRIMARY KEY or UNIQUE ' +
|
|
2330
|
+
'columns to resolve conflicts against (e.g. { conflictFields: ["id"] }), ' +
|
|
2331
|
+
'unless `doNothing` is set (which allows a bare `ON CONFLICT DO NOTHING`).');
|
|
2332
|
+
}
|
|
2333
|
+
sql += ` ON CONFLICT (${options.conflictFields.map((c) => this.escapeId(c)).join(', ')}) DO UPDATE SET ${updateColumns}`;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
return { sql, values: processedValues };
|
|
2337
|
+
}
|
|
2338
|
+
/**
|
|
2339
|
+
* Build one or more chained `ON CONFLICT (...) DO ...` clauses. SQLite allows
|
|
2340
|
+
* multiple `ON CONFLICT` clauses in a single INSERT, each targeting a
|
|
2341
|
+
* different PRIMARY KEY/UNIQUE constraint and evaluated in order until one
|
|
2342
|
+
* matches, e.g.:
|
|
2343
|
+
* INSERT INTO t (...) VALUES (...)
|
|
2344
|
+
* ON CONFLICT (a) DO NOTHING
|
|
2345
|
+
* ON CONFLICT (b) DO UPDATE SET ...
|
|
2346
|
+
*/
|
|
2347
|
+
buildConflictClausesSql(targets, insertedColumns) {
|
|
2348
|
+
let clause = '';
|
|
2349
|
+
for (const target of targets) {
|
|
2350
|
+
if (!target.fields || target.fields.length === 0) {
|
|
2351
|
+
throw new Error('Each entry in `conflictTargets` requires a non-empty `fields` array naming the ' +
|
|
2352
|
+
'PRIMARY KEY or UNIQUE columns to resolve conflicts against.');
|
|
2353
|
+
}
|
|
2354
|
+
const targetSql = `ON CONFLICT (${target.fields.map((c) => this.escapeId(c)).join(', ')})`;
|
|
2355
|
+
const action = target.action || 'DO UPDATE';
|
|
2356
|
+
if (action === 'DO NOTHING') {
|
|
2357
|
+
clause += ` ${targetSql} DO NOTHING`;
|
|
2358
|
+
}
|
|
2359
|
+
else {
|
|
2360
|
+
const updateColumns = target.updateColumns && target.updateColumns.length > 0
|
|
2361
|
+
? target.updateColumns
|
|
2362
|
+
: insertedColumns;
|
|
2363
|
+
const setClauses = updateColumns
|
|
2364
|
+
.map((c) => `${this.escapeId(c)} = excluded.${this.escapeId(c)}`)
|
|
2365
|
+
.join(', ');
|
|
2366
|
+
clause += ` ${targetSql} DO UPDATE SET ${setClauses}`;
|
|
2367
|
+
if (target.where) {
|
|
2368
|
+
clause += ` WHERE ${target.where}`;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
return clause;
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Build an UPSERT query (INSERT ... ON CONFLICT DO UPDATE)
|
|
2376
|
+
* SQLite supports: INSERT OR REPLACE, INSERT OR IGNORE, and ON CONFLICT DO UPDATE
|
|
2377
|
+
*/
|
|
2378
|
+
buildUpsertQuery(tableName, values, options) {
|
|
2379
|
+
const columns = Object.keys(values);
|
|
2380
|
+
const processedValues = [];
|
|
2381
|
+
const placeholders = [];
|
|
2382
|
+
// Process each value - handle Literal differently
|
|
2383
|
+
for (const value of Object.values(values)) {
|
|
2384
|
+
if (value instanceof prorm_1.Literal) {
|
|
2385
|
+
// For Literal values, inline the SQL directly
|
|
2386
|
+
placeholders.push(value.val);
|
|
2387
|
+
}
|
|
2388
|
+
else {
|
|
2389
|
+
// For regular values, use parameterized placeholder
|
|
2390
|
+
placeholders.push('?');
|
|
2391
|
+
processedValues.push(value);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
let sql = `INSERT INTO ${this.quoteTable(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${placeholders.join(', ')})`;
|
|
2395
|
+
// Handle upsert (ON CONFLICT)
|
|
2396
|
+
if (options) {
|
|
2397
|
+
if (options.conflictTargets && options.conflictTargets.length > 0) {
|
|
2398
|
+
sql += this.buildConflictClausesSql(options.conflictTargets, columns);
|
|
2399
|
+
}
|
|
2400
|
+
else if (options.doNothing) {
|
|
2401
|
+
// `DO NOTHING` is the one form of `ON CONFLICT` SQLite allows without an
|
|
2402
|
+
// explicit conflict target - it applies to any constraint violation.
|
|
2403
|
+
sql +=
|
|
2404
|
+
options.conflictFields && options.conflictFields.length > 0
|
|
2405
|
+
? ` ON CONFLICT (${options.conflictFields.map((c) => this.escapeId(c)).join(', ')}) DO NOTHING`
|
|
2406
|
+
: ' ON CONFLICT DO NOTHING';
|
|
2407
|
+
}
|
|
2408
|
+
else {
|
|
2409
|
+
// SQLite's `ON CONFLICT (...) DO UPDATE` requires an explicit conflict target
|
|
2410
|
+
// that matches a real PRIMARY KEY/UNIQUE index; it cannot be inferred from the
|
|
2411
|
+
// set of inserted columns (see buildInsertQuery for the same issue).
|
|
2412
|
+
if (!options.conflictFields || options.conflictFields.length === 0) {
|
|
2413
|
+
throw new Error('SQLite upsert requires `conflictFields` naming the PRIMARY KEY or UNIQUE ' +
|
|
2414
|
+
'columns to resolve conflicts against (e.g. { conflictFields: ["id"] }), ' +
|
|
2415
|
+
'unless `doNothing` is set (which allows a bare `ON CONFLICT DO NOTHING`).');
|
|
2416
|
+
}
|
|
2417
|
+
// Determine which columns to update
|
|
2418
|
+
const updateColumns = options.updateOnDuplicate && options.updateOnDuplicate.length > 0
|
|
2419
|
+
? options.updateOnDuplicate
|
|
2420
|
+
: columns;
|
|
2421
|
+
const setClauses = updateColumns
|
|
2422
|
+
.map((c) => `${this.escapeId(c)} = excluded.${this.escapeId(c)}`)
|
|
2423
|
+
.join(', ');
|
|
2424
|
+
sql += ` ON CONFLICT (${options.conflictFields.map((c) => this.escapeId(c)).join(', ')}) DO UPDATE SET ${setClauses}`;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
// Handle RETURNING clause (SQLite 3.35+)
|
|
2428
|
+
if (options?.returning) {
|
|
2429
|
+
if (options.returning === true) {
|
|
2430
|
+
sql += ' RETURNING *';
|
|
2431
|
+
}
|
|
2432
|
+
else if (Array.isArray(options.returning)) {
|
|
2433
|
+
sql += ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
return { sql, values: processedValues };
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Build an increment query
|
|
2440
|
+
* @param tableName - Table name
|
|
2441
|
+
* @param fields - Fields to increment
|
|
2442
|
+
* @param where - Where clause
|
|
2443
|
+
* @param options - Query options (by: number)
|
|
2444
|
+
*/
|
|
2445
|
+
buildIncrementQuery(tableName, fields, where, options) {
|
|
2446
|
+
const by = options?.by ?? 1;
|
|
2447
|
+
const setClauses = [];
|
|
2448
|
+
const queryValues = [];
|
|
2449
|
+
// Handle different field formats
|
|
2450
|
+
if (typeof fields === 'string') {
|
|
2451
|
+
// Single field: increment('count')
|
|
2452
|
+
setClauses.push(`${this.escapeId(fields)} = ${this.escapeId(fields)} + ?`);
|
|
2453
|
+
queryValues.push(by);
|
|
2454
|
+
}
|
|
2455
|
+
else if (Array.isArray(fields)) {
|
|
2456
|
+
// Array of fields: increment(['count', 'value'], { by: 5 })
|
|
2457
|
+
for (const field of fields) {
|
|
2458
|
+
setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + ?`);
|
|
2459
|
+
queryValues.push(by);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
else {
|
|
2463
|
+
// Object: increment({ count: 1, value: 10 })
|
|
2464
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
2465
|
+
setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + ?`);
|
|
2466
|
+
queryValues.push(value);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
// Build WHERE clause
|
|
2470
|
+
const whereClause = this.buildWhereClause(where);
|
|
2471
|
+
queryValues.push(...whereClause.values);
|
|
2472
|
+
const sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')} WHERE ${whereClause.sql}`;
|
|
2473
|
+
return { sql, values: queryValues };
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* Build an UPDATE query
|
|
2477
|
+
*/
|
|
2478
|
+
buildUpdateQuery(tableName, values, where, options) {
|
|
2479
|
+
const setClauses = [];
|
|
2480
|
+
const queryValues = [];
|
|
2481
|
+
for (const [key, value] of Object.entries(values)) {
|
|
2482
|
+
if (value instanceof prorm_1.Literal) {
|
|
2483
|
+
// For Literal values, inline the SQL directly
|
|
2484
|
+
setClauses.push(`${this.escapeId(key)} = ${value.val}`);
|
|
2485
|
+
}
|
|
2486
|
+
else {
|
|
2487
|
+
// For regular values, use parameterized placeholder
|
|
2488
|
+
setClauses.push(`${this.escapeId(key)} = ?`);
|
|
2489
|
+
queryValues.push(value);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
const whereClause = this.buildWhereClause(where);
|
|
2493
|
+
let sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')}`;
|
|
2494
|
+
if (whereClause.sql) {
|
|
2495
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
2496
|
+
}
|
|
2497
|
+
if (options?.limit) {
|
|
2498
|
+
sql += ` LIMIT ${options.limit}`;
|
|
2499
|
+
}
|
|
2500
|
+
// Handle RETURNING clause (SQLite 3.35+)
|
|
2501
|
+
if (options?.returning) {
|
|
2502
|
+
if (options.returning === true) {
|
|
2503
|
+
sql += ' RETURNING *';
|
|
2504
|
+
}
|
|
2505
|
+
else if (Array.isArray(options.returning)) {
|
|
2506
|
+
sql += ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
return { sql, values: [...queryValues, ...whereClause.values] };
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Build a DELETE query
|
|
2513
|
+
*/
|
|
2514
|
+
buildDeleteQuery(tableName, where, options) {
|
|
2515
|
+
const whereClause = this.buildWhereClause(where);
|
|
2516
|
+
let sql = `DELETE FROM ${this.escapeId(tableName)}`;
|
|
2517
|
+
if (whereClause.sql) {
|
|
2518
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
2519
|
+
}
|
|
2520
|
+
if (options?.limit) {
|
|
2521
|
+
sql += ` LIMIT ${options.limit}`;
|
|
2522
|
+
}
|
|
2523
|
+
// Handle RETURNING clause (SQLite 3.35+)
|
|
2524
|
+
if (options?.returning) {
|
|
2525
|
+
if (options.returning === true) {
|
|
2526
|
+
sql += ' RETURNING *';
|
|
2527
|
+
}
|
|
2528
|
+
else if (Array.isArray(options.returning)) {
|
|
2529
|
+
sql += ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
return { sql, values: whereClause.values };
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Check if a value is a function expression (fn('COUNT', ...))
|
|
2536
|
+
*/
|
|
2537
|
+
isFnExpression(value) {
|
|
2538
|
+
return value !== null && typeof value === 'object' && value.__type === 'fn';
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Check if a value is a column expression (col('name'))
|
|
2542
|
+
*/
|
|
2543
|
+
isColExpression(value) {
|
|
2544
|
+
return value !== null && typeof value === 'object' && value.__type === 'col';
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Check if a value is a literal expression (literal('sql'))
|
|
2548
|
+
*/
|
|
2549
|
+
isLiteralExpression(value) {
|
|
2550
|
+
return value !== null && typeof value === 'object' && value.__type === 'literal';
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Compile a function expression to SQL
|
|
2554
|
+
*/
|
|
2555
|
+
compileFnExpression(fnExpr) {
|
|
2556
|
+
const args = fnExpr.args.map((arg) => {
|
|
2557
|
+
if (this.isColExpression(arg)) {
|
|
2558
|
+
return this.escapeId(arg.col);
|
|
2559
|
+
}
|
|
2560
|
+
else if (typeof arg === 'string') {
|
|
2561
|
+
return this.escape(arg);
|
|
2562
|
+
}
|
|
2563
|
+
else if (this.isFnExpression(arg)) {
|
|
2564
|
+
return this.compileFnExpression(arg);
|
|
2565
|
+
}
|
|
2566
|
+
else {
|
|
2567
|
+
return String(arg);
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
return `${fnExpr.fn}(${args.join(', ')})`;
|
|
2571
|
+
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Build SELECT clause with support for function expressions and aliases
|
|
2574
|
+
* Handles: 'field', ['field'], ['field', 'alias'], [fn('COUNT', 'id'), 'count']
|
|
2575
|
+
*/
|
|
2576
|
+
buildSelectClause(attributes) {
|
|
2577
|
+
if (!attributes)
|
|
2578
|
+
return '*';
|
|
2579
|
+
if (Array.isArray(attributes)) {
|
|
2580
|
+
const parts = attributes.map((attr) => {
|
|
2581
|
+
// Handle array format: [expression, alias] or [fn, alias]
|
|
2582
|
+
if (Array.isArray(attr)) {
|
|
2583
|
+
const [expr, alias] = attr;
|
|
2584
|
+
let sql;
|
|
2585
|
+
if (this.isFnExpression(expr)) {
|
|
2586
|
+
sql = this.compileFnExpression(expr);
|
|
2587
|
+
}
|
|
2588
|
+
else if (this.isColExpression(expr)) {
|
|
2589
|
+
sql = this.escapeId(expr.col);
|
|
2590
|
+
}
|
|
2591
|
+
else if (this.isLiteralExpression(expr)) {
|
|
2592
|
+
sql = expr.sql;
|
|
2593
|
+
}
|
|
2594
|
+
else if (typeof expr === 'string') {
|
|
2595
|
+
// Check if it's a column reference or literal SQL
|
|
2596
|
+
if (expr.includes('(') || expr.includes(' ')) {
|
|
2597
|
+
sql = expr; // Literal SQL
|
|
2598
|
+
}
|
|
2599
|
+
else {
|
|
2600
|
+
sql = this.escapeId(expr);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
else {
|
|
2604
|
+
sql = String(expr);
|
|
2605
|
+
}
|
|
2606
|
+
return `${sql} AS ${this.escapeId(alias)}`;
|
|
2607
|
+
}
|
|
2608
|
+
else if (this.isFnExpression(attr)) {
|
|
2609
|
+
// Function without alias - let DB assign a name
|
|
2610
|
+
return this.compileFnExpression(attr);
|
|
2611
|
+
}
|
|
2612
|
+
else if (this.isColExpression(attr)) {
|
|
2613
|
+
return this.escapeId(attr.col);
|
|
2614
|
+
}
|
|
2615
|
+
else if (this.isLiteralExpression(attr)) {
|
|
2616
|
+
return attr.sql;
|
|
2617
|
+
}
|
|
2618
|
+
else if (typeof attr === 'string') {
|
|
2619
|
+
// Plain column name
|
|
2620
|
+
return this.escapeId(attr);
|
|
2621
|
+
}
|
|
2622
|
+
return String(attr);
|
|
2623
|
+
});
|
|
2624
|
+
return parts.join(', ');
|
|
2625
|
+
}
|
|
2626
|
+
else if (attributes.include) {
|
|
2627
|
+
return attributes.include.map((a) => this.escapeId(a)).join(', ');
|
|
2628
|
+
}
|
|
2629
|
+
else if (attributes.exclude) {
|
|
2630
|
+
return `* EXCEPT (${attributes.exclude.map((a) => this.escapeId(a)).join(', ')})`;
|
|
2631
|
+
}
|
|
2632
|
+
return '*';
|
|
2633
|
+
}
|
|
2634
|
+
/**
|
|
2635
|
+
* Build GROUP BY clause with support for function expressions and aliases
|
|
2636
|
+
* Handles: 'field', ['field1', 'field2'], [[fn('COUNT', 'id'), 'count']]
|
|
2637
|
+
*/
|
|
2638
|
+
buildGroupByClause(group) {
|
|
2639
|
+
if (!group)
|
|
2640
|
+
return '';
|
|
2641
|
+
const groupArray = Array.isArray(group) ? group : [group];
|
|
2642
|
+
if (groupArray.length === 0)
|
|
2643
|
+
return '';
|
|
2644
|
+
const parts = groupArray.map((field) => {
|
|
2645
|
+
if (Array.isArray(field)) {
|
|
2646
|
+
// Array format: [expression, alias] or [fn, alias]
|
|
2647
|
+
const [expr, alias] = field;
|
|
2648
|
+
let sql;
|
|
2649
|
+
if (this.isFnExpression(expr)) {
|
|
2650
|
+
sql = this.compileFnExpression(expr);
|
|
2651
|
+
}
|
|
2652
|
+
else if (this.isColExpression(expr)) {
|
|
2653
|
+
sql = this.escapeId(expr.col);
|
|
2654
|
+
}
|
|
2655
|
+
else if (this.isLiteralExpression(expr)) {
|
|
2656
|
+
sql = expr.sql;
|
|
2657
|
+
}
|
|
2658
|
+
else if (typeof expr === 'string') {
|
|
2659
|
+
sql = this.escapeId(expr);
|
|
2660
|
+
}
|
|
2661
|
+
else {
|
|
2662
|
+
sql = String(expr);
|
|
2663
|
+
}
|
|
2664
|
+
// Alias is optional in GROUP BY
|
|
2665
|
+
if (alias) {
|
|
2666
|
+
return `${sql} AS ${this.escapeId(alias)}`;
|
|
2667
|
+
}
|
|
2668
|
+
return sql;
|
|
2669
|
+
}
|
|
2670
|
+
else if (this.isFnExpression(field)) {
|
|
2671
|
+
return this.compileFnExpression(field);
|
|
2672
|
+
}
|
|
2673
|
+
else if (this.isColExpression(field)) {
|
|
2674
|
+
return this.escapeId(field.col);
|
|
2675
|
+
}
|
|
2676
|
+
else if (this.isLiteralExpression(field)) {
|
|
2677
|
+
return field.sql;
|
|
2678
|
+
}
|
|
2679
|
+
else if (typeof field === 'string') {
|
|
2680
|
+
if (field.includes('.')) {
|
|
2681
|
+
return field
|
|
2682
|
+
.split('.')
|
|
2683
|
+
.map((part) => this.escapeId(part))
|
|
2684
|
+
.join('.');
|
|
2685
|
+
}
|
|
2686
|
+
return this.escapeId(field);
|
|
2687
|
+
}
|
|
2688
|
+
return String(field);
|
|
2689
|
+
});
|
|
2690
|
+
return `GROUP BY ${parts.join(', ')}`;
|
|
2691
|
+
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Build a WHERE clause condition using standard operators (Op.gt, Op.eq, etc.)
|
|
2694
|
+
* Uses SQLite ? placeholders
|
|
2695
|
+
*/
|
|
2696
|
+
buildCondition(condition, values, paramChar = '?') {
|
|
2697
|
+
if (!condition || (typeof condition === 'object' && Object.keys(condition).length === 0)) {
|
|
2698
|
+
return '';
|
|
2699
|
+
}
|
|
2700
|
+
// Handle logical operators
|
|
2701
|
+
if ('$and' in condition) {
|
|
2702
|
+
const andParts = condition.$and.map((c) => this.buildCondition(c, values, paramChar));
|
|
2703
|
+
return `(${andParts.join(' AND ')})`;
|
|
2704
|
+
}
|
|
2705
|
+
if ('$or' in condition) {
|
|
2706
|
+
const orParts = condition.$or.map((c) => this.buildCondition(c, values, paramChar));
|
|
2707
|
+
return `(${orParts.join(' OR ')})`;
|
|
2708
|
+
}
|
|
2709
|
+
if ('$not' in condition) {
|
|
2710
|
+
return `NOT (${this.buildCondition(condition.$not, values, paramChar)})`;
|
|
2711
|
+
}
|
|
2712
|
+
// Handle regular conditions
|
|
2713
|
+
const conditions = [];
|
|
2714
|
+
for (const [key, value] of Object.entries(condition)) {
|
|
2715
|
+
if (key.startsWith('$'))
|
|
2716
|
+
continue; // Skip logical operators
|
|
2717
|
+
let sql = '';
|
|
2718
|
+
// Check if value is an operator object
|
|
2719
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
2720
|
+
const opKeys = Object.keys(value);
|
|
2721
|
+
if (opKeys.length === 1) {
|
|
2722
|
+
const opKey = opKeys[0];
|
|
2723
|
+
const opValue = value[opKey];
|
|
2724
|
+
// Map Op symbol to string
|
|
2725
|
+
if (opKey === 'Symbol(eq)' || opKey === 'Symbol(operators.eq)') {
|
|
2726
|
+
sql = `${this.escapeId(key)} = ${paramChar}`;
|
|
2727
|
+
values.push(opValue);
|
|
2728
|
+
}
|
|
2729
|
+
else if (opKey === 'Symbol(ne)' || opKey === 'Symbol(operators.ne)') {
|
|
2730
|
+
sql = `${this.escapeId(key)} != ${paramChar}`;
|
|
2731
|
+
values.push(opValue);
|
|
2732
|
+
}
|
|
2733
|
+
else if (opKey === 'Symbol(gt)' || opKey === 'Symbol(operators.gt)') {
|
|
2734
|
+
sql = `${this.escapeId(key)} > ${paramChar}`;
|
|
2735
|
+
values.push(opValue);
|
|
2736
|
+
}
|
|
2737
|
+
else if (opKey === 'Symbol(gte)' || opKey === 'Symbol(operators.gte)') {
|
|
2738
|
+
sql = `${this.escapeId(key)} >= ${paramChar}`;
|
|
2739
|
+
values.push(opValue);
|
|
2740
|
+
}
|
|
2741
|
+
else if (opKey === 'Symbol(lt)' || opKey === 'Symbol(operators.lt)') {
|
|
2742
|
+
sql = `${this.escapeId(key)} < ${paramChar}`;
|
|
2743
|
+
values.push(opValue);
|
|
2744
|
+
}
|
|
2745
|
+
else if (opKey === 'Symbol(lte)' || opKey === 'Symbol(operators.lte)') {
|
|
2746
|
+
sql = `${this.escapeId(key)} <= ${paramChar}`;
|
|
2747
|
+
values.push(opValue);
|
|
2748
|
+
}
|
|
2749
|
+
else if (opKey === 'Symbol(in)' || opKey === 'Symbol(operators.in)') {
|
|
2750
|
+
sql = `${this.escapeId(key)} IN (${paramChar})`;
|
|
2751
|
+
values.push(opValue);
|
|
2752
|
+
}
|
|
2753
|
+
else if (opKey === 'Symbol(notIn)' || opKey === 'Symbol(operators.notIn)') {
|
|
2754
|
+
sql = `${this.escapeId(key)} NOT IN (${paramChar})`;
|
|
2755
|
+
values.push(opValue);
|
|
2756
|
+
}
|
|
2757
|
+
else if (opKey === 'Symbol(between)' || opKey === 'Symbol(operators.between)') {
|
|
2758
|
+
sql = `${this.escapeId(key)} BETWEEN ${paramChar} AND ${paramChar}`;
|
|
2759
|
+
values.push(...opValue);
|
|
2760
|
+
}
|
|
2761
|
+
else if (opKey === 'Symbol(notBetween)' || opKey === 'Symbol(operators.notBetween)') {
|
|
2762
|
+
sql = `${this.escapeId(key)} NOT BETWEEN ${paramChar} AND ${paramChar}`;
|
|
2763
|
+
values.push(...opValue);
|
|
2764
|
+
}
|
|
2765
|
+
else if (opKey === 'Symbol(isNull)' || opKey === 'Symbol(operators.isNull)') {
|
|
2766
|
+
sql = `${this.escapeId(key)} IS NULL`;
|
|
2767
|
+
}
|
|
2768
|
+
else if (opKey === 'Symbol(isNotNull)' || opKey === 'Symbol(operators.isNotNull)') {
|
|
2769
|
+
sql = `${this.escapeId(key)} IS NOT NULL`;
|
|
2770
|
+
}
|
|
2771
|
+
else if (opKey === 'Symbol(like)' || opKey === 'Symbol(operators.like)') {
|
|
2772
|
+
sql = `${this.escapeId(key)} LIKE ${paramChar}`;
|
|
2773
|
+
values.push(opValue);
|
|
2774
|
+
}
|
|
2775
|
+
else if (opKey === 'Symbol(notLike)' || opKey === 'Symbol(operators.notLike)') {
|
|
2776
|
+
sql = `${this.escapeId(key)} NOT LIKE ${paramChar}`;
|
|
2777
|
+
values.push(opValue);
|
|
2778
|
+
}
|
|
2779
|
+
else if (opKey === 'Symbol(iLike)' || opKey === 'Symbol(operators.iLike)') {
|
|
2780
|
+
sql = `${this.escapeId(key)} LIKE ${paramChar}`; // SQLite doesn't have ILIKE
|
|
2781
|
+
values.push(opValue);
|
|
2782
|
+
}
|
|
2783
|
+
else if (opKey === 'Symbol(notILike)' || opKey === 'Symbol(operators.notILike)') {
|
|
2784
|
+
sql = `${this.escapeId(key)} NOT LIKE ${paramChar}`;
|
|
2785
|
+
values.push(opValue);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
if (!sql) {
|
|
2789
|
+
// Fallback: treat as equals
|
|
2790
|
+
sql = `${this.escapeId(key)} = ${paramChar}`;
|
|
2791
|
+
values.push(value);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
else if (Array.isArray(value)) {
|
|
2795
|
+
// Handle IN arrays
|
|
2796
|
+
sql = `${this.escapeId(key)} IN (${value.map(() => paramChar).join(', ')})`;
|
|
2797
|
+
values.push(...value);
|
|
2798
|
+
}
|
|
2799
|
+
else {
|
|
2800
|
+
// Simple equality
|
|
2801
|
+
sql = `${this.escapeId(key)} = ${paramChar}`;
|
|
2802
|
+
values.push(value);
|
|
2803
|
+
}
|
|
2804
|
+
if (sql) {
|
|
2805
|
+
conditions.push(sql);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
return conditions.join(' AND ');
|
|
2809
|
+
}
|
|
2810
|
+
/**
|
|
2811
|
+
* Build a HAVING clause with support for standard Op operators
|
|
2812
|
+
*/
|
|
2813
|
+
buildHavingClause(having) {
|
|
2814
|
+
const values = [];
|
|
2815
|
+
if (!having || (typeof having === 'object' && Object.keys(having).length === 0)) {
|
|
2816
|
+
return { sql: '', values };
|
|
2817
|
+
}
|
|
2818
|
+
// Use the condition builder with Op operator support
|
|
2819
|
+
const sql = this.buildCondition(having, values);
|
|
2820
|
+
return { sql, values };
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Build a `WITH [RECURSIVE] name [(cols)] AS (query), ...` clause from
|
|
2824
|
+
* `SelectOptions.cte`. Nested `SelectOptions` queries are compiled via a
|
|
2825
|
+
* recursive call to `buildSelectQuery`; raw SQL strings are used verbatim.
|
|
2826
|
+
*
|
|
2827
|
+
* SQLite uses unnumbered `?` placeholders (unlike Postgres/MSSQL's
|
|
2828
|
+
* numbered `$n`/`@pN`), so no placeholder renumbering is needed here -
|
|
2829
|
+
* each CTE's `values` are simply concatenated in the order their `?`
|
|
2830
|
+
* markers appear in the rendered SQL, and the caller prepends the whole
|
|
2831
|
+
* batch ahead of the main query's own values.
|
|
2832
|
+
*/
|
|
2833
|
+
buildWithClause(ctes) {
|
|
2834
|
+
if (!ctes || ctes.length === 0) {
|
|
2835
|
+
return { sql: '', values: [] };
|
|
2836
|
+
}
|
|
2837
|
+
const values = [];
|
|
2838
|
+
const anyRecursive = ctes.some((cte) => cte.recursive === true);
|
|
2839
|
+
const parts = ctes.map((cte) => {
|
|
2840
|
+
const columnsSql = cte.columns && cte.columns.length > 0
|
|
2841
|
+
? ` (${cte.columns.map((c) => this.escapeId(c)).join(', ')})`
|
|
2842
|
+
: '';
|
|
2843
|
+
let bodySql;
|
|
2844
|
+
if (typeof cte.query === 'string') {
|
|
2845
|
+
bodySql = cte.query;
|
|
2846
|
+
}
|
|
2847
|
+
else {
|
|
2848
|
+
const built = this.buildSelectQuery(cte.query);
|
|
2849
|
+
bodySql = built.sql;
|
|
2850
|
+
values.push(...built.values);
|
|
2851
|
+
}
|
|
2852
|
+
return `${this.escapeId(cte.name)}${columnsSql} AS (${bodySql})`;
|
|
2853
|
+
});
|
|
2854
|
+
const withKeyword = anyRecursive ? 'WITH RECURSIVE' : 'WITH';
|
|
2855
|
+
return { sql: `${withKeyword} ${parts.join(', ')} `, values };
|
|
2856
|
+
}
|
|
2857
|
+
/**
|
|
2858
|
+
* Build a SELECT query
|
|
2859
|
+
*/
|
|
2860
|
+
buildSelectQuery(options) {
|
|
2861
|
+
const queryValues = [];
|
|
2862
|
+
// Build WITH clause (Common Table Expressions), if provided
|
|
2863
|
+
let withSql = '';
|
|
2864
|
+
if (options.cte && options.cte.length > 0) {
|
|
2865
|
+
const withClause = this.buildWithClause(options.cte);
|
|
2866
|
+
withSql = withClause.sql;
|
|
2867
|
+
queryValues.push(...withClause.values);
|
|
2868
|
+
}
|
|
2869
|
+
// Helper function to format a column/expression for SELECT
|
|
2870
|
+
const formatSelectAttribute = (attr) => {
|
|
2871
|
+
// Handle Col class instance (has toSql method)
|
|
2872
|
+
if (attr && typeof attr === 'object' && typeof attr.toSql === 'function') {
|
|
2873
|
+
return attr.toSql();
|
|
2874
|
+
}
|
|
2875
|
+
// Handle the modern typed-expression shapes from src/operators.ts / the
|
|
2876
|
+
// src/query-builders/functions/** builder library: { __type: 'literal', sql },
|
|
2877
|
+
// { __type: 'fn', fn, args }, { __type: 'col', col }. Without this, these
|
|
2878
|
+
// objects fall through to the generic branches below (which look for the
|
|
2879
|
+
// older { val }/{ fn }/{ col }/{ literal } shapes) and end up stringified
|
|
2880
|
+
// as "[object Object]", producing invalid SQL like "[object Object] AS alias".
|
|
2881
|
+
if (attr && typeof attr === 'object' && attr.__type === 'literal' && typeof attr.sql === 'string') {
|
|
2882
|
+
return attr.sql;
|
|
2883
|
+
}
|
|
2884
|
+
if (attr && typeof attr === 'object' && attr.__type === 'col' && typeof attr.col === 'string') {
|
|
2885
|
+
return this.escapeId(attr.col);
|
|
2886
|
+
}
|
|
2887
|
+
if (attr && typeof attr === 'object' && attr.__type === 'fn' && Array.isArray(attr.args)) {
|
|
2888
|
+
const formattedArgs = attr.args.map((arg) => formatSelectAttribute(arg));
|
|
2889
|
+
return `${attr.fn}(${formattedArgs.join(', ')})`;
|
|
2890
|
+
}
|
|
2891
|
+
// Handle Literal class instance (has val property)
|
|
2892
|
+
if (attr && typeof attr === 'object' && 'val' in attr) {
|
|
2893
|
+
return attr.val;
|
|
2894
|
+
}
|
|
2895
|
+
// Handle Fn type: { fn: 'COUNT', args: [...] }
|
|
2896
|
+
if (attr && typeof attr === 'object' && 'fn' in attr) {
|
|
2897
|
+
const fn = attr;
|
|
2898
|
+
const formattedArgs = fn.args.map((arg) => {
|
|
2899
|
+
// Handle Col class or object
|
|
2900
|
+
if (arg && typeof arg === 'object') {
|
|
2901
|
+
if (typeof arg.toSql === 'function') {
|
|
2902
|
+
return arg.toSql();
|
|
2903
|
+
}
|
|
2904
|
+
// Handle Col type object: { col: 'name' }
|
|
2905
|
+
if ('col' in arg) {
|
|
2906
|
+
return this.escapeId(arg.col);
|
|
2907
|
+
}
|
|
2908
|
+
// Handle Fn type (nested)
|
|
2909
|
+
if ('fn' in arg) {
|
|
2910
|
+
return formatSelectAttribute(arg);
|
|
2911
|
+
}
|
|
2912
|
+
// Handle Literal type object: { literal: 'sql' }
|
|
2913
|
+
if ('literal' in arg) {
|
|
2914
|
+
return arg.literal;
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
// Handle string column names
|
|
2918
|
+
if (typeof arg === 'string') {
|
|
2919
|
+
// Check if it looks like a function call already
|
|
2920
|
+
if (arg.includes('(') && arg.includes(')')) {
|
|
2921
|
+
return arg;
|
|
2922
|
+
}
|
|
2923
|
+
return this.escapeId(arg);
|
|
2924
|
+
}
|
|
2925
|
+
// Handle literals
|
|
2926
|
+
return String(arg);
|
|
2927
|
+
});
|
|
2928
|
+
return `${fn.fn}(${formattedArgs.join(', ')})`;
|
|
2929
|
+
}
|
|
2930
|
+
// Handle Col type object: { col: 'name' }
|
|
2931
|
+
if (attr && typeof attr === 'object' && 'col' in attr) {
|
|
2932
|
+
return this.escapeId(attr.col);
|
|
2933
|
+
}
|
|
2934
|
+
// Handle Literal type object: { literal: 'sql' }
|
|
2935
|
+
if (attr && typeof attr === 'object' && 'literal' in attr) {
|
|
2936
|
+
return attr.literal;
|
|
2937
|
+
}
|
|
2938
|
+
// Handle string column names
|
|
2939
|
+
if (typeof attr === 'string') {
|
|
2940
|
+
return this.escapeId(attr);
|
|
2941
|
+
}
|
|
2942
|
+
// Fallback
|
|
2943
|
+
return String(attr);
|
|
2944
|
+
};
|
|
2945
|
+
// Helper function to format an attribute with optional alias
|
|
2946
|
+
// Handles [expression, alias] or [fn, alias] format
|
|
2947
|
+
const formatAttributeWithAlias = (attr) => {
|
|
2948
|
+
// Check if it's an array with alias [expression, alias]
|
|
2949
|
+
if (Array.isArray(attr) && attr.length === 2) {
|
|
2950
|
+
const [expr, alias] = attr;
|
|
2951
|
+
return `${formatSelectAttribute(expr)} AS ${this.escapeId(alias)}`;
|
|
2952
|
+
}
|
|
2953
|
+
return formatSelectAttribute(attr);
|
|
2954
|
+
};
|
|
2955
|
+
// Build SELECT clause
|
|
2956
|
+
let selectSql = '*';
|
|
2957
|
+
if (options.attributes) {
|
|
2958
|
+
if (Array.isArray(options.attributes)) {
|
|
2959
|
+
selectSql = options.attributes.map((a) => formatAttributeWithAlias(a)).join(', ');
|
|
2960
|
+
}
|
|
2961
|
+
else if (options.attributes.include) {
|
|
2962
|
+
selectSql = options.attributes.include.map((a) => this.escapeId(a)).join(', ');
|
|
2963
|
+
}
|
|
2964
|
+
else if (options.attributes.exclude) {
|
|
2965
|
+
selectSql = `* EXCEPT (${options.attributes.exclude.map((a) => this.escapeId(a)).join(', ')})`;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
// Handle DISTINCT (for COUNT(DISTINCT col))
|
|
2969
|
+
let distinctClause = '';
|
|
2970
|
+
if (options.distinct) {
|
|
2971
|
+
if (options.col) {
|
|
2972
|
+
distinctClause = `DISTINCT ${this.escapeId(options.col)} `;
|
|
2973
|
+
}
|
|
2974
|
+
else {
|
|
2975
|
+
distinctClause = 'DISTINCT ';
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
let sql = `${withSql}SELECT ${distinctClause}${selectSql} FROM ${this.quoteTable(options.tableName, options.schema)}`;
|
|
2979
|
+
// Build JOIN clause (for includes)
|
|
2980
|
+
if (options.include && options.include.length > 0) {
|
|
2981
|
+
for (const include of options.include) {
|
|
2982
|
+
const joinType = include.required ? 'INNER JOIN' : 'LEFT JOIN';
|
|
2983
|
+
const includeSchema = include.schema || include.model?.schema;
|
|
2984
|
+
const includeTableName = include.model.tableName;
|
|
2985
|
+
const tableName = includeTableName ? includeTableName : include.model.toString();
|
|
2986
|
+
const alias = include.as || tableName;
|
|
2987
|
+
sql += ` ${joinType} ${this.quoteTable(tableName, includeSchema)} ${this.escapeId(alias)}`;
|
|
2988
|
+
if (include.where) {
|
|
2989
|
+
const whereClause = this.buildWhereClause(include.where);
|
|
2990
|
+
sql += ` ON ${whereClause.sql}`;
|
|
2991
|
+
queryValues.push(...whereClause.values);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
// Build WHERE clause
|
|
2996
|
+
if (options.where) {
|
|
2997
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
2998
|
+
if (whereClause.sql) {
|
|
2999
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
3000
|
+
queryValues.push(...whereClause.values);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
// Build GROUP BY clause
|
|
3004
|
+
if (options.group) {
|
|
3005
|
+
const groupByClause = this.buildGroupByClause(options.group);
|
|
3006
|
+
if (groupByClause) {
|
|
3007
|
+
sql += ` ${groupByClause}`;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
// Build HAVING clause
|
|
3011
|
+
if (options.having) {
|
|
3012
|
+
const havingClause = this.buildHavingClause(options.having);
|
|
3013
|
+
if (havingClause.sql) {
|
|
3014
|
+
sql += ` HAVING ${havingClause.sql}`;
|
|
3015
|
+
queryValues.push(...havingClause.values);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
// Build ORDER BY clause
|
|
3019
|
+
if (options.order) {
|
|
3020
|
+
const orderClause = this.buildOrderClause(options.order);
|
|
3021
|
+
if (orderClause) {
|
|
3022
|
+
sql += ` ${orderClause}`;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
// Build LIMIT/OFFSET clause
|
|
3026
|
+
sql += this.buildLimitOffset(options.limit, options.offset);
|
|
3027
|
+
// Build UNION clause if provided
|
|
3028
|
+
if (options.union && options.union.length > 0) {
|
|
3029
|
+
const unionType = options.unionType || 'UNION';
|
|
3030
|
+
for (const unionQuery of options.union) {
|
|
3031
|
+
const unionModel = unionQuery.model;
|
|
3032
|
+
const unionTableName = unionModel.tableName || unionModel.name || '';
|
|
3033
|
+
// Build the union query
|
|
3034
|
+
let unionSql = `SELECT * FROM ${this.escapeId(unionTableName)}`;
|
|
3035
|
+
// Add WHERE clause for union query
|
|
3036
|
+
if (unionQuery.where) {
|
|
3037
|
+
const whereClause = this.buildWhereClause(unionQuery.where);
|
|
3038
|
+
if (whereClause.sql) {
|
|
3039
|
+
unionSql += ` WHERE ${whereClause.sql}`;
|
|
3040
|
+
queryValues.push(...whereClause.values);
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
// Add ORDER BY for union query
|
|
3044
|
+
if (unionQuery.order) {
|
|
3045
|
+
const orderClause = this.buildOrderClause(unionQuery.order);
|
|
3046
|
+
if (orderClause) {
|
|
3047
|
+
unionSql += ` ${orderClause}`;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
// Add LIMIT for union query
|
|
3051
|
+
if (unionQuery.limit) {
|
|
3052
|
+
unionSql += ` LIMIT ${unionQuery.limit}`;
|
|
3053
|
+
}
|
|
3054
|
+
// Add OFFSET for union query
|
|
3055
|
+
if (unionQuery.offset) {
|
|
3056
|
+
unionSql += ` OFFSET ${unionQuery.offset}`;
|
|
3057
|
+
}
|
|
3058
|
+
sql += ` ${unionType} ${unionSql}`;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
return { sql, values: queryValues };
|
|
3062
|
+
}
|
|
3063
|
+
/**
|
|
3064
|
+
* Recreate table without a column (for removeColumn)
|
|
3065
|
+
*/
|
|
3066
|
+
async recreateTableWithoutColumn(tableName, columnName) {
|
|
3067
|
+
const tableInfo = await this.describeTable(tableName);
|
|
3068
|
+
const columns = Object.keys(tableInfo).filter((col) => col !== columnName);
|
|
3069
|
+
// Preserve NOT NULL / DEFAULT / PRIMARY KEY / AUTOINCREMENT / UNIQUE constraints
|
|
3070
|
+
// of the remaining columns during the rebuild (see recreateTableWithConstraint).
|
|
3071
|
+
const columnsDef = columns.map((col) => this.getColumnDefinitionSql(col, this.toColumnDefinition(tableInfo[col])));
|
|
3072
|
+
const tempTable = `${tableName}_temp`;
|
|
3073
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} RENAME TO ${this.escapeId(tempTable)}`);
|
|
3074
|
+
await this.query(`CREATE TABLE ${this.escapeId(tableName)} (${columnsDef.join(', ')})`);
|
|
3075
|
+
await this.query(`INSERT INTO ${this.escapeId(tableName)} SELECT ${columns.map((c) => this.escapeId(c)).join(', ')} FROM ${this.escapeId(tempTable)}`);
|
|
3076
|
+
await this.query(`DROP TABLE ${this.escapeId(tempTable)}`);
|
|
3077
|
+
}
|
|
3078
|
+
/**
|
|
3079
|
+
* Recreate table to modify a column
|
|
3080
|
+
*/
|
|
3081
|
+
async recreateTableModifyColumn(tableName, columnName, newDefinition) {
|
|
3082
|
+
const tableInfo = await this.describeTable(tableName);
|
|
3083
|
+
const columns = [];
|
|
3084
|
+
for (const [colName, colInfo] of Object.entries(tableInfo)) {
|
|
3085
|
+
if (colName === columnName) {
|
|
3086
|
+
columns.push(this.getColumnDefinitionSql(colName, newDefinition));
|
|
3087
|
+
}
|
|
3088
|
+
else {
|
|
3089
|
+
// Preserve NOT NULL / DEFAULT / PRIMARY KEY / AUTOINCREMENT / UNIQUE constraints
|
|
3090
|
+
// of unaffected columns during the rebuild.
|
|
3091
|
+
columns.push(this.getColumnDefinitionSql(colName, this.toColumnDefinition(colInfo)));
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
const tempTable = `${tableName}_temp`;
|
|
3095
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} RENAME TO ${this.escapeId(tempTable)}`);
|
|
3096
|
+
await this.query(`CREATE TABLE ${this.escapeId(tableName)} (${columns.join(', ')})`);
|
|
3097
|
+
const colNames = Object.keys(tableInfo)
|
|
3098
|
+
.map((c) => this.escapeId(c))
|
|
3099
|
+
.join(', ');
|
|
3100
|
+
await this.query(`INSERT INTO ${this.escapeId(tableName)} SELECT ${colNames} FROM ${this.escapeId(tempTable)}`);
|
|
3101
|
+
await this.query(`DROP TABLE ${this.escapeId(tempTable)}`);
|
|
3102
|
+
}
|
|
3103
|
+
// ── User-management stubs (not supported by SQLite) ─────────────────────
|
|
3104
|
+
buildCreateUserQuery(_username, _options) {
|
|
3105
|
+
throw new Error('User management is not supported by SQLite');
|
|
3106
|
+
}
|
|
3107
|
+
buildAlterUserQuery(_username, _options) {
|
|
3108
|
+
throw new Error('User management is not supported by SQLite');
|
|
3109
|
+
}
|
|
3110
|
+
buildDropUserQuery(_username, _options) {
|
|
3111
|
+
throw new Error('User management is not supported by SQLite');
|
|
3112
|
+
}
|
|
3113
|
+
getUsersQuery() {
|
|
3114
|
+
throw new Error('User management is not supported by SQLite');
|
|
3115
|
+
}
|
|
3116
|
+
buildGrantQuery(_options) {
|
|
3117
|
+
throw new Error('GRANT is not supported by SQLite');
|
|
3118
|
+
}
|
|
3119
|
+
buildRevokeQuery(_options) {
|
|
3120
|
+
throw new Error('REVOKE is not supported by SQLite');
|
|
3121
|
+
}
|
|
3122
|
+
buildShowGrantsQuery(_username, _host) {
|
|
3123
|
+
throw new Error('SHOW GRANTS is not supported by SQLite');
|
|
3124
|
+
}
|
|
3125
|
+
buildFlushPrivilegesQuery() {
|
|
3126
|
+
throw new Error('FLUSH PRIVILEGES is not supported by SQLite');
|
|
3127
|
+
}
|
|
3128
|
+
buildCreateRoleQuery(_roleName, _options) {
|
|
3129
|
+
throw new Error('Role management is not supported by SQLite');
|
|
3130
|
+
}
|
|
3131
|
+
buildDropRoleQuery(_roleName, _options) {
|
|
3132
|
+
throw new Error('Role management is not supported by SQLite');
|
|
3133
|
+
}
|
|
3134
|
+
buildGrantRoleQuery(_role, _to, _options) {
|
|
3135
|
+
throw new Error('Role management is not supported by SQLite');
|
|
3136
|
+
}
|
|
3137
|
+
buildRevokeRoleQuery(_role, _from, _options) {
|
|
3138
|
+
throw new Error('Role management is not supported by SQLite');
|
|
3139
|
+
}
|
|
3140
|
+
getRolesQuery() {
|
|
3141
|
+
throw new Error('Role management is not supported by SQLite');
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* Bulk insert records into a table
|
|
3145
|
+
*/
|
|
3146
|
+
async bulkInsert(tableName, records, _options) {
|
|
3147
|
+
if (records.length === 0) {
|
|
3148
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
3149
|
+
}
|
|
3150
|
+
const columns = Object.keys(records[0]);
|
|
3151
|
+
const placeholders = records.map(() => `(${columns.map(() => '?').join(', ')})`);
|
|
3152
|
+
const values = records.flatMap((record) => columns.map((col) => record[col]));
|
|
3153
|
+
const sql = `INSERT INTO ${this.quoteTable(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES ${placeholders.join(', ')}`;
|
|
3154
|
+
return this.query(sql, { replacements: values });
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Update every row matching `where` with `values` in a single statement.
|
|
3158
|
+
*/
|
|
3159
|
+
async bulkUpdate(tableName, values, where = {}) {
|
|
3160
|
+
const cols = Object.keys(values);
|
|
3161
|
+
if (cols.length === 0)
|
|
3162
|
+
return undefined;
|
|
3163
|
+
const setValues = [];
|
|
3164
|
+
const setClause = cols
|
|
3165
|
+
.map((c) => {
|
|
3166
|
+
setValues.push(values[c]);
|
|
3167
|
+
return `${this.escapeId(c)} = ?`;
|
|
3168
|
+
})
|
|
3169
|
+
.join(', ');
|
|
3170
|
+
let sql = `UPDATE ${this.quoteTable(tableName)} SET ${setClause}`;
|
|
3171
|
+
const whereClause = this.buildWhereClause(where);
|
|
3172
|
+
if (whereClause.sql)
|
|
3173
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
3174
|
+
return this.query(sql, { replacements: [...setValues, ...whereClause.values] });
|
|
3175
|
+
}
|
|
3176
|
+
/**
|
|
3177
|
+
* Delete every row matching `where` (deletes all rows when `where` is empty).
|
|
3178
|
+
*/
|
|
3179
|
+
async bulkDelete(tableName, where = {}) {
|
|
3180
|
+
let sql = `DELETE FROM ${this.quoteTable(tableName)}`;
|
|
3181
|
+
const whereClause = this.buildWhereClause(where);
|
|
3182
|
+
if (whereClause.sql)
|
|
3183
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
3184
|
+
return this.query(sql, { replacements: whereClause.values });
|
|
3185
|
+
}
|
|
3186
|
+
/**
|
|
3187
|
+
* Add a foreign key to a table
|
|
3188
|
+
* Note: SQLite does not support ALTER TABLE ADD CONSTRAINT.
|
|
3189
|
+
* Foreign keys must be defined when the table is created.
|
|
3190
|
+
*/
|
|
3191
|
+
async addForeignKey(tableName, columnName, referencedTableName, referencedColumnName, options) {
|
|
3192
|
+
// SQLite has no ALTER TABLE ADD CONSTRAINT, so adding a foreign key to an
|
|
3193
|
+
// existing table requires the documented 12-step table rebuild: recreate the
|
|
3194
|
+
// table with the FK baked into its definition, copy the rows over, then swap.
|
|
3195
|
+
let createSql;
|
|
3196
|
+
try {
|
|
3197
|
+
createSql = await this.getCreateTable(tableName);
|
|
3198
|
+
}
|
|
3199
|
+
catch {
|
|
3200
|
+
throw new Error(`Cannot add foreign key: table "${tableName}" not found`);
|
|
3201
|
+
}
|
|
3202
|
+
if (!createSql) {
|
|
3203
|
+
throw new Error(`Cannot add foreign key: table "${tableName}" not found`);
|
|
3204
|
+
}
|
|
3205
|
+
let fkClause = `FOREIGN KEY (${this.escapeId(columnName)}) REFERENCES ${this.quoteTable(referencedTableName)} (${this.escapeId(referencedColumnName)})`;
|
|
3206
|
+
if (options?.onDelete)
|
|
3207
|
+
fkClause += ` ON DELETE ${options.onDelete}`;
|
|
3208
|
+
if (options?.onUpdate)
|
|
3209
|
+
fkClause += ` ON UPDATE ${options.onUpdate}`;
|
|
3210
|
+
// Insert the FK clause just before the closing paren of the CREATE TABLE body.
|
|
3211
|
+
const lastParen = createSql.lastIndexOf(')');
|
|
3212
|
+
const newCreateSql = createSql.slice(0, lastParen).trimEnd() + `,\n ${fkClause}\n` + createSql.slice(lastParen);
|
|
3213
|
+
const tmpName = `${tableName}__fk_rebuild`;
|
|
3214
|
+
// Setter PRAGMAs don't return rows, so run them via exec() rather than query()
|
|
3215
|
+
// (which would try stmt.all() and throw "statement does not return data").
|
|
3216
|
+
this.connection?.exec('PRAGMA foreign_keys=OFF');
|
|
3217
|
+
try {
|
|
3218
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} RENAME TO ${this.escapeId(tmpName)}`);
|
|
3219
|
+
await this.query(newCreateSql);
|
|
3220
|
+
await this.query(`INSERT INTO ${this.quoteTable(tableName)} SELECT * FROM ${this.escapeId(tmpName)}`);
|
|
3221
|
+
await this.query(`DROP TABLE ${this.escapeId(tmpName)}`);
|
|
3222
|
+
}
|
|
3223
|
+
finally {
|
|
3224
|
+
this.connection?.exec('PRAGMA foreign_keys=ON');
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* Rename a column
|
|
3229
|
+
*/
|
|
3230
|
+
async renameColumn(tableName, oldColumnName, newColumnName) {
|
|
3231
|
+
const sql = `ALTER TABLE ${this.quoteTable(tableName)} RENAME COLUMN ${this.escapeId(oldColumnName)} TO ${this.escapeId(newColumnName)}`;
|
|
3232
|
+
await this.query(sql);
|
|
3233
|
+
}
|
|
3234
|
+
/**
|
|
3235
|
+
* Create a fulltext index (SQLite supports FTS5)
|
|
3236
|
+
*/
|
|
3237
|
+
async createFulltextIndex(_tableName, indexName, fields, _options) {
|
|
3238
|
+
// SQLite uses FTS5 for fulltext search
|
|
3239
|
+
const sql = `CREATE VIRTUAL TABLE ${this.escapeId(indexName)} USING fts5(${fields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
3240
|
+
await this.query(sql);
|
|
3241
|
+
}
|
|
3242
|
+
/**
|
|
3243
|
+
* Create a spatial index (SQLite supports spatial indexing via RTree)
|
|
3244
|
+
*/
|
|
3245
|
+
async createSpatialIndex(_tableName, indexName, fields, _options) {
|
|
3246
|
+
// SQLite uses RTree for spatial indexing
|
|
3247
|
+
const sql = `CREATE VIRTUAL TABLE ${this.escapeId(indexName)} USING rtree(${fields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
3248
|
+
await this.query(sql);
|
|
3249
|
+
}
|
|
3250
|
+
/**
|
|
3251
|
+
* Create user mapping (SQLite doesn't support user mappings)
|
|
3252
|
+
*/
|
|
3253
|
+
async createUserMapping(_userName, _serverName, _options) {
|
|
3254
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
3255
|
+
}
|
|
3256
|
+
/**
|
|
3257
|
+
* Drop user mapping (SQLite doesn't support user mappings)
|
|
3258
|
+
*/
|
|
3259
|
+
async dropUserMapping(_userName, _serverName, _options) {
|
|
3260
|
+
throw new Error('Foreign Data Wrappers are not supported by SQLite. This is a PostgreSQL-specific feature.');
|
|
3261
|
+
}
|
|
3262
|
+
// ==================== Spatial Functions ====================
|
|
3263
|
+
/**
|
|
3264
|
+
* ST_Distance - calculate distance between two geometries
|
|
3265
|
+
* SQLite uses SpatiaLite extension or custom implementation
|
|
3266
|
+
*/
|
|
3267
|
+
stDistance(geom1, geom2, _srid) {
|
|
3268
|
+
// SQLite doesn't have built-in ST_Distance, use Haversine approximation for point coordinates
|
|
3269
|
+
// For full spatial support, SpatiaLite extension is required
|
|
3270
|
+
return `ST_Distance(${geom1}, ${geom2})`;
|
|
3271
|
+
}
|
|
3272
|
+
/**
|
|
3273
|
+
* ST_DWithin - check if geometries are within a given distance
|
|
3274
|
+
*/
|
|
3275
|
+
stDWithin(geom1, geom2, distance, _srid) {
|
|
3276
|
+
return `ST_DWithin(${geom1}, ${geom2}, ${distance})`;
|
|
3277
|
+
}
|
|
3278
|
+
/**
|
|
3279
|
+
* ST_Within - check if geometry A is within geometry B
|
|
3280
|
+
*/
|
|
3281
|
+
stWithin(geom1, geom2, _srid) {
|
|
3282
|
+
return `ST_Within(${geom1}, ${geom2})`;
|
|
3283
|
+
}
|
|
3284
|
+
/**
|
|
3285
|
+
* ST_Contains - check if geometry A contains geometry B
|
|
3286
|
+
*/
|
|
3287
|
+
stContains(geom1, geom2, _srid) {
|
|
3288
|
+
return `ST_Contains(${geom1}, ${geom2})`;
|
|
3289
|
+
}
|
|
3290
|
+
/**
|
|
3291
|
+
* ST_Intersects - check if geometries intersect
|
|
3292
|
+
*/
|
|
3293
|
+
stIntersects(geom1, geom2, _srid) {
|
|
3294
|
+
return `ST_Intersects(${geom1}, ${geom2})`;
|
|
3295
|
+
}
|
|
3296
|
+
/**
|
|
3297
|
+
* ST_AsText - convert geometry to text representation
|
|
3298
|
+
*/
|
|
3299
|
+
stAsText(geom) {
|
|
3300
|
+
return `ST_AsText(${geom})`;
|
|
3301
|
+
}
|
|
3302
|
+
/**
|
|
3303
|
+
* ST_GeomFromText - create geometry from text
|
|
3304
|
+
*/
|
|
3305
|
+
stGeomFromText(wkt, srid) {
|
|
3306
|
+
return srid ? `ST_GeomFromText('${wkt}', ${srid})` : `ST_GeomFromText('${wkt}')`;
|
|
3307
|
+
}
|
|
3308
|
+
/**
|
|
3309
|
+
* Build an expression to shred a JSON document/array into relational rows.
|
|
3310
|
+
* SQLite has no `JSON_TABLE` function; its real equivalent is the `json_each`
|
|
3311
|
+
* table-valued function, which yields fixed columns (key, value, type, atom, id,
|
|
3312
|
+
* parent, fullkey, path). To project those into named/typed columns (matching the
|
|
3313
|
+
* other dialects' JSON_TABLE/OPENJSON shape), this wraps json_each in a derived table
|
|
3314
|
+
* that applies `json_extract` per requested column.
|
|
3315
|
+
* SQLite: (SELECT json_extract(je.value, '$.path') AS name, ... FROM
|
|
3316
|
+
* json_each(expr, '$.rowPath') AS je) AS alias
|
|
3317
|
+
*/
|
|
3318
|
+
buildJsonTable(jsonExpression, rowPath, columns, alias) {
|
|
3319
|
+
const pathArg = rowPath && rowPath !== '$' ? `, '${rowPath}'` : '';
|
|
3320
|
+
const selectCols = columns
|
|
3321
|
+
.map((col) => {
|
|
3322
|
+
if (col.forOrdinality) {
|
|
3323
|
+
return `je."key" AS ${this.escapeId(col.name)}`;
|
|
3324
|
+
}
|
|
3325
|
+
return `json_extract(je.value, '${col.path}') AS ${this.escapeId(col.name)}`;
|
|
3326
|
+
})
|
|
3327
|
+
.join(', ');
|
|
3328
|
+
return `(SELECT ${selectCols} FROM json_each(${jsonExpression}${pathArg}) AS je) AS ${this.escapeId(alias)}`;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
exports.SQLiteDialect = SQLiteDialect;
|
|
3332
|
+
/**
|
|
3333
|
+
* SQLite-specific transaction class
|
|
3334
|
+
*/
|
|
3335
|
+
class SQLiteTransaction {
|
|
3336
|
+
constructor(connection, depth, options,
|
|
3337
|
+
// Owning dialect, used so commit()/rollback() called directly on the
|
|
3338
|
+
// transaction object (rather than via dialect.commitTransaction/
|
|
3339
|
+
// rollbackTransaction) still actually execute COMMIT/ROLLBACK/SAVEPOINT SQL.
|
|
3340
|
+
// Previously these were no-ops that only flipped `finished`, leaving the
|
|
3341
|
+
// underlying SQLite transaction open forever if a caller used this API.
|
|
3342
|
+
dialect) {
|
|
3343
|
+
this.connection = connection;
|
|
3344
|
+
this.dialect = dialect;
|
|
3345
|
+
this.finished = false;
|
|
3346
|
+
this.parent = null;
|
|
3347
|
+
this.savepoints = [];
|
|
3348
|
+
// `depth` is the pre-increment transactionDepth at creation time (0 for
|
|
3349
|
+
// the outermost transaction). The id itself is used verbatim as the
|
|
3350
|
+
// SAVEPOINT name for nested transactions, so it must not also get a
|
|
3351
|
+
// separate "sp_" prefix prepended at the call site (that produced
|
|
3352
|
+
// "SAVEPOINT sp_sp_0_..." double-prefixed names that never matched what
|
|
3353
|
+
// was actually created).
|
|
3354
|
+
this.id = `sp_${depth}_${Date.now()}`;
|
|
3355
|
+
this.isOuter = depth === 0;
|
|
3356
|
+
this.options = options || {};
|
|
3357
|
+
}
|
|
3358
|
+
async commit() {
|
|
3359
|
+
if (this.finished)
|
|
3360
|
+
return;
|
|
3361
|
+
if (this.dialect) {
|
|
3362
|
+
await this.dialect.commitTransaction(this);
|
|
3363
|
+
}
|
|
3364
|
+
this.finished = true;
|
|
3365
|
+
}
|
|
3366
|
+
async rollback() {
|
|
3367
|
+
if (this.finished)
|
|
3368
|
+
return;
|
|
3369
|
+
if (this.dialect) {
|
|
3370
|
+
await this.dialect.rollbackTransaction(this);
|
|
3371
|
+
}
|
|
3372
|
+
this.finished = true;
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
exports.SQLiteTransaction = SQLiteTransaction;
|
|
3376
|
+
/**
|
|
3377
|
+
* Create a new SQLite dialect instance
|
|
3378
|
+
*/
|
|
3379
|
+
function createSQLiteDialect(options) {
|
|
3380
|
+
return new SQLiteDialect(options || {});
|
|
3381
|
+
}
|
|
3382
|
+
exports.default = SQLiteDialect;
|