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,2014 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DuckDB dialect implementation
|
|
4
|
+
*
|
|
5
|
+
* DuckDB is an embedded, in-process OLAP database (similar in deployment
|
|
6
|
+
* model to SQLite - a single file or `:memory:`, no server/host/port/auth)
|
|
7
|
+
* but with a genuinely modern analytical SQL engine: real window functions,
|
|
8
|
+
* recursive CTEs, rich composite types (LIST/STRUCT/MAP), first-class
|
|
9
|
+
* Parquet/CSV/JSON table functions, native sequences, and a real extension
|
|
10
|
+
* system (INSTALL/LOAD).
|
|
11
|
+
*
|
|
12
|
+
* The `duckdb` npm driver's public API (`duckdb/lib/duckdb.d.ts`) is
|
|
13
|
+
* callback-based (`new duckdb.Database(path, cb)`, `db.all(sql, ...args, cb)`,
|
|
14
|
+
* `db.run(sql, ...args, cb)`). All of that is wrapped in Promises here.
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.DuckDBTransaction = exports.DuckDBDialect = void 0;
|
|
51
|
+
exports.createDuckDBDialect = createDuckDBDialect;
|
|
52
|
+
const duckdb = __importStar(require("duckdb"));
|
|
53
|
+
const query_stream_helper_1 = require("../query-stream-helper");
|
|
54
|
+
const prorm_1 = require("../../prorm");
|
|
55
|
+
const operators_1 = require("../../operators");
|
|
56
|
+
function promisifyAll(target, sql, params) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const cb = (err, rows) => {
|
|
59
|
+
if (err)
|
|
60
|
+
reject(err);
|
|
61
|
+
else
|
|
62
|
+
resolve(rows);
|
|
63
|
+
};
|
|
64
|
+
if (params.length > 0) {
|
|
65
|
+
target.all(sql, ...params, cb);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
target.all(sql, cb);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function promisifyRun(target, sql, params) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const cb = (err) => {
|
|
75
|
+
if (err)
|
|
76
|
+
reject(err);
|
|
77
|
+
else
|
|
78
|
+
resolve();
|
|
79
|
+
};
|
|
80
|
+
if (params.length > 0) {
|
|
81
|
+
target.run(sql, ...params, cb);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
target.run(sql, cb);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function promisifyExec(target, sql) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
target.exec(sql, (err) => {
|
|
91
|
+
if (err)
|
|
92
|
+
reject(err);
|
|
93
|
+
else
|
|
94
|
+
resolve();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* DuckDB dialect class implementing the shared `Dialect` interface.
|
|
100
|
+
*/
|
|
101
|
+
class DuckDBDialect {
|
|
102
|
+
constructor(config) {
|
|
103
|
+
this.name = 'duckdb';
|
|
104
|
+
this.library = 'duckdb';
|
|
105
|
+
this.db = null;
|
|
106
|
+
this.connection = null;
|
|
107
|
+
this._isConnected = false;
|
|
108
|
+
this.transactionDepth = 0;
|
|
109
|
+
this.config = {
|
|
110
|
+
storage: ':memory:',
|
|
111
|
+
...config,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Connect to the DuckDB database (embedded - opens a local file or :memory:).
|
|
116
|
+
* host/port/username/password (if present on the config for API parity with
|
|
117
|
+
* networked dialects) are intentionally ignored - DuckDB is single-process.
|
|
118
|
+
*/
|
|
119
|
+
async connect() {
|
|
120
|
+
try {
|
|
121
|
+
this.db = await new Promise((resolve, reject) => {
|
|
122
|
+
let accessMode = this.config.config;
|
|
123
|
+
if (this.config.readonly) {
|
|
124
|
+
if (accessMode && typeof accessMode === 'object') {
|
|
125
|
+
// Merge readonly into the existing config object rather than letting
|
|
126
|
+
// `config` silently override `readonly`.
|
|
127
|
+
accessMode = { ...accessMode, access_mode: 'READ_ONLY' };
|
|
128
|
+
}
|
|
129
|
+
else if (!accessMode) {
|
|
130
|
+
accessMode = duckdb.OPEN_READONLY;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const cb = (err) => {
|
|
134
|
+
// duckdb.Database's constructor callback fires asynchronously once the
|
|
135
|
+
// native handle has actually been opened (or failed to open). Only
|
|
136
|
+
// resolve/reject here - resolving eagerly right after construction
|
|
137
|
+
// would let open failures (corrupted file, permission denied,
|
|
138
|
+
// readonly-open against a locked writer) race past a settled promise
|
|
139
|
+
// and surface only later as a confusing error on the first query.
|
|
140
|
+
if (err)
|
|
141
|
+
reject(err);
|
|
142
|
+
else
|
|
143
|
+
resolve(database);
|
|
144
|
+
};
|
|
145
|
+
const database = accessMode
|
|
146
|
+
? new duckdb.Database(this.config.storage, accessMode, cb)
|
|
147
|
+
: new duckdb.Database(this.config.storage, cb);
|
|
148
|
+
});
|
|
149
|
+
this.connection = this.db.connect();
|
|
150
|
+
this._isConnected = true;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
throw new Error(`Failed to connect to DuckDB database: ${error}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async disconnect() {
|
|
157
|
+
if (this.connection) {
|
|
158
|
+
await new Promise((resolve, reject) => {
|
|
159
|
+
this.connection.close((err) => (err ? reject(err) : resolve()));
|
|
160
|
+
}).catch(() => undefined);
|
|
161
|
+
this.connection = null;
|
|
162
|
+
}
|
|
163
|
+
if (this.db) {
|
|
164
|
+
await new Promise((resolve, reject) => {
|
|
165
|
+
this.db.close((err) => (err ? reject(err) : resolve()));
|
|
166
|
+
}).catch(() => undefined);
|
|
167
|
+
this.db = null;
|
|
168
|
+
}
|
|
169
|
+
this._isConnected = false;
|
|
170
|
+
}
|
|
171
|
+
getConnection() {
|
|
172
|
+
return this.connection;
|
|
173
|
+
}
|
|
174
|
+
isConnected() {
|
|
175
|
+
return this._isConnected && this.connection !== null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Stream query results by paging through `sql` via repeated
|
|
179
|
+
* dialect-appropriate LIMIT/OFFSET queries (see
|
|
180
|
+
* `createPaginatedQueryStream()` in `src/dialects/query-stream-helper.ts`)
|
|
181
|
+
* instead of loading the whole result set into memory at once.
|
|
182
|
+
* @param sql - The SELECT statement to stream
|
|
183
|
+
* @param options - Streaming options (batch size, backpressure watermark, model mapping)
|
|
184
|
+
*/
|
|
185
|
+
queryStream(sql, options) {
|
|
186
|
+
return (0, query_stream_helper_1.createPaginatedQueryStream)(this, sql, options);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Execute a raw SQL query with retry support.
|
|
190
|
+
*/
|
|
191
|
+
async query(sql, options) {
|
|
192
|
+
const defaultQueryRetryOptions = {
|
|
193
|
+
max: 0,
|
|
194
|
+
timeout: 1000,
|
|
195
|
+
match: ['is closed', 'connection', 'write-write conflict', 'database is locked'],
|
|
196
|
+
backoff: false,
|
|
197
|
+
backoffMultiplier: 2,
|
|
198
|
+
backoffMax: 10000,
|
|
199
|
+
};
|
|
200
|
+
const retryOptions = options?.retry;
|
|
201
|
+
const effectiveRetry = retryOptions
|
|
202
|
+
? {
|
|
203
|
+
...defaultQueryRetryOptions,
|
|
204
|
+
...retryOptions,
|
|
205
|
+
match: retryOptions.match || defaultQueryRetryOptions.match,
|
|
206
|
+
}
|
|
207
|
+
: null;
|
|
208
|
+
const executeQuery = async () => {
|
|
209
|
+
if (!this.connection) {
|
|
210
|
+
throw new Error('Not connected to database');
|
|
211
|
+
}
|
|
212
|
+
const sqlUpper = sql.trim().toUpperCase();
|
|
213
|
+
const isSelect = sqlUpper.startsWith('SELECT') ||
|
|
214
|
+
sqlUpper.startsWith('WITH') ||
|
|
215
|
+
sqlUpper.startsWith('PRAGMA') ||
|
|
216
|
+
sqlUpper.startsWith('SHOW') ||
|
|
217
|
+
sqlUpper.startsWith('DESCRIBE') ||
|
|
218
|
+
sqlUpper.startsWith('EXPLAIN');
|
|
219
|
+
const params = Array.isArray(options?.replacements)
|
|
220
|
+
? options.replacements
|
|
221
|
+
: options?.replacements
|
|
222
|
+
? Object.values(options.replacements)
|
|
223
|
+
: Array.isArray(options?.bind)
|
|
224
|
+
? options.bind
|
|
225
|
+
: options?.bind
|
|
226
|
+
? Object.values(options.bind)
|
|
227
|
+
: [];
|
|
228
|
+
try {
|
|
229
|
+
if (isSelect || /\bRETURNING\b/i.test(sql)) {
|
|
230
|
+
const rows = await promisifyAll(this.connection, sql, params);
|
|
231
|
+
return {
|
|
232
|
+
rows: rows,
|
|
233
|
+
rowCount: rows.length,
|
|
234
|
+
fields: [],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
await promisifyRun(this.connection, sql, params);
|
|
239
|
+
return {
|
|
240
|
+
rows: [],
|
|
241
|
+
rowCount: 0,
|
|
242
|
+
fields: [],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
throw new Error(`Query error: ${error}`);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
if (effectiveRetry && effectiveRetry.max > 0) {
|
|
251
|
+
return this.executeWithRetry(executeQuery, effectiveRetry);
|
|
252
|
+
}
|
|
253
|
+
return executeQuery();
|
|
254
|
+
}
|
|
255
|
+
async executeWithRetry(fn, retryOptions) {
|
|
256
|
+
let lastError;
|
|
257
|
+
for (let attempt = 0; attempt <= retryOptions.max; attempt++) {
|
|
258
|
+
try {
|
|
259
|
+
return await fn();
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
lastError = error;
|
|
263
|
+
const errorMessage = lastError.message.toLowerCase();
|
|
264
|
+
const shouldRetry = retryOptions.match.some((pattern) => errorMessage.includes(pattern.toLowerCase()));
|
|
265
|
+
if (!shouldRetry || attempt === retryOptions.max) {
|
|
266
|
+
throw lastError;
|
|
267
|
+
}
|
|
268
|
+
await new Promise((resolve) => setTimeout(resolve, retryOptions.timeout));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
throw lastError;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Replace placeholders in SQL with actual values.
|
|
275
|
+
*/
|
|
276
|
+
replaceReplacements(sql, replacements) {
|
|
277
|
+
if (!replacements) {
|
|
278
|
+
return sql;
|
|
279
|
+
}
|
|
280
|
+
if (Array.isArray(replacements)) {
|
|
281
|
+
let result = sql;
|
|
282
|
+
for (const value of replacements) {
|
|
283
|
+
result = result.replace(/\?/, () => this.escape(value));
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
let result = sql;
|
|
288
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
289
|
+
if (Array.isArray(value)) {
|
|
290
|
+
const escapedValues = value.map((v) => this.escape(v)).join(', ');
|
|
291
|
+
const regex = new RegExp(`:${key}(?![\\w])`, 'g');
|
|
292
|
+
result = result.replace(regex, escapedValues);
|
|
293
|
+
}
|
|
294
|
+
else if (value === null) {
|
|
295
|
+
const regex = new RegExp(`:${key}(?![\\w])`, 'g');
|
|
296
|
+
result = result.replace(regex, 'NULL');
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const regex = new RegExp(`:${key}(?![\\w])`, 'g');
|
|
300
|
+
result = result.replace(regex, () => this.escape(value));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Escape a value for use in a query. DuckDB uses standard SQL literals -
|
|
307
|
+
* TRUE/FALSE for booleans (not 0/1), single-quoted strings, and a
|
|
308
|
+
* `'\x..'::BLOB` cast for binary data.
|
|
309
|
+
*/
|
|
310
|
+
escape(value) {
|
|
311
|
+
if (value === null || value === undefined) {
|
|
312
|
+
return 'NULL';
|
|
313
|
+
}
|
|
314
|
+
if (value instanceof prorm_1.Literal) {
|
|
315
|
+
return value.val;
|
|
316
|
+
}
|
|
317
|
+
if (typeof value === 'string') {
|
|
318
|
+
return `'${this.escapeString(value)}'`;
|
|
319
|
+
}
|
|
320
|
+
if (typeof value === 'number') {
|
|
321
|
+
return String(value);
|
|
322
|
+
}
|
|
323
|
+
if (typeof value === 'bigint') {
|
|
324
|
+
return value.toString();
|
|
325
|
+
}
|
|
326
|
+
if (typeof value === 'boolean') {
|
|
327
|
+
return value ? 'TRUE' : 'FALSE';
|
|
328
|
+
}
|
|
329
|
+
if (value instanceof Date) {
|
|
330
|
+
return `TIMESTAMP '${value.toISOString().replace('T', ' ').replace('Z', '')}'`;
|
|
331
|
+
}
|
|
332
|
+
if (Buffer.isBuffer(value)) {
|
|
333
|
+
const hex = value.toString('hex').match(/../g)?.map((b) => `\\x${b}`).join('') ?? '';
|
|
334
|
+
return `'${hex}'::BLOB`;
|
|
335
|
+
}
|
|
336
|
+
if (Array.isArray(value)) {
|
|
337
|
+
return `[${value.map((v) => this.escape(v)).join(', ')}]`;
|
|
338
|
+
}
|
|
339
|
+
return `'${this.escapeString(JSON.stringify(value))}'`;
|
|
340
|
+
}
|
|
341
|
+
escapeString(str) {
|
|
342
|
+
return str.replace(/'/g, "''");
|
|
343
|
+
}
|
|
344
|
+
escapeId(identifier) {
|
|
345
|
+
const id = String(identifier ?? '');
|
|
346
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
347
|
+
}
|
|
348
|
+
quoteIdentifier(identifier) {
|
|
349
|
+
return this.escapeId(identifier);
|
|
350
|
+
}
|
|
351
|
+
quoteTable(tableName, schema) {
|
|
352
|
+
if (schema) {
|
|
353
|
+
return `${this.escapeId(schema)}.${this.escapeId(tableName)}`;
|
|
354
|
+
}
|
|
355
|
+
return this.escapeId(tableName);
|
|
356
|
+
}
|
|
357
|
+
async getDatabaseVersion() {
|
|
358
|
+
const result = await this.query('SELECT version() AS version');
|
|
359
|
+
return String(result.rows[0]?.version ?? '');
|
|
360
|
+
}
|
|
361
|
+
createDatabaseSQL(options) {
|
|
362
|
+
// DuckDB has no CREATE DATABASE statement of its own (a "database" is a
|
|
363
|
+
// file, opened via ATTACH). Document this via attachDatabase() instead.
|
|
364
|
+
return `ATTACH '${options.name}' AS ${this.escapeId(options.name)}`;
|
|
365
|
+
}
|
|
366
|
+
dropDatabaseSQL(name) {
|
|
367
|
+
return `DETACH ${this.escapeId(name)}`;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Attach another DuckDB (or SQLite/Postgres, via the respective DuckDB
|
|
371
|
+
* extension) database file under `alias`, making it queryable as
|
|
372
|
+
* `alias.table`.
|
|
373
|
+
*/
|
|
374
|
+
async attachDatabase(path, alias, options) {
|
|
375
|
+
if (!alias || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(alias)) {
|
|
376
|
+
throw new Error(`Invalid database alias: ${alias}`);
|
|
377
|
+
}
|
|
378
|
+
const opts = [];
|
|
379
|
+
if (options?.readOnly)
|
|
380
|
+
opts.push('READ_ONLY');
|
|
381
|
+
if (options?.type)
|
|
382
|
+
opts.push(`TYPE ${options.type.toUpperCase()}`);
|
|
383
|
+
const optsSql = opts.length > 0 ? ` (${opts.join(', ')})` : '';
|
|
384
|
+
await this.query(`ATTACH '${path.replace(/'/g, "''")}' AS ${this.escapeId(alias)}${optsSql}`);
|
|
385
|
+
}
|
|
386
|
+
async detachDatabase(alias) {
|
|
387
|
+
if (!alias || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(alias)) {
|
|
388
|
+
throw new Error(`Invalid database alias: ${alias}`);
|
|
389
|
+
}
|
|
390
|
+
await this.query(`DETACH ${this.escapeId(alias)}`);
|
|
391
|
+
}
|
|
392
|
+
createSavepointSQL(name) {
|
|
393
|
+
const savepointName = name || `sp_${Date.now()}`;
|
|
394
|
+
return `SAVEPOINT ${savepointName}`;
|
|
395
|
+
}
|
|
396
|
+
releaseSavepointSQL(name) {
|
|
397
|
+
return `RELEASE SAVEPOINT ${name}`;
|
|
398
|
+
}
|
|
399
|
+
rollbackToSavepointSQL(name) {
|
|
400
|
+
return `ROLLBACK TO SAVEPOINT ${name}`;
|
|
401
|
+
}
|
|
402
|
+
// ==================== Schemas ====================
|
|
403
|
+
async createSchema(schema) {
|
|
404
|
+
await this.query(`CREATE SCHEMA IF NOT EXISTS ${this.escapeId(schema)}`);
|
|
405
|
+
}
|
|
406
|
+
async dropSchema(schema, options) {
|
|
407
|
+
let sql = 'DROP SCHEMA';
|
|
408
|
+
if (options?.ifExists)
|
|
409
|
+
sql += ' IF EXISTS';
|
|
410
|
+
sql += ` ${this.escapeId(schema)}`;
|
|
411
|
+
if (options?.cascade)
|
|
412
|
+
sql += ' CASCADE';
|
|
413
|
+
await this.query(sql);
|
|
414
|
+
}
|
|
415
|
+
async showAllSchemas() {
|
|
416
|
+
const result = await this.query(`SELECT schema_name FROM information_schema.schemata ORDER BY schema_name`);
|
|
417
|
+
return result.rows.map((row) => row.schema_name);
|
|
418
|
+
}
|
|
419
|
+
async listSchemas() {
|
|
420
|
+
return this.showAllSchemas();
|
|
421
|
+
}
|
|
422
|
+
// ==================== Extensions (real DuckDB feature) ====================
|
|
423
|
+
// Unlike SQLite/Postgres extensions, DuckDB's extension system (INSTALL/LOAD)
|
|
424
|
+
// is a core, well-known part of the engine and is implemented for real here
|
|
425
|
+
// rather than being stubbed out - it's how `parquet`, `httpfs`, `json`,
|
|
426
|
+
// `fts`, and `spatial` support are enabled.
|
|
427
|
+
async createExtension(extensionName, options) {
|
|
428
|
+
const name = this.escapeId(extensionName).replace(/"/g, '');
|
|
429
|
+
await this.query(`INSTALL ${name}${options?.version ? ` VERSION '${options.version}'` : ''}`);
|
|
430
|
+
await this.query(`LOAD ${name}`);
|
|
431
|
+
}
|
|
432
|
+
async dropExtension(_extensionName, _options) {
|
|
433
|
+
// DuckDB has no UNINSTALL/DROP EXTENSION statement - installed extensions
|
|
434
|
+
// simply live in the local extension directory. There is nothing to run;
|
|
435
|
+
// to stop using one, just don't LOAD it in future connections.
|
|
436
|
+
throw new Error('DuckDB does not support dropping/uninstalling extensions once installed. ' +
|
|
437
|
+
'Simply avoid LOADing the extension in future connections.');
|
|
438
|
+
}
|
|
439
|
+
async getExtensions() {
|
|
440
|
+
const result = await this.query(`SELECT extension_name, installed, loaded, description, extension_version FROM duckdb_extensions()`);
|
|
441
|
+
return result.rows.map((row) => ({
|
|
442
|
+
name: row.extension_name,
|
|
443
|
+
defaultVersion: row.extension_version ?? null,
|
|
444
|
+
installedVersion: row.installed ? (row.extension_version ?? '') : null,
|
|
445
|
+
comment: row.description ?? null,
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
async hasExtension(extensionName) {
|
|
449
|
+
const result = await this.query(`SELECT 1 FROM duckdb_extensions() WHERE extension_name = ? AND loaded = true`, { replacements: [extensionName] });
|
|
450
|
+
return result.rows.length > 0;
|
|
451
|
+
}
|
|
452
|
+
// ==================== Tables ====================
|
|
453
|
+
async createTable(tableName, columns, options) {
|
|
454
|
+
// AUTOINCREMENT columns need a backing SEQUENCE created first, since
|
|
455
|
+
// DuckDB has no AUTOINCREMENT keyword - it uses `DEFAULT nextval('seq')`.
|
|
456
|
+
for (const [columnName, definition] of Object.entries(columns)) {
|
|
457
|
+
if (definition.autoIncrement) {
|
|
458
|
+
const seqName = this._autoIncrementSequenceName(tableName, columnName);
|
|
459
|
+
await this.query(`CREATE SEQUENCE IF NOT EXISTS ${this.escapeId(seqName)}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const columnDefs = [];
|
|
463
|
+
for (const [columnName, definition] of Object.entries(columns)) {
|
|
464
|
+
const typeObj = definition.type;
|
|
465
|
+
if (typeObj && typeObj.key === 'VIRTUAL') {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
columnDefs.push(this.getColumnDefinitionSql(tableName, columnName, definition));
|
|
469
|
+
}
|
|
470
|
+
if (options?.constraints) {
|
|
471
|
+
for (const constraint of options.constraints) {
|
|
472
|
+
const constraintSql = this.buildConstraintSql(constraint);
|
|
473
|
+
if (constraintSql)
|
|
474
|
+
columnDefs.push(constraintSql);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
let sql = 'CREATE TABLE';
|
|
478
|
+
if (options?.ifNotExists)
|
|
479
|
+
sql += ' IF NOT EXISTS';
|
|
480
|
+
sql += ` ${this.escapeId(tableName)} (${columnDefs.join(', ')})`;
|
|
481
|
+
if (options?.engine || options?.charset || options?.rowFormat) {
|
|
482
|
+
console.warn(`DuckDB: engine/charset/rowFormat options are not supported and will be ignored`);
|
|
483
|
+
}
|
|
484
|
+
await this.query(sql);
|
|
485
|
+
if (options?.uniqueKeys) {
|
|
486
|
+
const uniqueKeysList = Array.isArray(options.uniqueKeys)
|
|
487
|
+
? options.uniqueKeys
|
|
488
|
+
: Object.entries(options.uniqueKeys).map(([keyName, fields]) => ({ name: keyName, fields }));
|
|
489
|
+
for (const uk of uniqueKeysList) {
|
|
490
|
+
await this.addIndex(tableName, uk.name || `uq_${tableName}_${uk.fields.join('_')}`, uk.fields, { unique: true });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (options?.indexes) {
|
|
494
|
+
for (const index of options.indexes) {
|
|
495
|
+
await this.addIndex(tableName, index.name || `idx_${tableName}_${index.fields.join('_')}`, index.fields, {
|
|
496
|
+
unique: index.unique,
|
|
497
|
+
type: index.type,
|
|
498
|
+
using: index.using,
|
|
499
|
+
where: index.where,
|
|
500
|
+
expression: index.expression,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (options?.comment) {
|
|
505
|
+
await this.commentTable(tableName, options.comment);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
_autoIncrementSequenceName(tableName, columnName) {
|
|
509
|
+
return `seq_${tableName}_${columnName}`;
|
|
510
|
+
}
|
|
511
|
+
buildConstraintSql(constraint) {
|
|
512
|
+
const name = constraint.name ? `CONSTRAINT ${this.escapeId(constraint.name)} ` : '';
|
|
513
|
+
const fields = constraint.fields?.map((f) => this.escapeId(f)).join(', ') || '';
|
|
514
|
+
switch (constraint.type) {
|
|
515
|
+
case 'PRIMARY KEY':
|
|
516
|
+
return `${name}PRIMARY KEY (${fields})`;
|
|
517
|
+
case 'UNIQUE':
|
|
518
|
+
return `${name}UNIQUE (${fields})`;
|
|
519
|
+
case 'FOREIGN KEY': {
|
|
520
|
+
if (!constraint.references)
|
|
521
|
+
return null;
|
|
522
|
+
const refField = constraint.references.field;
|
|
523
|
+
const refFieldSql = Array.isArray(refField)
|
|
524
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
525
|
+
: `(${this.escapeId(refField)})`;
|
|
526
|
+
let fkSql = `${name}FOREIGN KEY (${fields}) REFERENCES ${this.escapeId(constraint.references.table)}${refFieldSql}`;
|
|
527
|
+
if (constraint.references.onDelete)
|
|
528
|
+
fkSql += ` ON DELETE ${constraint.references.onDelete}`;
|
|
529
|
+
if (constraint.references.onUpdate)
|
|
530
|
+
fkSql += ` ON UPDATE ${constraint.references.onUpdate}`;
|
|
531
|
+
return fkSql;
|
|
532
|
+
}
|
|
533
|
+
case 'CHECK':
|
|
534
|
+
if (!constraint.check)
|
|
535
|
+
return null;
|
|
536
|
+
return `${name}CHECK (${constraint.check})`;
|
|
537
|
+
default:
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
getColumnDefinitionSql(tableName, columnName, definition) {
|
|
542
|
+
let sql = `${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type, columnName)}`;
|
|
543
|
+
if (definition.primaryKey) {
|
|
544
|
+
sql += ' PRIMARY KEY';
|
|
545
|
+
}
|
|
546
|
+
if (definition.allowNull === false) {
|
|
547
|
+
sql += ' NOT NULL';
|
|
548
|
+
}
|
|
549
|
+
if (definition.autoIncrement) {
|
|
550
|
+
const seqName = this._autoIncrementSequenceName(tableName, columnName);
|
|
551
|
+
sql += ` DEFAULT nextval(${this.escape(seqName)})`;
|
|
552
|
+
}
|
|
553
|
+
else if (definition.defaultValue !== undefined) {
|
|
554
|
+
sql += ` DEFAULT ${this.getDefaultValue(definition.defaultValue)}`;
|
|
555
|
+
}
|
|
556
|
+
if (definition.unique) {
|
|
557
|
+
sql += ' UNIQUE';
|
|
558
|
+
}
|
|
559
|
+
if (definition.references) {
|
|
560
|
+
const refField = definition.references.field;
|
|
561
|
+
const refFieldSql = Array.isArray(refField)
|
|
562
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
563
|
+
: `(${this.escapeId(refField)})`;
|
|
564
|
+
sql += ` REFERENCES ${this.escapeId(definition.references.table)}${refFieldSql}`;
|
|
565
|
+
if (definition.references.onDelete)
|
|
566
|
+
sql += ` ON DELETE ${definition.references.onDelete}`;
|
|
567
|
+
if (definition.references.onUpdate)
|
|
568
|
+
sql += ` ON UPDATE ${definition.references.onUpdate}`;
|
|
569
|
+
}
|
|
570
|
+
return sql;
|
|
571
|
+
}
|
|
572
|
+
getDefaultValue(value) {
|
|
573
|
+
if (value instanceof prorm_1.Literal) {
|
|
574
|
+
return value.val;
|
|
575
|
+
}
|
|
576
|
+
if (value === null)
|
|
577
|
+
return 'NULL';
|
|
578
|
+
if (typeof value === 'string') {
|
|
579
|
+
if (value.toUpperCase().includes('CURRENT_') || /^nextval\(/i.test(value)) {
|
|
580
|
+
return value;
|
|
581
|
+
}
|
|
582
|
+
return `'${this.escapeString(value)}'`;
|
|
583
|
+
}
|
|
584
|
+
if (typeof value === 'number')
|
|
585
|
+
return String(value);
|
|
586
|
+
if (typeof value === 'boolean')
|
|
587
|
+
return value ? 'TRUE' : 'FALSE';
|
|
588
|
+
if (value instanceof Date)
|
|
589
|
+
return `TIMESTAMP '${value.toISOString().replace('T', ' ').replace('Z', '')}'`;
|
|
590
|
+
if (typeof value === 'object' &&
|
|
591
|
+
value !== null &&
|
|
592
|
+
typeof value.toDefaultValue === 'function') {
|
|
593
|
+
return value.toDefaultValue();
|
|
594
|
+
}
|
|
595
|
+
return `'${this.escapeString(String(value))}'`;
|
|
596
|
+
}
|
|
597
|
+
async dropTable(tableName, options) {
|
|
598
|
+
let sql = 'DROP TABLE';
|
|
599
|
+
if (options?.ifExists)
|
|
600
|
+
sql += ' IF EXISTS';
|
|
601
|
+
sql += ` ${this.escapeId(tableName)}`;
|
|
602
|
+
if (options?.cascade)
|
|
603
|
+
sql += ' CASCADE';
|
|
604
|
+
await this.query(sql);
|
|
605
|
+
}
|
|
606
|
+
// ==================== Partitioning (emulated) ====================
|
|
607
|
+
// DuckDB has no ALTER-TABLE-level partitioning system like PostgreSQL. Its
|
|
608
|
+
// real partitioning concept lives at the storage/IO layer instead - Hive-
|
|
609
|
+
// style partitioned Parquet directories, read via `read_parquet(...,
|
|
610
|
+
// hive_partitioning=true)` and written via `COPY ... TO ... (PARTITION_BY
|
|
611
|
+
// (...))` (see `copyTo`/`readParquet` below). To still satisfy the shared
|
|
612
|
+
// partition-table API (used by cross-dialect model code), the same
|
|
613
|
+
// child-table + metadata-table + UNION ALL view emulation SQLite uses is
|
|
614
|
+
// applied here.
|
|
615
|
+
async createPartitionedTable(tableName, columns, options) {
|
|
616
|
+
const partitions = options?.partitions ?? [];
|
|
617
|
+
await this.createTable(tableName, columns, { ...options, ifNotExists: options?.ifNotExists ?? true });
|
|
618
|
+
const metadataTable = `${tableName}_partitions`;
|
|
619
|
+
await this.query(`CREATE TABLE IF NOT EXISTS ${this.escapeId(metadataTable)} (
|
|
620
|
+
partition_name VARCHAR PRIMARY KEY,
|
|
621
|
+
parent_table VARCHAR NOT NULL,
|
|
622
|
+
bound VARCHAR,
|
|
623
|
+
is_detached BOOLEAN DEFAULT FALSE,
|
|
624
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
625
|
+
)`);
|
|
626
|
+
for (const partition of partitions) {
|
|
627
|
+
await this.createPartition({
|
|
628
|
+
parentTable: tableName,
|
|
629
|
+
name: partition.name,
|
|
630
|
+
bound: partition.bound,
|
|
631
|
+
tablespace: partition.tablespace,
|
|
632
|
+
storageParameters: partition.storageParameters,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (partitions.length > 0) {
|
|
636
|
+
const partitionSelects = partitions
|
|
637
|
+
.map((p) => `SELECT * FROM ${this.escapeId(p.name)}`)
|
|
638
|
+
.join('\n UNION ALL\n ');
|
|
639
|
+
await this.createView(`${tableName}_union`, partitionSelects, { replace: true });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async createPartition(options) {
|
|
643
|
+
const { parentTable, name, bound } = options;
|
|
644
|
+
const parentColumns = await this.describeTable(parentTable);
|
|
645
|
+
const partitionColumns = {};
|
|
646
|
+
for (const [colName, colDef] of Object.entries(parentColumns)) {
|
|
647
|
+
partitionColumns[colName] = {
|
|
648
|
+
type: colDef.type,
|
|
649
|
+
allowNull: colDef.allowNull,
|
|
650
|
+
defaultValue: colDef.defaultValue,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
await this.createTable(name, partitionColumns, { ifNotExists: true });
|
|
654
|
+
const metadataTable = `${parentTable}_partitions`;
|
|
655
|
+
const boundStr = bound ? this.escape(JSON.stringify(bound)) : 'NULL';
|
|
656
|
+
await this.query(`INSERT OR IGNORE INTO ${this.escapeId(metadataTable)} (partition_name, parent_table, bound) VALUES (${this.escape(name)}, ${this.escape(parentTable)}, ${boundStr})`);
|
|
657
|
+
}
|
|
658
|
+
async attachPartition(options) {
|
|
659
|
+
const { parentTable, partitionName } = options;
|
|
660
|
+
const tables = await this.showTables();
|
|
661
|
+
if (!tables.includes(partitionName)) {
|
|
662
|
+
throw new Error(`Partition table '${partitionName}' does not exist`);
|
|
663
|
+
}
|
|
664
|
+
const metadataTable = `${parentTable}_partitions`;
|
|
665
|
+
await this.query(`INSERT INTO ${this.escapeId(metadataTable)} (partition_name, parent_table, is_detached) ` +
|
|
666
|
+
`VALUES (${this.escape(partitionName)}, ${this.escape(parentTable)}, FALSE) ` +
|
|
667
|
+
`ON CONFLICT (partition_name) DO UPDATE SET is_detached = FALSE`);
|
|
668
|
+
}
|
|
669
|
+
async detachPartition(options) {
|
|
670
|
+
const { partitionName } = options;
|
|
671
|
+
const partitions = await this.listPartitions(partitionName);
|
|
672
|
+
if (partitions.length === 0) {
|
|
673
|
+
throw new Error(`Partition '${partitionName}' not found`);
|
|
674
|
+
}
|
|
675
|
+
const metadataTable = `${partitions[0].parent_table}_partitions`;
|
|
676
|
+
await this.query(`UPDATE ${this.escapeId(metadataTable)} SET is_detached = TRUE WHERE partition_name = ${this.escape(partitionName)}`);
|
|
677
|
+
}
|
|
678
|
+
async dropPartition(partitionName, options) {
|
|
679
|
+
const partitions = await this.listPartitions(partitionName);
|
|
680
|
+
const parentTable = partitions[0]?.parent_table ?? null;
|
|
681
|
+
let sql = 'DROP TABLE';
|
|
682
|
+
if (options?.ifExists)
|
|
683
|
+
sql += ' IF EXISTS';
|
|
684
|
+
sql += ` ${this.escapeId(partitionName)}`;
|
|
685
|
+
if (options?.cascade)
|
|
686
|
+
sql += ' CASCADE';
|
|
687
|
+
await this.query(sql);
|
|
688
|
+
if (parentTable) {
|
|
689
|
+
const metadataTable = `${parentTable}_partitions`;
|
|
690
|
+
try {
|
|
691
|
+
await this.query(`DELETE FROM ${this.escapeId(metadataTable)} WHERE partition_name = ${this.escape(partitionName)}`);
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
// Ignore if metadata table doesn't exist
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async listPartitions(tableName) {
|
|
699
|
+
const metadataTable = `${tableName}_partitions`;
|
|
700
|
+
try {
|
|
701
|
+
const result = await this.query(`SELECT * FROM ${this.escapeId(metadataTable)}`);
|
|
702
|
+
return result.rows.map((row) => ({
|
|
703
|
+
name: row.partition_name,
|
|
704
|
+
parent_table: row.parent_table,
|
|
705
|
+
bound: row.bound,
|
|
706
|
+
is_detached: row.is_detached,
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
return [];
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// ==================== Views ====================
|
|
714
|
+
async createView(viewName, query, options) {
|
|
715
|
+
let sql = options?.replace ? 'CREATE OR REPLACE VIEW' : 'CREATE VIEW';
|
|
716
|
+
sql += ` ${options?.schema ? this.quoteTable(viewName, options.schema) : this.escapeId(viewName)} AS ${query}`;
|
|
717
|
+
await this.query(sql);
|
|
718
|
+
}
|
|
719
|
+
async dropView(viewName, options) {
|
|
720
|
+
let sql = 'DROP VIEW';
|
|
721
|
+
if (options?.ifExists)
|
|
722
|
+
sql += ' IF EXISTS';
|
|
723
|
+
sql += ` ${this.escapeId(viewName)}`;
|
|
724
|
+
if (options?.cascade)
|
|
725
|
+
sql += ' CASCADE';
|
|
726
|
+
await this.query(sql);
|
|
727
|
+
}
|
|
728
|
+
async showViews() {
|
|
729
|
+
const result = await this.query(`SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'`);
|
|
730
|
+
return result.rows.map((row) => row.table_name);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Materialized views (not supported). DuckDB has no `CREATE MATERIALIZED
|
|
734
|
+
* VIEW` statement - the closest equivalents are a plain table populated via
|
|
735
|
+
* `CREATE TABLE ... AS SELECT ...` (refreshed by re-running the query) or
|
|
736
|
+
* exporting to Parquet with `copyTo()` and reading it back with
|
|
737
|
+
* `readParquet()`.
|
|
738
|
+
*/
|
|
739
|
+
async createMaterializedView() {
|
|
740
|
+
throw new Error('DuckDB does not support materialized views. Use `CREATE TABLE ... AS SELECT ...` ' +
|
|
741
|
+
'(re-run to refresh) or `copyTo()` to a Parquet file plus `readParquet()` to read it back.');
|
|
742
|
+
}
|
|
743
|
+
async refreshMaterializedView() {
|
|
744
|
+
throw new Error('DuckDB does not support materialized views.');
|
|
745
|
+
}
|
|
746
|
+
async dropMaterializedView() {
|
|
747
|
+
throw new Error('DuckDB does not support materialized views.');
|
|
748
|
+
}
|
|
749
|
+
async hasMaterializedView() {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
async showMaterializedViews() {
|
|
753
|
+
return [];
|
|
754
|
+
}
|
|
755
|
+
// ==================== Columns ====================
|
|
756
|
+
async addColumn(tableName, columnName, definition) {
|
|
757
|
+
const sql = `ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${this.getColumnDefinitionSql(tableName, columnName, definition)}`;
|
|
758
|
+
await this.query(sql);
|
|
759
|
+
}
|
|
760
|
+
async removeColumn(tableName, columnName) {
|
|
761
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} DROP COLUMN ${this.escapeId(columnName)}`);
|
|
762
|
+
}
|
|
763
|
+
async changeColumn(tableName, columnName, definition) {
|
|
764
|
+
// DuckDB supports ALTER TABLE ... ALTER COLUMN ... TYPE / SET/DROP DEFAULT
|
|
765
|
+
// / SET/DROP NOT NULL as separate statements (no single combined form).
|
|
766
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} TYPE ${this.getDataTypeSql(definition.type, columnName)}`);
|
|
767
|
+
if (definition.defaultValue !== undefined) {
|
|
768
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} SET DEFAULT ${this.getDefaultValue(definition.defaultValue)}`);
|
|
769
|
+
}
|
|
770
|
+
if (definition.allowNull === false) {
|
|
771
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} SET NOT NULL`);
|
|
772
|
+
}
|
|
773
|
+
else if (definition.allowNull === true) {
|
|
774
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} DROP NOT NULL`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async renameColumn(tableName, oldColumnName, newColumnName) {
|
|
778
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} RENAME COLUMN ${this.escapeId(oldColumnName)} TO ${this.escapeId(newColumnName)}`);
|
|
779
|
+
}
|
|
780
|
+
async addForeignKey(tableName, _columnName, _referencedTableName, _referencedColumnName) {
|
|
781
|
+
throw new Error(`DuckDB does not support adding foreign keys to an existing table ("${tableName}") via ALTER TABLE. ` +
|
|
782
|
+
`Define the foreign key when creating the table instead.`);
|
|
783
|
+
}
|
|
784
|
+
async bulkInsert(tableName, records, _options) {
|
|
785
|
+
if (records.length === 0) {
|
|
786
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
787
|
+
}
|
|
788
|
+
const columns = Object.keys(records[0]);
|
|
789
|
+
const placeholders = records.map(() => `(${columns.map(() => '?').join(', ')})`);
|
|
790
|
+
const values = records.flatMap((record) => columns.map((col) => record[col]));
|
|
791
|
+
const sql = `INSERT INTO ${this.quoteTable(tableName)} (${columns
|
|
792
|
+
.map((c) => this.escapeId(c))
|
|
793
|
+
.join(', ')}) VALUES ${placeholders.join(', ')}`;
|
|
794
|
+
return this.query(sql, { replacements: values });
|
|
795
|
+
}
|
|
796
|
+
async showTables() {
|
|
797
|
+
const result = await this.query(`SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'`);
|
|
798
|
+
return result.rows.map((row) => row.table_name);
|
|
799
|
+
}
|
|
800
|
+
async showConstraints(tableName) {
|
|
801
|
+
const result = await this.query(`SELECT constraint_name, constraint_type FROM duckdb_constraints() WHERE table_name = ?`, { replacements: [tableName] });
|
|
802
|
+
return result.rows;
|
|
803
|
+
}
|
|
804
|
+
async addConstraint(tableName, options) {
|
|
805
|
+
const name = options.name ? `CONSTRAINT ${this.escapeId(options.name)} ` : '';
|
|
806
|
+
let clause = '';
|
|
807
|
+
switch (options.type) {
|
|
808
|
+
case 'UNIQUE':
|
|
809
|
+
clause = `${name}UNIQUE (${options.fields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
810
|
+
break;
|
|
811
|
+
case 'PRIMARY KEY':
|
|
812
|
+
clause = `${name}PRIMARY KEY (${options.fields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
813
|
+
break;
|
|
814
|
+
case 'CHECK':
|
|
815
|
+
clause = `${name}CHECK (${options.check})`;
|
|
816
|
+
break;
|
|
817
|
+
case 'FOREIGN KEY':
|
|
818
|
+
if (!options.references)
|
|
819
|
+
throw new Error('references required for FOREIGN KEY constraint');
|
|
820
|
+
clause = `${name}FOREIGN KEY (${options.fields.map((f) => this.escapeId(f)).join(', ')}) REFERENCES ${this.escapeId(options.references.table)} (${options.references.fields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} ADD ${clause}`);
|
|
824
|
+
}
|
|
825
|
+
async removeConstraint(tableName, constraintName) {
|
|
826
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} DROP CONSTRAINT ${this.escapeId(constraintName)}`);
|
|
827
|
+
}
|
|
828
|
+
async showIndexes(tableName) {
|
|
829
|
+
const result = await this.query(`SELECT * FROM duckdb_indexes() WHERE table_name = ?`, {
|
|
830
|
+
replacements: [tableName],
|
|
831
|
+
});
|
|
832
|
+
return result.rows;
|
|
833
|
+
}
|
|
834
|
+
async describeTable(tableName) {
|
|
835
|
+
const result = await this.query(`DESCRIBE ${this.escapeId(tableName)}`);
|
|
836
|
+
const description = {};
|
|
837
|
+
for (const row of result.rows) {
|
|
838
|
+
const name = row.column_name;
|
|
839
|
+
const nullStr = String(row.null ?? 'YES').toUpperCase();
|
|
840
|
+
const keyStr = String(row.key ?? '');
|
|
841
|
+
description[name] = {
|
|
842
|
+
type: row.column_type || 'VARCHAR',
|
|
843
|
+
allowNull: nullStr !== 'NO',
|
|
844
|
+
defaultValue: row.default,
|
|
845
|
+
primaryKey: keyStr.includes('PRI'),
|
|
846
|
+
autoIncrement: typeof row.default === 'string' && /nextval\(/i.test(row.default),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
return description;
|
|
850
|
+
}
|
|
851
|
+
async renameTable(oldName, newName) {
|
|
852
|
+
await this.query(`ALTER TABLE ${this.escapeId(oldName)} RENAME TO ${this.escapeId(newName)}`);
|
|
853
|
+
}
|
|
854
|
+
async addIndex(tableName, indexName, fields = [], options) {
|
|
855
|
+
let sql = 'CREATE';
|
|
856
|
+
if (options?.unique)
|
|
857
|
+
sql += ' UNIQUE';
|
|
858
|
+
sql += ' INDEX';
|
|
859
|
+
if (options?.ifNotExists !== false)
|
|
860
|
+
sql += ' IF NOT EXISTS';
|
|
861
|
+
sql += ` ${this.escapeId(indexName)} ON ${this.escapeId(tableName)}`;
|
|
862
|
+
if (options?.expression) {
|
|
863
|
+
sql += ` (${options.expression})`;
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
sql += ` (${fields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
867
|
+
}
|
|
868
|
+
// Note: DuckDB indexes do not support partial (WHERE) index predicates;
|
|
869
|
+
// silently ignoring `options.where` here would hide data-shape bugs, so
|
|
870
|
+
// surface it clearly instead.
|
|
871
|
+
if (options?.where) {
|
|
872
|
+
throw new Error('DuckDB does not support partial indexes (WHERE clause on CREATE INDEX).');
|
|
873
|
+
}
|
|
874
|
+
await this.query(sql);
|
|
875
|
+
}
|
|
876
|
+
async removeIndex(_tableName, indexName) {
|
|
877
|
+
await this.query(`DROP INDEX ${this.escapeId(indexName)}`);
|
|
878
|
+
}
|
|
879
|
+
async createIndex(tableName, indexDef) {
|
|
880
|
+
await this.addIndex(tableName, indexDef.name, indexDef.fields, {
|
|
881
|
+
unique: indexDef.unique,
|
|
882
|
+
type: indexDef.type,
|
|
883
|
+
using: indexDef.using,
|
|
884
|
+
where: indexDef.where,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
async createFulltextIndex() {
|
|
888
|
+
throw new Error('DuckDB does not have a built-in CREATE FULLTEXT INDEX statement. Install/load the ' +
|
|
889
|
+
"'fts' extension and use its `PRAGMA create_fts_index(...)` function instead.");
|
|
890
|
+
}
|
|
891
|
+
async createSpatialIndex() {
|
|
892
|
+
throw new Error("DuckDB spatial indexing requires the 'spatial' extension's RTREE index type " +
|
|
893
|
+
"(CREATE INDEX ... USING RTREE (...)); use createExtension('spatial') then addIndex " +
|
|
894
|
+
"with { using: 'RTREE' }.");
|
|
895
|
+
}
|
|
896
|
+
async dropIndex(_tableName, indexName, _options) {
|
|
897
|
+
await this.query(`DROP INDEX ${this.escapeId(indexName)}`);
|
|
898
|
+
}
|
|
899
|
+
async createConstraint(tableName, constraintDef) {
|
|
900
|
+
await this.addConstraint(tableName, {
|
|
901
|
+
type: constraintDef.type,
|
|
902
|
+
fields: constraintDef.fields || [],
|
|
903
|
+
name: constraintDef.name,
|
|
904
|
+
references: constraintDef.references
|
|
905
|
+
? { table: constraintDef.references.table, fields: [constraintDef.references.field] }
|
|
906
|
+
: undefined,
|
|
907
|
+
check: constraintDef.check,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
async dropConstraint(tableName, constraintName, options) {
|
|
911
|
+
let sql = `ALTER TABLE ${this.escapeId(tableName)} DROP CONSTRAINT`;
|
|
912
|
+
if (options?.ifExists)
|
|
913
|
+
sql += ' IF EXISTS';
|
|
914
|
+
sql += ` ${this.escapeId(constraintName)}`;
|
|
915
|
+
await this.query(sql);
|
|
916
|
+
}
|
|
917
|
+
async changeOwner() {
|
|
918
|
+
throw new Error('DuckDB has no per-table ownership model to change (it is a single-process embedded database).');
|
|
919
|
+
}
|
|
920
|
+
// ==================== Foreign Data Wrappers (PostgreSQL-only feature) ====================
|
|
921
|
+
fdwNotSupported() {
|
|
922
|
+
throw new Error('Foreign Data Wrappers are not supported by DuckDB. This is a PostgreSQL-specific feature.');
|
|
923
|
+
}
|
|
924
|
+
async createForeignDataWrapper() {
|
|
925
|
+
this.fdwNotSupported();
|
|
926
|
+
}
|
|
927
|
+
async dropForeignDataWrapper() {
|
|
928
|
+
this.fdwNotSupported();
|
|
929
|
+
}
|
|
930
|
+
async createForeignServer() {
|
|
931
|
+
this.fdwNotSupported();
|
|
932
|
+
}
|
|
933
|
+
async dropForeignServer() {
|
|
934
|
+
this.fdwNotSupported();
|
|
935
|
+
}
|
|
936
|
+
async createForeignTable() {
|
|
937
|
+
this.fdwNotSupported();
|
|
938
|
+
}
|
|
939
|
+
buildCreateServerQuery(_name, _opts) {
|
|
940
|
+
this.fdwNotSupported();
|
|
941
|
+
}
|
|
942
|
+
buildAlterServerQuery(_name, _opts) {
|
|
943
|
+
this.fdwNotSupported();
|
|
944
|
+
}
|
|
945
|
+
buildDropServerQuery() {
|
|
946
|
+
this.fdwNotSupported();
|
|
947
|
+
}
|
|
948
|
+
buildCreateUserMappingQuery(_opts) {
|
|
949
|
+
this.fdwNotSupported();
|
|
950
|
+
}
|
|
951
|
+
buildAlterUserMappingQuery(_opts) {
|
|
952
|
+
this.fdwNotSupported();
|
|
953
|
+
}
|
|
954
|
+
buildDropUserMappingQuery() {
|
|
955
|
+
this.fdwNotSupported();
|
|
956
|
+
}
|
|
957
|
+
async createUserMapping() {
|
|
958
|
+
this.fdwNotSupported();
|
|
959
|
+
}
|
|
960
|
+
async dropUserMapping() {
|
|
961
|
+
this.fdwNotSupported();
|
|
962
|
+
}
|
|
963
|
+
buildCreateForeignTableQuery(_tableName, _opts) {
|
|
964
|
+
this.fdwNotSupported();
|
|
965
|
+
}
|
|
966
|
+
buildDropForeignTableQuery() {
|
|
967
|
+
this.fdwNotSupported();
|
|
968
|
+
}
|
|
969
|
+
buildImportForeignSchemaQuery(_remoteSchema, _serverName, _opts) {
|
|
970
|
+
this.fdwNotSupported();
|
|
971
|
+
}
|
|
972
|
+
getServersQuery() {
|
|
973
|
+
this.fdwNotSupported();
|
|
974
|
+
}
|
|
975
|
+
// ==================== Security policies (MSSQL RLS-style feature) ====================
|
|
976
|
+
async createSecurityPolicy() {
|
|
977
|
+
throw new Error('Row-level security policies are not supported by DuckDB.');
|
|
978
|
+
}
|
|
979
|
+
async dropSecurityPolicy() {
|
|
980
|
+
throw new Error('Row-level security policies are not supported by DuckDB.');
|
|
981
|
+
}
|
|
982
|
+
// ==================== User / privilege management ====================
|
|
983
|
+
// DuckDB is embedded and single-process/single-user: there is no user or
|
|
984
|
+
// role system to manage.
|
|
985
|
+
userMgmtNotSupported() {
|
|
986
|
+
throw new Error('User/role management is not supported by DuckDB (it is an embedded, single-user database).');
|
|
987
|
+
}
|
|
988
|
+
buildCreateUserQuery() {
|
|
989
|
+
this.userMgmtNotSupported();
|
|
990
|
+
}
|
|
991
|
+
buildAlterUserQuery() {
|
|
992
|
+
this.userMgmtNotSupported();
|
|
993
|
+
}
|
|
994
|
+
buildDropUserQuery() {
|
|
995
|
+
this.userMgmtNotSupported();
|
|
996
|
+
}
|
|
997
|
+
getUsersQuery() {
|
|
998
|
+
this.userMgmtNotSupported();
|
|
999
|
+
}
|
|
1000
|
+
buildGrantQuery() {
|
|
1001
|
+
this.userMgmtNotSupported();
|
|
1002
|
+
}
|
|
1003
|
+
buildRevokeQuery() {
|
|
1004
|
+
this.userMgmtNotSupported();
|
|
1005
|
+
}
|
|
1006
|
+
buildShowGrantsQuery() {
|
|
1007
|
+
this.userMgmtNotSupported();
|
|
1008
|
+
}
|
|
1009
|
+
buildFlushPrivilegesQuery() {
|
|
1010
|
+
this.userMgmtNotSupported();
|
|
1011
|
+
}
|
|
1012
|
+
buildCreateRoleQuery() {
|
|
1013
|
+
this.userMgmtNotSupported();
|
|
1014
|
+
}
|
|
1015
|
+
buildDropRoleQuery() {
|
|
1016
|
+
this.userMgmtNotSupported();
|
|
1017
|
+
}
|
|
1018
|
+
buildGrantRoleQuery() {
|
|
1019
|
+
this.userMgmtNotSupported();
|
|
1020
|
+
}
|
|
1021
|
+
buildRevokeRoleQuery() {
|
|
1022
|
+
this.userMgmtNotSupported();
|
|
1023
|
+
}
|
|
1024
|
+
getRolesQuery() {
|
|
1025
|
+
this.userMgmtNotSupported();
|
|
1026
|
+
}
|
|
1027
|
+
// ==================== Stored procedures ====================
|
|
1028
|
+
// DuckDB has no procedural SQL language. Its closest analogue is a scalar/
|
|
1029
|
+
// table MACRO (`CREATE MACRO name(args) AS expr`), which is expression-only
|
|
1030
|
+
// (no statement bodies, loops, or side effects), so it cannot faithfully
|
|
1031
|
+
// stand in for a stored procedure - fail clearly rather than pretend.
|
|
1032
|
+
async createStoredProcedure(options) {
|
|
1033
|
+
throw new Error(`DuckDB does not support stored procedures. Cannot create '${options.name}'.\n\n` +
|
|
1034
|
+
`Workarounds:\n` +
|
|
1035
|
+
`- Move logic to application code\n` +
|
|
1036
|
+
`- Use a scalar/table MACRO for simple reusable expressions: ` +
|
|
1037
|
+
`CREATE MACRO ${options.name}(...) AS (...)\n` +
|
|
1038
|
+
`- Use views for read-only reusable queries`);
|
|
1039
|
+
}
|
|
1040
|
+
async createProcedure(options) {
|
|
1041
|
+
return this.createStoredProcedure(options);
|
|
1042
|
+
}
|
|
1043
|
+
async dropStoredProcedure(procedureName, _options) {
|
|
1044
|
+
throw new Error(`DuckDB does not support stored procedures. Cannot drop '${procedureName}'.`);
|
|
1045
|
+
}
|
|
1046
|
+
async dropProcedure(procedureName, options) {
|
|
1047
|
+
return this.dropStoredProcedure(procedureName, options);
|
|
1048
|
+
}
|
|
1049
|
+
async executeStoredProcedure(options) {
|
|
1050
|
+
throw new Error(`DuckDB does not support stored procedures. Cannot execute '${options.procedureName}'. ` +
|
|
1051
|
+
`Run the equivalent SQL directly via query(), or define a MACRO.`);
|
|
1052
|
+
}
|
|
1053
|
+
async hasStoredProcedure() {
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
// ==================== Triggers (not supported) ====================
|
|
1057
|
+
async createTrigger(options) {
|
|
1058
|
+
throw new Error(`DuckDB does not support triggers. Cannot create '${options.name}' on '${options.tableName}'. ` +
|
|
1059
|
+
`Move the reactive logic into application code around the write path instead.`);
|
|
1060
|
+
}
|
|
1061
|
+
async dropTrigger(triggerName, _tableName, _options) {
|
|
1062
|
+
throw new Error(`DuckDB does not support triggers. Cannot drop '${triggerName}'.`);
|
|
1063
|
+
}
|
|
1064
|
+
async hasTrigger() {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
// ==================== Sequences (real, native DuckDB feature) ====================
|
|
1068
|
+
async createSequence(options) {
|
|
1069
|
+
let sql = 'CREATE';
|
|
1070
|
+
if (options.replace)
|
|
1071
|
+
sql += ' OR REPLACE';
|
|
1072
|
+
sql += ' SEQUENCE';
|
|
1073
|
+
if (options.ifNotExists)
|
|
1074
|
+
sql += ' IF NOT EXISTS';
|
|
1075
|
+
sql += ` ${this.quoteTable(options.name, options.schema)}`;
|
|
1076
|
+
if (options.startWith !== undefined)
|
|
1077
|
+
sql += ` START WITH ${options.startWith}`;
|
|
1078
|
+
if (options.incrementBy !== undefined)
|
|
1079
|
+
sql += ` INCREMENT BY ${options.incrementBy}`;
|
|
1080
|
+
if (options.minvalue !== undefined)
|
|
1081
|
+
sql += ` MINVALUE ${options.minvalue}`;
|
|
1082
|
+
if (options.maxvalue !== undefined)
|
|
1083
|
+
sql += ` MAXVALUE ${options.maxvalue}`;
|
|
1084
|
+
if (options.cycle)
|
|
1085
|
+
sql += ' CYCLE';
|
|
1086
|
+
await this.query(sql);
|
|
1087
|
+
}
|
|
1088
|
+
async dropSequence(sequenceName, options) {
|
|
1089
|
+
let sql = 'DROP SEQUENCE';
|
|
1090
|
+
if (options?.ifExists)
|
|
1091
|
+
sql += ' IF EXISTS';
|
|
1092
|
+
sql += ` ${this.quoteTable(sequenceName, options?.schema)}`;
|
|
1093
|
+
if (options?.cascade)
|
|
1094
|
+
sql += ' CASCADE';
|
|
1095
|
+
await this.query(sql);
|
|
1096
|
+
}
|
|
1097
|
+
async nextSequenceValue(sequenceName) {
|
|
1098
|
+
const result = await this.query(`SELECT nextval(${this.escape(sequenceName)}) AS value`);
|
|
1099
|
+
return Number(result.rows[0].value);
|
|
1100
|
+
}
|
|
1101
|
+
async hasSequence(sequenceName, schema) {
|
|
1102
|
+
const result = await this.query(`SELECT 1 FROM duckdb_sequences() WHERE sequence_name = ?${schema ? ' AND schema_name = ?' : ''}`, { replacements: (schema ? [sequenceName, schema] : [sequenceName]) });
|
|
1103
|
+
return result.rows.length > 0;
|
|
1104
|
+
}
|
|
1105
|
+
async listSequences() {
|
|
1106
|
+
const result = await this.query(`SELECT sequence_name FROM duckdb_sequences()`);
|
|
1107
|
+
return result.rows.map((row) => row.sequence_name);
|
|
1108
|
+
}
|
|
1109
|
+
// ==================== Row-level security (not supported) ====================
|
|
1110
|
+
async createPolicy() {
|
|
1111
|
+
throw new Error('Row-level security is not supported by DuckDB.');
|
|
1112
|
+
}
|
|
1113
|
+
async dropPolicy() {
|
|
1114
|
+
throw new Error('Row-level security is not supported by DuckDB.');
|
|
1115
|
+
}
|
|
1116
|
+
async enableRLS() {
|
|
1117
|
+
throw new Error('Row-level security is not supported by DuckDB.');
|
|
1118
|
+
}
|
|
1119
|
+
async disableRLS() {
|
|
1120
|
+
throw new Error('Row-level security is not supported by DuckDB.');
|
|
1121
|
+
}
|
|
1122
|
+
async hasPolicy() {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
// ==================== Comments ====================
|
|
1126
|
+
async commentTable(tableName, comment) {
|
|
1127
|
+
await this.query(`COMMENT ON TABLE ${this.escapeId(tableName)} IS ${this.escape(comment)}`);
|
|
1128
|
+
}
|
|
1129
|
+
async commentColumn(tableName, columnName, comment) {
|
|
1130
|
+
await this.query(`COMMENT ON COLUMN ${this.escapeId(tableName)}.${this.escapeId(columnName)} IS ${this.escape(comment)}`);
|
|
1131
|
+
}
|
|
1132
|
+
// ==================== Advanced indexes ====================
|
|
1133
|
+
async createPartialIndex() {
|
|
1134
|
+
throw new Error('DuckDB does not support partial indexes (WHERE clause on CREATE INDEX).');
|
|
1135
|
+
}
|
|
1136
|
+
async createExpressionIndex(tableName, indexName, expression, options) {
|
|
1137
|
+
let sql = 'CREATE';
|
|
1138
|
+
if (options?.unique)
|
|
1139
|
+
sql += ' UNIQUE';
|
|
1140
|
+
sql += ` INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)} (${expression})`;
|
|
1141
|
+
await this.query(sql);
|
|
1142
|
+
}
|
|
1143
|
+
// ==================== Identity / computed columns ====================
|
|
1144
|
+
async createIdentityColumn(tableName, columnName, options) {
|
|
1145
|
+
// DuckDB has no GENERATED ... AS IDENTITY syntax; emulate with a sequence-backed default.
|
|
1146
|
+
const seqName = this._autoIncrementSequenceName(tableName, columnName);
|
|
1147
|
+
await this.query(`CREATE SEQUENCE IF NOT EXISTS ${this.escapeId(seqName)} START ${options?.startWith ?? 1} INCREMENT ${options?.incrementBy ?? 1}`);
|
|
1148
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} SET DEFAULT nextval(${this.escape(seqName)})`);
|
|
1149
|
+
}
|
|
1150
|
+
async createComputedColumn(tableName, columnName, expression, options) {
|
|
1151
|
+
const typeSql = options?.type || 'VARCHAR';
|
|
1152
|
+
const modeSql = options?.persisted ? '' : ' VIRTUAL';
|
|
1153
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${this.escapeId(columnName)} ${typeSql} GENERATED ALWAYS AS (${expression})${modeSql}`);
|
|
1154
|
+
}
|
|
1155
|
+
// ==================== Transactions ====================
|
|
1156
|
+
async startTransaction(options) {
|
|
1157
|
+
if (!this.connection) {
|
|
1158
|
+
throw new Error('Not connected to database');
|
|
1159
|
+
}
|
|
1160
|
+
const transaction = new DuckDBTransaction(this.transactionDepth++, options, this);
|
|
1161
|
+
if (this.transactionDepth === 1) {
|
|
1162
|
+
await this.query('BEGIN TRANSACTION');
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
await this.query(`SAVEPOINT sp_${transaction.id}`);
|
|
1166
|
+
}
|
|
1167
|
+
return transaction;
|
|
1168
|
+
}
|
|
1169
|
+
async commitTransaction(transaction) {
|
|
1170
|
+
const tx = transaction;
|
|
1171
|
+
if (this.transactionDepth === 1) {
|
|
1172
|
+
await this.query('COMMIT');
|
|
1173
|
+
}
|
|
1174
|
+
else {
|
|
1175
|
+
await this.query(`RELEASE SAVEPOINT sp_${tx.id}`);
|
|
1176
|
+
}
|
|
1177
|
+
this.transactionDepth = Math.max(0, this.transactionDepth - 1);
|
|
1178
|
+
}
|
|
1179
|
+
async rollbackTransaction(transaction) {
|
|
1180
|
+
const tx = transaction;
|
|
1181
|
+
if (this.transactionDepth === 1) {
|
|
1182
|
+
await this.query('ROLLBACK');
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
await this.query(`ROLLBACK TO SAVEPOINT sp_${tx.id}`);
|
|
1186
|
+
}
|
|
1187
|
+
this.transactionDepth = Math.max(0, this.transactionDepth - 1);
|
|
1188
|
+
}
|
|
1189
|
+
// ==================== Data types ====================
|
|
1190
|
+
getDataTypeSql(dataType, columnName) {
|
|
1191
|
+
if (typeof dataType === 'string') {
|
|
1192
|
+
return dataType;
|
|
1193
|
+
}
|
|
1194
|
+
if (typeof dataType === 'function') {
|
|
1195
|
+
try {
|
|
1196
|
+
dataType = dataType();
|
|
1197
|
+
}
|
|
1198
|
+
catch {
|
|
1199
|
+
return 'VARCHAR';
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
if (!dataType || typeof dataType !== 'object') {
|
|
1203
|
+
return 'VARCHAR';
|
|
1204
|
+
}
|
|
1205
|
+
const dt = dataType;
|
|
1206
|
+
switch (dt.key) {
|
|
1207
|
+
case 'STRING':
|
|
1208
|
+
return `VARCHAR(${dt.length || 255})`;
|
|
1209
|
+
case 'CHAR':
|
|
1210
|
+
return `VARCHAR(${dt.length || 1})`;
|
|
1211
|
+
case 'TEXT':
|
|
1212
|
+
return 'VARCHAR';
|
|
1213
|
+
case 'INTEGER':
|
|
1214
|
+
return dt.unsigned ? 'UINTEGER' : 'INTEGER';
|
|
1215
|
+
case 'BIGINT':
|
|
1216
|
+
return dt.unsigned ? 'UBIGINT' : 'BIGINT';
|
|
1217
|
+
case 'FLOAT':
|
|
1218
|
+
return 'REAL';
|
|
1219
|
+
case 'DOUBLE':
|
|
1220
|
+
return 'DOUBLE';
|
|
1221
|
+
case 'DECIMAL':
|
|
1222
|
+
return `DECIMAL(${dt.precision || 18},${dt.scale || 0})`;
|
|
1223
|
+
case 'BOOLEAN':
|
|
1224
|
+
return 'BOOLEAN';
|
|
1225
|
+
case 'DATE':
|
|
1226
|
+
return 'TIMESTAMP';
|
|
1227
|
+
case 'DATEONLY':
|
|
1228
|
+
return 'DATE';
|
|
1229
|
+
case 'TIME':
|
|
1230
|
+
return 'TIME';
|
|
1231
|
+
case 'BLOB':
|
|
1232
|
+
return 'BLOB';
|
|
1233
|
+
case 'ENUM': {
|
|
1234
|
+
// DuckDB supports a native ENUM type, but only via a separately named
|
|
1235
|
+
// type (`CREATE TYPE mood AS ENUM (...)`), which would require a
|
|
1236
|
+
// second DDL statement per enum column and out-of-band lifecycle
|
|
1237
|
+
// management. To keep column definitions self-contained (matching
|
|
1238
|
+
// how every other option here works as a single inline column def),
|
|
1239
|
+
// this uses the same VARCHAR + CHECK emulation SQLite uses rather
|
|
1240
|
+
// than a named DuckDB ENUM type.
|
|
1241
|
+
const colRef = columnName ? this.escapeId(columnName) : 'value';
|
|
1242
|
+
const values = dt.values;
|
|
1243
|
+
return `VARCHAR CHECK(${values ? `${colRef} IN (${values.map((v) => `'${this.escapeString(String(v))}'`).join(',')})` : '1=1'})`;
|
|
1244
|
+
}
|
|
1245
|
+
case 'JSON':
|
|
1246
|
+
case 'JSONB':
|
|
1247
|
+
return 'JSON';
|
|
1248
|
+
case 'UUID':
|
|
1249
|
+
return 'UUID';
|
|
1250
|
+
case 'GEOMETRY':
|
|
1251
|
+
// Requires the 'spatial' extension's GEOMETRY type; falls back to
|
|
1252
|
+
// BLOB when unloaded so schema creation doesn't hard-fail.
|
|
1253
|
+
return 'BLOB';
|
|
1254
|
+
case 'ARRAY':
|
|
1255
|
+
case 'LIST': {
|
|
1256
|
+
// DuckDB LIST columns are written as `<elementType>[]`. Accept both
|
|
1257
|
+
// DataTypes.LIST(elementType)/DataTypes.ARRAY(elementType) (which
|
|
1258
|
+
// expose the element type as `.type`) and a plain
|
|
1259
|
+
// `{ type: 'LIST', subtype: ... }` shape for parity with how other
|
|
1260
|
+
// dialects (e.g. Postgres' ARRAY handling above) accept `.subtype`.
|
|
1261
|
+
const elementType = dt.type ?? dt.subtype;
|
|
1262
|
+
if (elementType) {
|
|
1263
|
+
return `${this.getDataTypeSql(elementType, columnName)}[]`;
|
|
1264
|
+
}
|
|
1265
|
+
return 'VARCHAR[]';
|
|
1266
|
+
}
|
|
1267
|
+
case 'STRUCT': {
|
|
1268
|
+
// DuckDB STRUCT columns are written as `STRUCT(field TYPE, ...)`.
|
|
1269
|
+
const fields = dt.fields;
|
|
1270
|
+
if (fields && Object.keys(fields).length > 0) {
|
|
1271
|
+
const parts = Object.entries(fields).map(([name, type]) => `${this.escapeId(name)} ${this.getDataTypeSql(type, name)}`);
|
|
1272
|
+
return `STRUCT(${parts.join(', ')})`;
|
|
1273
|
+
}
|
|
1274
|
+
return 'STRUCT()';
|
|
1275
|
+
}
|
|
1276
|
+
case 'MAP': {
|
|
1277
|
+
// DuckDB MAP columns are written as `MAP(keyType, valueType)`.
|
|
1278
|
+
const keyType = dt.keyType;
|
|
1279
|
+
const valueType = dt.valueType;
|
|
1280
|
+
if (keyType && valueType) {
|
|
1281
|
+
return `MAP(${this.getDataTypeSql(keyType, columnName)}, ${this.getDataTypeSql(valueType, columnName)})`;
|
|
1282
|
+
}
|
|
1283
|
+
return 'MAP(VARCHAR, VARCHAR)';
|
|
1284
|
+
}
|
|
1285
|
+
case 'UNION': {
|
|
1286
|
+
// DuckDB UNION columns are written as `UNION(tag TYPE, ...)`.
|
|
1287
|
+
const fields = dt.fields;
|
|
1288
|
+
if (fields && Object.keys(fields).length > 0) {
|
|
1289
|
+
const parts = Object.entries(fields).map(([tag, type]) => `${this.escapeId(tag)} ${this.getDataTypeSql(type, tag)}`);
|
|
1290
|
+
return `UNION(${parts.join(', ')})`;
|
|
1291
|
+
}
|
|
1292
|
+
return 'VARCHAR';
|
|
1293
|
+
}
|
|
1294
|
+
case 'VIRTUAL':
|
|
1295
|
+
return '';
|
|
1296
|
+
default:
|
|
1297
|
+
return 'VARCHAR';
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
// ==================== Query builders (WHERE/ORDER/LIMIT/INSERT/UPDATE/DELETE/SELECT) ====================
|
|
1301
|
+
// These mirror SQLite's builder implementations closely - DuckDB shares the
|
|
1302
|
+
// same `?` positional placeholder convention and a very similar SQL dialect
|
|
1303
|
+
// for basic DML - but use DuckDB-correct literals (TRUE/FALSE, etc.) via
|
|
1304
|
+
// `escape()`/`escapeId()` above.
|
|
1305
|
+
buildWhereClause(where, _options) {
|
|
1306
|
+
const values = [];
|
|
1307
|
+
if (!where || (Object.keys(where).length === 0 && Object.getOwnPropertySymbols(where).length === 0)) {
|
|
1308
|
+
return { sql: '', values };
|
|
1309
|
+
}
|
|
1310
|
+
const buildCondition = (condition) => {
|
|
1311
|
+
if (!condition)
|
|
1312
|
+
return { sql: '' };
|
|
1313
|
+
if (typeof condition !== 'object') {
|
|
1314
|
+
values.push(condition);
|
|
1315
|
+
return { sql: '?' };
|
|
1316
|
+
}
|
|
1317
|
+
const rawCond = condition;
|
|
1318
|
+
const topSymbols = Object.getOwnPropertySymbols(rawCond);
|
|
1319
|
+
const normalizedCond = { ...rawCond };
|
|
1320
|
+
for (const sym of topSymbols) {
|
|
1321
|
+
const strKey = (0, operators_1.operatorToWhereKey)(sym);
|
|
1322
|
+
if (strKey)
|
|
1323
|
+
normalizedCond[strKey] = rawCond[sym];
|
|
1324
|
+
}
|
|
1325
|
+
const cond = normalizedCond;
|
|
1326
|
+
if (cond.$and || cond.$or || cond.$not) {
|
|
1327
|
+
const conditions = [];
|
|
1328
|
+
if (cond.$and) {
|
|
1329
|
+
const parts = cond.$and.map((c) => buildCondition(c).sql);
|
|
1330
|
+
conditions.push(`(${parts.join(' AND ')})`);
|
|
1331
|
+
}
|
|
1332
|
+
if (cond.$or) {
|
|
1333
|
+
const parts = cond.$or.map((c) => buildCondition(c).sql);
|
|
1334
|
+
conditions.push(`(${parts.join(' OR ')})`);
|
|
1335
|
+
}
|
|
1336
|
+
if (cond.$not) {
|
|
1337
|
+
conditions.push(`NOT (${buildCondition(cond.$not).sql})`);
|
|
1338
|
+
}
|
|
1339
|
+
return { sql: conditions.join(' AND ') };
|
|
1340
|
+
}
|
|
1341
|
+
const fieldConditions = [];
|
|
1342
|
+
for (const [key, value] of Object.entries(cond)) {
|
|
1343
|
+
if (key.startsWith('$'))
|
|
1344
|
+
continue;
|
|
1345
|
+
if (value && typeof value === 'object' && !(value instanceof Date) && !Array.isArray(value)) {
|
|
1346
|
+
const rawValueObj = value;
|
|
1347
|
+
const symbolKeys = Object.getOwnPropertySymbols(rawValueObj);
|
|
1348
|
+
const normalizedObj = { ...rawValueObj };
|
|
1349
|
+
for (const sym of symbolKeys) {
|
|
1350
|
+
const strKey = (0, operators_1.operatorToWhereKey)(sym);
|
|
1351
|
+
if (strKey)
|
|
1352
|
+
normalizedObj[strKey] = rawValueObj[sym];
|
|
1353
|
+
}
|
|
1354
|
+
const valueObj = normalizedObj;
|
|
1355
|
+
if (valueObj.$eq !== undefined) {
|
|
1356
|
+
values.push(valueObj.$eq);
|
|
1357
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
1358
|
+
}
|
|
1359
|
+
else if (valueObj.$ne !== undefined) {
|
|
1360
|
+
values.push(valueObj.$ne);
|
|
1361
|
+
fieldConditions.push(`${this.escapeId(key)} != ?`);
|
|
1362
|
+
}
|
|
1363
|
+
else if (valueObj.$gt !== undefined) {
|
|
1364
|
+
values.push(valueObj.$gt);
|
|
1365
|
+
fieldConditions.push(`${this.escapeId(key)} > ?`);
|
|
1366
|
+
}
|
|
1367
|
+
else if (valueObj.$gte !== undefined) {
|
|
1368
|
+
values.push(valueObj.$gte);
|
|
1369
|
+
fieldConditions.push(`${this.escapeId(key)} >= ?`);
|
|
1370
|
+
}
|
|
1371
|
+
else if (valueObj.$lt !== undefined) {
|
|
1372
|
+
values.push(valueObj.$lt);
|
|
1373
|
+
fieldConditions.push(`${this.escapeId(key)} < ?`);
|
|
1374
|
+
}
|
|
1375
|
+
else if (valueObj.$lte !== undefined) {
|
|
1376
|
+
values.push(valueObj.$lte);
|
|
1377
|
+
fieldConditions.push(`${this.escapeId(key)} <= ?`);
|
|
1378
|
+
}
|
|
1379
|
+
else if (valueObj.$like !== undefined) {
|
|
1380
|
+
values.push(valueObj.$like);
|
|
1381
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
1382
|
+
}
|
|
1383
|
+
else if (valueObj.$notLike !== undefined) {
|
|
1384
|
+
values.push(valueObj.$notLike);
|
|
1385
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
1386
|
+
}
|
|
1387
|
+
else if (valueObj.$startsWith !== undefined) {
|
|
1388
|
+
values.push(`${valueObj.$startsWith}%`);
|
|
1389
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
1390
|
+
}
|
|
1391
|
+
else if (valueObj.$notStartsWith !== undefined) {
|
|
1392
|
+
values.push(`${valueObj.$notStartsWith}%`);
|
|
1393
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
1394
|
+
}
|
|
1395
|
+
else if (valueObj.$endsWith !== undefined) {
|
|
1396
|
+
values.push(`%${valueObj.$endsWith}`);
|
|
1397
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
1398
|
+
}
|
|
1399
|
+
else if (valueObj.$notEndsWith !== undefined) {
|
|
1400
|
+
values.push(`%${valueObj.$notEndsWith}`);
|
|
1401
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
1402
|
+
}
|
|
1403
|
+
else if (valueObj.$substring !== undefined) {
|
|
1404
|
+
values.push(`%${valueObj.$substring}%`);
|
|
1405
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
1406
|
+
}
|
|
1407
|
+
else if (valueObj.$notSubstring !== undefined) {
|
|
1408
|
+
values.push(`%${valueObj.$notSubstring}%`);
|
|
1409
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
1410
|
+
}
|
|
1411
|
+
else if (valueObj.$in) {
|
|
1412
|
+
const inValues = valueObj.$in;
|
|
1413
|
+
values.push(...inValues);
|
|
1414
|
+
fieldConditions.push(`${this.escapeId(key)} IN (${inValues.map(() => '?').join(',')})`);
|
|
1415
|
+
}
|
|
1416
|
+
else if (valueObj.$notIn) {
|
|
1417
|
+
const notInValues = valueObj.$notIn;
|
|
1418
|
+
values.push(...notInValues);
|
|
1419
|
+
fieldConditions.push(`${this.escapeId(key)} NOT IN (${notInValues.map(() => '?').join(',')})`);
|
|
1420
|
+
}
|
|
1421
|
+
else if (valueObj.$between) {
|
|
1422
|
+
const between = valueObj.$between;
|
|
1423
|
+
values.push(between[0], between[1]);
|
|
1424
|
+
fieldConditions.push(`${this.escapeId(key)} BETWEEN ? AND ?`);
|
|
1425
|
+
}
|
|
1426
|
+
else if (valueObj.$notBetween) {
|
|
1427
|
+
const notBetween = valueObj.$notBetween;
|
|
1428
|
+
values.push(notBetween[0], notBetween[1]);
|
|
1429
|
+
fieldConditions.push(`${this.escapeId(key)} NOT BETWEEN ? AND ?`);
|
|
1430
|
+
}
|
|
1431
|
+
else if (valueObj.$isNull !== undefined) {
|
|
1432
|
+
fieldConditions.push(valueObj.$isNull ? `${this.escapeId(key)} IS NULL` : `${this.escapeId(key)} IS NOT NULL`);
|
|
1433
|
+
}
|
|
1434
|
+
else if (valueObj.$isNotNull !== undefined) {
|
|
1435
|
+
fieldConditions.push(valueObj.$isNotNull ? `${this.escapeId(key)} IS NOT NULL` : `${this.escapeId(key)} IS NULL`);
|
|
1436
|
+
}
|
|
1437
|
+
else if (valueObj.$col !== undefined) {
|
|
1438
|
+
fieldConditions.push(`${this.escapeId(key)} = ${this.escapeId(String(valueObj.$col))}`);
|
|
1439
|
+
}
|
|
1440
|
+
else {
|
|
1441
|
+
values.push(value);
|
|
1442
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
else {
|
|
1446
|
+
values.push(value);
|
|
1447
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return { sql: fieldConditions.join(' AND ') };
|
|
1451
|
+
};
|
|
1452
|
+
const result = buildCondition(where);
|
|
1453
|
+
return { sql: result.sql, values };
|
|
1454
|
+
}
|
|
1455
|
+
buildOrderClause(order) {
|
|
1456
|
+
const orderArray = order;
|
|
1457
|
+
if (!orderArray || !Array.isArray(orderArray) || orderArray.length === 0) {
|
|
1458
|
+
return '';
|
|
1459
|
+
}
|
|
1460
|
+
const orderParts = [];
|
|
1461
|
+
for (const item of orderArray) {
|
|
1462
|
+
if (Array.isArray(item)) {
|
|
1463
|
+
const field = typeof item[0] === 'string' ? this.escapeId(item[0]) : item[0];
|
|
1464
|
+
const direction = item[1] ? ` ${item[1]}` : '';
|
|
1465
|
+
orderParts.push(`${field}${direction}`);
|
|
1466
|
+
}
|
|
1467
|
+
else if (typeof item === 'string') {
|
|
1468
|
+
orderParts.push(item);
|
|
1469
|
+
}
|
|
1470
|
+
else if (item && typeof item === 'object') {
|
|
1471
|
+
const itemArr = item;
|
|
1472
|
+
const [modelOrString, field] = itemArr;
|
|
1473
|
+
const model = typeof modelOrString === 'string' ? modelOrString : modelOrString.tableName || '';
|
|
1474
|
+
const fieldStr = typeof field === 'string' ? field : '';
|
|
1475
|
+
orderParts.push(`${model ? `${this.escapeId(model)}.` : ''}${this.escapeId(fieldStr)}`);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
|
|
1479
|
+
}
|
|
1480
|
+
buildLimitOffset(limit, offset) {
|
|
1481
|
+
let sql = '';
|
|
1482
|
+
if (limit !== undefined) {
|
|
1483
|
+
sql += ` LIMIT ${Number(limit)}`;
|
|
1484
|
+
if (offset !== undefined)
|
|
1485
|
+
sql += ` OFFSET ${Number(offset)}`;
|
|
1486
|
+
}
|
|
1487
|
+
else if (offset !== undefined) {
|
|
1488
|
+
sql += ` OFFSET ${Number(offset)}`;
|
|
1489
|
+
}
|
|
1490
|
+
return sql;
|
|
1491
|
+
}
|
|
1492
|
+
buildInsertQuery(tableName, values, options) {
|
|
1493
|
+
const columns = Object.keys(values);
|
|
1494
|
+
if (columns.length === 0) {
|
|
1495
|
+
return { sql: `INSERT INTO ${this.quoteTable(tableName, options?.schema)} DEFAULT VALUES`, values: [] };
|
|
1496
|
+
}
|
|
1497
|
+
const processedValues = [];
|
|
1498
|
+
const placeholders = [];
|
|
1499
|
+
for (const value of Object.values(values)) {
|
|
1500
|
+
if (value instanceof prorm_1.Literal) {
|
|
1501
|
+
placeholders.push(value.val);
|
|
1502
|
+
}
|
|
1503
|
+
else {
|
|
1504
|
+
placeholders.push('?');
|
|
1505
|
+
processedValues.push(value);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
let sql = `INSERT INTO ${this.quoteTable(tableName, options?.schema)} (${columns
|
|
1509
|
+
.map((c) => this.escapeId(c))
|
|
1510
|
+
.join(', ')}) VALUES (${placeholders.join(', ')})`;
|
|
1511
|
+
if (options?.upsert) {
|
|
1512
|
+
sql += this.buildOnConflictClause(columns, options);
|
|
1513
|
+
}
|
|
1514
|
+
if (options?.returning) {
|
|
1515
|
+
sql +=
|
|
1516
|
+
options.returning === true
|
|
1517
|
+
? ' RETURNING *'
|
|
1518
|
+
: ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
|
|
1519
|
+
}
|
|
1520
|
+
return { sql, values: processedValues };
|
|
1521
|
+
}
|
|
1522
|
+
buildOnConflictClause(insertedColumns, options) {
|
|
1523
|
+
if (options.conflictTargets && options.conflictTargets.length > 0) {
|
|
1524
|
+
return this.buildConflictTargetsSql(options.conflictTargets, insertedColumns);
|
|
1525
|
+
}
|
|
1526
|
+
if (options.doNothing) {
|
|
1527
|
+
return options.conflictFields && options.conflictFields.length > 0
|
|
1528
|
+
? ` ON CONFLICT (${options.conflictFields.map((c) => this.escapeId(c)).join(', ')}) DO NOTHING`
|
|
1529
|
+
: ' ON CONFLICT DO NOTHING';
|
|
1530
|
+
}
|
|
1531
|
+
if (!options.conflictFields || options.conflictFields.length === 0) {
|
|
1532
|
+
throw new Error('DuckDB upsert requires `conflictFields` naming the PRIMARY KEY or UNIQUE columns to ' +
|
|
1533
|
+
'resolve conflicts against (e.g. { conflictFields: ["id"] }), unless `doNothing` is set.');
|
|
1534
|
+
}
|
|
1535
|
+
const updateColumns = options.updateOnDuplicate && options.updateOnDuplicate.length > 0
|
|
1536
|
+
? options.updateOnDuplicate
|
|
1537
|
+
: insertedColumns;
|
|
1538
|
+
const setClauses = updateColumns.map((c) => `${this.escapeId(c)} = excluded.${this.escapeId(c)}`).join(', ');
|
|
1539
|
+
return ` ON CONFLICT (${options.conflictFields.map((c) => this.escapeId(c)).join(', ')}) DO UPDATE SET ${setClauses}`;
|
|
1540
|
+
}
|
|
1541
|
+
buildConflictTargetsSql(targets, insertedColumns) {
|
|
1542
|
+
let clause = '';
|
|
1543
|
+
for (const target of targets) {
|
|
1544
|
+
if (!target.fields || target.fields.length === 0) {
|
|
1545
|
+
throw new Error('Each entry in `conflictTargets` requires a non-empty `fields` array naming the ' +
|
|
1546
|
+
'PRIMARY KEY or UNIQUE columns to resolve conflicts against.');
|
|
1547
|
+
}
|
|
1548
|
+
const targetSql = `ON CONFLICT (${target.fields.map((c) => this.escapeId(c)).join(', ')})`;
|
|
1549
|
+
const action = target.action || 'DO UPDATE';
|
|
1550
|
+
if (action === 'DO NOTHING') {
|
|
1551
|
+
clause += ` ${targetSql} DO NOTHING`;
|
|
1552
|
+
}
|
|
1553
|
+
else {
|
|
1554
|
+
const updateColumns = target.updateColumns && target.updateColumns.length > 0 ? target.updateColumns : insertedColumns;
|
|
1555
|
+
const setClauses = updateColumns.map((c) => `${this.escapeId(c)} = excluded.${this.escapeId(c)}`).join(', ');
|
|
1556
|
+
clause += ` ${targetSql} DO UPDATE SET ${setClauses}`;
|
|
1557
|
+
if (target.where)
|
|
1558
|
+
clause += ` WHERE ${target.where}`;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return clause;
|
|
1562
|
+
}
|
|
1563
|
+
buildUpsertQuery(tableName, values, options) {
|
|
1564
|
+
const columns = Object.keys(values);
|
|
1565
|
+
const processedValues = [];
|
|
1566
|
+
const placeholders = [];
|
|
1567
|
+
for (const value of Object.values(values)) {
|
|
1568
|
+
if (value instanceof prorm_1.Literal) {
|
|
1569
|
+
placeholders.push(value.val);
|
|
1570
|
+
}
|
|
1571
|
+
else {
|
|
1572
|
+
placeholders.push('?');
|
|
1573
|
+
processedValues.push(value);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
let sql = `INSERT INTO ${this.quoteTable(tableName, options?.schema)} (${columns
|
|
1577
|
+
.map((c) => this.escapeId(c))
|
|
1578
|
+
.join(', ')}) VALUES (${placeholders.join(', ')})`;
|
|
1579
|
+
if (options) {
|
|
1580
|
+
sql += this.buildOnConflictClause(columns, options);
|
|
1581
|
+
}
|
|
1582
|
+
if (options?.returning) {
|
|
1583
|
+
sql +=
|
|
1584
|
+
options.returning === true
|
|
1585
|
+
? ' RETURNING *'
|
|
1586
|
+
: ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
|
|
1587
|
+
}
|
|
1588
|
+
return { sql, values: processedValues };
|
|
1589
|
+
}
|
|
1590
|
+
buildIncrementQuery(tableName, fields, where, options) {
|
|
1591
|
+
const by = options?.by ?? 1;
|
|
1592
|
+
const setClauses = [];
|
|
1593
|
+
const queryValues = [];
|
|
1594
|
+
if (typeof fields === 'string') {
|
|
1595
|
+
setClauses.push(`${this.escapeId(fields)} = ${this.escapeId(fields)} + ?`);
|
|
1596
|
+
queryValues.push(by);
|
|
1597
|
+
}
|
|
1598
|
+
else if (Array.isArray(fields)) {
|
|
1599
|
+
for (const field of fields) {
|
|
1600
|
+
setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + ?`);
|
|
1601
|
+
queryValues.push(by);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
else {
|
|
1605
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
1606
|
+
setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + ?`);
|
|
1607
|
+
queryValues.push(value);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
const whereClause = this.buildWhereClause(where);
|
|
1611
|
+
queryValues.push(...whereClause.values);
|
|
1612
|
+
const sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')} WHERE ${whereClause.sql}`;
|
|
1613
|
+
return { sql, values: queryValues };
|
|
1614
|
+
}
|
|
1615
|
+
buildUpdateQuery(tableName, values, where, options) {
|
|
1616
|
+
const setClauses = [];
|
|
1617
|
+
const queryValues = [];
|
|
1618
|
+
for (const [key, value] of Object.entries(values)) {
|
|
1619
|
+
if (value instanceof prorm_1.Literal) {
|
|
1620
|
+
setClauses.push(`${this.escapeId(key)} = ${value.val}`);
|
|
1621
|
+
}
|
|
1622
|
+
else {
|
|
1623
|
+
setClauses.push(`${this.escapeId(key)} = ?`);
|
|
1624
|
+
queryValues.push(value);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
const whereClause = this.buildWhereClause(where);
|
|
1628
|
+
let sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')}`;
|
|
1629
|
+
if (whereClause.sql)
|
|
1630
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1631
|
+
if (options?.returning) {
|
|
1632
|
+
sql +=
|
|
1633
|
+
options.returning === true
|
|
1634
|
+
? ' RETURNING *'
|
|
1635
|
+
: ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
|
|
1636
|
+
}
|
|
1637
|
+
return { sql, values: [...queryValues, ...whereClause.values] };
|
|
1638
|
+
}
|
|
1639
|
+
buildDeleteQuery(tableName, where, options) {
|
|
1640
|
+
const whereClause = this.buildWhereClause(where);
|
|
1641
|
+
let sql = `DELETE FROM ${this.escapeId(tableName)}`;
|
|
1642
|
+
if (whereClause.sql)
|
|
1643
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1644
|
+
if (options?.returning) {
|
|
1645
|
+
sql +=
|
|
1646
|
+
options.returning === true
|
|
1647
|
+
? ' RETURNING *'
|
|
1648
|
+
: ` RETURNING ${options.returning.map((c) => this.escapeId(c)).join(', ')}`;
|
|
1649
|
+
}
|
|
1650
|
+
return { sql, values: whereClause.values };
|
|
1651
|
+
}
|
|
1652
|
+
buildSelectQuery(options) {
|
|
1653
|
+
const queryValues = [];
|
|
1654
|
+
let selectSql = '*';
|
|
1655
|
+
if (options.attributes) {
|
|
1656
|
+
if (Array.isArray(options.attributes)) {
|
|
1657
|
+
selectSql = options.attributes
|
|
1658
|
+
.map((a) => {
|
|
1659
|
+
if (Array.isArray(a)) {
|
|
1660
|
+
const [expr, alias] = a;
|
|
1661
|
+
const exprSql = typeof expr === 'string' ? this.formatMaybeExpression(expr) : String(expr);
|
|
1662
|
+
return `${exprSql} AS ${this.escapeId(alias)}`;
|
|
1663
|
+
}
|
|
1664
|
+
return typeof a === 'string' ? this.formatMaybeExpression(a) : String(a);
|
|
1665
|
+
})
|
|
1666
|
+
.join(', ');
|
|
1667
|
+
}
|
|
1668
|
+
else if (options.attributes.include) {
|
|
1669
|
+
selectSql = options.attributes.include.map((a) => this.escapeId(a)).join(', ');
|
|
1670
|
+
}
|
|
1671
|
+
else if (options.attributes.exclude) {
|
|
1672
|
+
// DuckDB's `SELECT * EXCLUDE (...)` syntax
|
|
1673
|
+
selectSql = `* EXCLUDE (${options.attributes.exclude.map((a) => this.escapeId(a)).join(', ')})`;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
let distinctClause = '';
|
|
1677
|
+
if (options.distinct) {
|
|
1678
|
+
distinctClause = options.col ? `DISTINCT ${this.escapeId(options.col)} ` : 'DISTINCT ';
|
|
1679
|
+
}
|
|
1680
|
+
let sql = `SELECT ${distinctClause}${selectSql} FROM ${this.quoteTable(options.tableName, options.schema)}`;
|
|
1681
|
+
if (options.include && options.include.length > 0) {
|
|
1682
|
+
for (const include of options.include) {
|
|
1683
|
+
const joinType = include.required ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1684
|
+
const includeSchema = include.schema || include.model?.schema;
|
|
1685
|
+
const includeTableName = include.model.tableName;
|
|
1686
|
+
const tableName = includeTableName ? includeTableName : include.model.toString();
|
|
1687
|
+
const alias = include.as || tableName;
|
|
1688
|
+
sql += ` ${joinType} ${this.quoteTable(tableName, includeSchema)} ${this.escapeId(alias)}`;
|
|
1689
|
+
if (include.where) {
|
|
1690
|
+
const whereClause = this.buildWhereClause(include.where);
|
|
1691
|
+
sql += ` ON ${whereClause.sql}`;
|
|
1692
|
+
queryValues.push(...whereClause.values);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
if (options.where) {
|
|
1697
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
1698
|
+
if (whereClause.sql) {
|
|
1699
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1700
|
+
queryValues.push(...whereClause.values);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (options.group) {
|
|
1704
|
+
const groupArray = Array.isArray(options.group) ? options.group : [options.group];
|
|
1705
|
+
if (groupArray.length > 0) {
|
|
1706
|
+
sql += ` GROUP BY ${groupArray.map((f) => (f === 'ALL' ? 'ALL' : this.escapeId(f))).join(', ')}`;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
if (options.having) {
|
|
1710
|
+
const havingClause = this.buildWhereClause(options.having);
|
|
1711
|
+
if (havingClause.sql) {
|
|
1712
|
+
sql += ` HAVING ${havingClause.sql}`;
|
|
1713
|
+
queryValues.push(...havingClause.values);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
if (options.order) {
|
|
1717
|
+
const orderClause = this.buildOrderClause(options.order);
|
|
1718
|
+
if (orderClause)
|
|
1719
|
+
sql += ` ${orderClause}`;
|
|
1720
|
+
}
|
|
1721
|
+
sql += this.buildLimitOffset(options.limit, options.offset);
|
|
1722
|
+
if (options.union && options.union.length > 0) {
|
|
1723
|
+
const unionType = options.unionType || 'UNION';
|
|
1724
|
+
for (const unionQuery of options.union) {
|
|
1725
|
+
const unionModel = unionQuery.model;
|
|
1726
|
+
const unionTableName = unionModel.tableName || unionModel.name || '';
|
|
1727
|
+
let unionSql = `SELECT * FROM ${this.escapeId(unionTableName)}`;
|
|
1728
|
+
if (unionQuery.where) {
|
|
1729
|
+
const whereClause = this.buildWhereClause(unionQuery.where);
|
|
1730
|
+
if (whereClause.sql) {
|
|
1731
|
+
unionSql += ` WHERE ${whereClause.sql}`;
|
|
1732
|
+
queryValues.push(...whereClause.values);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
if (unionQuery.order) {
|
|
1736
|
+
const orderClause = this.buildOrderClause(unionQuery.order);
|
|
1737
|
+
if (orderClause)
|
|
1738
|
+
unionSql += ` ${orderClause}`;
|
|
1739
|
+
}
|
|
1740
|
+
if (unionQuery.limit)
|
|
1741
|
+
unionSql += ` LIMIT ${unionQuery.limit}`;
|
|
1742
|
+
if (unionQuery.offset)
|
|
1743
|
+
unionSql += ` OFFSET ${unionQuery.offset}`;
|
|
1744
|
+
sql += ` ${unionType} ${unionSql}`;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return { sql, values: queryValues };
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Formats a SELECT attribute string: bare identifiers get quoted, anything
|
|
1751
|
+
* that already looks like an expression (function call, `*`, operators,
|
|
1752
|
+
* DuckDB's `EXCLUDE`/`REPLACE` column-transform syntax) passes through
|
|
1753
|
+
* verbatim so callers can supply raw analytical SQL.
|
|
1754
|
+
*/
|
|
1755
|
+
formatMaybeExpression(attr) {
|
|
1756
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(attr)) {
|
|
1757
|
+
return attr
|
|
1758
|
+
.split('.')
|
|
1759
|
+
.map((part) => this.escapeId(part))
|
|
1760
|
+
.join('.');
|
|
1761
|
+
}
|
|
1762
|
+
return attr;
|
|
1763
|
+
}
|
|
1764
|
+
// ==================== JSON operations ====================
|
|
1765
|
+
buildJsonExtract(column, path, asText = true) {
|
|
1766
|
+
const columnRef = this.escapeId(column);
|
|
1767
|
+
const normalizedPath = path.startsWith('$.') || path === '$' ? path : `$.${path}`;
|
|
1768
|
+
const extract = `json_extract(${columnRef}, '${this.escapeStringForJsonPath(normalizedPath)}')`;
|
|
1769
|
+
return asText ? `json_extract_string(${columnRef}, '${this.escapeStringForJsonPath(normalizedPath)}')` : extract;
|
|
1770
|
+
}
|
|
1771
|
+
escapeStringForJsonPath(path) {
|
|
1772
|
+
return path.replace(/'/g, "''");
|
|
1773
|
+
}
|
|
1774
|
+
buildJsonQuery(column, path, value, operator = '=') {
|
|
1775
|
+
const values = [];
|
|
1776
|
+
const extract = this.buildJsonExtract(column, path, true);
|
|
1777
|
+
if (value !== undefined) {
|
|
1778
|
+
values.push(value);
|
|
1779
|
+
return { sql: `${extract} ${operator} ?`, values };
|
|
1780
|
+
}
|
|
1781
|
+
return { sql: extract, values };
|
|
1782
|
+
}
|
|
1783
|
+
// ==================== DuckDB-specific value-add: file-as-table functions ====================
|
|
1784
|
+
// DuckDB's signature capability: query Parquet/CSV/JSON files directly as
|
|
1785
|
+
// tables via table functions usable in a FROM clause, and export query
|
|
1786
|
+
// results straight back out to those formats via COPY.
|
|
1787
|
+
/**
|
|
1788
|
+
* Build a `read_parquet(...)` table-function expression usable in a FROM
|
|
1789
|
+
* clause, e.g. `FROM ${dialect.readParquet('data/*.parquet')}`.
|
|
1790
|
+
*/
|
|
1791
|
+
readParquet(path, options) {
|
|
1792
|
+
const pathArg = Array.isArray(path)
|
|
1793
|
+
? `[${path.map((p) => `'${this.escapeString(p)}'`).join(', ')}]`
|
|
1794
|
+
: `'${this.escapeString(path)}'`;
|
|
1795
|
+
const namedArgs = [];
|
|
1796
|
+
if (options?.hivePartitioning !== undefined)
|
|
1797
|
+
namedArgs.push(`hive_partitioning=${options.hivePartitioning}`);
|
|
1798
|
+
if (options?.unionByName !== undefined)
|
|
1799
|
+
namedArgs.push(`union_by_name=${options.unionByName}`);
|
|
1800
|
+
if (options?.filename !== undefined)
|
|
1801
|
+
namedArgs.push(`filename=${options.filename}`);
|
|
1802
|
+
const argsSql = namedArgs.length > 0 ? `, ${namedArgs.join(', ')}` : '';
|
|
1803
|
+
return `read_parquet(${pathArg}${argsSql})`;
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Build a `read_csv_auto(...)` (or `read_csv(...)` with explicit options)
|
|
1807
|
+
* table-function expression usable in a FROM clause.
|
|
1808
|
+
*/
|
|
1809
|
+
readCsv(path, options) {
|
|
1810
|
+
const pathArg = Array.isArray(path)
|
|
1811
|
+
? `[${path.map((p) => `'${this.escapeString(p)}'`).join(', ')}]`
|
|
1812
|
+
: `'${this.escapeString(path)}'`;
|
|
1813
|
+
const namedArgs = [];
|
|
1814
|
+
if (options?.header !== undefined)
|
|
1815
|
+
namedArgs.push(`header=${options.header}`);
|
|
1816
|
+
if (options?.delim !== undefined)
|
|
1817
|
+
namedArgs.push(`delim='${this.escapeString(options.delim)}'`);
|
|
1818
|
+
if (options?.allVarchar !== undefined)
|
|
1819
|
+
namedArgs.push(`all_varchar=${options.allVarchar}`);
|
|
1820
|
+
if (options?.columns) {
|
|
1821
|
+
const cols = Object.entries(options.columns)
|
|
1822
|
+
.map(([col, type]) => `'${col}': '${type}'`)
|
|
1823
|
+
.join(', ');
|
|
1824
|
+
namedArgs.push(`columns={${cols}}`);
|
|
1825
|
+
}
|
|
1826
|
+
const fnName = options?.autoDetect === false ? 'read_csv' : 'read_csv_auto';
|
|
1827
|
+
const argsSql = namedArgs.length > 0 ? `, ${namedArgs.join(', ')}` : '';
|
|
1828
|
+
return `${fnName}(${pathArg}${argsSql})`;
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* Build a `read_json_auto(...)` (or `read_json(...)` with explicit options)
|
|
1832
|
+
* table-function expression usable in a FROM clause.
|
|
1833
|
+
*/
|
|
1834
|
+
readJson(path, options) {
|
|
1835
|
+
const pathArg = Array.isArray(path)
|
|
1836
|
+
? `[${path.map((p) => `'${this.escapeString(p)}'`).join(', ')}]`
|
|
1837
|
+
: `'${this.escapeString(path)}'`;
|
|
1838
|
+
const namedArgs = [];
|
|
1839
|
+
if (options?.format)
|
|
1840
|
+
namedArgs.push(`format='${options.format}'`);
|
|
1841
|
+
if (options?.columns) {
|
|
1842
|
+
const cols = Object.entries(options.columns)
|
|
1843
|
+
.map(([col, type]) => `'${col}': '${type}'`)
|
|
1844
|
+
.join(', ');
|
|
1845
|
+
namedArgs.push(`columns={${cols}}`);
|
|
1846
|
+
}
|
|
1847
|
+
const fnName = options?.autoDetect === false ? 'read_json' : 'read_json_auto';
|
|
1848
|
+
const argsSql = namedArgs.length > 0 ? `, ${namedArgs.join(', ')}` : '';
|
|
1849
|
+
return `${fnName}(${pathArg}${argsSql})`;
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Export the results of a query straight to a Parquet/CSV/JSON file via
|
|
1853
|
+
* DuckDB's `COPY (query) TO 'file' (FORMAT ...)`.
|
|
1854
|
+
*/
|
|
1855
|
+
async copyTo(query, path, format, options) {
|
|
1856
|
+
const opts = [`FORMAT ${format}`];
|
|
1857
|
+
if (options?.partitionBy && options.partitionBy.length > 0) {
|
|
1858
|
+
opts.push(`PARTITION_BY (${options.partitionBy.map((c) => this.escapeId(c)).join(', ')})`);
|
|
1859
|
+
}
|
|
1860
|
+
if (options?.overwrite)
|
|
1861
|
+
opts.push('OVERWRITE_OR_IGNORE');
|
|
1862
|
+
if (options?.compression)
|
|
1863
|
+
opts.push(`COMPRESSION ${options.compression.toUpperCase()}`);
|
|
1864
|
+
if (format === 'CSV' && options?.header !== undefined)
|
|
1865
|
+
opts.push(`HEADER ${options.header}`);
|
|
1866
|
+
await this.query(`COPY (${query}) TO '${this.escapeString(path)}' (${opts.join(', ')})`);
|
|
1867
|
+
}
|
|
1868
|
+
// ==================== S3 / httpfs credential configuration ====================
|
|
1869
|
+
// `createExtension('httpfs')` only INSTALLs/LOADs the extension - it does not
|
|
1870
|
+
// configure credentials, so `read_parquet('s3://...')` etc. would still fail
|
|
1871
|
+
// with a missing-credentials error against a real bucket. DuckDB supports two
|
|
1872
|
+
// ways to supply S3 credentials, and both are exposed here rather than
|
|
1873
|
+
// picking one:
|
|
1874
|
+
//
|
|
1875
|
+
// - `SET s3_...` variables (`configureS3Credentials`): simplest, but global
|
|
1876
|
+
// to the DuckDB instance/connection - one set of credentials at a time,
|
|
1877
|
+
// with no per-query or per-bucket scoping. This is the original httpfs
|
|
1878
|
+
// mechanism and still fully supported.
|
|
1879
|
+
// - `CREATE SECRET ... (TYPE S3, ...)` (`createSecret`/`dropSecret`): the
|
|
1880
|
+
// newer, DuckDB-recommended mechanism. Secrets are named, can be scoped to
|
|
1881
|
+
// a specific path prefix (`SCOPE`), can coexist (multiple buckets/
|
|
1882
|
+
// credentials at once), and can be persisted. Prefer this for anything
|
|
1883
|
+
// beyond a single quick script.
|
|
1884
|
+
//
|
|
1885
|
+
// See https://duckdb.org/docs/extensions/httpfs/s3api and
|
|
1886
|
+
// https://duckdb.org/docs/configuration/secrets_manager for the underlying
|
|
1887
|
+
// SET variable names (`s3_region`, `s3_access_key_id`, `s3_secret_access_key`,
|
|
1888
|
+
// `s3_session_token`, `s3_endpoint`, `s3_url_style`, `s3_use_ssl`) and
|
|
1889
|
+
// `CREATE SECRET` grammar (`TYPE S3`, `KEY_ID`, `SECRET`, `REGION`,
|
|
1890
|
+
// `SESSION_TOKEN`, `ENDPOINT`, `URL_STYLE`, `USE_SSL`, `SCOPE`) this wraps.
|
|
1891
|
+
/**
|
|
1892
|
+
* Configure S3 access via DuckDB httpfs `SET s3_...` session/global
|
|
1893
|
+
* variables. Only emits a `SET` statement for options that are actually
|
|
1894
|
+
* provided. Simpler than `createSecret()` but global to the instance (later
|
|
1895
|
+
* calls overwrite earlier ones; there is no per-bucket scoping) - prefer
|
|
1896
|
+
* `createSecret()` when multiple credentials/buckets are in play.
|
|
1897
|
+
*
|
|
1898
|
+
* Requires the `httpfs` extension to be installed/loaded first (see
|
|
1899
|
+
* `createExtension('httpfs')`).
|
|
1900
|
+
*/
|
|
1901
|
+
async configureS3Credentials(options) {
|
|
1902
|
+
const statements = [];
|
|
1903
|
+
if (options.region !== undefined) {
|
|
1904
|
+
statements.push(`SET s3_region='${this.escapeString(options.region)}'`);
|
|
1905
|
+
}
|
|
1906
|
+
if (options.accessKeyId !== undefined) {
|
|
1907
|
+
statements.push(`SET s3_access_key_id='${this.escapeString(options.accessKeyId)}'`);
|
|
1908
|
+
}
|
|
1909
|
+
if (options.secretAccessKey !== undefined) {
|
|
1910
|
+
statements.push(`SET s3_secret_access_key='${this.escapeString(options.secretAccessKey)}'`);
|
|
1911
|
+
}
|
|
1912
|
+
if (options.sessionToken !== undefined) {
|
|
1913
|
+
statements.push(`SET s3_session_token='${this.escapeString(options.sessionToken)}'`);
|
|
1914
|
+
}
|
|
1915
|
+
if (options.endpoint !== undefined) {
|
|
1916
|
+
statements.push(`SET s3_endpoint='${this.escapeString(options.endpoint)}'`);
|
|
1917
|
+
}
|
|
1918
|
+
if (options.urlStyle !== undefined) {
|
|
1919
|
+
statements.push(`SET s3_url_style='${this.escapeString(options.urlStyle)}'`);
|
|
1920
|
+
}
|
|
1921
|
+
if (options.useSsl !== undefined) {
|
|
1922
|
+
statements.push(`SET s3_use_ssl=${options.useSsl ? 'true' : 'false'}`);
|
|
1923
|
+
}
|
|
1924
|
+
for (const sql of statements) {
|
|
1925
|
+
await this.query(sql);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Create a named DuckDB Secrets Manager secret of `TYPE S3` for use with
|
|
1930
|
+
* `read_parquet`/`read_csv`/`read_json`/`copyTo` against `s3://` paths.
|
|
1931
|
+
* This is the newer, DuckDB-recommended alternative to
|
|
1932
|
+
* `configureS3Credentials()`'s global `SET s3_...` variables: secrets are
|
|
1933
|
+
* named, can be scoped to a path prefix, and multiple secrets (for
|
|
1934
|
+
* different buckets/credentials) can coexist.
|
|
1935
|
+
*
|
|
1936
|
+
* Requires the `httpfs` extension to be installed/loaded first (see
|
|
1937
|
+
* `createExtension('httpfs')`).
|
|
1938
|
+
*/
|
|
1939
|
+
async createSecret(name, options = {}) {
|
|
1940
|
+
const params = ['TYPE S3'];
|
|
1941
|
+
if (options.keyId !== undefined)
|
|
1942
|
+
params.push(`KEY_ID '${this.escapeString(options.keyId)}'`);
|
|
1943
|
+
if (options.secret !== undefined)
|
|
1944
|
+
params.push(`SECRET '${this.escapeString(options.secret)}'`);
|
|
1945
|
+
if (options.region !== undefined)
|
|
1946
|
+
params.push(`REGION '${this.escapeString(options.region)}'`);
|
|
1947
|
+
if (options.sessionToken !== undefined) {
|
|
1948
|
+
params.push(`SESSION_TOKEN '${this.escapeString(options.sessionToken)}'`);
|
|
1949
|
+
}
|
|
1950
|
+
if (options.endpoint !== undefined)
|
|
1951
|
+
params.push(`ENDPOINT '${this.escapeString(options.endpoint)}'`);
|
|
1952
|
+
if (options.urlStyle !== undefined)
|
|
1953
|
+
params.push(`URL_STYLE '${this.escapeString(options.urlStyle)}'`);
|
|
1954
|
+
if (options.useSsl !== undefined)
|
|
1955
|
+
params.push(`USE_SSL ${options.useSsl ? 'true' : 'false'}`);
|
|
1956
|
+
if (options.scope !== undefined) {
|
|
1957
|
+
const scopes = Array.isArray(options.scope) ? options.scope : [options.scope];
|
|
1958
|
+
params.push(`SCOPE '${scopes.map((s) => this.escapeString(s)).join(', ')}'`);
|
|
1959
|
+
}
|
|
1960
|
+
let sql = 'CREATE';
|
|
1961
|
+
if (options.replace)
|
|
1962
|
+
sql += ' OR REPLACE';
|
|
1963
|
+
sql += options.persistent ? ' PERSISTENT SECRET' : ' SECRET';
|
|
1964
|
+
sql += ` ${this.escapeId(name)} (${params.join(', ')})`;
|
|
1965
|
+
await this.query(sql);
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Drop a previously created `CREATE SECRET` (see `createSecret()`).
|
|
1969
|
+
*/
|
|
1970
|
+
async dropSecret(name, options) {
|
|
1971
|
+
let sql = 'DROP SECRET';
|
|
1972
|
+
if (options?.ifExists)
|
|
1973
|
+
sql += ' IF EXISTS';
|
|
1974
|
+
sql += ` ${this.escapeId(name)}`;
|
|
1975
|
+
await this.query(sql);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
exports.DuckDBDialect = DuckDBDialect;
|
|
1979
|
+
/**
|
|
1980
|
+
* DuckDB-specific transaction class. DuckDB supports SAVEPOINT for nested
|
|
1981
|
+
* transactions (in addition to top-level BEGIN/COMMIT/ROLLBACK).
|
|
1982
|
+
*/
|
|
1983
|
+
class DuckDBTransaction {
|
|
1984
|
+
constructor(depth, options, dialect) {
|
|
1985
|
+
this.dialect = dialect;
|
|
1986
|
+
this.finished = false;
|
|
1987
|
+
this.parent = null;
|
|
1988
|
+
this.savepoints = [];
|
|
1989
|
+
this.id = `${depth}_${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
|
|
1990
|
+
this.options = options || {};
|
|
1991
|
+
}
|
|
1992
|
+
async commit() {
|
|
1993
|
+
if (this.finished)
|
|
1994
|
+
return;
|
|
1995
|
+
if (this.dialect)
|
|
1996
|
+
await this.dialect.commitTransaction(this);
|
|
1997
|
+
this.finished = true;
|
|
1998
|
+
}
|
|
1999
|
+
async rollback() {
|
|
2000
|
+
if (this.finished)
|
|
2001
|
+
return;
|
|
2002
|
+
if (this.dialect)
|
|
2003
|
+
await this.dialect.rollbackTransaction(this);
|
|
2004
|
+
this.finished = true;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
exports.DuckDBTransaction = DuckDBTransaction;
|
|
2008
|
+
/**
|
|
2009
|
+
* Create a new DuckDB dialect instance.
|
|
2010
|
+
*/
|
|
2011
|
+
function createDuckDBDialect(options) {
|
|
2012
|
+
return new DuckDBDialect(options || {});
|
|
2013
|
+
}
|
|
2014
|
+
exports.default = DuckDBDialect;
|