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.
Files changed (310) hide show
  1. package/CHANGELOG.md +1111 -0
  2. package/LICENSE +21 -0
  3. package/README.md +573 -0
  4. package/dist/audit/history-query.js +180 -0
  5. package/dist/audit/index.js +23 -0
  6. package/dist/audit/logger.js +236 -0
  7. package/dist/cache/cache-manager.js +84 -0
  8. package/dist/cache/index.js +16 -0
  9. package/dist/cache/redis-cluster-cache.js +557 -0
  10. package/dist/cli.js +2200 -0
  11. package/dist/compliance/audit-trail.js +68 -0
  12. package/dist/compliance/backup-verification.js +685 -0
  13. package/dist/compliance/breach-detector.js +505 -0
  14. package/dist/compliance/consent-record.js +227 -0
  15. package/dist/compliance/consent-versioning.js +331 -0
  16. package/dist/compliance/cross-border-log.js +530 -0
  17. package/dist/compliance/data-classifier.js +258 -0
  18. package/dist/compliance/data-lineage.js +565 -0
  19. package/dist/compliance/data-masker.js +303 -0
  20. package/dist/compliance/data-portability.js +265 -0
  21. package/dist/compliance/data-retention.js +195 -0
  22. package/dist/compliance/dsar-workflow.js +342 -0
  23. package/dist/compliance/field-encryption.js +223 -0
  24. package/dist/compliance/immutable-record.js +148 -0
  25. package/dist/compliance/index.js +260 -0
  26. package/dist/compliance/privacy-impact-assessment.js +364 -0
  27. package/dist/compliance/pseudonymization.js +269 -0
  28. package/dist/compliance/query-firewall.js +1182 -0
  29. package/dist/compliance/rate-limiter.js +580 -0
  30. package/dist/compliance/right-to-erasure.js +117 -0
  31. package/dist/compliance/row-level-security.js +246 -0
  32. package/dist/compliance/security-decorator.js +574 -0
  33. package/dist/compliance/security-monitor.js +525 -0
  34. package/dist/compliance/sensitive-data-discovery.js +476 -0
  35. package/dist/compliance/session-isolation.js +472 -0
  36. package/dist/compliance/tls-enforcer.js +108 -0
  37. package/dist/compliance/worm-storage.js +712 -0
  38. package/dist/connection-manager.js +198 -0
  39. package/dist/connection-pool.js +519 -0
  40. package/dist/decorators/audit.js +136 -0
  41. package/dist/decorators/belongs-to-many.js +115 -0
  42. package/dist/decorators/belongs-to.js +115 -0
  43. package/dist/decorators/check.js +435 -0
  44. package/dist/decorators/collate.js +329 -0
  45. package/dist/decorators/comment.js +205 -0
  46. package/dist/decorators/database-settings.js +236 -0
  47. package/dist/decorators/default.js +244 -0
  48. package/dist/decorators/encryption.js +235 -0
  49. package/dist/decorators/engine.js +97 -0
  50. package/dist/decorators/fk-constraints.js +594 -0
  51. package/dist/decorators/foreign-table.js +136 -0
  52. package/dist/decorators/generated.js +274 -0
  53. package/dist/decorators/has-many.js +127 -0
  54. package/dist/decorators/has-one.js +116 -0
  55. package/dist/decorators/hstore.js +129 -0
  56. package/dist/decorators/index.js +355 -0
  57. package/dist/decorators/json-column.js +83 -0
  58. package/dist/decorators/jsonb.js +101 -0
  59. package/dist/decorators/orm-decorators.js +425 -0
  60. package/dist/decorators/permissions.js +294 -0
  61. package/dist/decorators/procedure.js +210 -0
  62. package/dist/decorators/query-options.js +556 -0
  63. package/dist/decorators/range.js +167 -0
  64. package/dist/decorators/set-column.js +83 -0
  65. package/dist/decorators/spatial.js +114 -0
  66. package/dist/decorators/storage.js +580 -0
  67. package/dist/decorators/timezone.js +512 -0
  68. package/dist/decorators/trigger.js +90 -0
  69. package/dist/decorators/uuid.js +135 -0
  70. package/dist/decorators/view.js +258 -0
  71. package/dist/diagrams/chen-diagram.js +354 -0
  72. package/dist/diagrams/class-diagram.js +384 -0
  73. package/dist/diagrams/dependency-diagram.js +432 -0
  74. package/dist/diagrams/er-diagram.js +605 -0
  75. package/dist/diagrams/flow-diagram.js +394 -0
  76. package/dist/diagrams/gantt-diagram.js +411 -0
  77. package/dist/diagrams/index-diagram.js +353 -0
  78. package/dist/diagrams/index.js +184 -0
  79. package/dist/diagrams/migration-diagram.js +316 -0
  80. package/dist/diagrams/model-diagram.js +616 -0
  81. package/dist/diagrams/package-diagram.js +376 -0
  82. package/dist/diagrams/palette.js +85 -0
  83. package/dist/diagrams/relational-diagram.js +455 -0
  84. package/dist/diagrams/schemadoc-diagram.js +309 -0
  85. package/dist/diagrams/sequence-diagram.js +307 -0
  86. package/dist/diagrams/state-diagram.js +344 -0
  87. package/dist/diagrams/svg-dom.js +111 -0
  88. package/dist/diagrams/tree-diagram.js +250 -0
  89. package/dist/dialects/clickhouse/index.js +1742 -0
  90. package/dist/dialects/cockroachdb/index.js +4677 -0
  91. package/dist/dialects/cratedb/index.js +441 -0
  92. package/dist/dialects/databricks/index.js +517 -0
  93. package/dist/dialects/db2/index.js +2225 -0
  94. package/dist/dialects/dialect.js +720 -0
  95. package/dist/dialects/duckdb/index.js +2014 -0
  96. package/dist/dialects/exasol/index.js +357 -0
  97. package/dist/dialects/firebird/index.js +495 -0
  98. package/dist/dialects/greenplum/index.js +457 -0
  99. package/dist/dialects/hana/index.js +1741 -0
  100. package/dist/dialects/mariadb/index.js +3590 -0
  101. package/dist/dialects/mssql/index.js +2296 -0
  102. package/dist/dialects/mysql/index.js +4058 -0
  103. package/dist/dialects/oracle/index.js +2999 -0
  104. package/dist/dialects/postgres/index.js +5319 -0
  105. package/dist/dialects/query-stream-helper.js +152 -0
  106. package/dist/dialects/questdb/index.js +433 -0
  107. package/dist/dialects/redshift/index.js +2277 -0
  108. package/dist/dialects/singlestore/index.js +354 -0
  109. package/dist/dialects/snowflake/index.js +2082 -0
  110. package/dist/dialects/spanner/index.js +1768 -0
  111. package/dist/dialects/sqlite/index.js +3382 -0
  112. package/dist/dialects/tidb/index.js +377 -0
  113. package/dist/dialects/timescaledb/index.js +164 -0
  114. package/dist/dialects/trino/index.js +420 -0
  115. package/dist/dialects/turso/index.js +227 -0
  116. package/dist/dialects/vertica/index.js +317 -0
  117. package/dist/dialects/yugabytedb/index.js +424 -0
  118. package/dist/errors/index.js +472 -0
  119. package/dist/errors/utils.js +340 -0
  120. package/dist/errors.js +21 -0
  121. package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
  122. package/dist/extensions/catalog/mssql-features.js +147 -0
  123. package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
  124. package/dist/extensions/catalog/oracle-db2-features.js +196 -0
  125. package/dist/extensions/catalog/postgres-extensions.js +516 -0
  126. package/dist/extensions/index.js +71 -0
  127. package/dist/extensions/types.js +13 -0
  128. package/dist/foreign-data.js +173 -0
  129. package/dist/hooks/hooks-manager.js +350 -0
  130. package/dist/hooks/index.js +37 -0
  131. package/dist/index.js +463 -0
  132. package/dist/logging.js +335 -0
  133. package/dist/migrations/index.js +40 -0
  134. package/dist/migrations/migration.js +196 -0
  135. package/dist/migrations/migrator.js +411 -0
  136. package/dist/migrations/prormmigration.js +275 -0
  137. package/dist/migrations/query-interface.js +435 -0
  138. package/dist/migrations/seeder.js +353 -0
  139. package/dist/models/associations.js +852 -0
  140. package/dist/models/constraints.js +288 -0
  141. package/dist/models/data-types.js +2518 -0
  142. package/dist/models/decorators.js +445 -0
  143. package/dist/models/index.js +33 -0
  144. package/dist/models/indexes.js +531 -0
  145. package/dist/models/methods.js +382 -0
  146. package/dist/models/model-manager.js +103 -0
  147. package/dist/models/model.js +5349 -0
  148. package/dist/models/operators.js +67 -0
  149. package/dist/models/scopes.js +189 -0
  150. package/dist/models/typescript-types.js +26 -0
  151. package/dist/nosql/aerospike/index.js +205 -0
  152. package/dist/nosql/allegrograph/index.js +169 -0
  153. package/dist/nosql/arangodb/index.js +364 -0
  154. package/dist/nosql/azure-blob/index.js +206 -0
  155. package/dist/nosql/beanstalkd/index.js +231 -0
  156. package/dist/nosql/beequeue/index.js +210 -0
  157. package/dist/nosql/bigchaindb/index.js +195 -0
  158. package/dist/nosql/bigtable/index.js +224 -0
  159. package/dist/nosql/blazegraph/index.js +173 -0
  160. package/dist/nosql/bullmq/index.js +191 -0
  161. package/dist/nosql/cassandra/index.js +174 -0
  162. package/dist/nosql/chroma/index.js +190 -0
  163. package/dist/nosql/cloudflare-kv/index.js +220 -0
  164. package/dist/nosql/coherence/index.js +200 -0
  165. package/dist/nosql/cosmosdb/index.js +157 -0
  166. package/dist/nosql/couchbase/index.js +213 -0
  167. package/dist/nosql/dax/index.js +212 -0
  168. package/dist/nosql/deno-kv/index.js +206 -0
  169. package/dist/nosql/dgraph/index.js +171 -0
  170. package/dist/nosql/doris/index.js +169 -0
  171. package/dist/nosql/druid/index.js +162 -0
  172. package/dist/nosql/dynamodb/index.js +937 -0
  173. package/dist/nosql/elasticsearch/index.js +377 -0
  174. package/dist/nosql/etcd/index.js +502 -0
  175. package/dist/nosql/eventhubs/index.js +213 -0
  176. package/dist/nosql/eventstore/index.js +254 -0
  177. package/dist/nosql/faunadb/index.js +188 -0
  178. package/dist/nosql/firestore/index.js +177 -0
  179. package/dist/nosql/fluree/index.js +148 -0
  180. package/dist/nosql/fuseki/index.js +170 -0
  181. package/dist/nosql/gcs/index.js +172 -0
  182. package/dist/nosql/gearman/index.js +160 -0
  183. package/dist/nosql/geode/index.js +196 -0
  184. package/dist/nosql/graphdb/index.js +169 -0
  185. package/dist/nosql/graylog/index.js +188 -0
  186. package/dist/nosql/gridgain/index.js +171 -0
  187. package/dist/nosql/hazelcast/index.js +162 -0
  188. package/dist/nosql/hbase/index.js +230 -0
  189. package/dist/nosql/ignite/index.js +173 -0
  190. package/dist/nosql/immudb/index.js +184 -0
  191. package/dist/nosql/index.js +232 -0
  192. package/dist/nosql/infinispan/index.js +200 -0
  193. package/dist/nosql/influxdb/index.js +0 -0
  194. package/dist/nosql/kafka/index.js +234 -0
  195. package/dist/nosql/keyspaces/index.js +189 -0
  196. package/dist/nosql/kinesis/index.js +253 -0
  197. package/dist/nosql/leveldb/index.js +153 -0
  198. package/dist/nosql/lmdb/index.js +160 -0
  199. package/dist/nosql/loki/index.js +201 -0
  200. package/dist/nosql/marklogic/index.js +204 -0
  201. package/dist/nosql/materialize/index.js +145 -0
  202. package/dist/nosql/meilisearch/index.js +154 -0
  203. package/dist/nosql/memcached/index.js +223 -0
  204. package/dist/nosql/milvus/index.js +410 -0
  205. package/dist/nosql/minio/index.js +265 -0
  206. package/dist/nosql/momento/index.js +179 -0
  207. package/dist/nosql/mongodb/index.js +461 -0
  208. package/dist/nosql/nats/index.js +247 -0
  209. package/dist/nosql/nedb/index.js +164 -0
  210. package/dist/nosql/neo4j/index.js +450 -0
  211. package/dist/nosql/neptune/index.js +470 -0
  212. package/dist/nosql/nsq/index.js +200 -0
  213. package/dist/nosql/opensearch/index.js +186 -0
  214. package/dist/nosql/orientdb/index.js +175 -0
  215. package/dist/nosql/papertrail/index.js +200 -0
  216. package/dist/nosql/pinecone/index.js +0 -0
  217. package/dist/nosql/pinot/index.js +133 -0
  218. package/dist/nosql/pouchdb/index.js +172 -0
  219. package/dist/nosql/prometheus/index.js +174 -0
  220. package/dist/nosql/provendb/index.js +147 -0
  221. package/dist/nosql/pubsub/index.js +187 -0
  222. package/dist/nosql/pulsar/index.js +232 -0
  223. package/dist/nosql/qdrant/index.js +295 -0
  224. package/dist/nosql/qldb/index.js +192 -0
  225. package/dist/nosql/r2/index.js +281 -0
  226. package/dist/nosql/rabbitmq/index.js +237 -0
  227. package/dist/nosql/ravendb/index.js +175 -0
  228. package/dist/nosql/redis/index.js +607 -0
  229. package/dist/nosql/redpanda/index.js +237 -0
  230. package/dist/nosql/resque/index.js +203 -0
  231. package/dist/nosql/rethinkdb/index.js +232 -0
  232. package/dist/nosql/rocksdb/index.js +152 -0
  233. package/dist/nosql/rockset/index.js +126 -0
  234. package/dist/nosql/s3/index.js +298 -0
  235. package/dist/nosql/scylladb/index.js +178 -0
  236. package/dist/nosql/signoz/index.js +227 -0
  237. package/dist/nosql/sns/index.js +201 -0
  238. package/dist/nosql/solr/index.js +231 -0
  239. package/dist/nosql/splunk/index.js +227 -0
  240. package/dist/nosql/sqs/index.js +246 -0
  241. package/dist/nosql/stardog/index.js +169 -0
  242. package/dist/nosql/starrocks/index.js +170 -0
  243. package/dist/nosql/store.js +2 -0
  244. package/dist/nosql/sumologic/index.js +213 -0
  245. package/dist/nosql/surrealdb/index.js +179 -0
  246. package/dist/nosql/terminusdb/index.js +151 -0
  247. package/dist/nosql/tigergraph/index.js +469 -0
  248. package/dist/nosql/typesense/index.js +148 -0
  249. package/dist/nosql/unqlite/index.js +157 -0
  250. package/dist/nosql/upstash/index.js +167 -0
  251. package/dist/nosql/vercel-kv/index.js +166 -0
  252. package/dist/nosql/victoriametrics/index.js +213 -0
  253. package/dist/nosql/virtuoso/index.js +169 -0
  254. package/dist/nosql/weaviate/index.js +276 -0
  255. package/dist/operators/index.js +108 -0
  256. package/dist/operators.js +2690 -0
  257. package/dist/prisma-migrate/index.js +38 -0
  258. package/dist/prisma-migrate/migration-generator.js +125 -0
  259. package/dist/prisma-migrate/model-generator.js +172 -0
  260. package/dist/prisma-migrate/relations.js +100 -0
  261. package/dist/prisma-migrate/schema-parser.js +167 -0
  262. package/dist/prisma-migrate/type-mapper.js +40 -0
  263. package/dist/prorm.js +6832 -0
  264. package/dist/query-builders/cte-builder.js +80 -0
  265. package/dist/query-builders/functions/aggregate.js +390 -0
  266. package/dist/query-builders/functions/conditional.js +503 -0
  267. package/dist/query-builders/functions/datetime.js +695 -0
  268. package/dist/query-builders/functions/fulltext.js +439 -0
  269. package/dist/query-builders/functions/index.js +93 -0
  270. package/dist/query-builders/functions/json.js +427 -0
  271. package/dist/query-builders/functions/math.js +399 -0
  272. package/dist/query-builders/functions/string.js +518 -0
  273. package/dist/query-builders/functions/window.js +328 -0
  274. package/dist/query-builders/include-builder.js +323 -0
  275. package/dist/query-builders/index-expression-builder.js +242 -0
  276. package/dist/query-builders/index.js +161 -0
  277. package/dist/query-builders/insert-builder.js +164 -0
  278. package/dist/query-builders/model-helpers.js +38 -0
  279. package/dist/query-builders/order-limit-builder.js +239 -0
  280. package/dist/query-builders/sql-compiler.js +2040 -0
  281. package/dist/query-builders/subquery-builder.js +152 -0
  282. package/dist/query-builders/update-builder.js +182 -0
  283. package/dist/query-builders/view-builder.js +218 -0
  284. package/dist/query-builders/where-builder.js +1532 -0
  285. package/dist/query-interface.js +622 -0
  286. package/dist/query-optimizers/batch-optimizer.js +426 -0
  287. package/dist/query-optimizers/explain-plans.js +415 -0
  288. package/dist/query-optimizers/index.js +51 -0
  289. package/dist/query-optimizers/prepared-statement-cache.js +423 -0
  290. package/dist/query-optimizers/query-hints.js +438 -0
  291. package/dist/query-optimizers/query-optimizer.js +278 -0
  292. package/dist/query-optimizers/slow-query-logger.js +271 -0
  293. package/dist/replica-manager.js +507 -0
  294. package/dist/schema/index.js +15 -0
  295. package/dist/schema/migration-generator.js +374 -0
  296. package/dist/schema/schema-differ.js +549 -0
  297. package/dist/schema/types.js +6 -0
  298. package/dist/sql-constants.js +300 -0
  299. package/dist/sqlite-advanced.js +1047 -0
  300. package/dist/streams/index.js +12 -0
  301. package/dist/streams/transforms.js +178 -0
  302. package/dist/transaction.js +456 -0
  303. package/dist/types/index.js +180 -0
  304. package/dist/user-management.js +127 -0
  305. package/dist/utils/date.js +300 -0
  306. package/dist/utils/index.js +1079 -0
  307. package/dist/utils/string.js +161 -0
  308. package/dist/validators/index.js +19 -0
  309. package/dist/validators/validator.js +911 -0
  310. 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.';