spanner dialect — orSpannerPostgresDialect is for the second: PostgreSQL syntax$n parameters over Spanner's storage, type system and schema rules.| Property | Value |
|---|---|
| Dialect name | spannerpg |
| Extends | PostgresDialect |
| Driver | pg, through PGAdapter |
| Default host | localhost |
| Default port | 5432 (PGAdapter's default) |
| Cloud SDK | none |
| Local runtime | PGAdapter in front of the Spanner emulator |
$ npm install ts-prorm-orm pg$ pnpm add ts-prorm-orm pg$ yarn add ts-prorm-orm pg$ bun add ts-prorm-orm pgpg is the same driver the postgres dialect uses. pg-copy-streams is optional: only the features that need it load it.import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'spannerpg',
host: 'localhost', // PGAdapter
database: 'my-database',
});import { DataTypes } from 'ts-prorm-orm';
const User = db.define('User', {
id: { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
email: { type: DataTypes.STRING(255), allowNull: false, unique: true },
name: { type: DataTypes.STRING(100) },
});
await db.sync(); // CREATE TABLE — see Generated SQL belowimport { Op } from 'ts-prorm-orm';
await User.create({ email: 'ada@example.com', name: 'Ada' });
const page = await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});query(), captured with the driver stubbed out. The model layer inlines the values.db.sync() for the model in the Quick start:CREATE TABLEawait User.create({ email: 'ada@example.com', name: 'Ada' });
await User.update({ name: 'Grace' }, { where: { email: 'ada@example.com' } });
await User.destroy({ where: { id: 1 } });await User.upsert({ id: 1, email: 'ada@example.com', name: 'Ada Lovelace' });upsert() also reads the row by primary key before and after the write, to return the instance.await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});await db.transaction(async (t) => {
await User.create({ email: 'grace@example.com', name: 'Grace' }, { transaction: t });
await User.update({ name: 'Grace Hopper' }, { where: { email: 'grace@example.com' }, transaction: t });
});[!WARNING] No savepoints
Nested transactions need savepoints, and spannerpg has none. Run the work in a single transaction, or restructure it so nothing nests.
STRING → varchar(255), auto-increment INTEGER → bigint, BOOLEAN → boolean, DATE → timestamptz, UUID → uuid, JSON → jsonb. The type comparison lines the major engines up side by side.$n binding are inherited fromPostgresDialect with no changes.@google-cloud/spanner withdatabaseDialect: POSTGRESQL) needs no sidecar, but exposes Spanner'sspanner dialect already does — so it is left for later.# sidecar: java -jar pgadapter.jar -p my-project -i my-instance -d my-database
docker run -d -p 5432:5432 \
-v $HOME/key.json:/key.json \
gcr.io/cloud-spanner-pg-adapter/pgadapter \
-p my-project -i my-instance -d my-database -c /key.json -xconst d = new SpannerPostgresDialect({ database: 'my-database' });
// host 'localhost', port 5432; username/password are not used for authorization-c <key file>), accepts connections from localhost only unless started-x (inside a container), and uses the IAM permissions of that principal.SpannerPostgresDialect extends PostgresDialect (postgres) andObject.getOwnPropertyNames(SpannerPostgresDialect.prototype),| Area | Overridden |
|---|---|
| Types | columnSql, createEnumType, getDataTypeSql |
| DDL and schema | addIndex, buildCreateIndexSql, buildCreateSequenceSql, buildCreateTableStatements, createDomain, createExtension, createForeignDataWrapper, createForeignServer, createForeignTable, createMaterializedView, createPartialIndex, createPartitionedTable, createProcedure, createSavepointSQL, createSequence, createStoredProcedure, createTable, createTrigger, dropExtension, dropSequence, dropTrigger, renameColumn |
| Introspection | checkName |
| Other | commentColumn, commentTable, constraintSql, defaultSql, id, nullFilter, onDelete, releaseSavepointSQL, rollbackToSavepointSQL |
| Model type | Spanner column |
|---|---|
STRING(n) / TEXT / ENUM | varchar(n) (n ≤ 2,621,440) / text / varchar(255) |
TINYINT … BIGINT | bigint (Spanner's only integer; int is an alias for int8) |
FLOAT / DOUBLE | real / double precision |
DECIMAL(p,s) | numeric — precision/scale are not accepted in DDL |
BOOLEAN / BLOB / UUID | boolean / bytea / uuid |
DATE / 'DATETIME' / 'TIMESTAMP' | timestamptz (no timestamp without time zone) |
DATEONLY | date |
JSON / JSONB | jsonb |
ARRAY(T) | T[] (one dimension) |
CHAR, TIME, multi-dimensional arrays, and everyHSTORE, CITEXT, geometry, ranges, INET, VECTOR as acreateTable() builds it as a tableoptions.primaryKey), and sends one statement atdb.dialect.buildCreateTableStatements(
'albums',
{
singer_id: { type: DataTypes.BIGINT, primaryKey: true },
album_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
title: { type: DataTypes.STRING(200), unique: true },
created_at: { type: DataTypes.DATE },
},
{
interleave: { parent: 'singers', onDelete: 'CASCADE' },
ttl: { days: 30, column: 'created_at' },
},
);
// CREATE TABLE "albums" ("singer_id" bigint NOT NULL,
// "album_id" bigint GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE) NOT NULL,
// "title" varchar(200), "created_at" timestamptz, PRIMARY KEY ("singer_id", "album_id"))
// INTERLEAVE IN PARENT "singers" ON DELETE CASCADE TTL INTERVAL '30 days' ON "created_at"
// CREATE UNIQUE INDEX "albums_title_key" ON "albums" ("title")spanner.default_sequence_kind option, which serial would require.interleave: { parent, onDelete } emits INTERLEAVE IN
PARENT (a child row needs its parent row); enforceParent: false emitsINTERLEAVE IN (same physical layout, no check, no ON DELETE). Thettl: { days, column } — a whole number of days on a timestampUNIQUE is an index: Spanner's CREATE TABLE has no unique constraint,UNIQUE constraints and uniqueKeys become CREATE
UNIQUE INDEX statements.ON DELETE CASCADE | NO ACTION; no ON UPDATE.db.dialect.buildCreateIndexSql('albums', 'by_title', ['singer_id', 'title'], {
include: ['year'],
interleaveIn: 'singers',
where: { title: { [Op.ne]: null } },
});
// CREATE INDEX "by_title" ON "albums" ("singer_id", "title") INCLUDE ("year")
// INTERLEAVE IN "singers" WHERE "title" IS NOT NULLwhere is accepted"col" IS NOT NULL [AND …] over indexed columns.USING is for vector indexes only) and expression indexes aredb.dialect.buildCreateSequenceSql({ name: 'ids', skipRange: [1, 1000], startCounterWith: 50 });
// CREATE SEQUENCE "ids" BIT_REVERSED_POSITIVE SKIP RANGE 1 1000 START COUNTER WITH 50
await db.dialect.nextSequenceValue('ids'); // SELECT nextval('ids')startWith, incrementBy, minvalue, maxvalue, cycle, cache,temporary, replace and ownedBy are refused. startCounterWith sets thedropSequence has no CASCADE.| Asked for | Why |
|---|---|
| Triggers | Not supported |
CREATE EXTENSION | No extensions |
CREATE TYPE / CREATE DOMAIN, stored procedures / functions | No user-defined types, functions, operators or procedures |
| Foreign data wrappers | Not supported |
SAVEPOINT (and nested transactions) | Not supported |
Partial indexes other than IS NOT NULL | Not supported |
COMMENT ON (tables, columns, comment option) | Not in Spanner DDL |
Materialized views, partitioned tables, INHERITS, tablespaces | Not in Spanner DDL |
| Renaming a column | ALTER TABLE has no RENAME COLUMN |
| A table with no primary key | Required |
@google-cloud/spanner clientdatabaseDialect: POSTGRESQL) is not.SERIAL as unsupported, while the dataspanner.default_sequence_kind is set. The dialect avoids the question byLISTEN/NOTIFY, ON CONFLICT, RETURNING, row locking and isolationPostgresDialect unchanged; their support throughcapabilitiesOf() (src/transaction-support.ts),whereSupportOf() (src/query-builders/expression-render.ts),sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the| Capability | spannerpg |
|---|---|
| Transactions atomic | yes |
| Savepoints | no — a nested transaction() throws |
| Isolation | up to SERIALIZABLE |
| Transaction modes | read-only yes; deferrable yes; constraint deferral yes |
| RETURNING | RETURNING on INSERT |
| Upsert | INSERT … ON CONFLICT (…) DO UPDATE |
| JSON column | jsonb (JSONB → jsonb) |
| Vector column | refused — The VECTOR type is not supported on Spanner (PostgreSQL interface). Spanner supports bigint, real, double precision, numeric, boolean, bytea, date, timestamptz, |
| Array column | bigint[] |
| WHERE builder | 62 operator keys; $and/$or/$not; $exists: yes; $in subquery: yes; expression operands: yes |
| SQL objects | family unknown: triggers no; procedures no; functions no; materialized views no; sequences no; enums check; RLS no |
| Streaming | queryStream() present; LISTEN present; createExtension() present |