alloydb SQL dialect (Google Cloud AlloyDB for PostgreSQL).alloydballoydbpg on port 5432) are PostgreSQL's,AlloyDBDialect extends PostgresDialect and inherits the wholeON CONFLICT, partitioning, LISTEN/NOTIFY| Property | Value |
|---|---|
| Dialect name | alloydb |
| Extends | PostgresDialect |
| Driver | pg |
| Default port | 5432 |
| Cloud SDK | google-auth-library (IAM only) |
| Local runtime | none — hosted service only |
google_columnar_engine_add / _drop /_refresh / _estimate, and sized by google_columnar_engine_recommend.google_ml_integration) — embedding(model, text)ml_predict_row calls a model endpoint, andgoogle_ml.create_model registers one.pgvector plus AlloyDB's ScaNN indexalloydb_scann, CREATE INDEX ... USING scann (col cosine)).CALL) values come back as $n bindings; where it isescape() and identifiers with escapeId(). NoProrm: alloydb is a SqlDialect enumDIALECT_LOADERS (src/dialects/loaders.ts), asrc/connection-url.ts, and is listed alongside Citussrc/schema-objects.ts as "PostgreSQL itself with extensionsProrm, or by importing the class directly.$ 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: 'alloydb',
host: '10.0.0.5',
database: 'app',
username: 'app',
password: process.env.DB_PASSWORD,
});import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'alloydb',
url: 'alloydb://app:secret@10.0.0.5:5432/app',
});host, port, database, username and password before the dialect is built; any of those passed alongside it wins.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) },
meta: { type: DataTypes.JSONB() },
});
await db.sync(); // CREATE TABLE — see Generated SQL belowmeta is JSONB, which the containment query below needs.import { 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,
});import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'alloydb',
host: '10.0.0.5',
database: 'app',
username: 'app',
password: process.env.DB_PASSWORD,
});import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'alloydb',
host: '10.0.0.5',
database: 'app',
username: 'svc@my-project.iam.gserviceaccount.com', // suffix stripped for you
dialectOptions: {
iam: true, // Application Default Credentials
},
});google-auth-library installed next to the driver.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,
});meta column:import { Op } from 'ts-prorm-orm';
await User.findAll({ where: { meta: { [Op.contains]: { plan: 'pro' } } } });await db.transaction(async (t) => {
await User.create({ email: 'grace@example.com', name: 'Grace' }, { transaction: t });
// A nested transaction is a SAVEPOINT; its failure undoes only its own work.
await db
.transaction(async (sp) => {
await User.destroy({ where: { id: 1 }, transaction: sp });
throw new Error('keep the row');
}, { transaction: t })
.catch(() => {});
});SKIP LOCKED)lock with skipLocked inside a transaction:await db.transaction(async (t) => {
await User.findAll({ where: { name: 'Ada' }, lock: { level: 'UPDATE', skipLocked: true }, transaction: t });
});STRING → VARCHAR(255), auto-increment INTEGER → SERIAL, BOOLEAN → BOOLEAN, DATE → TIMESTAMP, UUID → UUID, JSON → JSON. The type comparison lines the major engines up side by side.import { Prorm } from 'ts-prorm-orm';
// or, constructed directly:
const orm = new Prorm({
dialect: 'alloydb',
host: 'localhost',
// port defaults to 5432
username: 'postgres',
password: process.env.PG_PASSWORD,
database: 'app',
});
// equivalently:
const d = new Prorm({
dialect: 'alloydb',
host: 'localhost',
database: 'app',
});AlloyDBDialectOptions is PostgresDialectOptions plus iam. Theiam is set, builds the tokenimport { Prorm } from 'ts-prorm-orm';
const orm = new Prorm({
dialect: 'alloydb',
host: '10.0.0.5', // the instance's IP (direct connection)
database: 'app',
username: 'svc@my-project.iam.gserviceaccount.com', // suffix stripped for you
dialectOptions: {
iam: true, // Application Default Credentials
// iam: { keyFilename: '/secrets/sa.json' } | { credentials } | { authClient }
// iam: { tokenProvider } // any TokenProvider; google-auth-library is not loaded
},
});iam set, the dialect overrides the Postgres resolvePoolConfig hook:password becomes pgAuth(googleIamTokenProvider({ target: 'alloydb' }))'sLISTENalloydb.login scope;user becomes googleIamDatabaseUser(username, 'postgres') — the email,.gserviceaccount.com removed;ssl defaults to { rejectUnauthorized: false } when you give none.alloydb.iam_authentication flag on, and thegoogle-auth-library is anverify-ca / verify-full, so a token sent over one travels overpgAuth logs this once (passiam: { logger } to route it). For verified transport, run the AlloyDB Auth--auto-iam-authn performs the IAM loginalloydb dialect and no iam.tls: 'prefer-verify', the implicit default) an IAM-authenticatedssl sends ssl: { rejectUnauthorized: false } —tlsLogger, default console.warn):[prorm alloydb] Connection is encrypted but the server certificate is not verified
(ssl.rejectUnauthorized is false): AlloyDB direct connections don't support verify-ca.
To verify the server, connect through the AlloyDB Auth Proxy (mTLS) or pass your own
`ssl.ca`. To require verification instead of falling back, set tls: 'verify'.tls: 'verify' to refuse that fallback: with IAM on and no explicitssl, connecting throws before any network I/O, naming ssl (via the Authssl.ca) as what to set. An explicit ssl always wins overnew Prorm({
dialect: 'alloydb',
username: 'svc@proj.iam.gserviceaccount.com',
dialectOptions: {
iam: true,
tls: 'verify', // throws unless you also pass `ssl` yourself
},
});dialect.tlsPosture() reports the resulting posture without connecting —'verified' (explicit ssl that verifies), 'unverified' (the default'none' (IAM off, so no TLS default applies; or tls: 'verify'@google-cloud/alloydb-connector does not exist onAlloyDBDialect extends PostgresDialect (postgres) andObject.getOwnPropertyNames(AlloyDBDialect.prototype),| Area | Overridden |
|---|---|
| Connection and execution | resolvePoolConfig |
| DDL and schema | addIndex, buildCreateModelCall, buildCreateScannIndexSql, buildDropModelCall, buildPartialIndexSql, createExtension, createExtensionSQL |
| Other | buildEmbedColumnUpdate, buildEmbeddingQuery, buildPredictRowByModelIdQuery, buildPredictRowQuery, buildScannSettingSql, buildSimilaritySearchQuery, columnList, columnarCall, columnarEngineAdd, columnarEngineAddIndex, columnarEngineDrop, columnarEngineDropIndex, columnarEngineEstimate, columnarEngineRecommend, columnarEngineRefresh, columnarEngineResetRecommendation, relation, tlsPosture, warnUnverifiedTlsOnce, whereAfter |
d.createExtensionSQL('google_ml_integration');
// CREATE EXTENSION IF NOT EXISTS "google_ml_integration" CASCADE
d.createExtensionSQL('alloydb_scann'); // pulls in "vector"
// CREATE EXTENSION IF NOT EXISTS "alloydb_scann" CASCADEcreateExtensionSQL only builds these three extension namesgoogle_ml_integration, alloydb_scann, vector); anything else throws andcreateExtension().createExtension(name, options) overrides the inherited PostgreSQL method toawait d.createExtension('plpython3u');
// throws: "AlloyDB cannot install 'plpython3u': untrusted procedural
// languages require a PostgreSQL superuser, and AlloyDB is managed — the
// highest role is alloydbsuperuser. Use a trusted language such as plpgsql..."
await d.createExtension('vector', { ifNotExists: true }); // delegates to Postgres
// CREATE EXTENSION IF NOT EXISTS "vector"plpython3u, plpythonu, plperlu, pltclu.d.columnarEngineAdd('public.orders', ['total', 'placed_at']);
// SELECT google_columnar_engine_add(relation => 'public.orders', columns => 'total,placed_at')
d.columnarEngineAdd('orders');
// SELECT google_columnar_engine_add(relation => 'orders')
d.columnarEngineDrop('orders', ['total']);
// SELECT google_columnar_engine_drop(relation => 'orders', columns => 'total')
d.columnarEngineEstimate('orders');
// SELECT google_columnar_engine_estimate(relation => 'orders')
d.columnarEngineRefresh('orders');
// SELECT google_columnar_engine_refresh(relation => 'orders')
d.columnarEngineRecommend();
// SELECT google_columnar_engine_recommend()
d.columnarEngineRecommend('RECOMMEND_SIZE');
// SELECT * FROM google_columnar_engine_recommend(mode => 'RECOMMEND_SIZE')
d.columnarEngineResetRecommendation(true);
// SELECT google_columnar_engine_reset_recommendation(drop_columns => true)
d.columnarEngineAddIndex('docs_emb');
// SELECT google_columnar_engine_add_index(index => 'docs_emb')
d.columnarEngineDropIndex('docs_emb');
// SELECT google_columnar_engine_drop_index(index => 'docs_emb')escape()google_ml_integration)d.buildEmbeddingQuery('text-embedding-005', 'waterproof boots');
// { sql: 'SELECT embedding($1, $2)::vector AS "embedding"', values: [...] }
d.buildEmbedColumnUpdate({
tableName: 'docs',
sourceColumn: 'body',
targetColumn: 'emb',
model: 'text-embedding-005',
where: { lang: 'en' },
onlyMissing: true,
});
// UPDATE "docs" SET "emb" = embedding($1, "body")::vector
// WHERE "lang" = $2 AND "emb" IS NULL
// values: ['text-embedding-005', 'en']
d.buildPredictRowQuery('publishers/google/models/gemini-2.0-flash', { x: 1 });
// SELECT ml_predict_row($1, $2::json) AS "prediction"
d.buildPredictRowByModelIdQuery('my-model', { x: 1 });
// SELECT google_ml.predict_row(model_id => $1, request_body => $2::json) AS "prediction"
d.buildCreateModelCall({
modelId: 'emb',
provider: 'google',
type: 'text_embedding',
qualifiedName: 'text-embedding-005',
authType: 'alloydb_service_agent_iam',
});
// CALL google_ml.create_model(model_id => $1, model_provider => $2,
// model_type => $3, model_qualified_name => $4, model_auth_type => $5)
d.buildDropModelCall('emb');
// CALL google_ml.drop_model($1)buildEmbedColumnUpdate's where clause values are renumbered to continue$1 (the model), via a private whereAfter() helper shared withbuildSimilaritySearchQuery. where must be a where-object — a raw SQLbuildSimilaritySearchQuery builds a nearest-neighbour SELECT, eitherembedding(model,
text):d.buildSimilaritySearchQuery({
tableName: 'docs',
column: 'emb',
metric: 'cosine',
text: 'waterproof boots',
model: 'text-embedding-005',
where: { lang: 'en' },
attributes: ['id', 'title'],
distanceAs: 'distance',
limit: 5,
});
// SELECT "id", "title", "emb" <=> embedding($2, $3)::vector AS "distance"
// FROM "docs" WHERE "lang" = $1
// ORDER BY "emb" <=> embedding($2, $3)::vector LIMIT 5
// values: ['en', 'text-embedding-005', 'waterproof boots']
d.buildSimilaritySearchQuery({
tableName: 'docs',
column: 'emb',
metric: 'l2',
vector: [0.1, -2, 3e-4],
limit: 10,
});
// SELECT * FROM "docs" ORDER BY "emb" <-> $1::vector LIMIT 10
// values: ['[0.1,-2,0.0003]']metric | pgvector operator |
|---|---|
l2 | <-> |
inner_product | <#> |
cosine | <=> |
vector (a non-empty array of finite numbers) or text (withmodel) must be given; limit must be a positive integer. Anything elsed.buildCreateScannIndexSql('docs', { name: 'docs_emb', column: 'emb', distance: 'cosine' });
// CREATE INDEX "docs_emb" ON "docs" USING scann ("emb" cosine)
d.buildCreateScannIndexSql('docs', {
name: 'docs_emb',
column: 'emb',
distance: 'dot_product',
numLeaves: 1000,
quantizer: 'SQ8',
maxNumLevels: 2,
autoMaintenance: true,
});
// CREATE INDEX "docs_emb" ON "docs" USING scann ("emb" dot_product)
// WITH (mode = 'MANUAL', num_leaves = 1000, quantizer = 'SQ8',
// max_num_levels = 2, auto_maintenance = true)numLeaves, quantizer, maxNumLevels or autoMaintenancemode: 'MANUAL'; combining them with an explicit mode: 'AUTO'distance is l2 | dot_product | cosine; quantizer is SQ8 |AH | FLAT; maxNumLevels is 1 | 2 | 3. ifNotExists adds IF NOT
EXISTS.d.buildScannSettingSql('scann.num_leaves_to_search', 20);
// SET scann.num_leaves_to_search = 20scann.num_leaves_to_search, scann.pre_reordering_num_neighborsALLOYDB_SCANN_SETTINGS). value must be a positive integer.addIndex(tableName, indexName, fields, options) overrides the inheritedwhere predicate through a local builderawait d.addIndex('t', 'i', ['a'], { where: { b: { [Op.gte]: 5 } } });
// CREATE INDEX "i" ON "t" ("a") WHERE "b" >= 5
await d.addIndex('t', 'j', ['a']); // no predicate: delegates straight to PostgresDialect.addIndex
// CREATE INDEX "j" ON "t" ("a")buildPartialIndexSql(tableName, indexName, fields, options) is the pure,unique, using (validated as/^[a-z_]+$/i), include, and a where where-object (a raw SQL stringbuildPartialIndexPredicate.VECTOR(n) and SERIAL for autoIncrement.| Call | Why |
|---|---|
createExtensionSQL(name) for anything other than google_ml_integration, alloydb_scann, vector | Only builds the extensions its own builders depend on; use createExtension() for others. |
createExtension('plpython3u' | 'plpythonu' | 'plperlu' | 'pltclu') | Untrusted procedural languages need a true PostgreSQL superuser; AlloyDB's highest role is alloydbsuperuser. (Lifted in AlloyDB Omni, which has one.) |
iam with no username | The IAM principal is the database user. |
buildSimilaritySearchQuery with both/neither of vector/text, a non-finite vector, an empty vector, an unknown metric, or a non-positive limit | Malformed search input. |
buildCreateScannIndexSql with an unknown distance/quantizer/mode, a non-positive numLeaves, maxNumLevels outside 1–3, or manual params with mode: 'AUTO' | Malformed ScaNN DDL. |
buildScannSettingSql with an unknown setting or non-positive value | Only the two documented ScaNN session settings are supported. |
whereAfter / buildPartialIndexSql with where as a raw SQL string | Builders take a where-object; no raw SQL fragment is accepted. |
columnarEngineAdd/Drop/Estimate with a blank relation name or a column name containing a comma or empty | The function's argument is a single comma-separated string literal. |
tests/dialects/alloydb.test.tsgoogleIamTokenProviderpgAuth (see above) and covered with a mocked auth client and a mockedpg.Pool; it has not been exercised against a live instance. There is no@google-cloud/alloydb-connector is not on npm);alloyDbConnectorOptions() in cloud-auth still accepts a connector objectLISTEN/NOTIFY, triggers, partitioning,ON CONFLICT — is inherited from PostgresDialect unchanged.capabilitiesOf() (src/transaction-support.ts),whereSupportOf() (src/query-builders/expression-render.ts),sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the| Capability | alloydb |
|---|---|
| Transactions atomic | yes |
| Savepoints | yes |
| 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 | JSON (JSONB → JSONB) |
| Vector column | VECTOR(3) |
| Array column | INTEGER[] |
| WHERE builder | 62 operator keys; $and/$or/$not; $exists: yes; $in subquery: yes; expression operands: yes |
| SQL objects | family postgres: triggers yes; procedures yes; functions multiStatement; materialized views native; sequences yes; enums named; RLS policy |
| Streaming | queryStream() present; LISTEN present; createExtension() present |
googleIamTokenProvider, pgAuth