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,1768 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Google Cloud Spanner dialect implementation for the TypeScript ORM
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
* GoogleSQL vs. the Postgres interface
|
|
7
|
+
* ---------------------------------------------------------------------------
|
|
8
|
+
* Cloud Spanner exposes two SQL dialects: **GoogleSQL** (Spanner's native
|
|
9
|
+
* dialect, close to standard SQL/BigQuery syntax) and a newer **PostgreSQL
|
|
10
|
+
* interface** (`google-cloud/spanner` `databaseDialect: POSTGRESQL`, wire- and
|
|
11
|
+
* syntax-compatible with a useful subset of Postgres). This dialect targets
|
|
12
|
+
* **GoogleSQL**, which is the default/primary Spanner dialect and the one
|
|
13
|
+
* with full feature coverage (interleaved tables, the Mutation API,
|
|
14
|
+
* `GENERATE_UUID()`, bit-reversed sequences, `STORING` indexes, fine-grained
|
|
15
|
+
* IAM/database roles, etc.). The Postgres interface is a compatibility layer
|
|
16
|
+
* aimed at easing migrations from Postgres and still lags GoogleSQL in
|
|
17
|
+
* feature parity (e.g. no interleaved tables, different DDL for sequences).
|
|
18
|
+
* Since this codebase already has a Postgres-wire dialect (`postgres`) and a
|
|
19
|
+
* Postgres-*compatible* distributed dialect (`cockroachdb`), implementing
|
|
20
|
+
* Spanner via GoogleSQL is what actually makes it a meaningfully different,
|
|
21
|
+
* complementary dialect rather than a third near-duplicate of the Postgres
|
|
22
|
+
* query builder.
|
|
23
|
+
*
|
|
24
|
+
* ---------------------------------------------------------------------------
|
|
25
|
+
* Mutation API vs. SQL DML
|
|
26
|
+
* ---------------------------------------------------------------------------
|
|
27
|
+
* Spanner offers two ways to write data:
|
|
28
|
+
* 1. SQL DML (`INSERT`/`UPDATE`/`DELETE`), run inside a read-write
|
|
29
|
+
* transaction (`database.runTransaction()` + `transaction.runUpdate()`).
|
|
30
|
+
* 2. The **Mutation API** (`table.insert()`/`update()`/`upsert()`/
|
|
31
|
+
* `deleteRows()`), which encodes writes as structured mutations rather
|
|
32
|
+
* than parsed/planned SQL strings. Mutations are the cheaper, safer path
|
|
33
|
+
* for bulk/blind writes: no SQL parsing/planning overhead, no risk of
|
|
34
|
+
* conflicting on read-your-writes semantics, and they compose directly
|
|
35
|
+
* with `Table#insert/update/upsert/deleteRows`.
|
|
36
|
+
*
|
|
37
|
+
* This dialect exposes **both**: `buildUpsertQuery()`/`buildInsertQuery()`
|
|
38
|
+
* satisfy the shared `Dialect` interface using GoogleSQL's `INSERT OR UPDATE
|
|
39
|
+
* INTO ... VALUES (...)` upsert statement (Spanner has no `ON CONFLICT`
|
|
40
|
+
* clause - verified against the GoogleSQL DML reference - but it does support
|
|
41
|
+
* `INSERT OR UPDATE` / `INSERT OR IGNORE` / `INSERT OR REPLACE` as distinct
|
|
42
|
+
* conflict-action variants at the language level). **Only `INSERT OR UPDATE
|
|
43
|
+
* INTO` is currently implemented here** - there is no `conflictAction` option
|
|
44
|
+
* to select `IGNORE`/`REPLACE` semantics from `buildInsertQuery()`/
|
|
45
|
+
* `buildUpsertQuery()` yet; that's a gap, not a claim that this dialect wires
|
|
46
|
+
* up all three. Additionally, `mutationUpsert()`/`mutationInsert()`/
|
|
47
|
+
* `mutationUpdate()`/`mutationDelete()` and `bulkInsert()` are implemented
|
|
48
|
+
* using the Mutation API directly and are the **recommended, primary**
|
|
49
|
+
* mechanism for bulk/blind writes - they are simpler, avoid a SQL round-trip,
|
|
50
|
+
* and sidestep DML's read-write-transaction requirement entirely.
|
|
51
|
+
*
|
|
52
|
+
* ---------------------------------------------------------------------------
|
|
53
|
+
* Transactions & consistency model
|
|
54
|
+
* ---------------------------------------------------------------------------
|
|
55
|
+
* Spanner does **not** have a READ COMMITTED / REPEATABLE READ / SERIALIZABLE
|
|
56
|
+
* spectrum the way traditional RDBMSes do. Every read-write transaction is
|
|
57
|
+
* always externally consistent (equivalent to, and often described as
|
|
58
|
+
* stronger than, SERIALIZABLE) by construction, using TrueTime to assign a
|
|
59
|
+
* commit timestamp that reflects real-world transaction ordering. There is no
|
|
60
|
+
* meaningful "isolation level" knob to turn: `startTransaction()` accepts a
|
|
61
|
+
* `TransactionOptions.isolationLevel` for interface parity with the other
|
|
62
|
+
* dialects, but the value is ignored (with a warning) rather than silently
|
|
63
|
+
* mapped to something it isn't. Read-only transactions (strong reads or
|
|
64
|
+
* stale/bounded-staleness reads) are a distinct concept from read-write
|
|
65
|
+
* transactions in Spanner and are exposed here via `getSnapshot()`/
|
|
66
|
+
* `runReadOnlyTransaction()` rather than through `startTransaction()`.
|
|
67
|
+
*
|
|
68
|
+
* ---------------------------------------------------------------------------
|
|
69
|
+
* Hotspot avoidance / auto-generated keys
|
|
70
|
+
* ---------------------------------------------------------------------------
|
|
71
|
+
* Spanner shards key ranges across servers by primary key order. A naive
|
|
72
|
+
* monotonically-increasing integer primary key (the `autoIncrement` pattern
|
|
73
|
+
* used by every other dialect in this codebase) concentrates all writes on
|
|
74
|
+
* the single tablet holding the current "tail" of the key range - a hotspot.
|
|
75
|
+
* Spanner's own docs explicitly discourage monotonic keys. To keep the
|
|
76
|
+
* `ColumnDefinition.autoIncrement` flag meaningful without reproducing that
|
|
77
|
+
* anti-pattern, this dialect maps `autoIncrement: true` to a
|
|
78
|
+
* `STRING(36) DEFAULT (GENERATE_UUID())` column (a random, well-distributed
|
|
79
|
+
* key) and logs a warning explaining the tradeoff. For callers who need an
|
|
80
|
+
* ordered-but-distributed numeric key, `createBitReversedSequence()` /
|
|
81
|
+
* `nextBitReversedSequenceValue()` expose Spanner's bit-reversed sequence
|
|
82
|
+
* feature (`CREATE SEQUENCE ... OPTIONS (sequence_kind = 'bit_reversed_positive')`),
|
|
83
|
+
* which produces roughly-increasing-over-time but bit-shuffled values that
|
|
84
|
+
* spread writes across the keyspace.
|
|
85
|
+
*
|
|
86
|
+
* @see https://cloud.google.com/spanner/docs/reference/standard-sql/overview
|
|
87
|
+
* @see https://cloud.google.com/spanner/docs/reference/standard-sql/dml-syntax#insert_or_update
|
|
88
|
+
* @see https://cloud.google.com/spanner/docs/schema-and-data-model#choosing_a_primary_key
|
|
89
|
+
* @see https://cloud.google.com/spanner/docs/reference/standard-sql/data-definition-language#create_sequence
|
|
90
|
+
*/
|
|
91
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
92
|
+
exports.SpannerDialect = exports.SpannerTransaction = void 0;
|
|
93
|
+
// The `@google-cloud/spanner` package is declared as a dependency in
|
|
94
|
+
// package.json (and ships its own TypeScript types), but is intentionally
|
|
95
|
+
// required dynamically here rather than imported via `import ... from
|
|
96
|
+
// '@google-cloud/spanner'`. Using the dynamic `require()` form means this
|
|
97
|
+
// file - and anything that imports it - can be type-checked and unit-tested
|
|
98
|
+
// (against the Jest manual mock in __mocks__/@google-cloud/spanner.js) even
|
|
99
|
+
// in environments where the real package hasn't been installed yet, since
|
|
100
|
+
// TypeScript does not attempt to resolve string literals passed to the
|
|
101
|
+
// ambient `require()` function the way it does for `import` statements.
|
|
102
|
+
// The driver's actual object shapes (Spanner/Instance/Database/Table/
|
|
103
|
+
// Transaction) are treated as `any` below; this mirrors how the rest of this
|
|
104
|
+
// codebase treats third-party driver internals it doesn't want to couple to.
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
106
|
+
const { Spanner } = require('@google-cloud/spanner');
|
|
107
|
+
const types_1 = require("../../types");
|
|
108
|
+
const query_stream_helper_1 = require("../query-stream-helper");
|
|
109
|
+
const prorm_1 = require("../../prorm");
|
|
110
|
+
/**
|
|
111
|
+
* A Spanner read-write transaction, bridging the driver's callback-based
|
|
112
|
+
* `database.runTransaction()` API to the imperative
|
|
113
|
+
* `startTransaction()`/`commitTransaction()`/`rollbackTransaction()` shape
|
|
114
|
+
* used by every other dialect in this codebase. See `SpannerDialect.
|
|
115
|
+
* startTransaction()` for how `spannerTransaction`/completion signalling is
|
|
116
|
+
* wired up.
|
|
117
|
+
*/
|
|
118
|
+
class SpannerTransaction extends types_1.Transaction {
|
|
119
|
+
constructor(spannerTransaction, options) {
|
|
120
|
+
super(options);
|
|
121
|
+
this.spannerTransaction = spannerTransaction;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.SpannerTransaction = SpannerTransaction;
|
|
125
|
+
/**
|
|
126
|
+
* Google Cloud Spanner dialect class implementing the shared `Dialect` interface.
|
|
127
|
+
*/
|
|
128
|
+
class SpannerDialect {
|
|
129
|
+
constructor(config) {
|
|
130
|
+
this.name = 'spanner';
|
|
131
|
+
this.library = '@google-cloud/spanner';
|
|
132
|
+
this.spannerClient = null;
|
|
133
|
+
this.instance = null;
|
|
134
|
+
this.database = null;
|
|
135
|
+
this._isConnected = false;
|
|
136
|
+
/**
|
|
137
|
+
* Tracks in-flight `database.runTransaction()` calls started via
|
|
138
|
+
* `startTransaction()`, keyed by `Transaction#id`. Each entry is the
|
|
139
|
+
* resolver for the "I'm done with this transaction" promise that the
|
|
140
|
+
* `runTransaction()` callback returns (and therefore awaits) before the
|
|
141
|
+
* driver finalizes/retries the attempt. `commitTransaction()`/
|
|
142
|
+
* `rollbackTransaction()` call the resolver to let the callback - and the
|
|
143
|
+
* transaction lease - complete.
|
|
144
|
+
*/
|
|
145
|
+
this.pendingTx = new Map();
|
|
146
|
+
this.config = { ...config };
|
|
147
|
+
}
|
|
148
|
+
// =========================================================================
|
|
149
|
+
// Connection lifecycle
|
|
150
|
+
// =========================================================================
|
|
151
|
+
async connect() {
|
|
152
|
+
try {
|
|
153
|
+
this.spannerClient = new Spanner({
|
|
154
|
+
projectId: this.config.projectId,
|
|
155
|
+
keyFilename: this.config.keyFilename,
|
|
156
|
+
credentials: this.config.credentials,
|
|
157
|
+
apiEndpoint: this.config.apiEndpoint || (this.config.emulator ? 'localhost:9010' : undefined),
|
|
158
|
+
});
|
|
159
|
+
this.instance = this.spannerClient.instance(this.config.instanceId);
|
|
160
|
+
this.database = this.instance.database(this.config.databaseId);
|
|
161
|
+
this._isConnected = true;
|
|
162
|
+
if (!this.config.skipDialectCheck) {
|
|
163
|
+
await this.assertGoogleSqlDialect();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
this._isConnected = false;
|
|
168
|
+
this.database = null;
|
|
169
|
+
this.instance = null;
|
|
170
|
+
this.spannerClient = null;
|
|
171
|
+
throw new Error(`Failed to connect to Spanner database: ${error}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Verify the connected database is running in **GoogleSQL** dialect mode,
|
|
176
|
+
* not Spanner's PostgreSQL-interface mode. See the file header: this
|
|
177
|
+
* entire dialect - DDL (backtick-quoted identifiers, `STRING(n)`/`INT64`
|
|
178
|
+
* types, trailing `PRIMARY KEY (...)`), DML (`@pN` named parameters,
|
|
179
|
+
* `INSERT OR UPDATE INTO` upserts), and introspection queries
|
|
180
|
+
* (`information_schema` queried GoogleSQL-style) - assumes GoogleSQL.
|
|
181
|
+
* None of it is valid against a database provisioned with
|
|
182
|
+
* `databaseDialect: POSTGRESQL`, which uses Postgres syntax/wire behavior
|
|
183
|
+
* instead. Rather than let that surface as a confusing "SQL syntax error"
|
|
184
|
+
* on the first query, this checks the dialect via the Database Admin API
|
|
185
|
+
* (`Database#getDatabaseDialect()`, which wraps
|
|
186
|
+
* `DatabaseAdminClient#getDatabase()` and its `databaseDialect` field) and
|
|
187
|
+
* fails fast with an actionable error.
|
|
188
|
+
*
|
|
189
|
+
* Called automatically by `connect()` unless `skipDialectCheck: true` is
|
|
190
|
+
* set; also callable directly (e.g. to re-verify after a reconnect, or to
|
|
191
|
+
* probe a database before wiring up this dialect against it).
|
|
192
|
+
*/
|
|
193
|
+
async assertGoogleSqlDialect() {
|
|
194
|
+
if (!this.database) {
|
|
195
|
+
throw new Error('Not connected to database');
|
|
196
|
+
}
|
|
197
|
+
const dialect = await this.database.getDatabaseDialect();
|
|
198
|
+
if (dialect === 'POSTGRESQL') {
|
|
199
|
+
throw new Error(`Spanner database "${this.config.databaseId}" (instance "${this.config.instanceId}") is running in ` +
|
|
200
|
+
'PostgreSQL-interface dialect mode, but this dialect implementation only emits GoogleSQL DDL/DML ' +
|
|
201
|
+
'(backtick-quoted identifiers, @pN parameters, INSERT OR UPDATE INTO upserts, GoogleSQL data types, ' +
|
|
202
|
+
'etc.), which the PostgreSQL interface will reject or silently mis-parse. Either point this dialect at ' +
|
|
203
|
+
'a GOOGLE_STANDARD_SQL-dialect database, or use the `postgres` dialect (Spanner\'s PostgreSQL interface ' +
|
|
204
|
+
'is wire- and syntax-compatible with a useful subset of it) against this database instead.');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async disconnect() {
|
|
208
|
+
if (this.database) {
|
|
209
|
+
if (typeof this.database.close === 'function') {
|
|
210
|
+
await this.database.close();
|
|
211
|
+
}
|
|
212
|
+
this.database = null;
|
|
213
|
+
this.instance = null;
|
|
214
|
+
this.spannerClient = null;
|
|
215
|
+
this._isConnected = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
getConnection() {
|
|
219
|
+
return this.database;
|
|
220
|
+
}
|
|
221
|
+
isConnected() {
|
|
222
|
+
return this._isConnected && this.database !== null;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Determine whether an error should be retried by the caller. Spanner
|
|
226
|
+
* surfaces contention on read-write transactions as an `ABORTED` error
|
|
227
|
+
* (gRPC status code 10); `database.runTransaction()` already retries these
|
|
228
|
+
* internally, but callers driving their own retry loop around
|
|
229
|
+
* `startTransaction()`/`commitTransaction()` can use this helper too.
|
|
230
|
+
*/
|
|
231
|
+
isRetryableError(error) {
|
|
232
|
+
return !!error && (error.code === 10 || /aborted/i.test(String(error.message || '')));
|
|
233
|
+
}
|
|
234
|
+
// =========================================================================
|
|
235
|
+
// Query execution
|
|
236
|
+
// =========================================================================
|
|
237
|
+
/**
|
|
238
|
+
* Convert an array/object of replacement values into the named-parameter
|
|
239
|
+
* object Spanner's driver expects (`{ p1: value, ... }` for positional
|
|
240
|
+
* arrays, passed through as-is for already-named objects matching `@name`
|
|
241
|
+
* placeholders in the SQL).
|
|
242
|
+
*/
|
|
243
|
+
buildParams(replacements) {
|
|
244
|
+
if (!replacements)
|
|
245
|
+
return {};
|
|
246
|
+
if (Array.isArray(replacements)) {
|
|
247
|
+
const params = {};
|
|
248
|
+
replacements.forEach((value, index) => {
|
|
249
|
+
params[`p${index + 1}`] = value;
|
|
250
|
+
});
|
|
251
|
+
return params;
|
|
252
|
+
}
|
|
253
|
+
return { ...replacements };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Stream query results by paging through `sql` via repeated
|
|
257
|
+
* dialect-appropriate LIMIT/OFFSET queries (see
|
|
258
|
+
* `createPaginatedQueryStream()` in `src/dialects/query-stream-helper.ts`)
|
|
259
|
+
* instead of loading the whole result set into memory at once.
|
|
260
|
+
* @param sql - The SELECT statement to stream
|
|
261
|
+
* @param options - Streaming options (batch size, backpressure watermark, model mapping)
|
|
262
|
+
*/
|
|
263
|
+
queryStream(sql, options) {
|
|
264
|
+
return (0, query_stream_helper_1.createPaginatedQueryStream)(this, sql, options);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Execute a raw SQL query.
|
|
268
|
+
*
|
|
269
|
+
* Dispatches based on statement type, since Spanner requires different
|
|
270
|
+
* driver entry points for each:
|
|
271
|
+
* - `CREATE`/`ALTER`/`DROP` -> `database.updateSchema()` (a long-running
|
|
272
|
+
* DDL operation).
|
|
273
|
+
* - `SELECT`/`WITH` (including recursive CTEs and window functions, which
|
|
274
|
+
* GoogleSQL supports natively) -> `database.run()` (single-use read).
|
|
275
|
+
* - Everything else (`INSERT`/`UPDATE`/`DELETE`) -> a read-write
|
|
276
|
+
* transaction via `runDML()`, since Spanner only allows DML inside a
|
|
277
|
+
* transaction (there is no autocommit-single-statement DML path the way
|
|
278
|
+
* `pg`/`mysql2` provide).
|
|
279
|
+
*/
|
|
280
|
+
async query(sql, options) {
|
|
281
|
+
if (!this.database) {
|
|
282
|
+
throw new Error('Not connected to database');
|
|
283
|
+
}
|
|
284
|
+
const trimmed = sql.trim();
|
|
285
|
+
const replacements = options?.replacements ?? options?.bind;
|
|
286
|
+
const params = this.buildParams(replacements);
|
|
287
|
+
if (/^(CREATE|ALTER|DROP)\b/i.test(trimmed)) {
|
|
288
|
+
return this.runDDL(trimmed);
|
|
289
|
+
}
|
|
290
|
+
if (/^(SELECT|WITH)\b/i.test(trimmed)) {
|
|
291
|
+
return this.runRead(trimmed, params);
|
|
292
|
+
}
|
|
293
|
+
return this.runDML(trimmed, params);
|
|
294
|
+
}
|
|
295
|
+
async runDDL(sql) {
|
|
296
|
+
try {
|
|
297
|
+
const [operation] = await this.database.updateSchema([sql]);
|
|
298
|
+
await operation.promise();
|
|
299
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
throw new Error(`Spanner DDL error: ${error}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async runRead(sql, params) {
|
|
306
|
+
try {
|
|
307
|
+
const [rows] = await this.database.run({
|
|
308
|
+
sql,
|
|
309
|
+
params: Object.keys(params).length ? params : undefined,
|
|
310
|
+
json: true,
|
|
311
|
+
...this.config.queryOptions,
|
|
312
|
+
});
|
|
313
|
+
const fields = rows.length
|
|
314
|
+
? Object.keys(rows[0]).map((name) => ({
|
|
315
|
+
name,
|
|
316
|
+
type: 'unknown',
|
|
317
|
+
length: 0,
|
|
318
|
+
tableID: 0,
|
|
319
|
+
columnID: 0,
|
|
320
|
+
nullable: true,
|
|
321
|
+
isEnum: false,
|
|
322
|
+
isPrimaryKey: false,
|
|
323
|
+
}))
|
|
324
|
+
: [];
|
|
325
|
+
return { rows, rowCount: rows.length, fields };
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
throw new Error(`Spanner query error: ${error}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Run a DML statement (`INSERT`/`UPDATE`/`DELETE`) inside a fresh
|
|
333
|
+
* read-write transaction, committing (or rolling back on error)
|
|
334
|
+
* automatically. This is the "autocommit" convenience path used by
|
|
335
|
+
* `query()`; for multi-statement transactions, use
|
|
336
|
+
* `startTransaction()`/`commitTransaction()`/`rollbackTransaction()`
|
|
337
|
+
* directly and run DML against the returned transaction's connection.
|
|
338
|
+
*/
|
|
339
|
+
async runDML(sql, params) {
|
|
340
|
+
const transaction = await this.startTransaction();
|
|
341
|
+
try {
|
|
342
|
+
const spannerTx = transaction.spannerTransaction;
|
|
343
|
+
const [rowCount] = await spannerTx.runUpdate({
|
|
344
|
+
sql,
|
|
345
|
+
params: Object.keys(params).length ? params : undefined,
|
|
346
|
+
});
|
|
347
|
+
await this.commitTransaction(transaction);
|
|
348
|
+
return { rows: [], rowCount: rowCount || 0, fields: [] };
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
await this.rollbackTransaction(transaction);
|
|
352
|
+
throw new Error(`Spanner DML error: ${error}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// =========================================================================
|
|
356
|
+
// Escaping / identifiers
|
|
357
|
+
// =========================================================================
|
|
358
|
+
escape(value) {
|
|
359
|
+
if (value instanceof prorm_1.Literal) {
|
|
360
|
+
return value.val;
|
|
361
|
+
}
|
|
362
|
+
if (value === null || value === undefined) {
|
|
363
|
+
return 'NULL';
|
|
364
|
+
}
|
|
365
|
+
if (typeof value === 'string') {
|
|
366
|
+
return `'${this.escapeString(value)}'`;
|
|
367
|
+
}
|
|
368
|
+
if (typeof value === 'number') {
|
|
369
|
+
return String(value);
|
|
370
|
+
}
|
|
371
|
+
if (typeof value === 'bigint') {
|
|
372
|
+
return value.toString();
|
|
373
|
+
}
|
|
374
|
+
if (typeof value === 'boolean') {
|
|
375
|
+
return value ? 'TRUE' : 'FALSE';
|
|
376
|
+
}
|
|
377
|
+
if (value instanceof Date) {
|
|
378
|
+
// LIMITATION: `escape()` has no column-type context (it's `escape(value:
|
|
379
|
+
// any): string` on the shared `Dialect` interface, same as every other
|
|
380
|
+
// dialect in this codebase), so a JS `Date` written into a
|
|
381
|
+
// `DATEONLY`/`DATE`-typed column is always emitted as a `TIMESTAMP`
|
|
382
|
+
// literal here, which GoogleSQL will reject for a `DATE` column
|
|
383
|
+
// (Spanner has no implicit TIMESTAMP -> DATE coercion). Working around
|
|
384
|
+
// this would require threading the target column's declared type
|
|
385
|
+
// through every `escape()` call site (query building, replacement
|
|
386
|
+
// substitution, etc.) - a larger refactor than is justified here since
|
|
387
|
+
// no other dialect in this codebase does it either. Callers writing to
|
|
388
|
+
// `DATEONLY` columns should pass a `'YYYY-MM-DD'` string (which is
|
|
389
|
+
// escaped as a plain string literal below and is valid GoogleSQL DATE
|
|
390
|
+
// literal syntax) instead of a `Date` object.
|
|
391
|
+
return `TIMESTAMP '${value.toISOString()}'`;
|
|
392
|
+
}
|
|
393
|
+
if (Buffer.isBuffer(value)) {
|
|
394
|
+
return `B'${value.toString('base64')}'`;
|
|
395
|
+
}
|
|
396
|
+
if (Array.isArray(value)) {
|
|
397
|
+
return `[${value.map((v) => this.escape(v)).join(', ')}]`;
|
|
398
|
+
}
|
|
399
|
+
return `'${this.escapeString(JSON.stringify(value))}'`;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Escape a string literal for GoogleSQL. Unlike ANSI-SQL/Postgres (which
|
|
403
|
+
* escape embedded quotes by doubling them, `''`), GoogleSQL string
|
|
404
|
+
* literals use C-style backslash escaping (`\'`, `\\`).
|
|
405
|
+
*/
|
|
406
|
+
escapeString(str) {
|
|
407
|
+
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* GoogleSQL quotes identifiers with backticks (like BigQuery/MySQL), not
|
|
411
|
+
* double quotes.
|
|
412
|
+
*/
|
|
413
|
+
escapeId(identifier) {
|
|
414
|
+
const id = String(identifier ?? '');
|
|
415
|
+
return `\`${id.replace(/`/g, '``')}\``;
|
|
416
|
+
}
|
|
417
|
+
quoteIdentifier(identifier) {
|
|
418
|
+
return this.escapeId(identifier);
|
|
419
|
+
}
|
|
420
|
+
quoteTable(tableName, schema) {
|
|
421
|
+
// Spanner GoogleSQL databases are not schema-namespaced the way
|
|
422
|
+
// Postgres databases are (there is no `CREATE SCHEMA` in GoogleSQL);
|
|
423
|
+
// `schema` is accepted for interface parity but ignored.
|
|
424
|
+
if (schema) {
|
|
425
|
+
console.warn(`Spanner: schema "${schema}" was provided to quoteTable(), but Cloud Spanner GoogleSQL has no schema/namespace concept for tables; ignoring.`);
|
|
426
|
+
}
|
|
427
|
+
return this.escapeId(tableName);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Replace `@pN` / `@name` placeholders in SQL with escaped literal values.
|
|
431
|
+
*/
|
|
432
|
+
replaceReplacements(sql, replacements) {
|
|
433
|
+
if (!replacements) {
|
|
434
|
+
return sql;
|
|
435
|
+
}
|
|
436
|
+
let result = sql;
|
|
437
|
+
if (Array.isArray(replacements)) {
|
|
438
|
+
replacements.forEach((value, index) => {
|
|
439
|
+
const placeholder = new RegExp(`@p${index + 1}\\b`, 'g');
|
|
440
|
+
result = result.replace(placeholder, this.escape(value));
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
445
|
+
const placeholder = new RegExp(`@${key}\\b`, 'g');
|
|
446
|
+
result = result.replace(placeholder, this.escape(value));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
// =========================================================================
|
|
452
|
+
// Schemas - Spanner GoogleSQL has no CREATE SCHEMA; not supported
|
|
453
|
+
// =========================================================================
|
|
454
|
+
async createSchema(_schema) {
|
|
455
|
+
throw new Error('Cloud Spanner GoogleSQL has no CREATE SCHEMA/schema-namespace concept for tables.');
|
|
456
|
+
}
|
|
457
|
+
async dropSchema(_schema, _options) {
|
|
458
|
+
throw new Error('Cloud Spanner GoogleSQL has no CREATE SCHEMA/schema-namespace concept for tables.');
|
|
459
|
+
}
|
|
460
|
+
async showAllSchemas() {
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
async listSchemas() {
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
createDatabaseSQL(_options) {
|
|
467
|
+
throw new Error('Spanner databases are created via the Admin API (instance.createDatabase()), not SQL DDL.');
|
|
468
|
+
}
|
|
469
|
+
dropDatabaseSQL(_name) {
|
|
470
|
+
throw new Error('Spanner databases are dropped via the Admin API (database.delete()), not SQL DDL.');
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Spanner has no `SAVEPOINT`. Savepoints exist to allow partial rollback
|
|
474
|
+
* within a larger transaction; Spanner read-write transactions are
|
|
475
|
+
* expected to be short-lived and are retried wholesale on conflict, so
|
|
476
|
+
* there is no nested-rollback primitive.
|
|
477
|
+
*/
|
|
478
|
+
createSavepointSQL(_name) {
|
|
479
|
+
throw new Error('Cloud Spanner does not support SAVEPOINT.');
|
|
480
|
+
}
|
|
481
|
+
releaseSavepointSQL(_name) {
|
|
482
|
+
throw new Error('Cloud Spanner does not support SAVEPOINT.');
|
|
483
|
+
}
|
|
484
|
+
rollbackToSavepointSQL(_name) {
|
|
485
|
+
throw new Error('Cloud Spanner does not support SAVEPOINT.');
|
|
486
|
+
}
|
|
487
|
+
// =========================================================================
|
|
488
|
+
// Extensions - PostgreSQL only
|
|
489
|
+
// =========================================================================
|
|
490
|
+
async createExtension(_extensionName, _options) {
|
|
491
|
+
throw new Error('Extensions are only supported in PostgreSQL');
|
|
492
|
+
}
|
|
493
|
+
async dropExtension(_extensionName, _options) {
|
|
494
|
+
throw new Error('Extensions are only supported in PostgreSQL');
|
|
495
|
+
}
|
|
496
|
+
async getExtensions() {
|
|
497
|
+
throw new Error('Extensions are only supported in PostgreSQL');
|
|
498
|
+
}
|
|
499
|
+
async hasExtension(_extensionName) {
|
|
500
|
+
throw new Error('Extensions are only supported in PostgreSQL');
|
|
501
|
+
}
|
|
502
|
+
async getDatabaseVersion() {
|
|
503
|
+
// Spanner has no SELECT version()-style introspection; the closest
|
|
504
|
+
// analogue is the dialect/database type itself since engine version is
|
|
505
|
+
// not user-addressable (Spanner is a fully-managed service).
|
|
506
|
+
return 'Cloud Spanner (GoogleSQL)';
|
|
507
|
+
}
|
|
508
|
+
// =========================================================================
|
|
509
|
+
// Tables (DDL)
|
|
510
|
+
// =========================================================================
|
|
511
|
+
/**
|
|
512
|
+
* Create a table.
|
|
513
|
+
*
|
|
514
|
+
* Notable GoogleSQL DDL differences from every other dialect here:
|
|
515
|
+
* - The primary key is declared *after* the closing paren of the column
|
|
516
|
+
* list (`) PRIMARY KEY (col1, col2)`), not inline as a column/table
|
|
517
|
+
* constraint.
|
|
518
|
+
* - There is no `ALTER TABLE ... ADD CONSTRAINT` for primary keys - the
|
|
519
|
+
* primary key is fixed at table-creation time.
|
|
520
|
+
* - `autoIncrement` columns are mapped to `STRING(36) DEFAULT
|
|
521
|
+
* (GENERATE_UUID())` rather than a monotonic counter - see the
|
|
522
|
+
* hotspot-avoidance note in the file header.
|
|
523
|
+
*/
|
|
524
|
+
async createTable(tableName, columns, options) {
|
|
525
|
+
const columnDefs = [];
|
|
526
|
+
const inferredPk = [];
|
|
527
|
+
for (const [columnName, definition] of Object.entries(columns)) {
|
|
528
|
+
columnDefs.push(this.getColumnDefinitionSql(columnName, definition));
|
|
529
|
+
if (definition.primaryKey) {
|
|
530
|
+
inferredPk.push(columnName);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (options?.constraints) {
|
|
534
|
+
for (const constraint of options.constraints) {
|
|
535
|
+
if (constraint.type === 'PRIMARY KEY')
|
|
536
|
+
continue; // handled via PRIMARY KEY (...) clause below
|
|
537
|
+
const constraintSql = this.buildConstraintSql(constraint);
|
|
538
|
+
if (constraintSql)
|
|
539
|
+
columnDefs.push(constraintSql);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const primaryKey = options?.primaryKey && options.primaryKey.length ? options.primaryKey : inferredPk;
|
|
543
|
+
if (!primaryKey.length) {
|
|
544
|
+
throw new Error(`Spanner: table "${tableName}" needs a primary key (mark a column primaryKey: true, or pass options.primaryKey).`);
|
|
545
|
+
}
|
|
546
|
+
let sql = 'CREATE TABLE';
|
|
547
|
+
if (options?.ifNotExists)
|
|
548
|
+
sql += ' IF NOT EXISTS';
|
|
549
|
+
sql += ` ${this.escapeId(tableName)} (${columnDefs.join(', ')})`;
|
|
550
|
+
sql += ` PRIMARY KEY (${primaryKey.map((f) => this.escapeId(f)).join(', ')})`;
|
|
551
|
+
if (options?.interleaveInParent) {
|
|
552
|
+
sql += `,\nINTERLEAVE IN PARENT ${this.escapeId(options.interleaveInParent)}`;
|
|
553
|
+
sql += options.interleaveOnDeleteCascade ? ' ON DELETE CASCADE' : ' ON DELETE NO ACTION';
|
|
554
|
+
}
|
|
555
|
+
if (options?.rowDeletionPolicy) {
|
|
556
|
+
sql += `,\nROW DELETION POLICY (OLDER_THAN(${this.escapeId(options.rowDeletionPolicy.column)}, INTERVAL ${options.rowDeletionPolicy.olderThanDays} DAY))`;
|
|
557
|
+
}
|
|
558
|
+
if (options?.engine || options?.charset || options?.rowFormat || options?.tablespace) {
|
|
559
|
+
console.warn('Spanner: engine/charset/rowFormat/tablespace options are not applicable to Cloud Spanner and are ignored.');
|
|
560
|
+
}
|
|
561
|
+
await this.query(sql);
|
|
562
|
+
if (options?.uniqueKeys) {
|
|
563
|
+
const entries = Array.isArray(options.uniqueKeys)
|
|
564
|
+
? options.uniqueKeys.map((uk) => [uk.name, uk.fields])
|
|
565
|
+
: Object.entries(options.uniqueKeys);
|
|
566
|
+
for (const [keyName, fields] of entries) {
|
|
567
|
+
const indexName = keyName || `idx_${tableName}_${fields.join('_')}_unique`;
|
|
568
|
+
await this.addIndex(tableName, indexName, fields, { unique: true });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (options?.indexes) {
|
|
572
|
+
for (const index of options.indexes) {
|
|
573
|
+
await this.addIndex(tableName, index.name || `idx_${tableName}_${index.fields.join('_')}`, index.fields, { unique: index.unique, where: index.where, include: index.include });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
buildConstraintSql(constraint) {
|
|
578
|
+
const name = constraint.name ? `CONSTRAINT ${this.escapeId(constraint.name)} ` : '';
|
|
579
|
+
const fields = constraint.fields?.map((f) => this.escapeId(f)).join(', ') || '';
|
|
580
|
+
switch (constraint.type) {
|
|
581
|
+
case 'UNIQUE':
|
|
582
|
+
// GoogleSQL DDL has no inline UNIQUE column/table constraint; model
|
|
583
|
+
// uniqueness via a UNIQUE INDEX instead (see createTable's
|
|
584
|
+
// options.uniqueKeys handling / addIndex()).
|
|
585
|
+
return null;
|
|
586
|
+
case 'FOREIGN KEY': {
|
|
587
|
+
if (!constraint.references)
|
|
588
|
+
return null;
|
|
589
|
+
const refField = constraint.references.field;
|
|
590
|
+
const refFieldSql = Array.isArray(refField)
|
|
591
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
592
|
+
: `(${this.escapeId(refField)})`;
|
|
593
|
+
let fkSql = `${name}FOREIGN KEY (${fields}) REFERENCES ${this.escapeId(constraint.references.table)}${refFieldSql}`;
|
|
594
|
+
if (constraint.references.onDelete) {
|
|
595
|
+
// Spanner only supports ON DELETE CASCADE / NO ACTION for foreign keys.
|
|
596
|
+
fkSql += ` ON DELETE ${constraint.references.onDelete}`;
|
|
597
|
+
}
|
|
598
|
+
return fkSql;
|
|
599
|
+
}
|
|
600
|
+
case 'CHECK':
|
|
601
|
+
if (!constraint.check)
|
|
602
|
+
return null;
|
|
603
|
+
return `${name}CHECK (${constraint.check})`;
|
|
604
|
+
case 'PRIMARY KEY':
|
|
605
|
+
return null; // handled via the trailing PRIMARY KEY (...) clause
|
|
606
|
+
default:
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
getColumnDefinitionSql(columnName, definition) {
|
|
611
|
+
let sql;
|
|
612
|
+
if (definition.autoIncrement) {
|
|
613
|
+
// See the "Hotspot avoidance" note in the file header: Cloud Spanner
|
|
614
|
+
// explicitly discourages monotonically-increasing primary keys because
|
|
615
|
+
// they concentrate writes on a single key-range split. Rather than
|
|
616
|
+
// emulate a counter (which would reproduce the anti-pattern), map
|
|
617
|
+
// autoIncrement columns to a randomly-distributed UUID default.
|
|
618
|
+
console.warn(`Spanner: column "${columnName}" was declared autoIncrement; Cloud Spanner has no monotonic ` +
|
|
619
|
+
`auto-increment (it would hotspot a single key range). Generating "${columnName}" as ` +
|
|
620
|
+
`STRING(36) DEFAULT (GENERATE_UUID()) instead. For an ordered-but-distributed numeric key, ` +
|
|
621
|
+
`use createBitReversedSequence()/nextBitReversedSequenceValue() instead.`);
|
|
622
|
+
sql = `${this.escapeId(columnName)} STRING(36) DEFAULT (GENERATE_UUID())`;
|
|
623
|
+
if (definition.allowNull === false || definition.primaryKey) {
|
|
624
|
+
sql += ' NOT NULL';
|
|
625
|
+
}
|
|
626
|
+
return sql;
|
|
627
|
+
}
|
|
628
|
+
sql = `${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type).replace(/%%COLUMN%%/g, this.escapeId(columnName))}`;
|
|
629
|
+
if (definition.allowNull === false || definition.primaryKey) {
|
|
630
|
+
sql += ' NOT NULL';
|
|
631
|
+
}
|
|
632
|
+
if (definition.defaultValue !== undefined) {
|
|
633
|
+
sql += ` DEFAULT (${this.getDefaultValue(definition.defaultValue)})`;
|
|
634
|
+
}
|
|
635
|
+
if (definition.references) {
|
|
636
|
+
const refField = definition.references.field;
|
|
637
|
+
const refFieldSql = Array.isArray(refField)
|
|
638
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
639
|
+
: `(${this.escapeId(refField)})`;
|
|
640
|
+
sql += ` REFERENCES ${this.escapeId(definition.references.table)}${refFieldSql}`;
|
|
641
|
+
if (definition.references.onDelete) {
|
|
642
|
+
sql += ` ON DELETE ${definition.references.onDelete}`;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return sql;
|
|
646
|
+
}
|
|
647
|
+
getDefaultValue(value) {
|
|
648
|
+
if (value === null)
|
|
649
|
+
return 'NULL';
|
|
650
|
+
if (typeof value === 'string') {
|
|
651
|
+
if (/^(GENERATE_UUID|CURRENT_TIMESTAMP|PENDING_COMMIT_TIMESTAMP)\s*\(/i.test(value)) {
|
|
652
|
+
return value;
|
|
653
|
+
}
|
|
654
|
+
return `'${this.escapeString(value)}'`;
|
|
655
|
+
}
|
|
656
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
657
|
+
return String(value);
|
|
658
|
+
if (value instanceof Date)
|
|
659
|
+
return `TIMESTAMP '${value.toISOString()}'`;
|
|
660
|
+
if (value && typeof value.toDefaultValue === 'function')
|
|
661
|
+
return value.toDefaultValue();
|
|
662
|
+
return `'${this.escapeString(String(value))}'`;
|
|
663
|
+
}
|
|
664
|
+
async dropTable(tableName, options) {
|
|
665
|
+
// Spanner's DROP TABLE has no IF EXISTS/CASCADE; interleaved child tables
|
|
666
|
+
// and indexes on the table must be dropped first.
|
|
667
|
+
if (options?.cascade) {
|
|
668
|
+
console.warn('Spanner: DROP TABLE has no CASCADE - drop dependent indexes/interleaved child tables explicitly first.');
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
await this.query(`DROP TABLE ${this.escapeId(tableName)}`);
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
if (options?.ifExists)
|
|
675
|
+
return;
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Create a table interleaved inside a parent table
|
|
681
|
+
* (`INTERLEAVE IN PARENT ...`), Spanner's mechanism for physically
|
|
682
|
+
* co-locating related rows (e.g. Orders interleaved in Customers) so that
|
|
683
|
+
* point look-ups and joins across the relationship stay within a single
|
|
684
|
+
* storage split instead of fanning out across the cluster.
|
|
685
|
+
*
|
|
686
|
+
* `columns` must include the parent table's primary key column(s) as a
|
|
687
|
+
* prefix of `options.primaryKey`.
|
|
688
|
+
*/
|
|
689
|
+
async createInterleavedTable(tableName, parentTable, columns, options) {
|
|
690
|
+
await this.createTable(tableName, columns, {
|
|
691
|
+
...options,
|
|
692
|
+
primaryKey: options.primaryKey,
|
|
693
|
+
interleaveInParent: parentTable,
|
|
694
|
+
interleaveOnDeleteCascade: options.onDeleteCascade,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
// Partitioned tables (PostgreSQL-style declarative partitioning) have no
|
|
698
|
+
// GoogleSQL equivalent. Spanner distributes/splits data automatically and
|
|
699
|
+
// transparently based on key ranges and load; there is no user-facing
|
|
700
|
+
// `PARTITION BY`/`CREATE TABLE ... PARTITION OF` syntax. Interleaved tables
|
|
701
|
+
// (see createInterleavedTable) and table splitting are the closest
|
|
702
|
+
// Spanner-native analogues, but they solve a different problem (physical
|
|
703
|
+
// co-location vs. logical partitioning) and aren't drop-in replacements.
|
|
704
|
+
async createPartitionedTable() {
|
|
705
|
+
throw new Error('Cloud Spanner has no declarative table partitioning (PARTITION BY); it distributes data automatically. See createInterleavedTable() for physical co-location.');
|
|
706
|
+
}
|
|
707
|
+
async createPartition() {
|
|
708
|
+
throw new Error('Cloud Spanner has no declarative table partitioning.');
|
|
709
|
+
}
|
|
710
|
+
async attachPartition() {
|
|
711
|
+
throw new Error('Cloud Spanner has no declarative table partitioning.');
|
|
712
|
+
}
|
|
713
|
+
async detachPartition() {
|
|
714
|
+
throw new Error('Cloud Spanner has no declarative table partitioning.');
|
|
715
|
+
}
|
|
716
|
+
async dropPartition() {
|
|
717
|
+
throw new Error('Cloud Spanner has no declarative table partitioning.');
|
|
718
|
+
}
|
|
719
|
+
// =========================================================================
|
|
720
|
+
// Views
|
|
721
|
+
// =========================================================================
|
|
722
|
+
async createView(viewName, query, options) {
|
|
723
|
+
let sql = options?.replace ? 'CREATE OR REPLACE VIEW' : 'CREATE VIEW';
|
|
724
|
+
sql += ` ${this.escapeId(viewName)} SQL SECURITY INVOKER AS ${query}`;
|
|
725
|
+
await this.query(sql);
|
|
726
|
+
}
|
|
727
|
+
async dropView(viewName, options) {
|
|
728
|
+
if (options?.cascade) {
|
|
729
|
+
console.warn('Spanner: DROP VIEW has no CASCADE; drop dependent views explicitly first.');
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
await this.query(`DROP VIEW ${this.escapeId(viewName)}`);
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
if (options?.ifExists)
|
|
736
|
+
return;
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async showViews() {
|
|
741
|
+
const result = await this.query(`SELECT table_name FROM information_schema.views WHERE table_schema = ''`);
|
|
742
|
+
return result.rows.map((row) => row.table_name);
|
|
743
|
+
}
|
|
744
|
+
// =========================================================================
|
|
745
|
+
// Columns
|
|
746
|
+
// =========================================================================
|
|
747
|
+
async addColumn(tableName, columnName, definition) {
|
|
748
|
+
const colSql = this.getColumnDefinitionSql(columnName, definition);
|
|
749
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${colSql}`);
|
|
750
|
+
}
|
|
751
|
+
async removeColumn(tableName, columnName) {
|
|
752
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} DROP COLUMN ${this.escapeId(columnName)}`);
|
|
753
|
+
}
|
|
754
|
+
async changeColumn(tableName, columnName, definition) {
|
|
755
|
+
// Spanner's ALTER COLUMN can only change nullability/length/default of an
|
|
756
|
+
// existing column - it cannot change the underlying type across
|
|
757
|
+
// incompatible families the way Postgres' USING clause can.
|
|
758
|
+
let sql = `ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type).replace(/%%COLUMN%%/g, this.escapeId(columnName))}`;
|
|
759
|
+
if (definition.allowNull === false)
|
|
760
|
+
sql += ' NOT NULL';
|
|
761
|
+
await this.query(sql);
|
|
762
|
+
}
|
|
763
|
+
async renameColumn() {
|
|
764
|
+
throw new Error('Cloud Spanner GoogleSQL has no RENAME COLUMN; add a new column, backfill, and drop the old one.');
|
|
765
|
+
}
|
|
766
|
+
async addForeignKey(tableName, columnName, referencedTableName, referencedColumnName, options) {
|
|
767
|
+
const constraintName = options?.name || `fk_${tableName}_${columnName}`;
|
|
768
|
+
let sql = `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(constraintName)} FOREIGN KEY (${this.escapeId(columnName)}) REFERENCES ${this.escapeId(referencedTableName)} (${this.escapeId(referencedColumnName)})`;
|
|
769
|
+
if (options?.onDelete) {
|
|
770
|
+
// Spanner foreign keys only support ON DELETE CASCADE / NO ACTION (no ON UPDATE).
|
|
771
|
+
sql += ` ON DELETE ${options.onDelete}`;
|
|
772
|
+
}
|
|
773
|
+
await this.query(sql);
|
|
774
|
+
}
|
|
775
|
+
// =========================================================================
|
|
776
|
+
// Bulk insert / Mutation API
|
|
777
|
+
// =========================================================================
|
|
778
|
+
/**
|
|
779
|
+
* Bulk-insert records using the Mutation API (`table.insert()`) rather
|
|
780
|
+
* than a multi-row SQL `INSERT`. This is the recommended path for bulk
|
|
781
|
+
* writes on Spanner: mutations skip SQL parsing/planning entirely and are
|
|
782
|
+
* batched into a single commit.
|
|
783
|
+
*/
|
|
784
|
+
async bulkInsert(tableName, records, _options) {
|
|
785
|
+
if (records.length === 0) {
|
|
786
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
787
|
+
}
|
|
788
|
+
await this.mutationInsert(tableName, records);
|
|
789
|
+
return { rows: [], rowCount: records.length, fields: [] };
|
|
790
|
+
}
|
|
791
|
+
/** Mutation API: insert rows (fails if any row's key already exists). */
|
|
792
|
+
async mutationInsert(tableName, rows) {
|
|
793
|
+
if (!this.database)
|
|
794
|
+
throw new Error('Not connected to database');
|
|
795
|
+
await this.database.table(tableName).insert(rows);
|
|
796
|
+
}
|
|
797
|
+
/** Mutation API: update rows (fails if a row's key does not exist). */
|
|
798
|
+
async mutationUpdate(tableName, rows) {
|
|
799
|
+
if (!this.database)
|
|
800
|
+
throw new Error('Not connected to database');
|
|
801
|
+
await this.database.table(tableName).update(rows);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Mutation API: upsert rows (insert-or-update by primary key). This is the
|
|
805
|
+
* **primary/recommended** upsert mechanism for this dialect - see the file
|
|
806
|
+
* header for the tradeoff vs. `buildUpsertQuery()`'s SQL `INSERT OR
|
|
807
|
+
* UPDATE` fallback.
|
|
808
|
+
*/
|
|
809
|
+
async mutationUpsert(tableName, rows) {
|
|
810
|
+
if (!this.database)
|
|
811
|
+
throw new Error('Not connected to database');
|
|
812
|
+
await this.database.table(tableName).upsert(rows);
|
|
813
|
+
}
|
|
814
|
+
/** Mutation API: delete rows by primary key. */
|
|
815
|
+
async mutationDelete(tableName, keys) {
|
|
816
|
+
if (!this.database)
|
|
817
|
+
throw new Error('Not connected to database');
|
|
818
|
+
await this.database.table(tableName).deleteRows(keys);
|
|
819
|
+
}
|
|
820
|
+
// =========================================================================
|
|
821
|
+
// Introspection
|
|
822
|
+
// =========================================================================
|
|
823
|
+
async showTables() {
|
|
824
|
+
const result = await this.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = ''`);
|
|
825
|
+
return result.rows.map((row) => row.table_name);
|
|
826
|
+
}
|
|
827
|
+
async showConstraints(tableName) {
|
|
828
|
+
const result = await this.query(`SELECT constraint_name, constraint_type FROM information_schema.table_constraints WHERE table_name = @p1`, { replacements: [tableName] });
|
|
829
|
+
return result.rows;
|
|
830
|
+
}
|
|
831
|
+
async addConstraint(tableName, options) {
|
|
832
|
+
if (options.type === 'PRIMARY KEY') {
|
|
833
|
+
throw new Error('Cloud Spanner primary keys are fixed at CREATE TABLE time; ALTER TABLE ... ADD CONSTRAINT PRIMARY KEY is not supported.');
|
|
834
|
+
}
|
|
835
|
+
if (options.type === 'UNIQUE') {
|
|
836
|
+
const indexName = options.name || `idx_${tableName}_${options.fields.join('_')}_unique`;
|
|
837
|
+
await this.addIndex(tableName, indexName, options.fields, { unique: true });
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const name = options.name || `${tableName}_${options.fields.join('_')}_${options.type.toLowerCase().replace(/\s+/g, '_')}`;
|
|
841
|
+
const fieldsSql = options.fields.map((f) => this.escapeId(f)).join(', ');
|
|
842
|
+
let sql = `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${this.escapeId(name)} `;
|
|
843
|
+
if (options.type === 'FOREIGN KEY') {
|
|
844
|
+
sql += `FOREIGN KEY (${fieldsSql}) REFERENCES ${this.escapeId(options.references.table)} (${options.references.fields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
sql += `CHECK (${options.check})`;
|
|
848
|
+
}
|
|
849
|
+
await this.query(sql);
|
|
850
|
+
}
|
|
851
|
+
async removeConstraint(tableName, constraintName) {
|
|
852
|
+
await this.query(`ALTER TABLE ${this.escapeId(tableName)} DROP CONSTRAINT ${this.escapeId(constraintName)}`);
|
|
853
|
+
}
|
|
854
|
+
async showIndexes(tableName) {
|
|
855
|
+
const result = await this.query(`SELECT index_name, index_type, is_unique FROM information_schema.indexes WHERE table_name = @p1`, { replacements: [tableName] });
|
|
856
|
+
return result.rows;
|
|
857
|
+
}
|
|
858
|
+
async describeTable(tableName) {
|
|
859
|
+
const result = await this.query(`SELECT column_name, spanner_type, is_nullable FROM information_schema.columns WHERE table_name = @p1 ORDER BY ordinal_position`, { replacements: [tableName] });
|
|
860
|
+
const description = {};
|
|
861
|
+
for (const row of result.rows) {
|
|
862
|
+
description[row.column_name] = {
|
|
863
|
+
type: row.spanner_type,
|
|
864
|
+
allowNull: row.is_nullable === 'YES',
|
|
865
|
+
defaultValue: null,
|
|
866
|
+
primaryKey: false,
|
|
867
|
+
autoIncrement: false,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
return description;
|
|
871
|
+
}
|
|
872
|
+
async renameTable() {
|
|
873
|
+
throw new Error('Cloud Spanner GoogleSQL has no RENAME TABLE; create the new table, backfill, and drop the old one.');
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Add a secondary index. `options.include` maps to Spanner's `STORING
|
|
877
|
+
* (...)` clause - a covering index that duplicates the listed columns
|
|
878
|
+
* alongside the index entry so lookups can be satisfied without a join
|
|
879
|
+
* back to the base table.
|
|
880
|
+
*/
|
|
881
|
+
async addIndex(tableName, indexName, fields, options) {
|
|
882
|
+
let sql = 'CREATE';
|
|
883
|
+
if (options?.unique)
|
|
884
|
+
sql += ' UNIQUE';
|
|
885
|
+
sql += ` INDEX${options?.ifNotExists ? ' IF NOT EXISTS' : ''} ${this.escapeId(indexName)} ON ${this.escapeId(tableName)} (${fields.map((f) => this.escapeId(f)).join(', ')})`;
|
|
886
|
+
if (options?.include && options.include.length) {
|
|
887
|
+
sql += ` STORING (${options.include.map((f) => this.escapeId(f)).join(', ')})`;
|
|
888
|
+
}
|
|
889
|
+
if (options?.where) {
|
|
890
|
+
// Spanner has no partial-index WHERE clause; NULL_FILTERED is the
|
|
891
|
+
// closest analogue (filters out rows with NULL in any indexed key
|
|
892
|
+
// column) but is not a general predicate.
|
|
893
|
+
console.warn('Spanner: partial indexes with an arbitrary WHERE predicate are not supported; consider NULL_FILTERED for excluding NULL key columns instead. Ignoring `where`.');
|
|
894
|
+
}
|
|
895
|
+
await this.query(sql);
|
|
896
|
+
}
|
|
897
|
+
async removeIndex(tableName, indexName) {
|
|
898
|
+
void tableName; // Spanner's DROP INDEX does not take a table name.
|
|
899
|
+
await this.query(`DROP INDEX ${this.escapeId(indexName)}`);
|
|
900
|
+
}
|
|
901
|
+
async createIndex(tableName, indexDef) {
|
|
902
|
+
await this.addIndex(tableName, indexDef.name, indexDef.fields, {
|
|
903
|
+
unique: indexDef.unique,
|
|
904
|
+
where: indexDef.where,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
async createFulltextIndex() {
|
|
908
|
+
throw new Error('Cloud Spanner has no MySQL-style FULLTEXT index; use a SEARCH INDEX (GoogleSQL full-text search) via raw DDL instead.');
|
|
909
|
+
}
|
|
910
|
+
async createSpatialIndex() {
|
|
911
|
+
throw new Error('Cloud Spanner has no built-in spatial/geography index support.');
|
|
912
|
+
}
|
|
913
|
+
async dropIndex(tableName, indexName, options) {
|
|
914
|
+
void tableName;
|
|
915
|
+
try {
|
|
916
|
+
await this.query(`DROP INDEX ${this.escapeId(indexName)}`);
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
if (options?.ifExists)
|
|
920
|
+
return;
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
async createConstraint(tableName, constraintDef) {
|
|
925
|
+
await this.addConstraint(tableName, {
|
|
926
|
+
type: constraintDef.type,
|
|
927
|
+
fields: constraintDef.fields || [],
|
|
928
|
+
name: constraintDef.name,
|
|
929
|
+
references: constraintDef.references
|
|
930
|
+
? { table: constraintDef.references.table, fields: [constraintDef.references.field] }
|
|
931
|
+
: undefined,
|
|
932
|
+
check: constraintDef.check,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
async dropConstraint(tableName, constraintName, options) {
|
|
936
|
+
try {
|
|
937
|
+
await this.removeConstraint(tableName, constraintName);
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
if (options?.ifExists)
|
|
941
|
+
return;
|
|
942
|
+
throw error;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async changeOwner() {
|
|
946
|
+
throw new Error('Cloud Spanner has no per-object OWNER concept; access is governed by IAM roles/policies and database roles, not object ownership.');
|
|
947
|
+
}
|
|
948
|
+
async createForeignDataWrapper() {
|
|
949
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
950
|
+
}
|
|
951
|
+
async dropForeignDataWrapper() {
|
|
952
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
953
|
+
}
|
|
954
|
+
async createForeignServer() {
|
|
955
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
956
|
+
}
|
|
957
|
+
async dropForeignServer() {
|
|
958
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
959
|
+
}
|
|
960
|
+
async createForeignTable() {
|
|
961
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
962
|
+
}
|
|
963
|
+
buildCreateServerQuery(_name, _opts) {
|
|
964
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
965
|
+
}
|
|
966
|
+
buildAlterServerQuery(_name, _opts) {
|
|
967
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
968
|
+
}
|
|
969
|
+
buildDropServerQuery() {
|
|
970
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
971
|
+
}
|
|
972
|
+
buildCreateUserMappingQuery(_opts) {
|
|
973
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
974
|
+
}
|
|
975
|
+
buildAlterUserMappingQuery(_opts) {
|
|
976
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
977
|
+
}
|
|
978
|
+
buildDropUserMappingQuery() {
|
|
979
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
980
|
+
}
|
|
981
|
+
async createUserMapping() {
|
|
982
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
983
|
+
}
|
|
984
|
+
async dropUserMapping() {
|
|
985
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
986
|
+
}
|
|
987
|
+
buildCreateForeignTableQuery(_tableName, _opts) {
|
|
988
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
989
|
+
}
|
|
990
|
+
buildDropForeignTableQuery() {
|
|
991
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
992
|
+
}
|
|
993
|
+
buildImportForeignSchemaQuery(_remoteSchema, _serverName, _opts) {
|
|
994
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
995
|
+
}
|
|
996
|
+
getServersQuery() {
|
|
997
|
+
throw new Error(SpannerDialect.FDW_NOT_SUPPORTED);
|
|
998
|
+
}
|
|
999
|
+
buildCreateUserQuery() {
|
|
1000
|
+
throw new Error(SpannerDialect.USER_MGMT_NOT_SUPPORTED);
|
|
1001
|
+
}
|
|
1002
|
+
buildAlterUserQuery() {
|
|
1003
|
+
throw new Error(SpannerDialect.USER_MGMT_NOT_SUPPORTED);
|
|
1004
|
+
}
|
|
1005
|
+
buildDropUserQuery() {
|
|
1006
|
+
throw new Error(SpannerDialect.USER_MGMT_NOT_SUPPORTED);
|
|
1007
|
+
}
|
|
1008
|
+
getUsersQuery() {
|
|
1009
|
+
throw new Error(SpannerDialect.USER_MGMT_NOT_SUPPORTED);
|
|
1010
|
+
}
|
|
1011
|
+
buildGrantQuery() {
|
|
1012
|
+
throw new Error(SpannerDialect.USER_MGMT_NOT_SUPPORTED);
|
|
1013
|
+
}
|
|
1014
|
+
buildRevokeQuery() {
|
|
1015
|
+
throw new Error(SpannerDialect.USER_MGMT_NOT_SUPPORTED);
|
|
1016
|
+
}
|
|
1017
|
+
buildShowGrantsQuery() {
|
|
1018
|
+
throw new Error(SpannerDialect.USER_MGMT_NOT_SUPPORTED);
|
|
1019
|
+
}
|
|
1020
|
+
buildFlushPrivilegesQuery() {
|
|
1021
|
+
throw new Error(SpannerDialect.USER_MGMT_NOT_SUPPORTED);
|
|
1022
|
+
}
|
|
1023
|
+
buildCreateRoleQuery(roleName) {
|
|
1024
|
+
// Unlike the rest of this section, GoogleSQL *does* support database
|
|
1025
|
+
// roles - exposed here as a plain SQL builder (not backed by a
|
|
1026
|
+
// full grant/revoke-to-role implementation).
|
|
1027
|
+
return `CREATE ROLE ${this.escapeId(roleName)}`;
|
|
1028
|
+
}
|
|
1029
|
+
buildDropRoleQuery(roleName) {
|
|
1030
|
+
return `DROP ROLE ${this.escapeId(roleName)}`;
|
|
1031
|
+
}
|
|
1032
|
+
buildGrantRoleQuery(role, to) {
|
|
1033
|
+
return `GRANT ROLE ${this.escapeId(role)} TO ROLE ${this.escapeId(String(to))}`;
|
|
1034
|
+
}
|
|
1035
|
+
buildRevokeRoleQuery(role, from) {
|
|
1036
|
+
return `REVOKE ROLE ${this.escapeId(role)} FROM ROLE ${this.escapeId(String(from))}`;
|
|
1037
|
+
}
|
|
1038
|
+
getRolesQuery() {
|
|
1039
|
+
return `SELECT role_name FROM information_schema.enabled_roles`;
|
|
1040
|
+
}
|
|
1041
|
+
// =========================================================================
|
|
1042
|
+
// Row-level security - not supported (no RLS/policy concept in Spanner)
|
|
1043
|
+
// =========================================================================
|
|
1044
|
+
async createSecurityPolicy() {
|
|
1045
|
+
throw new Error('Cloud Spanner has no row-level security / policy concept; enforce row-level access in application code or via views.');
|
|
1046
|
+
}
|
|
1047
|
+
async dropSecurityPolicy() {
|
|
1048
|
+
throw new Error('Cloud Spanner has no row-level security / policy concept; enforce row-level access in application code or via views.');
|
|
1049
|
+
}
|
|
1050
|
+
async createStoredProcedure(_options) {
|
|
1051
|
+
throw new Error(SpannerDialect.PROC_NOT_SUPPORTED);
|
|
1052
|
+
}
|
|
1053
|
+
async createProcedure(options) {
|
|
1054
|
+
return this.createStoredProcedure(options);
|
|
1055
|
+
}
|
|
1056
|
+
async dropStoredProcedure(_procedureName, _options) {
|
|
1057
|
+
throw new Error(SpannerDialect.PROC_NOT_SUPPORTED);
|
|
1058
|
+
}
|
|
1059
|
+
async dropProcedure(procedureName, options) {
|
|
1060
|
+
return this.dropStoredProcedure(procedureName, options);
|
|
1061
|
+
}
|
|
1062
|
+
async executeStoredProcedure(_options) {
|
|
1063
|
+
throw new Error(SpannerDialect.PROC_NOT_SUPPORTED);
|
|
1064
|
+
}
|
|
1065
|
+
async hasStoredProcedure() {
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
// =========================================================================
|
|
1069
|
+
// Bit-reversed sequences (hotspot-safe ordered keys)
|
|
1070
|
+
// =========================================================================
|
|
1071
|
+
/**
|
|
1072
|
+
* Create a bit-reversed sequence: an ordered-but-shuffled numeric
|
|
1073
|
+
* generator (`CREATE SEQUENCE ... OPTIONS (sequence_kind =
|
|
1074
|
+
* 'bit_reversed_positive')`) that spreads generated values across the
|
|
1075
|
+
* keyspace instead of producing a monotonic run, avoiding the hotspotting
|
|
1076
|
+
* that a plain auto-increment counter would cause.
|
|
1077
|
+
*/
|
|
1078
|
+
async createBitReversedSequence(name, options) {
|
|
1079
|
+
let sql = `CREATE SEQUENCE ${this.escapeId(name)} OPTIONS (sequence_kind = 'bit_reversed_positive'`;
|
|
1080
|
+
if (options?.startWithCounter !== undefined) {
|
|
1081
|
+
sql += `, start_with_counter = ${options.startWithCounter}`;
|
|
1082
|
+
}
|
|
1083
|
+
if (options?.skipRangeMin !== undefined && options?.skipRangeMax !== undefined) {
|
|
1084
|
+
sql += `, skip_range_min = ${options.skipRangeMin}, skip_range_max = ${options.skipRangeMax}`;
|
|
1085
|
+
}
|
|
1086
|
+
sql += ')';
|
|
1087
|
+
await this.query(sql);
|
|
1088
|
+
}
|
|
1089
|
+
async dropSequence(name, options) {
|
|
1090
|
+
try {
|
|
1091
|
+
await this.query(`DROP SEQUENCE ${this.escapeId(name)}`);
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
if (options?.ifExists)
|
|
1095
|
+
return;
|
|
1096
|
+
throw error;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
/** SQL fragment to pull the next value from a bit-reversed sequence. */
|
|
1100
|
+
nextBitReversedSequenceValueSql(name) {
|
|
1101
|
+
return `GET_NEXT_SEQUENCE_VALUE(SEQUENCE ${this.escapeId(name)})`;
|
|
1102
|
+
}
|
|
1103
|
+
async nextBitReversedSequenceValue(name) {
|
|
1104
|
+
const result = await this.query(`SELECT ${this.nextBitReversedSequenceValueSql(name)} AS next_val`);
|
|
1105
|
+
return Number(result.rows[0]?.next_val);
|
|
1106
|
+
}
|
|
1107
|
+
// =========================================================================
|
|
1108
|
+
// Transactions
|
|
1109
|
+
// =========================================================================
|
|
1110
|
+
/**
|
|
1111
|
+
* Start a read-write transaction.
|
|
1112
|
+
*
|
|
1113
|
+
* See the file header: Spanner read-write transactions have no
|
|
1114
|
+
* configurable isolation level - they are always externally consistent by
|
|
1115
|
+
* construction. `options.isolationLevel` is accepted for interface parity
|
|
1116
|
+
* with every other dialect but is ignored (with a warning) rather than
|
|
1117
|
+
* silently mapped to a Spanner concept it doesn't correspond to.
|
|
1118
|
+
*
|
|
1119
|
+
* Implementation note: `database.runTransaction()` is callback-based
|
|
1120
|
+
* (`(err, transaction) => ...`) and keeps the transaction lease open only
|
|
1121
|
+
* as long as the promise returned from that callback is pending. To
|
|
1122
|
+
* expose the imperative `startTransaction()`/`commitTransaction()`/
|
|
1123
|
+
* `rollbackTransaction()` shape used elsewhere in this codebase, this
|
|
1124
|
+
* method opens the transaction, hands the driver a promise that resolves
|
|
1125
|
+
* only once `commitTransaction()`/`rollbackTransaction()` is called (see
|
|
1126
|
+
* `pendingTx`), and returns immediately with the transaction handle.
|
|
1127
|
+
*/
|
|
1128
|
+
async startTransaction(options) {
|
|
1129
|
+
if (!this.database) {
|
|
1130
|
+
throw new Error('Not connected to database');
|
|
1131
|
+
}
|
|
1132
|
+
if (options?.isolationLevel) {
|
|
1133
|
+
console.warn(`Spanner: isolation level "${options.isolationLevel}" was requested, but Cloud Spanner read-write ` +
|
|
1134
|
+
'transactions have no configurable isolation level - they are always externally consistent ' +
|
|
1135
|
+
'(TrueTime-ordered, effectively stronger than SERIALIZABLE) by construction. Ignoring.');
|
|
1136
|
+
}
|
|
1137
|
+
let resolveTxn;
|
|
1138
|
+
let rejectTxn;
|
|
1139
|
+
const txnReady = new Promise((resolve, reject) => {
|
|
1140
|
+
resolveTxn = resolve;
|
|
1141
|
+
rejectTxn = reject;
|
|
1142
|
+
});
|
|
1143
|
+
let resolveDone;
|
|
1144
|
+
const done = new Promise((resolve) => {
|
|
1145
|
+
resolveDone = resolve;
|
|
1146
|
+
});
|
|
1147
|
+
this.database.runTransaction((err, txn) => {
|
|
1148
|
+
if (err) {
|
|
1149
|
+
rejectTxn(err);
|
|
1150
|
+
return undefined;
|
|
1151
|
+
}
|
|
1152
|
+
resolveTxn(txn);
|
|
1153
|
+
// The driver awaits this returned promise before finalizing/retrying
|
|
1154
|
+
// the transaction attempt; it resolves once commitTransaction()/
|
|
1155
|
+
// rollbackTransaction() signals completion via `pendingTx`.
|
|
1156
|
+
return done;
|
|
1157
|
+
});
|
|
1158
|
+
const spannerTxn = await txnReady;
|
|
1159
|
+
const transaction = new SpannerTransaction(spannerTxn, options);
|
|
1160
|
+
this.pendingTx.set(transaction.id, resolveDone);
|
|
1161
|
+
return transaction;
|
|
1162
|
+
}
|
|
1163
|
+
async commitTransaction(transaction) {
|
|
1164
|
+
const tx = transaction;
|
|
1165
|
+
await tx.spannerTransaction.commit();
|
|
1166
|
+
this.pendingTx.get(tx.id)?.();
|
|
1167
|
+
this.pendingTx.delete(tx.id);
|
|
1168
|
+
tx.finished = true;
|
|
1169
|
+
}
|
|
1170
|
+
async rollbackTransaction(transaction) {
|
|
1171
|
+
const tx = transaction;
|
|
1172
|
+
if (typeof tx.spannerTransaction.rollback === 'function') {
|
|
1173
|
+
await tx.spannerTransaction.rollback();
|
|
1174
|
+
}
|
|
1175
|
+
this.pendingTx.get(tx.id)?.();
|
|
1176
|
+
this.pendingTx.delete(tx.id);
|
|
1177
|
+
tx.finished = true;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Run a read-only transaction (strong read, or a stale/bounded-staleness
|
|
1181
|
+
* read for lower latency at the cost of recency). This is a distinct
|
|
1182
|
+
* concept from a read-write transaction in Spanner - it takes a
|
|
1183
|
+
* consistent snapshot and never participates in commit/abort/retry - so
|
|
1184
|
+
* it is exposed as its own method rather than forced through
|
|
1185
|
+
* `startTransaction()`.
|
|
1186
|
+
*/
|
|
1187
|
+
async runReadOnlyTransaction(fn, options) {
|
|
1188
|
+
if (!this.database)
|
|
1189
|
+
throw new Error('Not connected to database');
|
|
1190
|
+
const [snapshot] = await this.database.getSnapshot(options || { strong: true });
|
|
1191
|
+
try {
|
|
1192
|
+
const run = async (sql, params) => {
|
|
1193
|
+
const [rows] = await snapshot.run({ sql, params, json: true });
|
|
1194
|
+
return rows;
|
|
1195
|
+
};
|
|
1196
|
+
return await fn(run);
|
|
1197
|
+
}
|
|
1198
|
+
finally {
|
|
1199
|
+
snapshot.end();
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
// =========================================================================
|
|
1203
|
+
// Backup / restore (Database Admin API)
|
|
1204
|
+
// =========================================================================
|
|
1205
|
+
//
|
|
1206
|
+
// Cloud Spanner backups are a Database Admin API concept, not SQL - there
|
|
1207
|
+
// is no `BACKUP DATABASE` DDL statement the way some other engines have.
|
|
1208
|
+
// These methods wrap `DatabaseAdminClient#createBackup/getBackup/
|
|
1209
|
+
// listBackups/deleteBackup/restoreDatabase` (verified against
|
|
1210
|
+
// `@google-cloud/spanner`'s `v1.DatabaseAdminClient` type definitions -
|
|
1211
|
+
// `node_modules/@google-cloud/spanner/build/src/v1/database_admin_client.d.ts`
|
|
1212
|
+
// - rather than guessed). `createBackup`/`restoreDatabase` both return
|
|
1213
|
+
// long-running operations; these methods await `operation.promise()` so
|
|
1214
|
+
// callers get the finished `Backup`/`Database` resource back directly,
|
|
1215
|
+
// matching the synchronous, promise-returning shape used elsewhere in this
|
|
1216
|
+
// dialect (`runDDL()` does the same for `updateSchema()`).
|
|
1217
|
+
/** Resolve the project ID used to build Admin API resource paths. */
|
|
1218
|
+
getProjectId() {
|
|
1219
|
+
return this.config.projectId || this.spannerClient?.projectId;
|
|
1220
|
+
}
|
|
1221
|
+
getDatabaseAdminClient() {
|
|
1222
|
+
if (!this.spannerClient) {
|
|
1223
|
+
throw new Error('Not connected to database');
|
|
1224
|
+
}
|
|
1225
|
+
return this.spannerClient.getDatabaseAdminClient();
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Start a backup of `databaseId` (defaults to this dialect's configured
|
|
1229
|
+
* database), storing it as `backupId` with the given expiration time.
|
|
1230
|
+
* Returns the completed `Backup` resource once the long-running operation
|
|
1231
|
+
* finishes (`Backup.state` will be `READY`).
|
|
1232
|
+
*/
|
|
1233
|
+
async createBackup(backupId, expireTime, databaseId) {
|
|
1234
|
+
const adminClient = this.getDatabaseAdminClient();
|
|
1235
|
+
const projectId = this.getProjectId();
|
|
1236
|
+
const instanceId = this.config.instanceId;
|
|
1237
|
+
const dbId = databaseId || this.config.databaseId;
|
|
1238
|
+
const [operation] = await adminClient.createBackup({
|
|
1239
|
+
parent: adminClient.instancePath(projectId, instanceId),
|
|
1240
|
+
backupId,
|
|
1241
|
+
backup: {
|
|
1242
|
+
database: adminClient.databasePath(projectId, instanceId, dbId),
|
|
1243
|
+
expireTime: { seconds: Math.floor(expireTime.getTime() / 1000), nanos: 0 },
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1246
|
+
const [backup] = await operation.promise();
|
|
1247
|
+
return backup;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* List backups in this dialect's instance. `filter` is a Cloud Spanner
|
|
1251
|
+
* backup filter expression (e.g. `state:READY`,
|
|
1252
|
+
* `database:my-db`) - see `DatabaseAdminClient#listBackups()`'s
|
|
1253
|
+
* `request.filter` for the supported grammar; omit for all backups in the
|
|
1254
|
+
* instance.
|
|
1255
|
+
*/
|
|
1256
|
+
async listBackups(filter) {
|
|
1257
|
+
const adminClient = this.getDatabaseAdminClient();
|
|
1258
|
+
const projectId = this.getProjectId();
|
|
1259
|
+
const [backups] = await adminClient.listBackups({
|
|
1260
|
+
parent: adminClient.instancePath(projectId, this.config.instanceId),
|
|
1261
|
+
filter,
|
|
1262
|
+
});
|
|
1263
|
+
return backups;
|
|
1264
|
+
}
|
|
1265
|
+
/** Fetch a single backup's metadata by ID. */
|
|
1266
|
+
async getBackup(backupId) {
|
|
1267
|
+
const adminClient = this.getDatabaseAdminClient();
|
|
1268
|
+
const projectId = this.getProjectId();
|
|
1269
|
+
const [backup] = await adminClient.getBackup({
|
|
1270
|
+
name: adminClient.backupPath(projectId, this.config.instanceId, backupId),
|
|
1271
|
+
});
|
|
1272
|
+
return backup;
|
|
1273
|
+
}
|
|
1274
|
+
/** Delete a backup by ID. */
|
|
1275
|
+
async deleteBackup(backupId) {
|
|
1276
|
+
const adminClient = this.getDatabaseAdminClient();
|
|
1277
|
+
const projectId = this.getProjectId();
|
|
1278
|
+
await adminClient.deleteBackup({
|
|
1279
|
+
name: adminClient.backupPath(projectId, this.config.instanceId, backupId),
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Restore `backupId` into a brand-new database `newDatabaseId` in this
|
|
1284
|
+
* dialect's instance. The new database must not already exist, and must
|
|
1285
|
+
* be created in an instance with the same instance configuration as the
|
|
1286
|
+
* one holding the backup. Returns the completed `Database` resource once
|
|
1287
|
+
* the long-running restore operation finishes.
|
|
1288
|
+
*/
|
|
1289
|
+
async restoreDatabase(backupId, newDatabaseId) {
|
|
1290
|
+
const adminClient = this.getDatabaseAdminClient();
|
|
1291
|
+
const projectId = this.getProjectId();
|
|
1292
|
+
const instanceId = this.config.instanceId;
|
|
1293
|
+
const [operation] = await adminClient.restoreDatabase({
|
|
1294
|
+
parent: adminClient.instancePath(projectId, instanceId),
|
|
1295
|
+
databaseId: newDatabaseId,
|
|
1296
|
+
backup: adminClient.backupPath(projectId, instanceId, backupId),
|
|
1297
|
+
});
|
|
1298
|
+
const [database] = await operation.promise();
|
|
1299
|
+
return database;
|
|
1300
|
+
}
|
|
1301
|
+
// =========================================================================
|
|
1302
|
+
// Data types
|
|
1303
|
+
// =========================================================================
|
|
1304
|
+
getDataTypeSql(dataType) {
|
|
1305
|
+
if (typeof dataType === 'string')
|
|
1306
|
+
return dataType;
|
|
1307
|
+
if (!dataType || typeof dataType !== 'object')
|
|
1308
|
+
return 'STRING(MAX)';
|
|
1309
|
+
const dt = dataType;
|
|
1310
|
+
switch (dt.key) {
|
|
1311
|
+
case 'STRING':
|
|
1312
|
+
case 'CHAR':
|
|
1313
|
+
return `STRING(${dt.length || 255})`;
|
|
1314
|
+
case 'TEXT':
|
|
1315
|
+
return 'STRING(MAX)';
|
|
1316
|
+
case 'NUMBER':
|
|
1317
|
+
case 'INTEGER':
|
|
1318
|
+
case 'BIGINT':
|
|
1319
|
+
return 'INT64';
|
|
1320
|
+
case 'FLOAT':
|
|
1321
|
+
case 'DOUBLE':
|
|
1322
|
+
return 'FLOAT64';
|
|
1323
|
+
case 'DECIMAL':
|
|
1324
|
+
return 'NUMERIC';
|
|
1325
|
+
case 'BOOLEAN':
|
|
1326
|
+
return 'BOOL';
|
|
1327
|
+
case 'DATE':
|
|
1328
|
+
return 'TIMESTAMP';
|
|
1329
|
+
case 'DATEONLY':
|
|
1330
|
+
return 'DATE';
|
|
1331
|
+
case 'TIME':
|
|
1332
|
+
console.warn('Spanner: no native TIME type; mapping to STRING(MAX).');
|
|
1333
|
+
return 'STRING(MAX)';
|
|
1334
|
+
case 'BLOB':
|
|
1335
|
+
return `BYTES(${dt.length || 'MAX'})`;
|
|
1336
|
+
case 'UUID':
|
|
1337
|
+
return 'STRING(36)';
|
|
1338
|
+
case 'JSON':
|
|
1339
|
+
case 'JSONB':
|
|
1340
|
+
return 'JSON';
|
|
1341
|
+
case 'ENUM': {
|
|
1342
|
+
const values = dt.values || [];
|
|
1343
|
+
const checkValues = values.map((v) => `'${this.escapeString(v)}'`).join(', ');
|
|
1344
|
+
return `STRING(MAX)${values.length ? ` CHECK (%%COLUMN%% IN (${checkValues}))` : ''}`;
|
|
1345
|
+
}
|
|
1346
|
+
case 'ARRAY': {
|
|
1347
|
+
// Support both 'type' (DataTypes.ARRAY(DataTypes.INTEGER)) and
|
|
1348
|
+
// 'subtype' (a plain { type: 'ARRAY', subtype: 'INTEGER' } literal),
|
|
1349
|
+
// mirroring the postgres/cockroachdb dialects' ARRAY handling.
|
|
1350
|
+
const elementType = dt.type ?? dt.subtype;
|
|
1351
|
+
const elementSql = elementType ? this.getDataTypeSql(elementType) : 'STRING(MAX)';
|
|
1352
|
+
return `ARRAY<${elementSql}>`;
|
|
1353
|
+
}
|
|
1354
|
+
case 'HSTORE':
|
|
1355
|
+
return 'JSON';
|
|
1356
|
+
case 'GEOMETRY':
|
|
1357
|
+
throw new Error('Cloud Spanner has no built-in geometry/geography type.');
|
|
1358
|
+
case 'SET':
|
|
1359
|
+
case 'RANGE':
|
|
1360
|
+
case 'INET':
|
|
1361
|
+
case 'CIDR':
|
|
1362
|
+
case 'MACADDR':
|
|
1363
|
+
console.warn(`Spanner: no native "${dt.key}" type; mapping to STRING(MAX).`);
|
|
1364
|
+
return 'STRING(MAX)';
|
|
1365
|
+
default:
|
|
1366
|
+
return 'STRING(MAX)';
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
// =========================================================================
|
|
1370
|
+
// Query building
|
|
1371
|
+
// =========================================================================
|
|
1372
|
+
buildWhereClause(where, _options) {
|
|
1373
|
+
const values = [];
|
|
1374
|
+
if (!where || Object.keys(where).length === 0) {
|
|
1375
|
+
return { sql: '', values };
|
|
1376
|
+
}
|
|
1377
|
+
const nextParam = () => `@p${values.length}`;
|
|
1378
|
+
const buildCondition = (condition) => {
|
|
1379
|
+
if (!condition)
|
|
1380
|
+
return '';
|
|
1381
|
+
const cond = condition;
|
|
1382
|
+
if (cond.$and || cond.$or || cond.$not) {
|
|
1383
|
+
const parts = [];
|
|
1384
|
+
if (cond.$and)
|
|
1385
|
+
parts.push(`(${cond.$and.map(buildCondition).join(' AND ')})`);
|
|
1386
|
+
if (cond.$or)
|
|
1387
|
+
parts.push(`(${cond.$or.map(buildCondition).join(' OR ')})`);
|
|
1388
|
+
if (cond.$not)
|
|
1389
|
+
parts.push(`NOT (${buildCondition(cond.$not)})`);
|
|
1390
|
+
return parts.join(' AND ');
|
|
1391
|
+
}
|
|
1392
|
+
const fieldConditions = [];
|
|
1393
|
+
for (const [key, value] of Object.entries(cond)) {
|
|
1394
|
+
if (key.startsWith('$'))
|
|
1395
|
+
continue;
|
|
1396
|
+
const col = this.escapeId(key);
|
|
1397
|
+
if (value && typeof value === 'object' && !(value instanceof Date) && !Array.isArray(value)) {
|
|
1398
|
+
const v = value;
|
|
1399
|
+
if (v.$eq !== undefined) {
|
|
1400
|
+
values.push(v.$eq);
|
|
1401
|
+
fieldConditions.push(`${col} = ${nextParam()}`);
|
|
1402
|
+
}
|
|
1403
|
+
else if (v.$ne !== undefined) {
|
|
1404
|
+
values.push(v.$ne);
|
|
1405
|
+
fieldConditions.push(`${col} != ${nextParam()}`);
|
|
1406
|
+
}
|
|
1407
|
+
else if (v.$gt !== undefined) {
|
|
1408
|
+
values.push(v.$gt);
|
|
1409
|
+
fieldConditions.push(`${col} > ${nextParam()}`);
|
|
1410
|
+
}
|
|
1411
|
+
else if (v.$gte !== undefined) {
|
|
1412
|
+
values.push(v.$gte);
|
|
1413
|
+
fieldConditions.push(`${col} >= ${nextParam()}`);
|
|
1414
|
+
}
|
|
1415
|
+
else if (v.$lt !== undefined) {
|
|
1416
|
+
values.push(v.$lt);
|
|
1417
|
+
fieldConditions.push(`${col} < ${nextParam()}`);
|
|
1418
|
+
}
|
|
1419
|
+
else if (v.$lte !== undefined) {
|
|
1420
|
+
values.push(v.$lte);
|
|
1421
|
+
fieldConditions.push(`${col} <= ${nextParam()}`);
|
|
1422
|
+
}
|
|
1423
|
+
else if (v.$like !== undefined) {
|
|
1424
|
+
values.push(v.$like);
|
|
1425
|
+
fieldConditions.push(`${col} LIKE ${nextParam()}`);
|
|
1426
|
+
}
|
|
1427
|
+
else if (v.$notLike !== undefined) {
|
|
1428
|
+
values.push(v.$notLike);
|
|
1429
|
+
fieldConditions.push(`${col} NOT LIKE ${nextParam()}`);
|
|
1430
|
+
}
|
|
1431
|
+
else if (v.$iLike !== undefined) {
|
|
1432
|
+
// GoogleSQL has no ILIKE; approximate case-insensitive LIKE via LOWER().
|
|
1433
|
+
values.push(String(v.$iLike).toLowerCase());
|
|
1434
|
+
fieldConditions.push(`LOWER(${col}) LIKE ${nextParam()}`);
|
|
1435
|
+
}
|
|
1436
|
+
else if (v.$notILike !== undefined) {
|
|
1437
|
+
values.push(String(v.$notILike).toLowerCase());
|
|
1438
|
+
fieldConditions.push(`LOWER(${col}) NOT LIKE ${nextParam()}`);
|
|
1439
|
+
}
|
|
1440
|
+
else if (v.$in) {
|
|
1441
|
+
const inValues = v.$in;
|
|
1442
|
+
const placeholders = inValues.map((val) => {
|
|
1443
|
+
values.push(val);
|
|
1444
|
+
return nextParam();
|
|
1445
|
+
});
|
|
1446
|
+
fieldConditions.push(`${col} IN (${placeholders.join(', ')})`);
|
|
1447
|
+
}
|
|
1448
|
+
else if (v.$notIn) {
|
|
1449
|
+
const notInValues = v.$notIn;
|
|
1450
|
+
const placeholders = notInValues.map((val) => {
|
|
1451
|
+
values.push(val);
|
|
1452
|
+
return nextParam();
|
|
1453
|
+
});
|
|
1454
|
+
fieldConditions.push(`${col} NOT IN (${placeholders.join(', ')})`);
|
|
1455
|
+
}
|
|
1456
|
+
else if (v.$between) {
|
|
1457
|
+
const [lo, hi] = v.$between;
|
|
1458
|
+
values.push(lo);
|
|
1459
|
+
const p1 = nextParam();
|
|
1460
|
+
values.push(hi);
|
|
1461
|
+
const p2 = nextParam();
|
|
1462
|
+
fieldConditions.push(`${col} BETWEEN ${p1} AND ${p2}`);
|
|
1463
|
+
}
|
|
1464
|
+
else if (v.$notBetween) {
|
|
1465
|
+
const [lo, hi] = v.$notBetween;
|
|
1466
|
+
values.push(lo);
|
|
1467
|
+
const p1 = nextParam();
|
|
1468
|
+
values.push(hi);
|
|
1469
|
+
const p2 = nextParam();
|
|
1470
|
+
fieldConditions.push(`${col} NOT BETWEEN ${p1} AND ${p2}`);
|
|
1471
|
+
}
|
|
1472
|
+
else if (v.$isNull !== undefined) {
|
|
1473
|
+
fieldConditions.push(`${col} IS ${v.$isNull ? '' : 'NOT '}NULL`);
|
|
1474
|
+
}
|
|
1475
|
+
else {
|
|
1476
|
+
values.push(value);
|
|
1477
|
+
fieldConditions.push(`${col} = ${nextParam()}`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
else if (value === null) {
|
|
1481
|
+
fieldConditions.push(`${col} IS NULL`);
|
|
1482
|
+
}
|
|
1483
|
+
else {
|
|
1484
|
+
values.push(value);
|
|
1485
|
+
fieldConditions.push(`${col} = ${nextParam()}`);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return fieldConditions.join(' AND ');
|
|
1489
|
+
};
|
|
1490
|
+
const sql = buildCondition(where);
|
|
1491
|
+
return { sql, values };
|
|
1492
|
+
}
|
|
1493
|
+
buildOrderClause(order) {
|
|
1494
|
+
const orderArray = order;
|
|
1495
|
+
if (!orderArray || !Array.isArray(orderArray) || orderArray.length === 0)
|
|
1496
|
+
return '';
|
|
1497
|
+
const parts = orderArray.map((item) => {
|
|
1498
|
+
if (Array.isArray(item)) {
|
|
1499
|
+
const field = typeof item[0] === 'string' ? this.escapeId(item[0]) : item[0];
|
|
1500
|
+
const direction = item[1] ? ` ${item[1]}` : '';
|
|
1501
|
+
return `${field}${direction}`;
|
|
1502
|
+
}
|
|
1503
|
+
if (typeof item === 'string')
|
|
1504
|
+
return this.escapeId(item);
|
|
1505
|
+
return String(item);
|
|
1506
|
+
});
|
|
1507
|
+
return `ORDER BY ${parts.join(', ')}`;
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Build a `LIMIT`/`OFFSET` clause. GoogleSQL requires `LIMIT` whenever
|
|
1511
|
+
* `OFFSET` is used (there's no bare `OFFSET n` the way Postgres allows) -
|
|
1512
|
+
* when only an offset is given, this emits a very large LIMIT to satisfy
|
|
1513
|
+
* that requirement.
|
|
1514
|
+
*/
|
|
1515
|
+
buildLimitOffset(limit, offset) {
|
|
1516
|
+
let sql = '';
|
|
1517
|
+
if (limit !== undefined) {
|
|
1518
|
+
sql += ` LIMIT ${Number(limit)}`;
|
|
1519
|
+
if (offset !== undefined)
|
|
1520
|
+
sql += ` OFFSET ${Number(offset)}`;
|
|
1521
|
+
}
|
|
1522
|
+
else if (offset !== undefined) {
|
|
1523
|
+
sql += ` LIMIT ${Number.MAX_SAFE_INTEGER} OFFSET ${Number(offset)}`;
|
|
1524
|
+
}
|
|
1525
|
+
return sql;
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Build an INSERT statement. When `options.upsert` is set, emits
|
|
1529
|
+
* GoogleSQL's `INSERT OR UPDATE INTO` upsert DML (verified: Spanner has no
|
|
1530
|
+
* `ON CONFLICT` clause; GoogleSQL's DML upsert extension also offers
|
|
1531
|
+
* `INSERT OR IGNORE` / `INSERT OR REPLACE` conflict-action variants, but
|
|
1532
|
+
* only `OR UPDATE` is wired up here - there is no `conflictAction` option
|
|
1533
|
+
* to select the other two). For bulk/blind writes prefer
|
|
1534
|
+
* `mutationInsert()`/`mutationUpsert()` (the Mutation API) instead - see
|
|
1535
|
+
* the file header.
|
|
1536
|
+
*/
|
|
1537
|
+
buildInsertQuery(tableName, values, options) {
|
|
1538
|
+
const columns = Object.keys(values);
|
|
1539
|
+
const processedValues = [];
|
|
1540
|
+
const placeholders = [];
|
|
1541
|
+
for (const value of Object.values(values)) {
|
|
1542
|
+
if (value instanceof prorm_1.Literal) {
|
|
1543
|
+
placeholders.push(value.val);
|
|
1544
|
+
}
|
|
1545
|
+
else {
|
|
1546
|
+
processedValues.push(value);
|
|
1547
|
+
placeholders.push(`@p${processedValues.length}`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
const verb = options?.upsert ? 'INSERT OR UPDATE INTO' : 'INSERT INTO';
|
|
1551
|
+
const sql = `${verb} ${this.escapeId(tableName)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${placeholders.join(', ')})`;
|
|
1552
|
+
if (options?.returning) {
|
|
1553
|
+
console.warn('Spanner: GoogleSQL DML has no RETURNING clause; ignoring options.returning.');
|
|
1554
|
+
}
|
|
1555
|
+
return { sql, values: processedValues };
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Build an upsert statement using GoogleSQL's `INSERT OR UPDATE INTO`.
|
|
1559
|
+
* Note this is a *SQL DML* fallback for interface parity - the primary,
|
|
1560
|
+
* recommended mechanism for upserts on this dialect is the Mutation API
|
|
1561
|
+
* (`mutationUpsert()` / `database.table(name).upsert(rows)`); see the file
|
|
1562
|
+
* header for why.
|
|
1563
|
+
*/
|
|
1564
|
+
buildUpsertQuery(tableName, values, options) {
|
|
1565
|
+
void options;
|
|
1566
|
+
return this.buildInsertQuery(tableName, values, { upsert: true });
|
|
1567
|
+
}
|
|
1568
|
+
buildIncrementQuery(tableName, fields, where, options) {
|
|
1569
|
+
const by = options?.by ?? 1;
|
|
1570
|
+
const setClauses = [];
|
|
1571
|
+
const queryValues = [];
|
|
1572
|
+
const pushField = (field, amount) => {
|
|
1573
|
+
queryValues.push(amount);
|
|
1574
|
+
setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + @p${queryValues.length}`);
|
|
1575
|
+
};
|
|
1576
|
+
if (typeof fields === 'string') {
|
|
1577
|
+
pushField(fields, by);
|
|
1578
|
+
}
|
|
1579
|
+
else if (Array.isArray(fields)) {
|
|
1580
|
+
fields.forEach((field) => pushField(field, by));
|
|
1581
|
+
}
|
|
1582
|
+
else {
|
|
1583
|
+
for (const [field, amount] of Object.entries(fields))
|
|
1584
|
+
pushField(field, amount);
|
|
1585
|
+
}
|
|
1586
|
+
const whereClause = this.buildWhereClause(where);
|
|
1587
|
+
const offset = queryValues.length;
|
|
1588
|
+
const reindexedWhereSql = whereClause.sql.replace(/@p(\d+)/g, (_, n) => `@p${offset + Number(n)}`);
|
|
1589
|
+
queryValues.push(...whereClause.values);
|
|
1590
|
+
let sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')}`;
|
|
1591
|
+
sql += ` WHERE ${reindexedWhereSql || 'TRUE'}`;
|
|
1592
|
+
return { sql, values: queryValues };
|
|
1593
|
+
}
|
|
1594
|
+
buildUpdateQuery(tableName, values, where, options) {
|
|
1595
|
+
const setClauses = [];
|
|
1596
|
+
const queryValues = [];
|
|
1597
|
+
for (const [key, value] of Object.entries(values)) {
|
|
1598
|
+
if (value instanceof prorm_1.Literal) {
|
|
1599
|
+
setClauses.push(`${this.escapeId(key)} = ${value.val}`);
|
|
1600
|
+
}
|
|
1601
|
+
else {
|
|
1602
|
+
queryValues.push(value);
|
|
1603
|
+
setClauses.push(`${this.escapeId(key)} = @p${queryValues.length}`);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
const whereClause = this.buildWhereClause(where);
|
|
1607
|
+
const offset = queryValues.length;
|
|
1608
|
+
const reindexedWhereSql = whereClause.sql.replace(/@p(\d+)/g, (_, n) => `@p${offset + Number(n)}`);
|
|
1609
|
+
queryValues.push(...whereClause.values);
|
|
1610
|
+
let sql = `UPDATE ${this.escapeId(tableName)} SET ${setClauses.join(', ')}`;
|
|
1611
|
+
// GoogleSQL UPDATE requires a WHERE clause (no unconditional UPDATE).
|
|
1612
|
+
sql += ` WHERE ${reindexedWhereSql || 'TRUE'}`;
|
|
1613
|
+
if (options?.limit) {
|
|
1614
|
+
console.warn('Spanner: UPDATE has no LIMIT clause; ignoring options.limit.');
|
|
1615
|
+
}
|
|
1616
|
+
if (options?.returning) {
|
|
1617
|
+
console.warn('Spanner: GoogleSQL DML has no RETURNING clause; ignoring options.returning.');
|
|
1618
|
+
}
|
|
1619
|
+
return { sql, values: queryValues };
|
|
1620
|
+
}
|
|
1621
|
+
buildDeleteQuery(tableName, where, options) {
|
|
1622
|
+
if (options?.truncate) {
|
|
1623
|
+
// GoogleSQL has no TRUNCATE; DELETE with an always-true predicate is
|
|
1624
|
+
// the standard workaround (Spanner DELETE also requires a WHERE clause).
|
|
1625
|
+
return { sql: `DELETE FROM ${this.escapeId(tableName)} WHERE TRUE`, values: [] };
|
|
1626
|
+
}
|
|
1627
|
+
const whereClause = this.buildWhereClause(where);
|
|
1628
|
+
let sql = `DELETE FROM ${this.escapeId(tableName)} WHERE ${whereClause.sql || 'TRUE'}`;
|
|
1629
|
+
if (options?.limit) {
|
|
1630
|
+
console.warn('Spanner: DELETE has no LIMIT clause; ignoring options.limit.');
|
|
1631
|
+
}
|
|
1632
|
+
if (options?.returning) {
|
|
1633
|
+
console.warn('Spanner: GoogleSQL DML has no RETURNING clause; ignoring options.returning.');
|
|
1634
|
+
}
|
|
1635
|
+
return { sql, values: whereClause.values };
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Build a SELECT statement. GoogleSQL supports standard window functions
|
|
1639
|
+
* (`OVER (...)`) and CTEs (including `WITH RECURSIVE`) natively as part of
|
|
1640
|
+
* its query syntax; pass them straight through as raw SQL fragments via
|
|
1641
|
+
* `Literal`/`attributes` (window functions) or `buildRecursiveCteQuery()`
|
|
1642
|
+
* (CTEs) - no special dialect-layer plumbing is required beyond what's
|
|
1643
|
+
* already here, since both are just SELECT-query syntax GoogleSQL parses
|
|
1644
|
+
* natively.
|
|
1645
|
+
*/
|
|
1646
|
+
buildSelectQuery(options) {
|
|
1647
|
+
const values = [];
|
|
1648
|
+
let attributesSql = '*';
|
|
1649
|
+
if (options.attributes) {
|
|
1650
|
+
if (Array.isArray(options.attributes)) {
|
|
1651
|
+
attributesSql = options.attributes
|
|
1652
|
+
.map((attr) => (typeof attr === 'string' ? this.escapeId(attr) : String(attr)))
|
|
1653
|
+
.join(', ');
|
|
1654
|
+
}
|
|
1655
|
+
else {
|
|
1656
|
+
const exclude = new Set(options.attributes.exclude || []);
|
|
1657
|
+
const include = options.attributes.include || [];
|
|
1658
|
+
attributesSql = include.length
|
|
1659
|
+
? include.map((a) => this.escapeId(a)).join(', ')
|
|
1660
|
+
: Array.from(exclude).length
|
|
1661
|
+
? '*'
|
|
1662
|
+
: '*';
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
if (options.distinct) {
|
|
1666
|
+
attributesSql = `DISTINCT ${attributesSql}`;
|
|
1667
|
+
}
|
|
1668
|
+
const tableSql = options.schema
|
|
1669
|
+
? this.quoteTable(options.tableName, options.schema)
|
|
1670
|
+
: this.escapeId(options.tableName);
|
|
1671
|
+
let sql = `SELECT ${attributesSql} FROM ${tableSql}`;
|
|
1672
|
+
if (options.where) {
|
|
1673
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
1674
|
+
if (whereClause.sql) {
|
|
1675
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1676
|
+
values.push(...whereClause.values);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
if (options.group) {
|
|
1680
|
+
const groupFields = Array.isArray(options.group) ? options.group : [options.group];
|
|
1681
|
+
sql += ` GROUP BY ${groupFields.map((f) => this.escapeId(f)).join(', ')}`;
|
|
1682
|
+
}
|
|
1683
|
+
if (options.having) {
|
|
1684
|
+
const offset = values.length;
|
|
1685
|
+
const havingClause = this.buildWhereClause(options.having);
|
|
1686
|
+
const reindexed = havingClause.sql.replace(/@p(\d+)/g, (_, n) => `@p${offset + Number(n)}`);
|
|
1687
|
+
sql += ` HAVING ${reindexed}`;
|
|
1688
|
+
values.push(...havingClause.values);
|
|
1689
|
+
}
|
|
1690
|
+
if (options.order) {
|
|
1691
|
+
const orderClause = this.buildOrderClause(options.order);
|
|
1692
|
+
if (orderClause)
|
|
1693
|
+
sql += ` ${orderClause}`;
|
|
1694
|
+
}
|
|
1695
|
+
sql += this.buildLimitOffset(options.limit, options.offset);
|
|
1696
|
+
if (options.union && options.union.length) {
|
|
1697
|
+
const unionType = options.unionType || 'UNION';
|
|
1698
|
+
for (const unionQuery of options.union) {
|
|
1699
|
+
sql += ` ${unionType} `;
|
|
1700
|
+
const subOptions = {
|
|
1701
|
+
tableName: unionQuery.model?.tableName || options.tableName,
|
|
1702
|
+
attributes: unionQuery.attributes,
|
|
1703
|
+
where: unionQuery.where,
|
|
1704
|
+
order: unionQuery.order,
|
|
1705
|
+
limit: unionQuery.limit,
|
|
1706
|
+
offset: unionQuery.offset,
|
|
1707
|
+
};
|
|
1708
|
+
const offset = values.length;
|
|
1709
|
+
const sub = this.buildSelectQuery(subOptions);
|
|
1710
|
+
const reindexed = sub.sql.replace(/@p(\d+)/g, (_, n) => `@p${offset + Number(n)}`);
|
|
1711
|
+
sql += reindexed;
|
|
1712
|
+
values.push(...sub.values);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return { sql, values };
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Build a (optionally recursive) CTE-prefixed query -
|
|
1719
|
+
* `WITH [RECURSIVE] name AS (...), ... <mainQuery>` - which GoogleSQL
|
|
1720
|
+
* supports natively, including recursive CTEs (`WITH RECURSIVE`).
|
|
1721
|
+
*/
|
|
1722
|
+
buildRecursiveCteQuery(ctes, mainQuery) {
|
|
1723
|
+
const recursive = ctes.some((c) => c.recursive);
|
|
1724
|
+
const cteSql = ctes.map((c) => `${this.escapeId(c.name)} AS (${c.query})`).join(', ');
|
|
1725
|
+
return `WITH ${recursive ? 'RECURSIVE ' : ''}${cteSql} ${mainQuery}`;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Build a `ROW_NUMBER()`/`RANK()`/`DENSE_RANK()`-style window function
|
|
1729
|
+
* expression string, for use inside `attributes`/`Literal`. GoogleSQL's
|
|
1730
|
+
* window function syntax matches standard SQL exactly, so this is a thin
|
|
1731
|
+
* convenience wrapper rather than something requiring dialect-specific
|
|
1732
|
+
* translation.
|
|
1733
|
+
*/
|
|
1734
|
+
buildWindowFunction(fn, options) {
|
|
1735
|
+
const partition = options?.partitionBy?.length
|
|
1736
|
+
? `PARTITION BY ${options.partitionBy.map((f) => this.escapeId(f)).join(', ')}`
|
|
1737
|
+
: '';
|
|
1738
|
+
const order = options?.orderBy?.length
|
|
1739
|
+
? `ORDER BY ${options.orderBy.map(([f, dir]) => `${this.escapeId(f)}${dir ? ` ${dir}` : ''}`).join(', ')}`
|
|
1740
|
+
: '';
|
|
1741
|
+
const frame = options?.frame || '';
|
|
1742
|
+
const overParts = [partition, order, frame].filter(Boolean).join(' ');
|
|
1743
|
+
return `${fn} OVER (${overParts})`;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
exports.SpannerDialect = SpannerDialect;
|
|
1747
|
+
// =========================================================================
|
|
1748
|
+
// Foreign Data Wrappers - PostgreSQL-only concept, not applicable to Spanner
|
|
1749
|
+
// =========================================================================
|
|
1750
|
+
SpannerDialect.FDW_NOT_SUPPORTED = 'Foreign Data Wrappers are a PostgreSQL concept and are not supported by Cloud Spanner. ' +
|
|
1751
|
+
'Spanner federates external data via BigQuery federated queries / Data Boost export, which is out of scope for this ORM.';
|
|
1752
|
+
// =========================================================================
|
|
1753
|
+
// User / privilege management
|
|
1754
|
+
// =========================================================================
|
|
1755
|
+
//
|
|
1756
|
+
// Spanner does not have classic per-connection database USERs/GRANT-to-user
|
|
1757
|
+
// the way MySQL/Postgres do - access is governed by Cloud IAM (at the
|
|
1758
|
+
// project/instance/database level) plus, within GoogleSQL, fine-grained
|
|
1759
|
+
// "database roles" (`CREATE ROLE` / `GRANT <privilege> ON TABLE t TO ROLE
|
|
1760
|
+
// r`). The classic CREATE/ALTER/DROP USER surface has no Spanner analogue
|
|
1761
|
+
// at all, so those throw; a full database-roles implementation is out of
|
|
1762
|
+
// scope for this pass.
|
|
1763
|
+
SpannerDialect.USER_MGMT_NOT_SUPPORTED = 'Cloud Spanner has no per-connection database users; access is governed by Cloud IAM and ' +
|
|
1764
|
+
'(for fine-grained table/column privileges) GoogleSQL database roles (CREATE ROLE / GRANT ... TO ROLE), not CREATE USER.';
|
|
1765
|
+
// =========================================================================
|
|
1766
|
+
// Stored procedures - not supported (no procedural SQL in GoogleSQL)
|
|
1767
|
+
// =========================================================================
|
|
1768
|
+
SpannerDialect.PROC_NOT_SUPPORTED = 'Cloud Spanner GoogleSQL has no stored procedures / procedural SQL (no CREATE PROCEDURE, PL/pgSQL, or T-SQL equivalent). Move procedural logic into the application layer.';
|