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,2082 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Snowflake dialect implementation for the TypeScript ORM
|
|
4
|
+
*
|
|
5
|
+
* Notes on Snowflake-specific semantics that differ from most other dialects
|
|
6
|
+
* implemented in this project:
|
|
7
|
+
* - Snowflake has no traditional B-tree indexes. Storage is organized via
|
|
8
|
+
* automatic micro-partitioning, and the closest user-facing concept is a
|
|
9
|
+
* "clustering key" (ALTER TABLE ... CLUSTER BY (...)). Index-shaped
|
|
10
|
+
* interface methods (addIndex/createIndex/removeIndex/dropIndex/etc.) are
|
|
11
|
+
* therefore implemented as documented no-ops that emit a console warning,
|
|
12
|
+
* mirroring how src/dialects/sqlite/index.ts handles options that are
|
|
13
|
+
* accepted for interface-compatibility but have no real effect for that
|
|
14
|
+
* dialect (e.g. `engine`/`charset`/table comments).
|
|
15
|
+
* - Semi-structured data uses VARIANT/OBJECT/ARRAY types with PARSE_JSON(),
|
|
16
|
+
* `:` path access and GET/GET_PATH functions instead of JSON/JSONB.
|
|
17
|
+
* - Upserts use MERGE INTO (standard SQL, well supported).
|
|
18
|
+
* - Pagination uses standard LIMIT/OFFSET (unlike Oracle/MSSQL's
|
|
19
|
+
* OFFSET...FETCH-only style).
|
|
20
|
+
* - Auto-increment uses AUTOINCREMENT/IDENTITY column properties.
|
|
21
|
+
* - Sequences are supported via CREATE SEQUENCE / NEXTVAL.
|
|
22
|
+
* - Connections require an "account" identifier and may specify a
|
|
23
|
+
* "warehouse" (the compute resource that must be active for queries to
|
|
24
|
+
* run). If provided, `USE WAREHOUSE <name>` is issued right after connect.
|
|
25
|
+
* - Auth supports both password auth and key-pair auth (privateKey /
|
|
26
|
+
* privateKeyPath), matching the snowflake-sdk driver's ConnectionOptions.
|
|
27
|
+
*/
|
|
28
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
29
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
30
|
+
};
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.SnowflakeDialect = void 0;
|
|
33
|
+
const snowflake_sdk_1 = __importDefault(require("snowflake-sdk"));
|
|
34
|
+
const types_1 = require("../../types");
|
|
35
|
+
const query_stream_helper_1 = require("../query-stream-helper");
|
|
36
|
+
const prorm_1 = require("../../prorm");
|
|
37
|
+
/**
|
|
38
|
+
* Default retry options for connection/query errors
|
|
39
|
+
*/
|
|
40
|
+
const DEFAULT_RETRY_OPTIONS = {
|
|
41
|
+
max: 3,
|
|
42
|
+
timeout: 1000,
|
|
43
|
+
match: [
|
|
44
|
+
'ECONNREFUSED',
|
|
45
|
+
'ENOTFOUND',
|
|
46
|
+
'ETIMEDOUT',
|
|
47
|
+
'connection timeout',
|
|
48
|
+
'connect timeout',
|
|
49
|
+
'Connection is down',
|
|
50
|
+
// Transient warehouse states worth retrying (auto-suspend/resume churn).
|
|
51
|
+
// Deliberately NOT the bare word 'warehouse' — that would also match
|
|
52
|
+
// permanent, non-transient errors like "warehouse does not exist" or
|
|
53
|
+
// permission-denied messages that merely mention a warehouse name,
|
|
54
|
+
// wasting time on retries that can never succeed and masking real
|
|
55
|
+
// configuration problems from the caller.
|
|
56
|
+
'warehouse is currently suspended',
|
|
57
|
+
'warehouse is resuming',
|
|
58
|
+
'warehouse is being resized',
|
|
59
|
+
],
|
|
60
|
+
backoff: false,
|
|
61
|
+
backoffMultiplier: 2,
|
|
62
|
+
backoffMax: 10000,
|
|
63
|
+
};
|
|
64
|
+
class SnowflakeDialect {
|
|
65
|
+
constructor(config) {
|
|
66
|
+
this.name = 'snowflake';
|
|
67
|
+
this.library = 'snowflake-sdk';
|
|
68
|
+
this.connection = null;
|
|
69
|
+
this._isConnected = false;
|
|
70
|
+
this.config = { ...config };
|
|
71
|
+
this.retryOptions = config.retry || {};
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Connection lifecycle
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/**
|
|
77
|
+
* Connect to Snowflake. Wraps the callback-based snowflake-sdk API
|
|
78
|
+
* (`createConnection(...).connect(callback)`) in a Promise. If a
|
|
79
|
+
* `warehouse` was configured, activates it with `USE WAREHOUSE` right
|
|
80
|
+
* after the connection is established.
|
|
81
|
+
*/
|
|
82
|
+
async connect() {
|
|
83
|
+
const options = {
|
|
84
|
+
account: this.config.account,
|
|
85
|
+
username: this.config.username,
|
|
86
|
+
password: this.config.password,
|
|
87
|
+
database: this.config.database,
|
|
88
|
+
schema: this.config.schema,
|
|
89
|
+
warehouse: this.config.warehouse,
|
|
90
|
+
role: this.config.role,
|
|
91
|
+
privateKey: this.config.privateKey,
|
|
92
|
+
privateKeyPath: this.config.privateKeyPath,
|
|
93
|
+
privateKeyPass: this.config.privateKeyPass,
|
|
94
|
+
authenticator: this.config.authenticator,
|
|
95
|
+
application: this.config.application,
|
|
96
|
+
clientSessionKeepAlive: this.config.clientSessionKeepAlive,
|
|
97
|
+
};
|
|
98
|
+
try {
|
|
99
|
+
const conn = snowflake_sdk_1.default.createConnection(options);
|
|
100
|
+
await new Promise((resolve, reject) => {
|
|
101
|
+
conn.connect((err) => {
|
|
102
|
+
if (err) {
|
|
103
|
+
reject(err);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
this.connection = conn;
|
|
110
|
+
this._isConnected = true;
|
|
111
|
+
if (this.config.warehouse) {
|
|
112
|
+
await this.query(`USE WAREHOUSE ${this.quoteIdentifier(this.config.warehouse)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new Error(`Failed to connect to Snowflake database: ${error}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Disconnect from Snowflake, terminating the connection immediately.
|
|
121
|
+
*
|
|
122
|
+
* A failed `destroy()` callback is surfaced by rejecting this promise
|
|
123
|
+
* (rather than silently resolving as if the disconnect succeeded), so
|
|
124
|
+
* callers relying on disconnect() aren't misled into thinking the
|
|
125
|
+
* underlying socket was cleanly torn down. The local connection state is
|
|
126
|
+
* still cleared in a `finally` regardless of outcome, since from this
|
|
127
|
+
* dialect's perspective the connection should no longer be used either way.
|
|
128
|
+
*/
|
|
129
|
+
async disconnect() {
|
|
130
|
+
if (this.connection) {
|
|
131
|
+
const conn = this.connection;
|
|
132
|
+
try {
|
|
133
|
+
await new Promise((resolve, reject) => {
|
|
134
|
+
conn.destroy((err) => {
|
|
135
|
+
if (err) {
|
|
136
|
+
reject(new Error(`Failed to disconnect from Snowflake: ${err.message || err}`));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
resolve();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
this.connection = null;
|
|
145
|
+
this._isConnected = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
getConnection() {
|
|
150
|
+
return this.connection;
|
|
151
|
+
}
|
|
152
|
+
isConnected() {
|
|
153
|
+
return this._isConnected && this.connection !== null && this.connection.isUp();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Builds the shared `complete` callback used by both `query()` and
|
|
157
|
+
* `queryWithCancel()` to translate a snowflake-sdk statement completion
|
|
158
|
+
* into a resolved/rejected `QueryResult` promise.
|
|
159
|
+
*/
|
|
160
|
+
makeCompleteHandler(resolve, reject) {
|
|
161
|
+
return (err, stmt, rows) => {
|
|
162
|
+
if (err) {
|
|
163
|
+
reject(new Error(`Snowflake Query error: ${err.message || err}`));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const resultRows = rows || [];
|
|
167
|
+
const columns = stmt.getColumns ? stmt.getColumns() : undefined;
|
|
168
|
+
const fields = columns
|
|
169
|
+
? columns.map((col) => ({
|
|
170
|
+
name: typeof col.getName === 'function' ? col.getName() : col.name,
|
|
171
|
+
type: typeof col.getType === 'function' ? col.getType() : 'VARIANT',
|
|
172
|
+
length: 0,
|
|
173
|
+
tableID: 0,
|
|
174
|
+
columnID: 0,
|
|
175
|
+
nullable: typeof col.isNullable === 'function' ? col.isNullable() : true,
|
|
176
|
+
isEnum: false,
|
|
177
|
+
isPrimaryKey: false,
|
|
178
|
+
}))
|
|
179
|
+
: [];
|
|
180
|
+
resolve({
|
|
181
|
+
rows: resultRows,
|
|
182
|
+
rowCount: stmt.getNumRows ? stmt.getNumRows() : resultRows.length,
|
|
183
|
+
fields,
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
resolveExecuteArgs(sql, options) {
|
|
188
|
+
const replacements = options?.replacements;
|
|
189
|
+
const binds = Array.isArray(replacements) ? replacements : undefined;
|
|
190
|
+
const finalSql = replacements !== undefined && !Array.isArray(replacements)
|
|
191
|
+
? this.replaceReplacements(sql, replacements)
|
|
192
|
+
: sql;
|
|
193
|
+
return { sqlText: finalSql, binds };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Stream query results by paging through `sql` via repeated
|
|
197
|
+
* dialect-appropriate LIMIT/OFFSET queries (see
|
|
198
|
+
* `createPaginatedQueryStream()` in `src/dialects/query-stream-helper.ts`)
|
|
199
|
+
* instead of loading the whole result set into memory at once.
|
|
200
|
+
* @param sql - The SELECT statement to stream
|
|
201
|
+
* @param options - Streaming options (batch size, backpressure watermark, model mapping)
|
|
202
|
+
*/
|
|
203
|
+
queryStream(sql, options) {
|
|
204
|
+
return (0, query_stream_helper_1.createPaginatedQueryStream)(this, sql, options);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Execute a raw SQL query, wrapping the callback-based
|
|
208
|
+
* `connection.execute({ sqlText, binds, complete })` API in a Promise.
|
|
209
|
+
*/
|
|
210
|
+
async query(sql, options) {
|
|
211
|
+
const executeQuery = async () => {
|
|
212
|
+
if (!this.connection) {
|
|
213
|
+
throw new Error('Not connected to database');
|
|
214
|
+
}
|
|
215
|
+
const { sqlText, binds } = this.resolveExecuteArgs(sql, options);
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
this.connection.execute({
|
|
218
|
+
sqlText,
|
|
219
|
+
binds: binds,
|
|
220
|
+
complete: this.makeCompleteHandler(resolve, reject),
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
const retryConfig = { ...DEFAULT_RETRY_OPTIONS, ...this.retryOptions };
|
|
225
|
+
if (retryConfig.max && retryConfig.max > 0) {
|
|
226
|
+
return this.executeWithRetry(executeQuery, retryConfig);
|
|
227
|
+
}
|
|
228
|
+
return executeQuery();
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Execute a query and return a handle that allows cancelling it before it
|
|
232
|
+
* completes, via snowflake-sdk's `Statement.cancel()` API. This is the
|
|
233
|
+
* long-running-query abort path that plain `query()` does not offer: once
|
|
234
|
+
* a query has been submitted, `query()` gives the caller no way to stop it
|
|
235
|
+
* short of waiting (or retrying, which only compounds wait time under
|
|
236
|
+
* warehouse auto-suspend/resume scenarios).
|
|
237
|
+
*
|
|
238
|
+
* Unlike `query()`, this method does not go through the retry wrapper —
|
|
239
|
+
* callers that need cancellation are expected to manage their own
|
|
240
|
+
* retry/backoff around the returned `result` promise if desired.
|
|
241
|
+
*/
|
|
242
|
+
queryWithCancel(sql, options) {
|
|
243
|
+
if (!this.connection) {
|
|
244
|
+
throw new Error('Not connected to database');
|
|
245
|
+
}
|
|
246
|
+
const { sqlText, binds } = this.resolveExecuteArgs(sql, options);
|
|
247
|
+
let statement;
|
|
248
|
+
const result = new Promise((resolve, reject) => {
|
|
249
|
+
statement = this.connection.execute({
|
|
250
|
+
sqlText,
|
|
251
|
+
binds: binds,
|
|
252
|
+
complete: this.makeCompleteHandler(resolve, reject),
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
const cancel = () => new Promise((resolve, reject) => {
|
|
256
|
+
const stmt = statement;
|
|
257
|
+
if (!stmt || typeof stmt.cancel !== 'function') {
|
|
258
|
+
resolve();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
stmt.cancel((err) => {
|
|
262
|
+
if (err) {
|
|
263
|
+
reject(new Error(`Failed to cancel Snowflake statement: ${err.message || err}`));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
resolve();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
const getQueryId = () => {
|
|
270
|
+
const stmt = statement;
|
|
271
|
+
return stmt && typeof stmt.getQueryId === 'function' ? stmt.getQueryId() : undefined;
|
|
272
|
+
};
|
|
273
|
+
return { result, cancel, getQueryId };
|
|
274
|
+
}
|
|
275
|
+
async executeWithRetry(fn, retryOptions) {
|
|
276
|
+
let lastError;
|
|
277
|
+
for (let attempt = 0; attempt <= retryOptions.max; attempt++) {
|
|
278
|
+
try {
|
|
279
|
+
return await fn();
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
lastError = error;
|
|
283
|
+
const message = lastError.message.toLowerCase();
|
|
284
|
+
const shouldRetry = retryOptions.match.some((pattern) => message.includes(pattern.toLowerCase()));
|
|
285
|
+
if (!shouldRetry || attempt === retryOptions.max) {
|
|
286
|
+
throw lastError;
|
|
287
|
+
}
|
|
288
|
+
const delay = retryOptions.backoff
|
|
289
|
+
? Math.min(retryOptions.timeout * Math.pow(retryOptions.backoffMultiplier || 2, attempt), retryOptions.backoffMax || 10000)
|
|
290
|
+
: retryOptions.timeout;
|
|
291
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
throw lastError;
|
|
295
|
+
}
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Escaping / identifiers
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
escape(value) {
|
|
300
|
+
if (value instanceof prorm_1.Literal) {
|
|
301
|
+
return value.val;
|
|
302
|
+
}
|
|
303
|
+
if (value === null || value === undefined) {
|
|
304
|
+
return 'NULL';
|
|
305
|
+
}
|
|
306
|
+
if (typeof value === 'string') {
|
|
307
|
+
return `'${this.escapeString(value)}'`;
|
|
308
|
+
}
|
|
309
|
+
if (typeof value === 'number') {
|
|
310
|
+
return String(value);
|
|
311
|
+
}
|
|
312
|
+
if (typeof value === 'bigint') {
|
|
313
|
+
return value.toString();
|
|
314
|
+
}
|
|
315
|
+
if (typeof value === 'boolean') {
|
|
316
|
+
return value ? 'TRUE' : 'FALSE';
|
|
317
|
+
}
|
|
318
|
+
if (value instanceof Date) {
|
|
319
|
+
return `'${this.formatDate(value)}'`;
|
|
320
|
+
}
|
|
321
|
+
if (Buffer.isBuffer(value)) {
|
|
322
|
+
return `TO_BINARY('${value.toString('hex')}', 'HEX')`;
|
|
323
|
+
}
|
|
324
|
+
if (Array.isArray(value)) {
|
|
325
|
+
// Represent as an Snowflake ARRAY literal via PARSE_JSON
|
|
326
|
+
return `PARSE_JSON('${this.escapeString(JSON.stringify(value))}')`;
|
|
327
|
+
}
|
|
328
|
+
// Objects map to VARIANT/OBJECT using PARSE_JSON
|
|
329
|
+
return `PARSE_JSON('${this.escapeString(JSON.stringify(value))}')`;
|
|
330
|
+
}
|
|
331
|
+
escapeString(str) {
|
|
332
|
+
return str.replace(/'/g, "''").replace(/\\/g, '\\\\');
|
|
333
|
+
}
|
|
334
|
+
formatDate(date) {
|
|
335
|
+
const year = date.getFullYear();
|
|
336
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
337
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
338
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
339
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
340
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
341
|
+
const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
|
|
342
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
|
|
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
|
+
if (this.config.schema) {
|
|
356
|
+
return `${this.escapeId(this.config.schema)}.${this.escapeId(tableName)}`;
|
|
357
|
+
}
|
|
358
|
+
return this.escapeId(tableName);
|
|
359
|
+
}
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Schemas / database
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
async createSchema(schema) {
|
|
364
|
+
await this.query(`CREATE SCHEMA ${this.escapeId(schema)}`);
|
|
365
|
+
}
|
|
366
|
+
async dropSchema(schema, options) {
|
|
367
|
+
let sql = 'DROP SCHEMA';
|
|
368
|
+
if (options?.ifExists)
|
|
369
|
+
sql += ' IF EXISTS';
|
|
370
|
+
sql += ` ${this.escapeId(schema)}`;
|
|
371
|
+
if (options?.cascade)
|
|
372
|
+
sql += ' CASCADE';
|
|
373
|
+
await this.query(sql);
|
|
374
|
+
}
|
|
375
|
+
async showAllSchemas() {
|
|
376
|
+
const result = await this.query(`SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('INFORMATION_SCHEMA')`);
|
|
377
|
+
return result.rows?.map((row) => row.SCHEMA_NAME || row.schema_name) || [];
|
|
378
|
+
}
|
|
379
|
+
async listSchemas() {
|
|
380
|
+
return this.showAllSchemas();
|
|
381
|
+
}
|
|
382
|
+
createDatabaseSQL(options) {
|
|
383
|
+
return `CREATE DATABASE ${this.quoteIdentifier(options.name)}`;
|
|
384
|
+
}
|
|
385
|
+
dropDatabaseSQL(name) {
|
|
386
|
+
return `DROP DATABASE IF EXISTS ${this.quoteIdentifier(name)}`;
|
|
387
|
+
}
|
|
388
|
+
createSavepointSQL(name) {
|
|
389
|
+
// Snowflake does not support savepoints; this is a documented no-op that
|
|
390
|
+
// returns an inert SQL comment so callers that always issue this SQL
|
|
391
|
+
// don't blow up. Real transactional rollback should use ROLLBACK.
|
|
392
|
+
const savepointName = name || `sp_${Date.now()}`;
|
|
393
|
+
console.warn(`Snowflake: savepoints are not supported; '${savepointName}' will not create a real savepoint`);
|
|
394
|
+
return `-- SAVEPOINT ${savepointName} (no-op: not supported by Snowflake)`;
|
|
395
|
+
}
|
|
396
|
+
releaseSavepointSQL(name) {
|
|
397
|
+
return `-- RELEASE SAVEPOINT ${name} (no-op: not supported by Snowflake)`;
|
|
398
|
+
}
|
|
399
|
+
rollbackToSavepointSQL(name) {
|
|
400
|
+
return `-- ROLLBACK TO SAVEPOINT ${name} (no-op: not supported by Snowflake)`;
|
|
401
|
+
}
|
|
402
|
+
// Extensions are a PostgreSQL-only concept
|
|
403
|
+
createExtension(_extensionName, _options) {
|
|
404
|
+
throw new Error('Extensions are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
405
|
+
}
|
|
406
|
+
dropExtension(_extensionName, _options) {
|
|
407
|
+
throw new Error('Extensions are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
408
|
+
}
|
|
409
|
+
getExtensions() {
|
|
410
|
+
throw new Error('Extensions are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
411
|
+
}
|
|
412
|
+
hasExtension(_extensionName) {
|
|
413
|
+
throw new Error('Extensions are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
414
|
+
}
|
|
415
|
+
async getDatabaseVersion() {
|
|
416
|
+
const result = await this.query(`SELECT CURRENT_VERSION() AS version`);
|
|
417
|
+
return result.rows?.[0]?.VERSION || result.rows?.[0]?.version || 'Unknown';
|
|
418
|
+
}
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// Tables / columns
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
async createTable(tableName, columns, options) {
|
|
423
|
+
const columnDefs = [];
|
|
424
|
+
for (const [columnName, definition] of Object.entries(columns)) {
|
|
425
|
+
columnDefs.push(this.getColumnDefinitionSql(columnName, definition));
|
|
426
|
+
}
|
|
427
|
+
if (options?.constraints) {
|
|
428
|
+
for (const constraint of options.constraints) {
|
|
429
|
+
const constraintSql = this.buildConstraintSql(constraint);
|
|
430
|
+
if (constraintSql)
|
|
431
|
+
columnDefs.push(constraintSql);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
let sql = 'CREATE TABLE';
|
|
435
|
+
if (options?.ifNotExists)
|
|
436
|
+
sql += ' IF NOT EXISTS';
|
|
437
|
+
sql += ` ${this.quoteTable(tableName)} (${columnDefs.join(', ')})`;
|
|
438
|
+
if (options?.comment) {
|
|
439
|
+
sql += ` COMMENT = '${this.escapeString(options.comment)}'`;
|
|
440
|
+
}
|
|
441
|
+
if (options?.engine) {
|
|
442
|
+
console.warn(`Snowflake: engine option '${options.engine}' is not supported and will be ignored`);
|
|
443
|
+
}
|
|
444
|
+
if (options?.charset) {
|
|
445
|
+
console.warn(`Snowflake: charset option '${options.charset}' is not supported and will be ignored`);
|
|
446
|
+
}
|
|
447
|
+
if (options?.rowFormat) {
|
|
448
|
+
console.warn(`Snowflake: rowFormat option '${options.rowFormat}' is not supported and will be ignored`);
|
|
449
|
+
}
|
|
450
|
+
if (options?.tablespace) {
|
|
451
|
+
console.warn('Snowflake: tablespaces are not supported and will be ignored');
|
|
452
|
+
}
|
|
453
|
+
await this.query(sql);
|
|
454
|
+
if (options?.uniqueKeys) {
|
|
455
|
+
const entries = Array.isArray(options.uniqueKeys)
|
|
456
|
+
? options.uniqueKeys.map((uk) => [uk.name, uk.fields])
|
|
457
|
+
: Object.entries(options.uniqueKeys);
|
|
458
|
+
for (const [keyName, fields] of entries) {
|
|
459
|
+
const constraintName = keyName || `uq_${tableName}_${fields.join('_')}`;
|
|
460
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} UNIQUE (${fields
|
|
461
|
+
.map((f) => this.escapeId(f))
|
|
462
|
+
.join(', ')})`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Index-like options are not applicable to Snowflake's storage engine;
|
|
466
|
+
// addIndex() itself documents and no-ops this, so we simply delegate.
|
|
467
|
+
if (options?.indexes) {
|
|
468
|
+
for (const index of options.indexes) {
|
|
469
|
+
await this.addIndex(tableName, index.name || `idx_${tableName}_${index.fields.join('_')}`, index.fields, { unique: index.unique });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
buildConstraintSql(constraint) {
|
|
474
|
+
const name = constraint.name ? this.escapeId(constraint.name) : '';
|
|
475
|
+
const fields = constraint.fields?.map((f) => this.escapeId(f)).join(', ') || '';
|
|
476
|
+
switch (constraint.type) {
|
|
477
|
+
case 'PRIMARY KEY':
|
|
478
|
+
return `${name ? `CONSTRAINT ${name} ` : ''}PRIMARY KEY (${fields})`;
|
|
479
|
+
case 'UNIQUE':
|
|
480
|
+
return `${name ? `CONSTRAINT ${name} ` : ''}UNIQUE (${fields})`;
|
|
481
|
+
case 'FOREIGN KEY': {
|
|
482
|
+
if (!constraint.references)
|
|
483
|
+
return null;
|
|
484
|
+
const refField = constraint.references.field;
|
|
485
|
+
const refFieldSql = Array.isArray(refField)
|
|
486
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
487
|
+
: `(${this.escapeId(refField)})`;
|
|
488
|
+
let fkSql = `${name ? `CONSTRAINT ${name} ` : ''}FOREIGN KEY (${fields}) REFERENCES ${this.escapeId(constraint.references.table)}${refFieldSql}`;
|
|
489
|
+
if (constraint.references.onDelete)
|
|
490
|
+
fkSql += ` ON DELETE ${constraint.references.onDelete}`;
|
|
491
|
+
if (constraint.references.onUpdate)
|
|
492
|
+
fkSql += ` ON UPDATE ${constraint.references.onUpdate}`;
|
|
493
|
+
return fkSql;
|
|
494
|
+
}
|
|
495
|
+
case 'CHECK':
|
|
496
|
+
if (!constraint.check)
|
|
497
|
+
return null;
|
|
498
|
+
return `${name ? `CONSTRAINT ${name} ` : ''}CHECK (${constraint.check})`;
|
|
499
|
+
default:
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
getColumnDefinitionSql(columnName, definition) {
|
|
504
|
+
let sql = `${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type)}`;
|
|
505
|
+
if (definition.autoIncrement) {
|
|
506
|
+
sql += ' AUTOINCREMENT';
|
|
507
|
+
}
|
|
508
|
+
if (definition.allowNull === false) {
|
|
509
|
+
sql += ' NOT NULL';
|
|
510
|
+
}
|
|
511
|
+
if (definition.defaultValue !== undefined) {
|
|
512
|
+
sql += ` DEFAULT ${this.getDefaultValue(definition.defaultValue)}`;
|
|
513
|
+
}
|
|
514
|
+
if (definition.primaryKey) {
|
|
515
|
+
sql += ' PRIMARY KEY';
|
|
516
|
+
}
|
|
517
|
+
if (definition.unique) {
|
|
518
|
+
sql += ' UNIQUE';
|
|
519
|
+
}
|
|
520
|
+
if (definition.references) {
|
|
521
|
+
const refField = definition.references.field;
|
|
522
|
+
const refFieldSql = Array.isArray(refField)
|
|
523
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
524
|
+
: this.escapeId(refField);
|
|
525
|
+
sql += ` REFERENCES ${this.escapeId(definition.references.table)}(${refFieldSql})`;
|
|
526
|
+
if (definition.references.onDelete)
|
|
527
|
+
sql += ` ON DELETE ${definition.references.onDelete}`;
|
|
528
|
+
if (definition.references.onUpdate)
|
|
529
|
+
sql += ` ON UPDATE ${definition.references.onUpdate}`;
|
|
530
|
+
}
|
|
531
|
+
if (definition.comment) {
|
|
532
|
+
sql += ` COMMENT '${this.escapeString(definition.comment)}'`;
|
|
533
|
+
}
|
|
534
|
+
return sql;
|
|
535
|
+
}
|
|
536
|
+
getDefaultValue(value) {
|
|
537
|
+
if (value === null)
|
|
538
|
+
return 'NULL';
|
|
539
|
+
if (typeof value === 'string') {
|
|
540
|
+
if (value.toUpperCase().includes('CURRENT_') || value.toUpperCase() === 'NULL') {
|
|
541
|
+
return value;
|
|
542
|
+
}
|
|
543
|
+
return `'${this.escapeString(value)}'`;
|
|
544
|
+
}
|
|
545
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
546
|
+
return String(value);
|
|
547
|
+
}
|
|
548
|
+
if (value instanceof Date) {
|
|
549
|
+
return `'${this.formatDate(value)}'`;
|
|
550
|
+
}
|
|
551
|
+
if (typeof value === 'object' &&
|
|
552
|
+
value !== null &&
|
|
553
|
+
typeof value.toDefaultValue === 'function') {
|
|
554
|
+
return value.toDefaultValue();
|
|
555
|
+
}
|
|
556
|
+
return `'${this.escapeString(String(value))}'`;
|
|
557
|
+
}
|
|
558
|
+
async dropTable(tableName, options) {
|
|
559
|
+
let sql = 'DROP TABLE';
|
|
560
|
+
if (options?.ifExists)
|
|
561
|
+
sql += ' IF EXISTS';
|
|
562
|
+
sql += ` ${this.quoteTable(tableName)}`;
|
|
563
|
+
if (options?.cascade)
|
|
564
|
+
sql += ' CASCADE';
|
|
565
|
+
await this.query(sql);
|
|
566
|
+
}
|
|
567
|
+
// Native range/list/hash partitioning is not a user-facing Snowflake
|
|
568
|
+
// concept (micro-partitioning is automatic); these throw clear errors
|
|
569
|
+
// rather than emitting invalid SQL, consistent with SQLite's handling of
|
|
570
|
+
// partitioning in this codebase.
|
|
571
|
+
async createPartitionedTable() {
|
|
572
|
+
throw new Error('Table partitioning is not supported by Snowflake. Snowflake automatically manages ' +
|
|
573
|
+
'micro-partitions; consider a clustering key (ALTER TABLE ... CLUSTER BY (...)) instead.');
|
|
574
|
+
}
|
|
575
|
+
async createPartition(_options) {
|
|
576
|
+
throw new Error('Table partitioning is not supported by Snowflake.');
|
|
577
|
+
}
|
|
578
|
+
async attachPartition(_options) {
|
|
579
|
+
throw new Error('Table partitioning is not supported by Snowflake.');
|
|
580
|
+
}
|
|
581
|
+
async detachPartition(_options) {
|
|
582
|
+
throw new Error('Table partitioning is not supported by Snowflake.');
|
|
583
|
+
}
|
|
584
|
+
async dropPartition() {
|
|
585
|
+
throw new Error('Table partitioning is not supported by Snowflake.');
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Set (or clear) a clustering key for a table — Snowflake's replacement
|
|
589
|
+
* for manual indexing/partitioning to influence micro-partition pruning.
|
|
590
|
+
*/
|
|
591
|
+
async setClusteringKey(tableName, fields) {
|
|
592
|
+
const sql = `ALTER TABLE ${this.quoteTable(tableName)} CLUSTER BY (${fields
|
|
593
|
+
.map((f) => this.escapeId(f))
|
|
594
|
+
.join(', ')})`;
|
|
595
|
+
await this.query(sql);
|
|
596
|
+
}
|
|
597
|
+
async dropClusteringKey(tableName) {
|
|
598
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} DROP CLUSTERING KEY`);
|
|
599
|
+
}
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// Views
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
async createView(viewName, query, options) {
|
|
604
|
+
let sql = options?.replace ? 'CREATE OR REPLACE VIEW' : 'CREATE VIEW';
|
|
605
|
+
sql += ` ${this.quoteTable(viewName, options?.schema)}`;
|
|
606
|
+
if (options?.comment) {
|
|
607
|
+
sql += ` COMMENT = '${this.escapeString(options.comment)}'`;
|
|
608
|
+
}
|
|
609
|
+
sql += ` AS ${query}`;
|
|
610
|
+
await this.query(sql);
|
|
611
|
+
}
|
|
612
|
+
async dropView(viewName, options) {
|
|
613
|
+
let sql = 'DROP VIEW';
|
|
614
|
+
if (options?.ifExists)
|
|
615
|
+
sql += ' IF EXISTS';
|
|
616
|
+
sql += ` ${this.quoteTable(viewName)}`;
|
|
617
|
+
if (options?.cascade)
|
|
618
|
+
sql += ' CASCADE';
|
|
619
|
+
await this.query(sql);
|
|
620
|
+
}
|
|
621
|
+
async showViews() {
|
|
622
|
+
const result = await this.query('SHOW VIEWS');
|
|
623
|
+
return result.rows?.map((row) => row.name || row.NAME) || [];
|
|
624
|
+
}
|
|
625
|
+
async showMaterializedViews() {
|
|
626
|
+
const result = await this.query('SHOW MATERIALIZED VIEWS');
|
|
627
|
+
return result.rows?.map((row) => row.name || row.NAME) || [];
|
|
628
|
+
}
|
|
629
|
+
// ---------------------------------------------------------------------------
|
|
630
|
+
// Columns / constraints / tables metadata
|
|
631
|
+
// ---------------------------------------------------------------------------
|
|
632
|
+
async addColumn(tableName, columnName, definition) {
|
|
633
|
+
const sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD COLUMN ${this.getColumnDefinitionSql(columnName, definition)}`;
|
|
634
|
+
await this.query(sql);
|
|
635
|
+
}
|
|
636
|
+
async removeColumn(tableName, columnName) {
|
|
637
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} DROP COLUMN ${this.escapeId(columnName)}`);
|
|
638
|
+
}
|
|
639
|
+
async changeColumn(tableName, columnName, definition) {
|
|
640
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ALTER COLUMN ${this.escapeId(columnName)} SET DATA TYPE ${this.getDataTypeSql(definition.type)}`);
|
|
641
|
+
if (definition.allowNull === false) {
|
|
642
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ALTER COLUMN ${this.escapeId(columnName)} SET NOT NULL`);
|
|
643
|
+
}
|
|
644
|
+
else if (definition.allowNull === true) {
|
|
645
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ALTER COLUMN ${this.escapeId(columnName)} DROP NOT NULL`);
|
|
646
|
+
}
|
|
647
|
+
if (definition.defaultValue !== undefined) {
|
|
648
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ALTER COLUMN ${this.escapeId(columnName)} SET DEFAULT ${this.getDefaultValue(definition.defaultValue)}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async renameColumn(tableName, oldColumnName, newColumnName) {
|
|
652
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} RENAME COLUMN ${this.escapeId(oldColumnName)} TO ${this.escapeId(newColumnName)}`);
|
|
653
|
+
}
|
|
654
|
+
async addForeignKey(tableName, columnName, referencedTableName, referencedColumnName, options) {
|
|
655
|
+
const constraintName = options?.name || `fk_${tableName}_${columnName}`;
|
|
656
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} FOREIGN KEY (${this.escapeId(columnName)}) REFERENCES ${this.quoteTable(referencedTableName)} (${this.escapeId(referencedColumnName)})`;
|
|
657
|
+
if (options?.onDelete)
|
|
658
|
+
sql += ` ON DELETE ${options.onDelete}`;
|
|
659
|
+
if (options?.onUpdate)
|
|
660
|
+
sql += ` ON UPDATE ${options.onUpdate}`;
|
|
661
|
+
await this.query(sql);
|
|
662
|
+
}
|
|
663
|
+
async bulkInsert(tableName, records, _options) {
|
|
664
|
+
if (records.length === 0) {
|
|
665
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
666
|
+
}
|
|
667
|
+
const columns = Object.keys(records[0]);
|
|
668
|
+
const values = [];
|
|
669
|
+
const placeholders = [];
|
|
670
|
+
for (const record of records) {
|
|
671
|
+
const rowPlaceholders = columns.map((c) => {
|
|
672
|
+
values.push(record[c]);
|
|
673
|
+
return '?';
|
|
674
|
+
});
|
|
675
|
+
placeholders.push(`(${rowPlaceholders.join(', ')})`);
|
|
676
|
+
}
|
|
677
|
+
const sql = `INSERT INTO ${this.quoteTable(tableName)} (${columns
|
|
678
|
+
.map((c) => this.escapeId(c))
|
|
679
|
+
.join(', ')}) VALUES ${placeholders.join(', ')}`;
|
|
680
|
+
return this.query(sql, { replacements: values });
|
|
681
|
+
}
|
|
682
|
+
async showTables() {
|
|
683
|
+
const result = await this.query('SHOW TABLES');
|
|
684
|
+
return result.rows?.map((row) => row.name || row.NAME) || [];
|
|
685
|
+
}
|
|
686
|
+
async showConstraints(tableName) {
|
|
687
|
+
const result = await this.query(`SHOW UNIQUE KEYS IN TABLE ${this.quoteTable(tableName)}`);
|
|
688
|
+
return result.rows || [];
|
|
689
|
+
}
|
|
690
|
+
async addConstraint(tableName, options) {
|
|
691
|
+
const constraintName = options.name || `${tableName}_${options.fields.join('_')}_${options.type.toLowerCase().replace(/\s+/g, '_')}`;
|
|
692
|
+
const fieldsSql = options.fields.map((f) => this.escapeId(f)).join(', ');
|
|
693
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} `;
|
|
694
|
+
switch (options.type) {
|
|
695
|
+
case 'UNIQUE':
|
|
696
|
+
sql += `UNIQUE (${fieldsSql})`;
|
|
697
|
+
break;
|
|
698
|
+
case 'PRIMARY KEY':
|
|
699
|
+
sql += `PRIMARY KEY (${fieldsSql})`;
|
|
700
|
+
break;
|
|
701
|
+
case 'FOREIGN KEY':
|
|
702
|
+
sql += `FOREIGN KEY (${fieldsSql}) REFERENCES ${this.quoteTable(options.references.table)} (${options.references.fields
|
|
703
|
+
.map((f) => this.escapeId(f))
|
|
704
|
+
.join(', ')})`;
|
|
705
|
+
break;
|
|
706
|
+
case 'CHECK':
|
|
707
|
+
sql += `CHECK (${options.check})`;
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
await this.query(sql);
|
|
711
|
+
}
|
|
712
|
+
async removeConstraint(tableName, constraintName) {
|
|
713
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.escapeId(constraintName)}`);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Snowflake has no traditional indexes; it returns an empty list rather
|
|
717
|
+
* than throwing so generic ORM code paths that inspect "current indexes"
|
|
718
|
+
* degrade gracefully.
|
|
719
|
+
*/
|
|
720
|
+
async showIndexes(_tableName) {
|
|
721
|
+
console.warn('Snowflake: showIndexes() is a no-op — Snowflake has no traditional indexes (micro-partitioning ' +
|
|
722
|
+
'and clustering keys are used instead). Returning an empty array.');
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
async describeTable(tableName) {
|
|
726
|
+
const result = await this.query(`DESCRIBE TABLE ${this.quoteTable(tableName)}`);
|
|
727
|
+
const description = {};
|
|
728
|
+
for (const row of result.rows || []) {
|
|
729
|
+
const name = row.name || row.NAME;
|
|
730
|
+
const type = String(row.type || row.TYPE || '');
|
|
731
|
+
const isNullable = (row['null?'] ?? row['NULL?'] ?? 'Y') === 'Y';
|
|
732
|
+
const defaultValue = row.default ?? row.DEFAULT ?? null;
|
|
733
|
+
const isPrimaryKey = (row['primary key'] ?? row['PRIMARY KEY'] ?? 'N') === 'Y';
|
|
734
|
+
description[name] = {
|
|
735
|
+
type,
|
|
736
|
+
allowNull: isNullable,
|
|
737
|
+
defaultValue,
|
|
738
|
+
primaryKey: isPrimaryKey,
|
|
739
|
+
autoIncrement: /AUTOINCREMENT|IDENTITY/i.test(String(row.default ?? row.DEFAULT ?? '')),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
return description;
|
|
743
|
+
}
|
|
744
|
+
async renameTable(oldName, newName) {
|
|
745
|
+
await this.query(`ALTER TABLE ${this.quoteTable(oldName)} RENAME TO ${this.quoteTable(newName)}`);
|
|
746
|
+
}
|
|
747
|
+
// ---------------------------------------------------------------------------
|
|
748
|
+
// Indexes — Snowflake has no traditional index structures. These are
|
|
749
|
+
// documented no-ops (they warn instead of throwing) so ORM-level code that
|
|
750
|
+
// unconditionally calls addIndex/createIndex/etc. after createTable (as
|
|
751
|
+
// this dialect's own createTable does) keeps working across all dialects.
|
|
752
|
+
// ---------------------------------------------------------------------------
|
|
753
|
+
async addIndex(tableName, indexName, _fields, _options) {
|
|
754
|
+
console.warn(`Snowflake: addIndex('${indexName}' on '${tableName}') is a no-op — Snowflake has no traditional ` +
|
|
755
|
+
'indexes. Use a clustering key (setClusteringKey) to influence micro-partition pruning instead.');
|
|
756
|
+
}
|
|
757
|
+
async removeIndex(tableName, indexName) {
|
|
758
|
+
console.warn(`Snowflake: removeIndex('${indexName}' on '${tableName}') is a no-op — Snowflake has no traditional indexes.`);
|
|
759
|
+
}
|
|
760
|
+
async createIndex(tableName, indexDef) {
|
|
761
|
+
console.warn(`Snowflake: createIndex('${indexDef.name}' on '${tableName}') is a no-op — Snowflake has no traditional indexes.`);
|
|
762
|
+
}
|
|
763
|
+
async dropIndex(tableName, indexName, _options) {
|
|
764
|
+
console.warn(`Snowflake: dropIndex('${indexName}' on '${tableName}') is a no-op — Snowflake has no traditional indexes.`);
|
|
765
|
+
}
|
|
766
|
+
async createFulltextIndex(tableName, indexName, _fields, _options) {
|
|
767
|
+
console.warn(`Snowflake: createFulltextIndex('${indexName}' on '${tableName}') is a no-op — Snowflake has no ` +
|
|
768
|
+
'fulltext index concept; consider Snowflake Search (CORTEX SEARCH) or SEARCH OPTIMIZATION instead.');
|
|
769
|
+
}
|
|
770
|
+
async createSpatialIndex(tableName, indexName, _fields, _options) {
|
|
771
|
+
console.warn(`Snowflake: createSpatialIndex('${indexName}' on '${tableName}') is a no-op — Snowflake has no spatial index concept.`);
|
|
772
|
+
}
|
|
773
|
+
async createConstraint(tableName, constraintDef) {
|
|
774
|
+
const sql = this.buildConstraintSql({
|
|
775
|
+
name: constraintDef.name,
|
|
776
|
+
type: constraintDef.type,
|
|
777
|
+
fields: constraintDef.fields,
|
|
778
|
+
references: constraintDef.references
|
|
779
|
+
? {
|
|
780
|
+
table: constraintDef.references.table,
|
|
781
|
+
field: constraintDef.references.field,
|
|
782
|
+
onDelete: constraintDef.references.onDelete,
|
|
783
|
+
onUpdate: constraintDef.references.onUpdate,
|
|
784
|
+
}
|
|
785
|
+
: undefined,
|
|
786
|
+
check: constraintDef.check,
|
|
787
|
+
});
|
|
788
|
+
if (sql) {
|
|
789
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD ${sql}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async dropConstraint(tableName, constraintName, options) {
|
|
793
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT`;
|
|
794
|
+
if (options?.ifExists)
|
|
795
|
+
sql += ' IF EXISTS';
|
|
796
|
+
sql += ` ${this.escapeId(constraintName)}`;
|
|
797
|
+
if (options?.cascade)
|
|
798
|
+
sql += ' CASCADE';
|
|
799
|
+
await this.query(sql);
|
|
800
|
+
}
|
|
801
|
+
async changeOwner(newOwner, tableName) {
|
|
802
|
+
await this.query(`GRANT OWNERSHIP ON TABLE ${this.quoteTable(tableName)} TO ROLE ${this.escapeId(newOwner)}`);
|
|
803
|
+
}
|
|
804
|
+
// ---------------------------------------------------------------------------
|
|
805
|
+
// Foreign Data Wrappers — not a Snowflake concept (external tables /
|
|
806
|
+
// stages / integrations serve a similar purpose but are not equivalent).
|
|
807
|
+
// ---------------------------------------------------------------------------
|
|
808
|
+
async createForeignDataWrapper() {
|
|
809
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
810
|
+
}
|
|
811
|
+
async dropForeignDataWrapper() {
|
|
812
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
813
|
+
}
|
|
814
|
+
async createForeignServer() {
|
|
815
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
816
|
+
}
|
|
817
|
+
async dropForeignServer() {
|
|
818
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
819
|
+
}
|
|
820
|
+
async createForeignTable() {
|
|
821
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
822
|
+
}
|
|
823
|
+
async createSecurityPolicy(_policyName, _tableName, _options) {
|
|
824
|
+
throw new Error('createSecurityPolicy is not supported by the Snowflake dialect; use Snowflake row access ' +
|
|
825
|
+
'policies (CREATE ROW ACCESS POLICY / ALTER TABLE ... ADD ROW ACCESS POLICY) directly.');
|
|
826
|
+
}
|
|
827
|
+
async dropSecurityPolicy(_policyName, _tableName) {
|
|
828
|
+
throw new Error('dropSecurityPolicy is not supported by the Snowflake dialect');
|
|
829
|
+
}
|
|
830
|
+
// ---------------------------------------------------------------------------
|
|
831
|
+
// Transactions
|
|
832
|
+
// ---------------------------------------------------------------------------
|
|
833
|
+
async startTransaction(_options) {
|
|
834
|
+
await this.query('BEGIN');
|
|
835
|
+
return new types_1.Transaction({ autocommit: false });
|
|
836
|
+
}
|
|
837
|
+
async commitTransaction(_transaction) {
|
|
838
|
+
await this.query('COMMIT');
|
|
839
|
+
}
|
|
840
|
+
async rollbackTransaction(_transaction) {
|
|
841
|
+
await this.query('ROLLBACK');
|
|
842
|
+
}
|
|
843
|
+
// ---------------------------------------------------------------------------
|
|
844
|
+
// Data types
|
|
845
|
+
// ---------------------------------------------------------------------------
|
|
846
|
+
getDataTypeSql(dataType) {
|
|
847
|
+
if (typeof dataType === 'string') {
|
|
848
|
+
return dataType;
|
|
849
|
+
}
|
|
850
|
+
if (!dataType || typeof dataType !== 'object') {
|
|
851
|
+
return 'VARCHAR(255)';
|
|
852
|
+
}
|
|
853
|
+
const dt = dataType;
|
|
854
|
+
switch (dt.key) {
|
|
855
|
+
case 'STRING':
|
|
856
|
+
return `VARCHAR(${dt.length || 255})`;
|
|
857
|
+
case 'CHAR':
|
|
858
|
+
return `CHAR(${dt.length || 1})`;
|
|
859
|
+
case 'TEXT':
|
|
860
|
+
return 'VARCHAR';
|
|
861
|
+
case 'INTEGER':
|
|
862
|
+
return 'NUMBER(38,0)';
|
|
863
|
+
case 'BIGINT':
|
|
864
|
+
return 'NUMBER(38,0)';
|
|
865
|
+
case 'FLOAT':
|
|
866
|
+
return 'FLOAT';
|
|
867
|
+
case 'DOUBLE':
|
|
868
|
+
return 'DOUBLE';
|
|
869
|
+
case 'DECIMAL':
|
|
870
|
+
return `NUMBER(${dt.precision || 38},${dt.scale || 0})`;
|
|
871
|
+
case 'BOOLEAN':
|
|
872
|
+
return 'BOOLEAN';
|
|
873
|
+
case 'DATE':
|
|
874
|
+
return dt.precision ? `TIMESTAMP_NTZ(${dt.precision})` : 'TIMESTAMP_NTZ';
|
|
875
|
+
case 'DATEONLY':
|
|
876
|
+
return 'DATE';
|
|
877
|
+
case 'TIME':
|
|
878
|
+
return dt.precision ? `TIME(${dt.precision})` : 'TIME';
|
|
879
|
+
case 'BLOB':
|
|
880
|
+
return 'BINARY';
|
|
881
|
+
case 'ENUM':
|
|
882
|
+
// Snowflake has no native ENUM type; approximate with VARCHAR plus a
|
|
883
|
+
// CHECK constraint would be added at the table level by callers.
|
|
884
|
+
return 'VARCHAR';
|
|
885
|
+
// Semi-structured types: Snowflake uses VARIANT/OBJECT/ARRAY instead of
|
|
886
|
+
// JSON/JSONB. VARIANT can hold any semi-structured value and is queried
|
|
887
|
+
// with `:`-path syntax or GET()/GET_PATH(), so JSON/JSONB map to it.
|
|
888
|
+
case 'JSON':
|
|
889
|
+
case 'JSONB':
|
|
890
|
+
return 'VARIANT';
|
|
891
|
+
case 'UUID':
|
|
892
|
+
return 'VARCHAR(36)';
|
|
893
|
+
case 'ARRAY':
|
|
894
|
+
return 'ARRAY';
|
|
895
|
+
case 'GEOMETRY':
|
|
896
|
+
case 'GEOGRAPHY':
|
|
897
|
+
return 'GEOGRAPHY';
|
|
898
|
+
case 'VIRTUAL':
|
|
899
|
+
return '';
|
|
900
|
+
default:
|
|
901
|
+
return 'VARCHAR(255)';
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Build a Snowflake VARIANT/OBJECT path query using `:` path access
|
|
906
|
+
* (e.g. `data:key::STRING`) rather than JSON_EXTRACT.
|
|
907
|
+
*/
|
|
908
|
+
buildJsonQuery(column, path, value, operator = '=') {
|
|
909
|
+
const values = [];
|
|
910
|
+
const columnRef = this.quoteIdentifier(column);
|
|
911
|
+
const pathAccess = path
|
|
912
|
+
.split('.')
|
|
913
|
+
.map((p, i) => (i === 0 ? `:${p}` : `.${p}`))
|
|
914
|
+
.join('');
|
|
915
|
+
const extraction = `${columnRef}${pathAccess}::STRING`;
|
|
916
|
+
if (value !== undefined) {
|
|
917
|
+
values.push(value);
|
|
918
|
+
return { sql: `${extraction} ${operator} ?`, values };
|
|
919
|
+
}
|
|
920
|
+
return { sql: extraction, values };
|
|
921
|
+
}
|
|
922
|
+
// ---------------------------------------------------------------------------
|
|
923
|
+
// Query builders
|
|
924
|
+
// ---------------------------------------------------------------------------
|
|
925
|
+
buildWhereClause(where, _options) {
|
|
926
|
+
const values = [];
|
|
927
|
+
if (!where || Object.keys(where).length === 0) {
|
|
928
|
+
return { sql: '', values };
|
|
929
|
+
}
|
|
930
|
+
const buildCondition = (condition) => {
|
|
931
|
+
if (!condition)
|
|
932
|
+
return '';
|
|
933
|
+
if (typeof condition !== 'object') {
|
|
934
|
+
values.push(condition);
|
|
935
|
+
return '?';
|
|
936
|
+
}
|
|
937
|
+
const cond = condition;
|
|
938
|
+
// Handle logical operators
|
|
939
|
+
if (cond.$and || cond.$or || cond.$not) {
|
|
940
|
+
const conditions = [];
|
|
941
|
+
if (cond.$and) {
|
|
942
|
+
const andConditions = cond.$and.map((c) => buildCondition(c));
|
|
943
|
+
conditions.push(`(${andConditions.join(' AND ')})`);
|
|
944
|
+
}
|
|
945
|
+
if (cond.$or) {
|
|
946
|
+
const orConditions = cond.$or.map((c) => buildCondition(c));
|
|
947
|
+
conditions.push(`(${orConditions.join(' OR ')})`);
|
|
948
|
+
}
|
|
949
|
+
if (cond.$not) {
|
|
950
|
+
const result = buildCondition(cond.$not);
|
|
951
|
+
conditions.push(`NOT (${result})`);
|
|
952
|
+
}
|
|
953
|
+
return conditions.join(' AND ');
|
|
954
|
+
}
|
|
955
|
+
// Handle field conditions
|
|
956
|
+
const fieldConditions = [];
|
|
957
|
+
for (const [key, value] of Object.entries(cond)) {
|
|
958
|
+
if (key.startsWith('$'))
|
|
959
|
+
continue;
|
|
960
|
+
if (value === null) {
|
|
961
|
+
fieldConditions.push(`${this.escapeId(key)} IS NULL`);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (Array.isArray(value)) {
|
|
965
|
+
for (const v of value)
|
|
966
|
+
values.push(v);
|
|
967
|
+
fieldConditions.push(`${this.escapeId(key)} IN (${value.map(() => '?').join(', ')})`);
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (value && typeof value === 'object' && !(value instanceof Date)) {
|
|
971
|
+
const valueObj = value;
|
|
972
|
+
if (valueObj.$eq !== undefined) {
|
|
973
|
+
values.push(valueObj.$eq);
|
|
974
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
975
|
+
}
|
|
976
|
+
else if (valueObj.$ne !== undefined) {
|
|
977
|
+
values.push(valueObj.$ne);
|
|
978
|
+
fieldConditions.push(`${this.escapeId(key)} != ?`);
|
|
979
|
+
}
|
|
980
|
+
else if (valueObj.$gt !== undefined) {
|
|
981
|
+
values.push(valueObj.$gt);
|
|
982
|
+
fieldConditions.push(`${this.escapeId(key)} > ?`);
|
|
983
|
+
}
|
|
984
|
+
else if (valueObj.$gte !== undefined) {
|
|
985
|
+
values.push(valueObj.$gte);
|
|
986
|
+
fieldConditions.push(`${this.escapeId(key)} >= ?`);
|
|
987
|
+
}
|
|
988
|
+
else if (valueObj.$lt !== undefined) {
|
|
989
|
+
values.push(valueObj.$lt);
|
|
990
|
+
fieldConditions.push(`${this.escapeId(key)} < ?`);
|
|
991
|
+
}
|
|
992
|
+
else if (valueObj.$lte !== undefined) {
|
|
993
|
+
values.push(valueObj.$lte);
|
|
994
|
+
fieldConditions.push(`${this.escapeId(key)} <= ?`);
|
|
995
|
+
}
|
|
996
|
+
else if (valueObj.$like !== undefined) {
|
|
997
|
+
values.push(valueObj.$like);
|
|
998
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
999
|
+
}
|
|
1000
|
+
else if (valueObj.$notLike !== undefined) {
|
|
1001
|
+
values.push(valueObj.$notLike);
|
|
1002
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
1003
|
+
}
|
|
1004
|
+
else if (valueObj.$iLike !== undefined) {
|
|
1005
|
+
values.push(valueObj.$iLike);
|
|
1006
|
+
fieldConditions.push(`ILIKE(${this.escapeId(key)}, ?)`);
|
|
1007
|
+
}
|
|
1008
|
+
else if (valueObj.$notILike !== undefined) {
|
|
1009
|
+
values.push(valueObj.$notILike);
|
|
1010
|
+
fieldConditions.push(`NOT ILIKE(${this.escapeId(key)}, ?)`);
|
|
1011
|
+
}
|
|
1012
|
+
else if (valueObj.$startsWith !== undefined) {
|
|
1013
|
+
values.push(`${valueObj.$startsWith}%`);
|
|
1014
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
1015
|
+
}
|
|
1016
|
+
else if (valueObj.$notStartsWith !== undefined) {
|
|
1017
|
+
values.push(`${valueObj.$notStartsWith}%`);
|
|
1018
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
1019
|
+
}
|
|
1020
|
+
else if (valueObj.$endsWith !== undefined) {
|
|
1021
|
+
values.push(`%${valueObj.$endsWith}`);
|
|
1022
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
1023
|
+
}
|
|
1024
|
+
else if (valueObj.$notEndsWith !== undefined) {
|
|
1025
|
+
values.push(`%${valueObj.$notEndsWith}`);
|
|
1026
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
1027
|
+
}
|
|
1028
|
+
else if (valueObj.$substring !== undefined) {
|
|
1029
|
+
values.push(`%${valueObj.$substring}%`);
|
|
1030
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
1031
|
+
}
|
|
1032
|
+
else if (valueObj.$notSubstring !== undefined) {
|
|
1033
|
+
values.push(`%${valueObj.$notSubstring}%`);
|
|
1034
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
1035
|
+
}
|
|
1036
|
+
else if (valueObj.$in) {
|
|
1037
|
+
const inValues = valueObj.$in;
|
|
1038
|
+
values.push(...inValues);
|
|
1039
|
+
fieldConditions.push(`${this.escapeId(key)} IN (${inValues.map(() => '?').join(', ')})`);
|
|
1040
|
+
}
|
|
1041
|
+
else if (valueObj.$notIn) {
|
|
1042
|
+
const notInValues = valueObj.$notIn;
|
|
1043
|
+
values.push(...notInValues);
|
|
1044
|
+
fieldConditions.push(`${this.escapeId(key)} NOT IN (${notInValues.map(() => '?').join(', ')})`);
|
|
1045
|
+
}
|
|
1046
|
+
else if (valueObj.$between) {
|
|
1047
|
+
const between = valueObj.$between;
|
|
1048
|
+
values.push(between[0], between[1]);
|
|
1049
|
+
fieldConditions.push(`${this.escapeId(key)} BETWEEN ? AND ?`);
|
|
1050
|
+
}
|
|
1051
|
+
else if (valueObj.$notBetween) {
|
|
1052
|
+
const notBetween = valueObj.$notBetween;
|
|
1053
|
+
values.push(notBetween[0], notBetween[1]);
|
|
1054
|
+
fieldConditions.push(`${this.escapeId(key)} NOT BETWEEN ? AND ?`);
|
|
1055
|
+
}
|
|
1056
|
+
else if (valueObj.$isNull !== undefined) {
|
|
1057
|
+
fieldConditions.push(valueObj.$isNull
|
|
1058
|
+
? `${this.escapeId(key)} IS NULL`
|
|
1059
|
+
: `${this.escapeId(key)} IS NOT NULL`);
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
values.push(value);
|
|
1063
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
values.push(value);
|
|
1068
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return fieldConditions.join(' AND ');
|
|
1072
|
+
};
|
|
1073
|
+
const sql = buildCondition(where);
|
|
1074
|
+
return { sql, values };
|
|
1075
|
+
}
|
|
1076
|
+
buildOrderClause(order, _options) {
|
|
1077
|
+
if (!order || (Array.isArray(order) && order.length === 0))
|
|
1078
|
+
return '';
|
|
1079
|
+
const orderParts = [];
|
|
1080
|
+
const processOrder = (orderItem) => {
|
|
1081
|
+
if (typeof orderItem === 'string') {
|
|
1082
|
+
const parts = orderItem.split(' ');
|
|
1083
|
+
if (parts.length === 1) {
|
|
1084
|
+
orderParts.push(this.escapeId(parts[0]));
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
orderParts.push(`${this.escapeId(parts[0])} ${parts[1].toUpperCase()}`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
else if (typeof orderItem === 'object' && orderItem !== null) {
|
|
1091
|
+
for (const [key, value] of Object.entries(orderItem)) {
|
|
1092
|
+
if (key === '$raw') {
|
|
1093
|
+
orderParts.push(String(value));
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
const direction = value?.toUpperCase() || 'ASC';
|
|
1097
|
+
orderParts.push(`${this.escapeId(key)} ${direction}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
if (Array.isArray(order)) {
|
|
1103
|
+
order.forEach(processOrder);
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
processOrder(order);
|
|
1107
|
+
}
|
|
1108
|
+
return orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Snowflake supports standard LIMIT/OFFSET pagination (unlike Oracle/MSSQL's
|
|
1112
|
+
* FETCH FIRST-only style), so this is a direct passthrough.
|
|
1113
|
+
*/
|
|
1114
|
+
buildLimitOffset(limit, offset) {
|
|
1115
|
+
let sql = '';
|
|
1116
|
+
if (limit !== undefined) {
|
|
1117
|
+
sql += ` LIMIT ${Number(limit)}`;
|
|
1118
|
+
}
|
|
1119
|
+
if (offset !== undefined) {
|
|
1120
|
+
sql += ` OFFSET ${Number(offset)}`;
|
|
1121
|
+
}
|
|
1122
|
+
return sql;
|
|
1123
|
+
}
|
|
1124
|
+
buildInsertQuery(tableName, values, options) {
|
|
1125
|
+
const columns = Object.keys(values);
|
|
1126
|
+
const processedValues = [];
|
|
1127
|
+
const placeholders = [];
|
|
1128
|
+
for (const value of Object.values(values)) {
|
|
1129
|
+
if (value instanceof prorm_1.Literal) {
|
|
1130
|
+
placeholders.push(value.val);
|
|
1131
|
+
}
|
|
1132
|
+
else {
|
|
1133
|
+
processedValues.push(value);
|
|
1134
|
+
placeholders.push('?');
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
const sql = `INSERT INTO ${this.quoteTable(tableName, options?.schema)} (${columns
|
|
1138
|
+
.map((c) => this.escapeId(c))
|
|
1139
|
+
.join(', ')}) VALUES (${placeholders.join(', ')})`;
|
|
1140
|
+
// Snowflake's INSERT does not support a RETURNING clause; callers should
|
|
1141
|
+
// follow up with a SELECT if they need the inserted row back.
|
|
1142
|
+
return { sql, values: processedValues };
|
|
1143
|
+
}
|
|
1144
|
+
buildUpdateQuery(tableName, values, where, options) {
|
|
1145
|
+
const processedValues = [];
|
|
1146
|
+
const setClauses = [];
|
|
1147
|
+
for (const [key, value] of Object.entries(values)) {
|
|
1148
|
+
if (value instanceof prorm_1.Literal) {
|
|
1149
|
+
setClauses.push(`${this.escapeId(key)} = ${value.val}`);
|
|
1150
|
+
}
|
|
1151
|
+
else {
|
|
1152
|
+
processedValues.push(value);
|
|
1153
|
+
setClauses.push(`${this.escapeId(key)} = ?`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
let sql = `UPDATE ${this.quoteTable(tableName)} SET ${setClauses.join(', ')}`;
|
|
1157
|
+
if (where && Object.keys(where).length > 0) {
|
|
1158
|
+
const whereClause = this.buildWhereClause(where);
|
|
1159
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1160
|
+
processedValues.push(...whereClause.values);
|
|
1161
|
+
}
|
|
1162
|
+
if (options?.limit) {
|
|
1163
|
+
console.warn('Snowflake: UPDATE does not support LIMIT; the limit option will be ignored');
|
|
1164
|
+
}
|
|
1165
|
+
return { sql, values: processedValues };
|
|
1166
|
+
}
|
|
1167
|
+
buildDeleteQuery(tableName, where, options) {
|
|
1168
|
+
if (options?.truncate) {
|
|
1169
|
+
return { sql: `TRUNCATE TABLE ${this.quoteTable(tableName)}`, values: [] };
|
|
1170
|
+
}
|
|
1171
|
+
let sql = `DELETE FROM ${this.quoteTable(tableName)}`;
|
|
1172
|
+
const processedValues = [];
|
|
1173
|
+
if (where && Object.keys(where).length > 0) {
|
|
1174
|
+
const whereClause = this.buildWhereClause(where);
|
|
1175
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1176
|
+
processedValues.push(...whereClause.values);
|
|
1177
|
+
}
|
|
1178
|
+
if (options?.limit) {
|
|
1179
|
+
console.warn('Snowflake: DELETE does not support LIMIT; the limit option will be ignored');
|
|
1180
|
+
}
|
|
1181
|
+
return { sql, values: processedValues };
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Formats a single SELECT attribute. Plain column names are identifier-
|
|
1185
|
+
* quoted as before; entries that look like SQL expressions rather than
|
|
1186
|
+
* bare identifiers (e.g. window functions like
|
|
1187
|
+
* `ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn`, or
|
|
1188
|
+
* any other expression containing parentheses/whitespace/an `AS` alias)
|
|
1189
|
+
* are passed through unescaped so callers can use analytical SQL that a
|
|
1190
|
+
* plain identifier-quoting pass would otherwise mangle.
|
|
1191
|
+
*/
|
|
1192
|
+
formatSelectAttribute(attribute) {
|
|
1193
|
+
if (attribute === '*')
|
|
1194
|
+
return attribute;
|
|
1195
|
+
if (/[\s(]/.test(attribute)) {
|
|
1196
|
+
// Looks like a raw SQL expression (function call, window function,
|
|
1197
|
+
// "expr AS alias", etc.) rather than a bare column name.
|
|
1198
|
+
return attribute;
|
|
1199
|
+
}
|
|
1200
|
+
return this.escapeId(attribute);
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Builds a `WITH [RECURSIVE] ...` prefix from `options.cte`, Snowflake's
|
|
1204
|
+
* (standard-SQL) Common Table Expression syntax. Each entry may set its
|
|
1205
|
+
* own `recursive` flag; if any entry is recursive the whole `WITH` clause
|
|
1206
|
+
* is emitted as `WITH RECURSIVE` (Snowflake, like most engines, applies
|
|
1207
|
+
* RECURSIVE to the entire WITH clause rather than per-CTE).
|
|
1208
|
+
*/
|
|
1209
|
+
buildCteClause(cte) {
|
|
1210
|
+
if (!cte || cte.length === 0)
|
|
1211
|
+
return '';
|
|
1212
|
+
const recursive = cte.some((c) => c.recursive);
|
|
1213
|
+
const body = cte
|
|
1214
|
+
.map((c) => {
|
|
1215
|
+
const columns = c.columns && c.columns.length > 0 ? `(${c.columns.map((col) => this.escapeId(col)).join(', ')})` : '';
|
|
1216
|
+
return `${this.escapeId(c.name)}${columns} AS (${c.query})`;
|
|
1217
|
+
})
|
|
1218
|
+
.join(', ');
|
|
1219
|
+
return `WITH ${recursive ? 'RECURSIVE ' : ''}${body} `;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Builds Snowflake's Time Travel clause (`AT(...)`/`BEFORE(...)`) to
|
|
1223
|
+
* append right after a table reference in the FROM clause, e.g.
|
|
1224
|
+
* `FROM "T" AT(TIMESTAMP => '2024-01-01 00:00:00')`.
|
|
1225
|
+
*/
|
|
1226
|
+
buildTimeTravelClause(options) {
|
|
1227
|
+
if (options.atTimestamp !== undefined) {
|
|
1228
|
+
const ts = options.atTimestamp instanceof Date
|
|
1229
|
+
? this.formatDate(options.atTimestamp)
|
|
1230
|
+
: options.atTimestamp;
|
|
1231
|
+
return ` AT(TIMESTAMP => '${this.escapeString(String(ts))}'::TIMESTAMP_LTZ)`;
|
|
1232
|
+
}
|
|
1233
|
+
if (options.atOffset !== undefined) {
|
|
1234
|
+
return ` AT(OFFSET => ${Number(options.atOffset)})`;
|
|
1235
|
+
}
|
|
1236
|
+
if (options.atStatement !== undefined) {
|
|
1237
|
+
return ` AT(STATEMENT => '${this.escapeString(options.atStatement)}')`;
|
|
1238
|
+
}
|
|
1239
|
+
if (options.beforeStatement !== undefined) {
|
|
1240
|
+
return ` BEFORE(STATEMENT => '${this.escapeString(options.beforeStatement)}')`;
|
|
1241
|
+
}
|
|
1242
|
+
return '';
|
|
1243
|
+
}
|
|
1244
|
+
buildSelectQuery(options) {
|
|
1245
|
+
const queryValues = [];
|
|
1246
|
+
const extended = options;
|
|
1247
|
+
let sql = this.buildCteClause(extended.cte);
|
|
1248
|
+
sql += 'SELECT';
|
|
1249
|
+
if (options.distinct)
|
|
1250
|
+
sql += ' DISTINCT';
|
|
1251
|
+
if (options.attributes) {
|
|
1252
|
+
if (Array.isArray(options.attributes)) {
|
|
1253
|
+
sql += ` ${options.attributes.map((c) => this.formatSelectAttribute(c)).join(', ')}`;
|
|
1254
|
+
}
|
|
1255
|
+
else if (options.attributes.include) {
|
|
1256
|
+
sql += ` ${options.attributes.include.map((c) => this.escapeId(c)).join(', ')}`;
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
sql += ' *';
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
sql += ' *';
|
|
1264
|
+
}
|
|
1265
|
+
const tableName = this.quoteTable(options.tableName, options.schema);
|
|
1266
|
+
sql += ` FROM ${tableName}`;
|
|
1267
|
+
const timeTravelClause = this.buildTimeTravelClause(extended);
|
|
1268
|
+
if (timeTravelClause)
|
|
1269
|
+
sql += timeTravelClause;
|
|
1270
|
+
if (options.include && options.include.length > 0) {
|
|
1271
|
+
for (const include of options.include) {
|
|
1272
|
+
const joinType = include.required ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1273
|
+
const joinModel = include.model;
|
|
1274
|
+
const joinTableName = joinModel.tableName || joinModel.name || '';
|
|
1275
|
+
const joinAlias = include.as || joinTableName;
|
|
1276
|
+
sql += ` ${joinType} ${this.quoteTable(joinTableName)} AS ${this.escapeId(joinAlias)}`;
|
|
1277
|
+
if (include.on) {
|
|
1278
|
+
const onClause = this.buildWhereClause(include.on);
|
|
1279
|
+
sql += ` ON ${onClause.sql}`;
|
|
1280
|
+
queryValues.push(...onClause.values);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (options.where && Object.keys(options.where).length > 0) {
|
|
1285
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
1286
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1287
|
+
queryValues.push(...whereClause.values);
|
|
1288
|
+
}
|
|
1289
|
+
if (options.group) {
|
|
1290
|
+
const groupBy = Array.isArray(options.group) ? options.group : [options.group];
|
|
1291
|
+
sql += ` GROUP BY ${groupBy.map((g) => this.escapeId(g)).join(', ')}`;
|
|
1292
|
+
if (options.having && Object.keys(options.having).length > 0) {
|
|
1293
|
+
const havingClause = this.buildWhereClause(options.having);
|
|
1294
|
+
sql += ` HAVING ${havingClause.sql}`;
|
|
1295
|
+
queryValues.push(...havingClause.values);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
// QUALIFY filters on the results of window functions without requiring
|
|
1299
|
+
// a wrapping subquery, e.g. `QUALIFY ROW_NUMBER() OVER (...) = 1`.
|
|
1300
|
+
// Supported by Snowflake (and a handful of other analytical warehouses)
|
|
1301
|
+
// but not standard SQL, so it's exposed here as a raw expression string
|
|
1302
|
+
// rather than a structured WhereOptions object.
|
|
1303
|
+
if (extended.qualify) {
|
|
1304
|
+
sql += ` QUALIFY ${extended.qualify}`;
|
|
1305
|
+
}
|
|
1306
|
+
if (options.order) {
|
|
1307
|
+
const orderClause = this.buildOrderClause(options.order);
|
|
1308
|
+
if (orderClause)
|
|
1309
|
+
sql += ` ${orderClause}`;
|
|
1310
|
+
}
|
|
1311
|
+
// Standard LIMIT/OFFSET pagination
|
|
1312
|
+
const limitOffset = this.buildLimitOffset(options.limit, options.offset);
|
|
1313
|
+
if (limitOffset)
|
|
1314
|
+
sql += limitOffset;
|
|
1315
|
+
return { sql, values: queryValues };
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Build an upsert using Snowflake's MERGE INTO, which is well-supported
|
|
1319
|
+
* and close to standard SQL MERGE.
|
|
1320
|
+
*/
|
|
1321
|
+
buildUpsertQuery(tableName, values, options) {
|
|
1322
|
+
if (!options?.conflictFields || options.conflictFields.length === 0) {
|
|
1323
|
+
throw new Error('Snowflake buildUpsertQuery requires explicit `conflictFields` to build the MERGE ' +
|
|
1324
|
+
'ON clause — omitting it (and matching on all columns) would never find an existing ' +
|
|
1325
|
+
'row to update, causing every upsert to insert a duplicate instead.');
|
|
1326
|
+
}
|
|
1327
|
+
const columns = Object.keys(values);
|
|
1328
|
+
const queryValues = Object.values(values);
|
|
1329
|
+
const updateFields = options?.updateOnDuplicate && options.updateOnDuplicate.length > 0
|
|
1330
|
+
? options.updateOnDuplicate
|
|
1331
|
+
: columns;
|
|
1332
|
+
const conflictFields = options.conflictFields;
|
|
1333
|
+
const sourceValues = columns.map((c) => `? AS ${this.escapeId(c)}`).join(', ');
|
|
1334
|
+
const onClauses = conflictFields
|
|
1335
|
+
.map((c) => `target.${this.escapeId(c)} = source.${this.escapeId(c)}`)
|
|
1336
|
+
.join(' AND ');
|
|
1337
|
+
const updateSet = updateFields
|
|
1338
|
+
.map((c) => `${this.escapeId(c)} = source.${this.escapeId(c)}`)
|
|
1339
|
+
.join(', ');
|
|
1340
|
+
const insertColumns = columns.map((c) => this.escapeId(c)).join(', ');
|
|
1341
|
+
const insertValues = columns.map((c) => `source.${this.escapeId(c)}`).join(', ');
|
|
1342
|
+
const sql = `MERGE INTO ${this.quoteTable(tableName, options?.schema)} AS target ` +
|
|
1343
|
+
`USING (SELECT ${sourceValues}) AS source ON ${onClauses} ` +
|
|
1344
|
+
`WHEN MATCHED THEN UPDATE SET ${updateSet} ` +
|
|
1345
|
+
`WHEN NOT MATCHED THEN INSERT (${insertColumns}) VALUES (${insertValues})`;
|
|
1346
|
+
return { sql, values: queryValues };
|
|
1347
|
+
}
|
|
1348
|
+
buildIncrementQuery(tableName, fields, where, options) {
|
|
1349
|
+
const by = options?.by ?? 1;
|
|
1350
|
+
const queryValues = [];
|
|
1351
|
+
let setClauses;
|
|
1352
|
+
if (typeof fields === 'string') {
|
|
1353
|
+
queryValues.push(by);
|
|
1354
|
+
setClauses = [`${this.escapeId(fields)} = ${this.escapeId(fields)} + ?`];
|
|
1355
|
+
}
|
|
1356
|
+
else if (Array.isArray(fields)) {
|
|
1357
|
+
setClauses = fields.map((field) => {
|
|
1358
|
+
queryValues.push(by);
|
|
1359
|
+
return `${this.escapeId(field)} = ${this.escapeId(field)} + ?`;
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
else {
|
|
1363
|
+
setClauses = Object.entries(fields).map(([field, value]) => {
|
|
1364
|
+
queryValues.push(value);
|
|
1365
|
+
return `${this.escapeId(field)} = ${this.escapeId(field)} + ?`;
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
let sql = `UPDATE ${this.quoteTable(tableName)} SET ${setClauses.join(', ')}`;
|
|
1369
|
+
if (where && Object.keys(where).length > 0) {
|
|
1370
|
+
const whereClause = this.buildWhereClause(where);
|
|
1371
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1372
|
+
queryValues.push(...whereClause.values);
|
|
1373
|
+
}
|
|
1374
|
+
return { sql, values: queryValues };
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Replace placeholders in SQL with actual escaped values. Supports both
|
|
1378
|
+
* named (:param) and positional (?) placeholders.
|
|
1379
|
+
*/
|
|
1380
|
+
replaceReplacements(sql, replacements) {
|
|
1381
|
+
if (!replacements)
|
|
1382
|
+
return sql;
|
|
1383
|
+
if (Array.isArray(replacements)) {
|
|
1384
|
+
let result = sql;
|
|
1385
|
+
let index = 0;
|
|
1386
|
+
result = result.replace(/\?/g, () => {
|
|
1387
|
+
const value = replacements[index++];
|
|
1388
|
+
return this.escape(value);
|
|
1389
|
+
});
|
|
1390
|
+
return result;
|
|
1391
|
+
}
|
|
1392
|
+
let result = sql;
|
|
1393
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
1394
|
+
const regex = new RegExp(`:${key}\\b`, 'g');
|
|
1395
|
+
result = result.replace(regex, this.escape(value));
|
|
1396
|
+
}
|
|
1397
|
+
return result;
|
|
1398
|
+
}
|
|
1399
|
+
// ---------------------------------------------------------------------------
|
|
1400
|
+
// Foreign Data Wrapper query builders — not supported by Snowflake
|
|
1401
|
+
// ---------------------------------------------------------------------------
|
|
1402
|
+
buildCreateServerQuery(_name, _opts) {
|
|
1403
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
1404
|
+
}
|
|
1405
|
+
buildAlterServerQuery(_name, _opts) {
|
|
1406
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
1407
|
+
}
|
|
1408
|
+
buildDropServerQuery(_name, _opts) {
|
|
1409
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
1410
|
+
}
|
|
1411
|
+
buildCreateUserMappingQuery(_opts) {
|
|
1412
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
1413
|
+
}
|
|
1414
|
+
buildAlterUserMappingQuery(_opts) {
|
|
1415
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
1416
|
+
}
|
|
1417
|
+
buildDropUserMappingQuery(_serverName, _user, _opts) {
|
|
1418
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
1419
|
+
}
|
|
1420
|
+
async createUserMapping() {
|
|
1421
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
1422
|
+
}
|
|
1423
|
+
async dropUserMapping() {
|
|
1424
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
1425
|
+
}
|
|
1426
|
+
buildCreateForeignTableQuery(_tableName, _opts) {
|
|
1427
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
1428
|
+
}
|
|
1429
|
+
buildDropForeignTableQuery(_tableName, _opts) {
|
|
1430
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
1431
|
+
}
|
|
1432
|
+
buildImportForeignSchemaQuery(_remoteSchema, _serverName, _opts) {
|
|
1433
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
1434
|
+
}
|
|
1435
|
+
getServersQuery() {
|
|
1436
|
+
throw new Error('Foreign Data Wrappers are not supported by Snowflake. This is a PostgreSQL-specific feature.');
|
|
1437
|
+
}
|
|
1438
|
+
// ---------------------------------------------------------------------------
|
|
1439
|
+
// User / privilege management — Snowflake has real user/role management,
|
|
1440
|
+
// but it is not the focus of this dialect implementation; throw a clear
|
|
1441
|
+
// error rather than emitting incorrect SQL.
|
|
1442
|
+
// ---------------------------------------------------------------------------
|
|
1443
|
+
buildCreateUserQuery(_username, _options) {
|
|
1444
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1445
|
+
}
|
|
1446
|
+
buildAlterUserQuery(_username, _options) {
|
|
1447
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1448
|
+
}
|
|
1449
|
+
buildDropUserQuery(_username, _options) {
|
|
1450
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1451
|
+
}
|
|
1452
|
+
getUsersQuery() {
|
|
1453
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1454
|
+
}
|
|
1455
|
+
buildGrantQuery(_options) {
|
|
1456
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1457
|
+
}
|
|
1458
|
+
buildRevokeQuery(_options) {
|
|
1459
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1460
|
+
}
|
|
1461
|
+
buildShowGrantsQuery(_username, _host) {
|
|
1462
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1463
|
+
}
|
|
1464
|
+
buildFlushPrivilegesQuery() {
|
|
1465
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1466
|
+
}
|
|
1467
|
+
buildCreateRoleQuery(_roleName, _options) {
|
|
1468
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1469
|
+
}
|
|
1470
|
+
buildDropRoleQuery(_roleName, _options) {
|
|
1471
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1472
|
+
}
|
|
1473
|
+
buildGrantRoleQuery(_role, _to, _options) {
|
|
1474
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1475
|
+
}
|
|
1476
|
+
buildRevokeRoleQuery(_role, _from, _options) {
|
|
1477
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1478
|
+
}
|
|
1479
|
+
getRolesQuery() {
|
|
1480
|
+
throw new Error('User management is not implemented by the Snowflake dialect');
|
|
1481
|
+
}
|
|
1482
|
+
// ---------------------------------------------------------------------------
|
|
1483
|
+
// Sequences — Snowflake supports CREATE SEQUENCE / NEXTVAL natively.
|
|
1484
|
+
// ---------------------------------------------------------------------------
|
|
1485
|
+
async createSequence(options) {
|
|
1486
|
+
const seqName = options.schema
|
|
1487
|
+
? this.quoteTable(options.name, options.schema)
|
|
1488
|
+
: this.escapeId(options.name);
|
|
1489
|
+
let sql = options.ifNotExists ? 'CREATE SEQUENCE IF NOT EXISTS' : 'CREATE SEQUENCE';
|
|
1490
|
+
sql += ` ${seqName}`;
|
|
1491
|
+
if (options.startWith !== undefined)
|
|
1492
|
+
sql += ` START = ${options.startWith}`;
|
|
1493
|
+
if (options.incrementBy !== undefined)
|
|
1494
|
+
sql += ` INCREMENT = ${options.incrementBy}`;
|
|
1495
|
+
if (options.minvalue !== undefined || options.maxvalue !== undefined || options.cycle) {
|
|
1496
|
+
console.warn('Snowflake: MINVALUE/MAXVALUE/CYCLE are not supported by CREATE SEQUENCE and will be ignored');
|
|
1497
|
+
}
|
|
1498
|
+
await this.query(sql);
|
|
1499
|
+
}
|
|
1500
|
+
async dropSequence(sequenceName, options) {
|
|
1501
|
+
const seqName = options?.schema
|
|
1502
|
+
? this.quoteTable(sequenceName, options.schema)
|
|
1503
|
+
: this.escapeId(sequenceName);
|
|
1504
|
+
let sql = 'DROP SEQUENCE';
|
|
1505
|
+
if (options?.ifExists)
|
|
1506
|
+
sql += ' IF EXISTS';
|
|
1507
|
+
sql += ` ${seqName}`;
|
|
1508
|
+
await this.query(sql);
|
|
1509
|
+
}
|
|
1510
|
+
async nextSequenceValue(sequenceName) {
|
|
1511
|
+
const result = await this.query(`SELECT ${this.escapeId(sequenceName)}.NEXTVAL AS value`);
|
|
1512
|
+
return Number(result.rows?.[0]?.VALUE ?? result.rows?.[0]?.value);
|
|
1513
|
+
}
|
|
1514
|
+
async hasSequence(sequenceName) {
|
|
1515
|
+
const result = await this.query(`SHOW SEQUENCES LIKE '${sequenceName.replace(/'/g, "''")}'`);
|
|
1516
|
+
return (result.rows?.length || 0) > 0;
|
|
1517
|
+
}
|
|
1518
|
+
async listSequences() {
|
|
1519
|
+
const result = await this.query('SHOW SEQUENCES');
|
|
1520
|
+
return result.rows?.map((row) => row.name || row.NAME) || [];
|
|
1521
|
+
}
|
|
1522
|
+
// ---------------------------------------------------------------------------
|
|
1523
|
+
// Stored procedures — Snowflake supports JavaScript/SQL/Python/etc.
|
|
1524
|
+
// procedures via CREATE PROCEDURE ... AS $$ ... $$
|
|
1525
|
+
// ---------------------------------------------------------------------------
|
|
1526
|
+
async createStoredProcedure(options) {
|
|
1527
|
+
const procName = options.schema
|
|
1528
|
+
? this.quoteTable(options.name, options.schema)
|
|
1529
|
+
: this.escapeId(options.name);
|
|
1530
|
+
let sql = options.replace ? 'CREATE OR REPLACE PROCEDURE' : 'CREATE PROCEDURE';
|
|
1531
|
+
if (!options.replace && options.ifNotExists) {
|
|
1532
|
+
sql = 'CREATE PROCEDURE IF NOT EXISTS';
|
|
1533
|
+
}
|
|
1534
|
+
const params = (options.params || [])
|
|
1535
|
+
.map((p) => `${this.escapeId(p.name)} ${p.type}`)
|
|
1536
|
+
.join(', ');
|
|
1537
|
+
sql += ` ${procName} (${params})`;
|
|
1538
|
+
sql += ` RETURNS ${options.returnType || 'STRING'}`;
|
|
1539
|
+
sql += ` LANGUAGE ${options.language || 'SQL'}`;
|
|
1540
|
+
sql += `\nAS\n$$\n${options.body}\n$$`;
|
|
1541
|
+
await this.query(sql);
|
|
1542
|
+
}
|
|
1543
|
+
async createProcedure(options) {
|
|
1544
|
+
return this.createStoredProcedure(options);
|
|
1545
|
+
}
|
|
1546
|
+
async dropStoredProcedure(procedureName, options) {
|
|
1547
|
+
const procName = options?.schema
|
|
1548
|
+
? this.quoteTable(procedureName, options.schema)
|
|
1549
|
+
: this.escapeId(procedureName);
|
|
1550
|
+
// Snowflake identifies procedures by their full signature (name + argument
|
|
1551
|
+
// types), since procedures can be overloaded — DROP PROCEDURE foo() only
|
|
1552
|
+
// matches a zero-argument overload. Callers that created a procedure with
|
|
1553
|
+
// parameters must pass the matching `paramTypes` so the correct overload is
|
|
1554
|
+
// targeted; this falls back to the zero-arg signature when omitted.
|
|
1555
|
+
const signature = (options?.paramTypes || []).join(', ');
|
|
1556
|
+
let sql = 'DROP PROCEDURE';
|
|
1557
|
+
if (options?.ifExists)
|
|
1558
|
+
sql += ' IF EXISTS';
|
|
1559
|
+
sql += ` ${procName}(${signature})`;
|
|
1560
|
+
await this.query(sql);
|
|
1561
|
+
}
|
|
1562
|
+
async dropProcedure(procedureName, options) {
|
|
1563
|
+
return this.dropStoredProcedure(procedureName, options);
|
|
1564
|
+
}
|
|
1565
|
+
async executeStoredProcedure(options) {
|
|
1566
|
+
const procName = options.schema
|
|
1567
|
+
? this.quoteTable(options.procedureName, options.schema)
|
|
1568
|
+
: this.escapeId(options.procedureName);
|
|
1569
|
+
const args = Object.values(options.params || {})
|
|
1570
|
+
.map((v) => this.escape(v))
|
|
1571
|
+
.join(', ');
|
|
1572
|
+
return this.query(`CALL ${procName}(${args})`);
|
|
1573
|
+
}
|
|
1574
|
+
async hasStoredProcedure(procedureName, schema) {
|
|
1575
|
+
const result = await this.query(`SHOW PROCEDURES LIKE '${procedureName.replace(/'/g, "''")}'${schema ? ` IN SCHEMA ${this.escapeId(schema)}` : ''}`);
|
|
1576
|
+
return (result.rows?.length || 0) > 0;
|
|
1577
|
+
}
|
|
1578
|
+
// ---------------------------------------------------------------------------
|
|
1579
|
+
// Zero-copy CLONE — Snowflake's flagship instant, storage-efficient
|
|
1580
|
+
// dev/test/backup mechanism (`CREATE TABLE/SCHEMA/DATABASE ... CLONE ...`).
|
|
1581
|
+
// Optionally combined with Time Travel (`AT`/`BEFORE`) to clone a historical
|
|
1582
|
+
// version of the source object.
|
|
1583
|
+
// ---------------------------------------------------------------------------
|
|
1584
|
+
/**
|
|
1585
|
+
* Builds the optional `AT(...)`/`BEFORE(...)` suffix used by CLONE
|
|
1586
|
+
* statements, reusing the same Time Travel clause builder as SELECT.
|
|
1587
|
+
*/
|
|
1588
|
+
buildCloneTimeTravelClause(options) {
|
|
1589
|
+
if (!options)
|
|
1590
|
+
return '';
|
|
1591
|
+
return this.buildTimeTravelClause({
|
|
1592
|
+
atTimestamp: options.atTimestamp,
|
|
1593
|
+
atOffset: options.atOffset,
|
|
1594
|
+
atStatement: options.atStatement,
|
|
1595
|
+
beforeStatement: options.beforeStatement,
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Zero-copy clone of a table: `CREATE TABLE <target> CLONE <source>`.
|
|
1600
|
+
* Supports an optional Time Travel clause (`atTimestamp`/`atOffset`/
|
|
1601
|
+
* `atStatement`/`beforeStatement`) to clone a historical version of the
|
|
1602
|
+
* source table, and `ifNotExists`/`replace` like `createTable`.
|
|
1603
|
+
*/
|
|
1604
|
+
async cloneTable(source, target, options) {
|
|
1605
|
+
let sql = options?.replace ? 'CREATE OR REPLACE TABLE' : 'CREATE TABLE';
|
|
1606
|
+
if (!options?.replace && options?.ifNotExists)
|
|
1607
|
+
sql = 'CREATE TABLE IF NOT EXISTS';
|
|
1608
|
+
sql += ` ${this.quoteTable(target, options?.schema)} CLONE ${this.quoteTable(source, options?.schema)}`;
|
|
1609
|
+
sql += this.buildCloneTimeTravelClause(options);
|
|
1610
|
+
await this.query(sql);
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Zero-copy clone of a schema: `CREATE SCHEMA <target> CLONE <source>`.
|
|
1614
|
+
*/
|
|
1615
|
+
async cloneSchema(source, target, options) {
|
|
1616
|
+
let sql = options?.replace ? 'CREATE OR REPLACE SCHEMA' : 'CREATE SCHEMA';
|
|
1617
|
+
if (!options?.replace && options?.ifNotExists)
|
|
1618
|
+
sql = 'CREATE SCHEMA IF NOT EXISTS';
|
|
1619
|
+
sql += ` ${this.escapeId(target)} CLONE ${this.escapeId(source)}`;
|
|
1620
|
+
sql += this.buildCloneTimeTravelClause(options);
|
|
1621
|
+
await this.query(sql);
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Zero-copy clone of a database: `CREATE DATABASE <target> CLONE <source>`.
|
|
1625
|
+
*/
|
|
1626
|
+
async cloneDatabase(source, target, options) {
|
|
1627
|
+
let sql = options?.replace ? 'CREATE OR REPLACE DATABASE' : 'CREATE DATABASE';
|
|
1628
|
+
if (!options?.replace && options?.ifNotExists)
|
|
1629
|
+
sql = 'CREATE DATABASE IF NOT EXISTS';
|
|
1630
|
+
sql += ` ${this.escapeId(target)} CLONE ${this.escapeId(source)}`;
|
|
1631
|
+
sql += this.buildCloneTimeTravelClause(options);
|
|
1632
|
+
await this.query(sql);
|
|
1633
|
+
}
|
|
1634
|
+
// ---------------------------------------------------------------------------
|
|
1635
|
+
// Bulk load/unload — COPY INTO, FILE FORMAT and STAGE, Snowflake's primary
|
|
1636
|
+
// bulk data-movement mechanism (analogous to the Redshift dialect's
|
|
1637
|
+
// copyFromS3).
|
|
1638
|
+
// ---------------------------------------------------------------------------
|
|
1639
|
+
/**
|
|
1640
|
+
* `CREATE FILE FORMAT <name> TYPE = <CSV|JSON|PARQUET|...> [options...]`.
|
|
1641
|
+
* Supports the most commonly used per-type options; any additional/rare
|
|
1642
|
+
* option can be passed through `raw` and is appended verbatim.
|
|
1643
|
+
*/
|
|
1644
|
+
async createFileFormat(name, options) {
|
|
1645
|
+
let sql = options.replace ? 'CREATE OR REPLACE FILE FORMAT' : 'CREATE FILE FORMAT';
|
|
1646
|
+
if (!options.replace && options.ifNotExists)
|
|
1647
|
+
sql = 'CREATE FILE FORMAT IF NOT EXISTS';
|
|
1648
|
+
const formatName = options.schema ? this.quoteTable(name, options.schema) : this.escapeId(name);
|
|
1649
|
+
sql += ` ${formatName} TYPE = ${options.type}`;
|
|
1650
|
+
const clauses = [];
|
|
1651
|
+
const type = options.type.toUpperCase();
|
|
1652
|
+
if (type === 'CSV') {
|
|
1653
|
+
if (options.fieldDelimiter !== undefined) {
|
|
1654
|
+
clauses.push(`FIELD_DELIMITER = '${this.escapeString(options.fieldDelimiter)}'`);
|
|
1655
|
+
}
|
|
1656
|
+
if (options.recordDelimiter !== undefined) {
|
|
1657
|
+
clauses.push(`RECORD_DELIMITER = '${this.escapeString(options.recordDelimiter)}'`);
|
|
1658
|
+
}
|
|
1659
|
+
if (options.skipHeader !== undefined)
|
|
1660
|
+
clauses.push(`SKIP_HEADER = ${options.skipHeader}`);
|
|
1661
|
+
if (options.fieldOptionallyEnclosedBy !== undefined) {
|
|
1662
|
+
clauses.push(`FIELD_OPTIONALLY_ENCLOSED_BY = '${this.escapeString(options.fieldOptionallyEnclosedBy)}'`);
|
|
1663
|
+
}
|
|
1664
|
+
if (options.nullIf !== undefined) {
|
|
1665
|
+
const values = (Array.isArray(options.nullIf) ? options.nullIf : [options.nullIf])
|
|
1666
|
+
.map((v) => `'${this.escapeString(v)}'`)
|
|
1667
|
+
.join(', ');
|
|
1668
|
+
clauses.push(`NULL_IF = (${values})`);
|
|
1669
|
+
}
|
|
1670
|
+
if (options.compression !== undefined)
|
|
1671
|
+
clauses.push(`COMPRESSION = ${options.compression}`);
|
|
1672
|
+
if (options.emptyFieldAsNull !== undefined)
|
|
1673
|
+
clauses.push(`EMPTY_FIELD_AS_NULL = ${options.emptyFieldAsNull}`);
|
|
1674
|
+
}
|
|
1675
|
+
else if (type === 'JSON') {
|
|
1676
|
+
if (options.stripOuterArray !== undefined)
|
|
1677
|
+
clauses.push(`STRIP_OUTER_ARRAY = ${options.stripOuterArray}`);
|
|
1678
|
+
if (options.compression !== undefined)
|
|
1679
|
+
clauses.push(`COMPRESSION = ${options.compression}`);
|
|
1680
|
+
}
|
|
1681
|
+
else if (type === 'PARQUET') {
|
|
1682
|
+
if (options.compression !== undefined)
|
|
1683
|
+
clauses.push(`COMPRESSION = ${options.compression}`);
|
|
1684
|
+
if (options.binaryAsText !== undefined)
|
|
1685
|
+
clauses.push(`BINARY_AS_TEXT = ${options.binaryAsText}`);
|
|
1686
|
+
}
|
|
1687
|
+
if (options.dateFormat !== undefined)
|
|
1688
|
+
clauses.push(`DATE_FORMAT = '${this.escapeString(options.dateFormat)}'`);
|
|
1689
|
+
if (options.timestampFormat !== undefined) {
|
|
1690
|
+
clauses.push(`TIMESTAMP_FORMAT = '${this.escapeString(options.timestampFormat)}'`);
|
|
1691
|
+
}
|
|
1692
|
+
if (options.raw)
|
|
1693
|
+
clauses.push(options.raw);
|
|
1694
|
+
if (clauses.length > 0)
|
|
1695
|
+
sql += ` ${clauses.join(' ')}`;
|
|
1696
|
+
await this.query(sql);
|
|
1697
|
+
}
|
|
1698
|
+
async dropFileFormat(name, options) {
|
|
1699
|
+
const formatName = options?.schema ? this.quoteTable(name, options.schema) : this.escapeId(name);
|
|
1700
|
+
let sql = 'DROP FILE FORMAT';
|
|
1701
|
+
if (options?.ifExists)
|
|
1702
|
+
sql += ' IF EXISTS';
|
|
1703
|
+
sql += ` ${formatName}`;
|
|
1704
|
+
await this.query(sql);
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* `CREATE STAGE <name> [URL = '...'] [CREDENTIALS = (...)] [FILE_FORMAT = ...]`.
|
|
1708
|
+
* A stage is Snowflake's named reference to a bulk-load/unload location
|
|
1709
|
+
* (internal, or external S3/Azure/GCS); required before `COPY INTO` can
|
|
1710
|
+
* reference `@<stage>`.
|
|
1711
|
+
*/
|
|
1712
|
+
async createStage(name, options) {
|
|
1713
|
+
let sql = options?.replace ? 'CREATE OR REPLACE STAGE' : 'CREATE STAGE';
|
|
1714
|
+
if (!options?.replace && options?.ifNotExists)
|
|
1715
|
+
sql = 'CREATE STAGE IF NOT EXISTS';
|
|
1716
|
+
const stageName = options?.schema ? this.quoteTable(name, options.schema) : this.escapeId(name);
|
|
1717
|
+
sql += ` ${stageName}`;
|
|
1718
|
+
if (options?.url)
|
|
1719
|
+
sql += ` URL = '${this.escapeString(options.url)}'`;
|
|
1720
|
+
if (options?.credentials) {
|
|
1721
|
+
const creds = Object.entries(options.credentials)
|
|
1722
|
+
.map(([k, v]) => `${k.toUpperCase()} = '${this.escapeString(String(v))}'`)
|
|
1723
|
+
.join(' ');
|
|
1724
|
+
sql += ` CREDENTIALS = (${creds})`;
|
|
1725
|
+
}
|
|
1726
|
+
if (options?.fileFormatName)
|
|
1727
|
+
sql += ` FILE_FORMAT = (FORMAT_NAME = ${this.escapeId(options.fileFormatName)})`;
|
|
1728
|
+
await this.query(sql);
|
|
1729
|
+
}
|
|
1730
|
+
async dropStage(name, options) {
|
|
1731
|
+
const stageName = options?.schema ? this.quoteTable(name, options.schema) : this.escapeId(name);
|
|
1732
|
+
let sql = 'DROP STAGE';
|
|
1733
|
+
if (options?.ifExists)
|
|
1734
|
+
sql += ' IF EXISTS';
|
|
1735
|
+
sql += ` ${stageName}`;
|
|
1736
|
+
await this.query(sql);
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Bulk load: `COPY INTO <table> FROM @<stage>[/path] [FILES = (...)]
|
|
1740
|
+
* [PATTERN = '...'] [FILE_FORMAT = (...)] [ON_ERROR = ...] [FORCE = ...]`.
|
|
1741
|
+
* `stagePath` may be a bare stage name (`my_stage`) or already include the
|
|
1742
|
+
* `@` prefix / sub-path (`@my_stage/2024/`); both are normalized.
|
|
1743
|
+
*/
|
|
1744
|
+
async copyInto(tableName, stagePath, options) {
|
|
1745
|
+
const stageRef = stagePath.startsWith('@') ? stagePath : `@${stagePath}`;
|
|
1746
|
+
let sql = `COPY INTO ${this.quoteTable(tableName, options?.schema)} FROM ${stageRef}`;
|
|
1747
|
+
if (options?.files && options.files.length > 0) {
|
|
1748
|
+
sql += ` FILES = (${options.files.map((f) => `'${this.escapeString(f)}'`).join(', ')})`;
|
|
1749
|
+
}
|
|
1750
|
+
if (options?.pattern) {
|
|
1751
|
+
sql += ` PATTERN = '${this.escapeString(options.pattern)}'`;
|
|
1752
|
+
}
|
|
1753
|
+
if (options?.fileFormatName) {
|
|
1754
|
+
sql += ` FILE_FORMAT = (FORMAT_NAME = ${this.escapeId(options.fileFormatName)})`;
|
|
1755
|
+
}
|
|
1756
|
+
else if (options?.fileFormat) {
|
|
1757
|
+
const ffEntries = Object.entries(options.fileFormat)
|
|
1758
|
+
.map(([k, v]) => `${k.toUpperCase()} = ${typeof v === 'string' ? `'${this.escapeString(v)}'` : v}`)
|
|
1759
|
+
.join(', ');
|
|
1760
|
+
sql += ` FILE_FORMAT = (${ffEntries})`;
|
|
1761
|
+
}
|
|
1762
|
+
if (options?.onError)
|
|
1763
|
+
sql += ` ON_ERROR = ${options.onError}`;
|
|
1764
|
+
if (options?.force !== undefined)
|
|
1765
|
+
sql += ` FORCE = ${options.force}`;
|
|
1766
|
+
if (options?.purge !== undefined)
|
|
1767
|
+
sql += ` PURGE = ${options.purge}`;
|
|
1768
|
+
if (options?.matchByColumnName)
|
|
1769
|
+
sql += ` MATCH_BY_COLUMN_NAME = ${options.matchByColumnName}`;
|
|
1770
|
+
return this.query(sql);
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Bulk unload: `COPY INTO @<stage>[/path] FROM <table> [FILE_FORMAT = (...)]
|
|
1774
|
+
* [OVERWRITE = ...] [SINGLE = ...] [HEADER = ...]`.
|
|
1775
|
+
*/
|
|
1776
|
+
async unloadTo(tableName, stagePath, options) {
|
|
1777
|
+
const stageRef = stagePath.startsWith('@') ? stagePath : `@${stagePath}`;
|
|
1778
|
+
let sql = `COPY INTO ${stageRef} FROM ${this.quoteTable(tableName, options?.schema)}`;
|
|
1779
|
+
if (options?.fileFormatName) {
|
|
1780
|
+
sql += ` FILE_FORMAT = (FORMAT_NAME = ${this.escapeId(options.fileFormatName)})`;
|
|
1781
|
+
}
|
|
1782
|
+
else if (options?.fileFormat) {
|
|
1783
|
+
const ffEntries = Object.entries(options.fileFormat)
|
|
1784
|
+
.map(([k, v]) => `${k.toUpperCase()} = ${typeof v === 'string' ? `'${this.escapeString(v)}'` : v}`)
|
|
1785
|
+
.join(', ');
|
|
1786
|
+
sql += ` FILE_FORMAT = (${ffEntries})`;
|
|
1787
|
+
}
|
|
1788
|
+
if (options?.overwrite !== undefined)
|
|
1789
|
+
sql += ` OVERWRITE = ${options.overwrite}`;
|
|
1790
|
+
if (options?.single !== undefined)
|
|
1791
|
+
sql += ` SINGLE = ${options.single}`;
|
|
1792
|
+
if (options?.header !== undefined)
|
|
1793
|
+
sql += ` HEADER = ${options.header}`;
|
|
1794
|
+
if (options?.maxFileSize !== undefined)
|
|
1795
|
+
sql += ` MAX_FILE_SIZE = ${options.maxFileSize}`;
|
|
1796
|
+
return this.query(sql);
|
|
1797
|
+
}
|
|
1798
|
+
// ---------------------------------------------------------------------------
|
|
1799
|
+
// Warehouses — virtual warehouses are Snowflake's compute layer. Only
|
|
1800
|
+
// `USE WAREHOUSE` (activation on connect) existed previously; these add
|
|
1801
|
+
// full lifecycle DDL management.
|
|
1802
|
+
//
|
|
1803
|
+
// Real Snowflake syntax (https://docs.snowflake.com/en/sql-reference/sql/create-warehouse,
|
|
1804
|
+
// .../alter-warehouse, .../drop-warehouse):
|
|
1805
|
+
// CREATE [ OR REPLACE ] WAREHOUSE [ IF NOT EXISTS ] <name> [ WITH ]
|
|
1806
|
+
// [ WAREHOUSE_SIZE = '<size>' ]
|
|
1807
|
+
// [ AUTO_SUSPEND = <num> ]
|
|
1808
|
+
// [ AUTO_RESUME = TRUE | FALSE ]
|
|
1809
|
+
// [ MIN_CLUSTER_COUNT = <num> ]
|
|
1810
|
+
// [ MAX_CLUSTER_COUNT = <num> ]
|
|
1811
|
+
// [ SCALING_POLICY = 'STANDARD' | 'ECONOMY' ]
|
|
1812
|
+
// [ INITIALLY_SUSPENDED = TRUE | FALSE ]
|
|
1813
|
+
// [ RESOURCE_MONITOR = <name> ]
|
|
1814
|
+
// [ COMMENT = '<string>' ]
|
|
1815
|
+
// ALTER WAREHOUSE [ IF EXISTS ] <name> SET <same object properties, minus IF NOT EXISTS/INITIALLY_SUSPENDED>
|
|
1816
|
+
// ALTER WAREHOUSE [ IF EXISTS ] <name> SUSPEND | RESUME [ IF SUSPENDED ]
|
|
1817
|
+
// DROP WAREHOUSE [ IF EXISTS ] <name>
|
|
1818
|
+
// ---------------------------------------------------------------------------
|
|
1819
|
+
/**
|
|
1820
|
+
* Builds the `WAREHOUSE_SIZE = '...' AUTO_SUSPEND = ... ...` object
|
|
1821
|
+
* properties shared by `CREATE WAREHOUSE ... WITH ...` and
|
|
1822
|
+
* `ALTER WAREHOUSE ... SET ...`. `includeCreateOnly` controls whether
|
|
1823
|
+
* create-only properties (`INITIALLY_SUSPENDED`) are included, since
|
|
1824
|
+
* `ALTER WAREHOUSE ... SET` does not accept them.
|
|
1825
|
+
*/
|
|
1826
|
+
buildWarehousePropertiesSql(options, includeCreateOnly) {
|
|
1827
|
+
const clauses = [];
|
|
1828
|
+
if (options.size !== undefined)
|
|
1829
|
+
clauses.push(`WAREHOUSE_SIZE = '${this.escapeString(options.size)}'`);
|
|
1830
|
+
if (options.autoSuspend !== undefined)
|
|
1831
|
+
clauses.push(`AUTO_SUSPEND = ${options.autoSuspend}`);
|
|
1832
|
+
if (options.autoResume !== undefined)
|
|
1833
|
+
clauses.push(`AUTO_RESUME = ${options.autoResume}`);
|
|
1834
|
+
if (options.minClusterCount !== undefined)
|
|
1835
|
+
clauses.push(`MIN_CLUSTER_COUNT = ${options.minClusterCount}`);
|
|
1836
|
+
if (options.maxClusterCount !== undefined)
|
|
1837
|
+
clauses.push(`MAX_CLUSTER_COUNT = ${options.maxClusterCount}`);
|
|
1838
|
+
if (options.scalingPolicy !== undefined) {
|
|
1839
|
+
clauses.push(`SCALING_POLICY = '${this.escapeString(options.scalingPolicy)}'`);
|
|
1840
|
+
}
|
|
1841
|
+
if (includeCreateOnly && options.initiallySuspended !== undefined) {
|
|
1842
|
+
clauses.push(`INITIALLY_SUSPENDED = ${options.initiallySuspended}`);
|
|
1843
|
+
}
|
|
1844
|
+
if (options.resourceMonitor !== undefined) {
|
|
1845
|
+
clauses.push(`RESOURCE_MONITOR = ${this.escapeId(options.resourceMonitor)}`);
|
|
1846
|
+
}
|
|
1847
|
+
if (options.comment !== undefined)
|
|
1848
|
+
clauses.push(`COMMENT = '${this.escapeString(options.comment)}'`);
|
|
1849
|
+
return clauses.join(' ');
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* `CREATE [OR REPLACE] WAREHOUSE [IF NOT EXISTS] <name> WITH <properties>`.
|
|
1853
|
+
*/
|
|
1854
|
+
async createWarehouse(name, options) {
|
|
1855
|
+
let sql = options?.replace ? 'CREATE OR REPLACE WAREHOUSE' : 'CREATE WAREHOUSE';
|
|
1856
|
+
if (!options?.replace && options?.ifNotExists)
|
|
1857
|
+
sql = 'CREATE WAREHOUSE IF NOT EXISTS';
|
|
1858
|
+
sql += ` ${this.escapeId(name)}`;
|
|
1859
|
+
const properties = options ? this.buildWarehousePropertiesSql(options, true) : '';
|
|
1860
|
+
if (properties)
|
|
1861
|
+
sql += ` WITH ${properties}`;
|
|
1862
|
+
await this.query(sql);
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* `ALTER WAREHOUSE <name> SET <properties>`. Also used to (re)assign a
|
|
1866
|
+
* resource monitor to a warehouse via `options.resourceMonitor`
|
|
1867
|
+
* (`ALTER WAREHOUSE <name> SET RESOURCE_MONITOR = <monitor_name>`).
|
|
1868
|
+
*/
|
|
1869
|
+
async alterWarehouse(name, options) {
|
|
1870
|
+
const properties = this.buildWarehousePropertiesSql(options, false);
|
|
1871
|
+
if (!properties) {
|
|
1872
|
+
throw new Error('alterWarehouse requires at least one property to change');
|
|
1873
|
+
}
|
|
1874
|
+
await this.query(`ALTER WAREHOUSE ${this.escapeId(name)} SET ${properties}`);
|
|
1875
|
+
}
|
|
1876
|
+
async dropWarehouse(name, options) {
|
|
1877
|
+
let sql = 'DROP WAREHOUSE';
|
|
1878
|
+
if (options?.ifExists)
|
|
1879
|
+
sql += ' IF EXISTS';
|
|
1880
|
+
sql += ` ${this.escapeId(name)}`;
|
|
1881
|
+
await this.query(sql);
|
|
1882
|
+
}
|
|
1883
|
+
async suspendWarehouse(name) {
|
|
1884
|
+
await this.query(`ALTER WAREHOUSE ${this.escapeId(name)} SUSPEND`);
|
|
1885
|
+
}
|
|
1886
|
+
async resumeWarehouse(name) {
|
|
1887
|
+
await this.query(`ALTER WAREHOUSE ${this.escapeId(name)} RESUME`);
|
|
1888
|
+
}
|
|
1889
|
+
// ---------------------------------------------------------------------------
|
|
1890
|
+
// Resource monitors — track Snowflake credit usage against a quota and
|
|
1891
|
+
// trigger NOTIFY/SUSPEND/SUSPEND_IMMEDIATE actions on the warehouse(s) (or
|
|
1892
|
+
// account) they're assigned to once usage crosses configured thresholds.
|
|
1893
|
+
//
|
|
1894
|
+
// Real Snowflake syntax (https://docs.snowflake.com/en/sql-reference/sql/create-resource-monitor,
|
|
1895
|
+
// .../alter-resource-monitor, .../drop-resource-monitor):
|
|
1896
|
+
// CREATE [ OR REPLACE ] RESOURCE MONITOR <name> WITH
|
|
1897
|
+
// [ CREDIT_QUOTA = <num> ]
|
|
1898
|
+
// [ FREQUENCY = MONTHLY | DAILY | WEEKLY | YEARLY | NEVER ]
|
|
1899
|
+
// [ START_TIMESTAMP = <timestamp> | IMMEDIATELY ]
|
|
1900
|
+
// [ END_TIMESTAMP = <timestamp> ]
|
|
1901
|
+
// [ NOTIFY_USERS = ( <user_name> [ , <user_name> ... ] ) ]
|
|
1902
|
+
// [ TRIGGERS ( ON <num> PERCENT DO { NOTIFY | SUSPEND | SUSPEND_IMMEDIATE } )+ ]
|
|
1903
|
+
// ALTER RESOURCE MONITOR [ IF EXISTS ] <name> SET
|
|
1904
|
+
// [ CREDIT_QUOTA = <num> ] [ FREQUENCY = ... ] [ START_TIMESTAMP = ... ]
|
|
1905
|
+
// [ END_TIMESTAMP = ... ] [ NOTIFY_USERS = ( ... ) ]
|
|
1906
|
+
// [ TRIGGERS ( ON <num> PERCENT DO ... )+ ]
|
|
1907
|
+
// DROP RESOURCE MONITOR [ IF EXISTS ] <name>
|
|
1908
|
+
//
|
|
1909
|
+
// A monitor is assigned to a warehouse via `alterWarehouse(name, { resourceMonitor })`
|
|
1910
|
+
// (`ALTER WAREHOUSE <name> SET RESOURCE_MONITOR = <monitor_name>`), and to
|
|
1911
|
+
// the whole account via `ALTER ACCOUNT SET RESOURCE_MONITOR = <monitor_name>`.
|
|
1912
|
+
// ---------------------------------------------------------------------------
|
|
1913
|
+
/**
|
|
1914
|
+
* Builds the `CREDIT_QUOTA = ... FREQUENCY = ... TRIGGERS ON ... PERCENT DO ...`
|
|
1915
|
+
* body shared by `CREATE RESOURCE MONITOR ... WITH ...` and
|
|
1916
|
+
* `ALTER RESOURCE MONITOR ... SET ...`.
|
|
1917
|
+
*/
|
|
1918
|
+
buildResourceMonitorPropertiesSql(options) {
|
|
1919
|
+
const clauses = [];
|
|
1920
|
+
if (options.creditQuota !== undefined)
|
|
1921
|
+
clauses.push(`CREDIT_QUOTA = ${options.creditQuota}`);
|
|
1922
|
+
if (options.frequency !== undefined)
|
|
1923
|
+
clauses.push(`FREQUENCY = ${options.frequency}`);
|
|
1924
|
+
if (options.startTimestamp !== undefined) {
|
|
1925
|
+
const isKeyword = options.startTimestamp.toUpperCase() === 'IMMEDIATELY';
|
|
1926
|
+
clauses.push(`START_TIMESTAMP = ${isKeyword ? 'IMMEDIATELY' : `'${this.escapeString(options.startTimestamp)}'`}`);
|
|
1927
|
+
}
|
|
1928
|
+
if (options.endTimestamp !== undefined) {
|
|
1929
|
+
clauses.push(`END_TIMESTAMP = '${this.escapeString(options.endTimestamp)}'`);
|
|
1930
|
+
}
|
|
1931
|
+
if (options.notifyUsers && options.notifyUsers.length > 0) {
|
|
1932
|
+
clauses.push(`NOTIFY_USERS = (${options.notifyUsers.map((u) => this.escapeId(u)).join(', ')})`);
|
|
1933
|
+
}
|
|
1934
|
+
let sql = clauses.join(' ');
|
|
1935
|
+
if (options.triggers && options.triggers.length > 0) {
|
|
1936
|
+
const triggersSql = options.triggers
|
|
1937
|
+
.map((t) => `ON ${t.threshold} PERCENT DO ${t.action}`)
|
|
1938
|
+
.join(' ');
|
|
1939
|
+
sql += `${sql ? ' ' : ''}TRIGGERS ${triggersSql}`;
|
|
1940
|
+
}
|
|
1941
|
+
return sql;
|
|
1942
|
+
}
|
|
1943
|
+
/** `CREATE [OR REPLACE] RESOURCE MONITOR [IF NOT EXISTS] <name> WITH <properties>`. */
|
|
1944
|
+
async createResourceMonitor(name, options) {
|
|
1945
|
+
let sql = options.replace ? 'CREATE OR REPLACE RESOURCE MONITOR' : 'CREATE RESOURCE MONITOR';
|
|
1946
|
+
if (!options.replace && options.ifNotExists)
|
|
1947
|
+
sql = 'CREATE RESOURCE MONITOR IF NOT EXISTS';
|
|
1948
|
+
sql += ` ${this.escapeId(name)}`;
|
|
1949
|
+
const properties = this.buildResourceMonitorPropertiesSql(options);
|
|
1950
|
+
if (properties)
|
|
1951
|
+
sql += ` WITH ${properties}`;
|
|
1952
|
+
await this.query(sql);
|
|
1953
|
+
}
|
|
1954
|
+
/** `ALTER RESOURCE MONITOR <name> SET <properties>` (updates quota/frequency/triggers/etc). */
|
|
1955
|
+
async alterResourceMonitor(name, options) {
|
|
1956
|
+
const properties = this.buildResourceMonitorPropertiesSql(options);
|
|
1957
|
+
if (!properties) {
|
|
1958
|
+
throw new Error('alterResourceMonitor requires at least one property to change');
|
|
1959
|
+
}
|
|
1960
|
+
await this.query(`ALTER RESOURCE MONITOR ${this.escapeId(name)} SET ${properties}`);
|
|
1961
|
+
}
|
|
1962
|
+
async dropResourceMonitor(name, options) {
|
|
1963
|
+
let sql = 'DROP RESOURCE MONITOR';
|
|
1964
|
+
if (options?.ifExists)
|
|
1965
|
+
sql += ' IF EXISTS';
|
|
1966
|
+
sql += ` ${this.escapeId(name)}`;
|
|
1967
|
+
await this.query(sql);
|
|
1968
|
+
}
|
|
1969
|
+
// ---------------------------------------------------------------------------
|
|
1970
|
+
// Streams and Tasks — Snowflake's native CDC (change data capture) and
|
|
1971
|
+
// scheduled/DAG job primitives. Minimal wrappers to support incremental
|
|
1972
|
+
// ETL workflows from ORM consumers.
|
|
1973
|
+
// ---------------------------------------------------------------------------
|
|
1974
|
+
/**
|
|
1975
|
+
* `CREATE STREAM <name> ON TABLE <table> [APPEND_ONLY = ...] [SHOW_INITIAL_ROWS = ...]`.
|
|
1976
|
+
* A stream tracks row-level INSERT/UPDATE/DELETE changes on a table (or
|
|
1977
|
+
* view) since it was created or last consumed, Snowflake's native CDC
|
|
1978
|
+
* mechanism.
|
|
1979
|
+
*/
|
|
1980
|
+
async createStream(name, tableName, options) {
|
|
1981
|
+
let sql = options?.replace ? 'CREATE OR REPLACE STREAM' : 'CREATE STREAM';
|
|
1982
|
+
if (!options?.replace && options?.ifNotExists)
|
|
1983
|
+
sql = 'CREATE STREAM IF NOT EXISTS';
|
|
1984
|
+
const streamName = options?.schema ? this.quoteTable(name, options.schema) : this.escapeId(name);
|
|
1985
|
+
sql += ` ${streamName} ON TABLE ${this.quoteTable(tableName, options?.schema)}`;
|
|
1986
|
+
if (options?.appendOnly !== undefined)
|
|
1987
|
+
sql += ` APPEND_ONLY = ${options.appendOnly}`;
|
|
1988
|
+
if (options?.showInitialRows !== undefined)
|
|
1989
|
+
sql += ` SHOW_INITIAL_ROWS = ${options.showInitialRows}`;
|
|
1990
|
+
if (options?.comment)
|
|
1991
|
+
sql += ` COMMENT = '${this.escapeString(options.comment)}'`;
|
|
1992
|
+
await this.query(sql);
|
|
1993
|
+
}
|
|
1994
|
+
async dropStream(name, options) {
|
|
1995
|
+
const streamName = options?.schema ? this.quoteTable(name, options.schema) : this.escapeId(name);
|
|
1996
|
+
let sql = 'DROP STREAM';
|
|
1997
|
+
if (options?.ifExists)
|
|
1998
|
+
sql += ' IF EXISTS';
|
|
1999
|
+
sql += ` ${streamName}`;
|
|
2000
|
+
await this.query(sql);
|
|
2001
|
+
}
|
|
2002
|
+
async showStreams() {
|
|
2003
|
+
const result = await this.query('SHOW STREAMS');
|
|
2004
|
+
return result.rows?.map((row) => row.name || row.NAME) || [];
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* `CREATE TASK <name> [WAREHOUSE = ...] [SCHEDULE = '...'] [AFTER <task>]
|
|
2008
|
+
* AS <sql>`. A task runs a single SQL statement (often `CALL` on a stored
|
|
2009
|
+
* procedure) on a schedule (cron/interval) or as part of a DAG (`AFTER`).
|
|
2010
|
+
*/
|
|
2011
|
+
async createTask(name, sql, options) {
|
|
2012
|
+
let ddl = options?.replace ? 'CREATE OR REPLACE TASK' : 'CREATE TASK';
|
|
2013
|
+
if (!options?.replace && options?.ifNotExists)
|
|
2014
|
+
ddl = 'CREATE TASK IF NOT EXISTS';
|
|
2015
|
+
const taskName = options?.schema ? this.quoteTable(name, options.schema) : this.escapeId(name);
|
|
2016
|
+
ddl += ` ${taskName}`;
|
|
2017
|
+
if (options?.warehouse)
|
|
2018
|
+
ddl += ` WAREHOUSE = ${this.escapeId(options.warehouse)}`;
|
|
2019
|
+
if (options?.schedule)
|
|
2020
|
+
ddl += ` SCHEDULE = '${this.escapeString(options.schedule)}'`;
|
|
2021
|
+
if (options?.after) {
|
|
2022
|
+
const predecessors = Array.isArray(options.after) ? options.after : [options.after];
|
|
2023
|
+
ddl += ` AFTER ${predecessors.map((t) => this.quoteTable(t, options.schema)).join(', ')}`;
|
|
2024
|
+
}
|
|
2025
|
+
if (options?.when)
|
|
2026
|
+
ddl += ` WHEN ${options.when}`;
|
|
2027
|
+
if (options?.comment)
|
|
2028
|
+
ddl += ` COMMENT = '${this.escapeString(options.comment)}'`;
|
|
2029
|
+
ddl += ` AS ${sql}`;
|
|
2030
|
+
await this.query(ddl);
|
|
2031
|
+
}
|
|
2032
|
+
async dropTask(name, options) {
|
|
2033
|
+
const taskName = options?.schema ? this.quoteTable(name, options.schema) : this.escapeId(name);
|
|
2034
|
+
let sql = 'DROP TASK';
|
|
2035
|
+
if (options?.ifExists)
|
|
2036
|
+
sql += ' IF EXISTS';
|
|
2037
|
+
sql += ` ${taskName}`;
|
|
2038
|
+
await this.query(sql);
|
|
2039
|
+
}
|
|
2040
|
+
async resumeTask(name, schema) {
|
|
2041
|
+
await this.query(`ALTER TASK ${this.quoteTable(name, schema)} RESUME`);
|
|
2042
|
+
}
|
|
2043
|
+
async suspendTask(name, schema) {
|
|
2044
|
+
await this.query(`ALTER TASK ${this.quoteTable(name, schema)} SUSPEND`);
|
|
2045
|
+
}
|
|
2046
|
+
/** Runs a task immediately, outside of its schedule: `EXECUTE TASK <name>`. */
|
|
2047
|
+
async executeTask(name, schema) {
|
|
2048
|
+
return this.query(`EXECUTE TASK ${this.quoteTable(name, schema)}`);
|
|
2049
|
+
}
|
|
2050
|
+
async showTasks() {
|
|
2051
|
+
const result = await this.query('SHOW TASKS');
|
|
2052
|
+
return result.rows?.map((row) => row.name || row.NAME) || [];
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Build an expression to shred a JSON document/array into relational rows.
|
|
2056
|
+
* Snowflake has no `JSON_TABLE`; its real equivalent is the `FLATTEN` table function
|
|
2057
|
+
* (`TABLE(FLATTEN(input => ..., path => ...))`), which explodes a VARIANT array/object
|
|
2058
|
+
* into rows exposing `value` (and `index` for array position). This wraps FLATTEN in a
|
|
2059
|
+
* derived table that projects `value:<key>::<type>` per requested column, matching the
|
|
2060
|
+
* named/typed column shape of JSON_TABLE/OPENJSON in the other dialects.
|
|
2061
|
+
* Snowflake: (SELECT f.value:name::type AS name, ... FROM
|
|
2062
|
+
* TABLE(FLATTEN(input => expr, path => 'rowPath')) AS f) AS alias
|
|
2063
|
+
*/
|
|
2064
|
+
buildJsonTable(jsonExpression, rowPath, columns, alias) {
|
|
2065
|
+
const flattenAlias = `${alias}_f`;
|
|
2066
|
+
const normalizedRowPath = rowPath ? rowPath.replace(/^\$\.?/, '') : '';
|
|
2067
|
+
const pathArg = normalizedRowPath ? `, path => '${normalizedRowPath}'` : '';
|
|
2068
|
+
const selectCols = columns
|
|
2069
|
+
.map((col) => {
|
|
2070
|
+
if (col.forOrdinality) {
|
|
2071
|
+
return `${flattenAlias}.index AS ${this.escapeId(col.name)}`;
|
|
2072
|
+
}
|
|
2073
|
+
const key = (col.path || col.name).replace(/^\$\.?/, '');
|
|
2074
|
+
const cast = col.type ? `::${col.type}` : '';
|
|
2075
|
+
return `${flattenAlias}.value:${key}${cast} AS ${this.escapeId(col.name)}`;
|
|
2076
|
+
})
|
|
2077
|
+
.join(', ');
|
|
2078
|
+
return `(SELECT ${selectCols} FROM TABLE(FLATTEN(input => ${jsonExpression}${pathArg})) AS ${flattenAlias}) AS ${this.escapeId(alias)}`;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
exports.SnowflakeDialect = SnowflakeDialect;
|
|
2082
|
+
exports.default = SnowflakeDialect;
|