Backtick identifiers, and a backtick cannot be escaped inside one, so
escapeId() throws on one instead of emitting something injectable.
dataset.table naming. A "schema" is a dataset; a "database" is a GCP
project. CREATE DATABASE throws.
No enforced constraints. PRIMARY KEY and FOREIGN KEY exist only as
NOT ENFORCED optimizer hints. UNIQUE and CHECK do not exist at all, and
every path that would emit one throws.
No indexes. Partitioning and clustering replace them; addIndex,
createIndex, removeIndex and dropIndex all throw. CREATE SEARCH INDEX
and CREATE VECTOR INDEX are real and are exposed under their own names.
MERGE for upserts. There is no ON CONFLICT.
No RETURNING. DML returns an affected-row count and nothing else.
UPDATE/DELETE require a WHERE — WHERE TRUE is emitted when none is
given.
Bare UNION is a syntax error. GoogleSQL demands UNION ALL /
UNION DISTINCT, EXCEPT DISTINCT, INTERSECT DISTINCT.
SELECT * EXCEPT (...) makes attributes.exclude expressible directly.
String literals use backslash escapes (\'), not doubling — 'it''s'
parses as two adjacent literals in GoogleSQL and fails.
Transactions cannot span statements, so startTransaction() throws.
import { DataTypes } from 'ts-prorm-orm';
const User = db.define('User', {
id: { type: DataTypes.INTEGER(), primaryKey: true, allowNull: false },
email: { type: DataTypes.STRING(255), allowNull: false },
name: { type: DataTypes.STRING(100) },
meta: { type: DataTypes.JSON() },
}, { timestamps: false });
await db.sync(); // CREATE TABLE — see Generated SQL below
import { Op } from 'ts-prorm-orm';
await User.create({ id: 1, 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';
const db = new Prorm({
dialect: 'bigquery',
dialectOptions: {
projectId: 'my-project', // billing project; falls back to the ADC default
dataset: 'analytics', // default dataset for unqualified table names
location: 'US', // must match the dataset's location
maximumBytesBilled: 10 * 1024 ** 3, // guard rail: fail a runaway scan
},
});
await db.connect();
projectId — project jobs are billed to. dataProjectId overrides the
project used to qualify table names when the data lives elsewhere.
dataset — default dataset (database is accepted as an alias). Set on every
job as defaultDataset and used by quoteTable().
location — job location; must match the dataset's.
keyFilename / credentials — explicit service-account auth; otherwise ADC.
apiEndpoint, clientOptions — forwarded to the BigQuery constructor.
qualifyTablesWithProject — emit `project`.`dataset`.`table`. Off by
default, so cross-project reads are not broken by a hardcoded billing project.
maximumBytesBilled — fail a job that would scan more than this. On BigQuery
a runaway query costs money rather than time; this is worth setting.
jobTimeoutMs, labels — applied to every job (labels for cost attribution).
validateOnConnect (default true) — runs a zero-byte SELECT 1 so auth and
permission failures surface at connect() rather than mid-migration.
wrapIntegers — how the driver returns INT64 values that exceed
Number.MAX_SAFE_INTEGER.
await db.dialect.createTable(
'events',
{
event_id: { type: 'UUID', primaryKey: true, autoIncrement: true },
ts: { type: 'DATE' }, // → TIMESTAMP
user_id: { type: 'STRING' },
payload: { type: 'JSON' },
},
{
partition: { type: 'time', column: 'ts', columnType: 'TIMESTAMP', granularity: 'DAY' },
clusterBy: ['user_id'],
requirePartitionFilter: true,
description: 'raw events',
},
);
The primary key is written at table level with NOT ENFORCED. BigQuery
rejects one declared without it, and a composite key can only be written that
way. Nothing enforces uniqueness.
autoIncrement on a STRING/UUID column becomes DEFAULT GENERATE_UUID() (a
random key, not a counter) and warns. On a numeric column it throws —
BigQuery has no IDENTITY, no AUTO_INCREMENT and no sequences, so quietly
turning an INT64 key into a string one would be worse than failing.
clusterBy takes at most four columns; a fifth throws.
require_partition_filter rejects queries that do not filter on the partition
column — the most effective guard against a surprise five-figure scan.
await db.dialect.createSearchIndex('logs', 'idx_logs', ['message']);
// CREATE SEARCH INDEX `idx_logs` ON `analytics`.`logs`(`message`)
await db.dialect.createVectorIndex('docs', 'idx_vec', 'embedding', { distanceType: 'COSINE' });
// CREATE VECTOR INDEX `idx_vec` ON `analytics`.`docs`(`embedding`)
// OPTIONS(index_type = 'IVF', distance_type = 'COSINE')
db.dialect.buildInsertQuery('users', { id: 'a', name: 'Ada' });
// INSERT INTO `analytics`.`users` (`id`, `name`) VALUES (?, ?)
db.dialect.buildUpdateQuery('users', { name: 'Ada L.' }, {});
// UPDATE `analytics`.`users` SET `name` = ? WHERE TRUE
RETURNING throws on INSERT, UPDATE, DELETE and MERGE. Re-SELECT
if you need the row back.
limit on UPDATE/DELETE throws. GoogleSQL has no such clause, and
ignoring the option would change more rows than the caller asked for.
truncate: true maps to TRUNCATE TABLE, which BigQuery does have.
bulkInsert() emits one multi-row INSERT, padding a ragged record set with
NULL so the column list stays consistent. BigQuery meters DML per table,
so a high-rate load should use streamInsert() (the tabledata.insertAll
API, which bypasses the DML quota at the cost of streaming-buffer semantics)
or a load job.
QUALIFY is supported as a raw expression on the select options, for
filtering on a window function without a wrapping subquery.
union / unionType translate to GoogleSQL's set operators: 'UNION' becomes
UNION DISTINCT, 'EXCEPT' becomes EXCEPT DISTINCT, 'INTERSECT' becomes
INTERSECT DISTINCT. Each operand is parenthesised, because GoogleSQL rejects
a set-operation input carrying its own ORDER BY/LIMIT otherwise.
TABLESAMPLE SYSTEM (n PERCENT) is supported; BERNOULLI and
REPEATABLE(seed) throw — BigQuery has neither.
lock (SELECT … FOR UPDATE) throws: BigQuery uses snapshot isolation and
has no row locks.
distinctOn throws, pointing at the
QUALIFY ROW_NUMBER() OVER (PARTITION BY …) = 1 idiom that does the same job.
Operators without a GoogleSQL spelling are translated rather than passed
through: $iLike → LOWER(col) LIKE LOWER(?), $startsWith/$endsWith →
STARTS_WITH/ENDS_WITH, $substring → STRPOS(...) > 0, $regexp →
REGEXP_CONTAINS.
An empty $in compiles to FALSE and an empty $notIn to TRUE, because
IN () is a syntax error.
order: random() renders RAND().
ARRAY with no element type throws — GoogleSQL has no bare ARRAY.
ARRAY<ARRAY<…>> throws — BigQuery cannot nest repeated fields directly.
RANGE of anything but DATE/DATETIME/TIMESTAMP throws.
SET throws — mapping it to ARRAY<STRING> would silently drop both the
value whitelist and the deduplication SET implies.
escape() has no column-type context. A JS Date is always emitted as a
TIMESTAMP literal, which BigQuery rejects for a DATE column — pass a
'YYYY-MM-DD' string to DATEONLY columns. A JS array or object becomes a
JSON literal, not an ARRAY/STRUCT one; use arrayLiteral() /
structLiteral() (or a Literal) for repeated and struct columns.
escapeId() throws on a backtick, backslash, newline or NUL. There is no
escape for them inside a backtick-quoted identifier, so passing one through
would close the quote early.
Prefer passing values as positional ? replacements — those become real
BigQuery query parameters and never reach the SQL text.
Constraints — UNIQUE and CHECK anywhere (column, table constraint,
uniqueKeys); ON DELETE/ON UPDATE referential actions (an unenforced
foreign key can never honour them).
Indexes — addIndex, createIndex, removeIndex, dropIndex,
createSpatialIndex.
Partition DDL — createPartitionedTable and the partition
create/attach/detach/drop methods.
changeColumn — BigQuery's ALTER COLUMN only widens a type, sets or
drops a default, drops NOT NULL, and sets a description; it cannot narrow a
type or add NOT NULL. Use setColumnDataType(), setColumnDefault(),
dropColumnDefault(), dropColumnNotNull(), or rebuild with
CREATE OR REPLACE TABLE … AS SELECT. Adding a NOT NULL column with no
default also throws — existing rows would have no value for it.
CASCADE on DROP TABLE / DROP VIEW (BigQuery has none; DROP SCHEMA
… CASCADE is supported).
Transactions and savepoints — see above.
CREATE DATABASE / DROP DATABASE — a project, not SQL.
Extensions and Foreign Data Wrappers — Postgres concepts. External data
is read through external tables and BigLake connections.
changeOwner, SQL users and SQL roles — identity is Cloud IAM.
buildGrantQuery/buildRevokeQuery throw because BigQuery's GRANT takes
IAM roles (roles/bigquery.dataViewer), which do not correspond one-to-one
with SQL privileges; use buildGrantRolesQuery() / buildRevokeRolesQuery()
for the real DCL.
Backtick quoting, the identifier rejections, and dataset / project.dataset
/ already-dotted table paths.
Backslash string escaping (explicitly not '' doubling), typed TIMESTAMP
/ FROM_BASE64 / JSON literals, NaN/Infinity spellings, and the
arrayLiteral/structLiteral helpers.
The full type-mapping table plus every type-level refusal.
CREATE TABLE clause order, table-level PRIMARY KEY … NOT ENFORCED,
composite keys, unenforced foreign keys, the autoIncrement split, the
clustering cap, and the UNIQUE/CHECK/index/uniqueKeys refusals.
Every partition expression form, and the partition-DDL refusals.
ALTER TABLE grammar (ADD/DROP/RENAME COLUMN, the four supported
ALTER COLUMN edits) and the changeColumn refusal.
Index refusals, CREATE SEARCH INDEX, CREATE VECTOR INDEX.
WHERE operator translation, the empty-IN constants, ORDER BY shapes,
RAND(), and that a standalone OFFSET never appears without a LIMIT.
SELECT assembly, * EXCEPT (...), QUALIFY, CTEs, TABLESAMPLE, and that
no bare UNION is emitted.
INSERT/bulk INSERT/UPDATE/DELETE/TRUNCATE/MERGE shapes and
parameter order, and the RETURNING and LIMIT refusals.
Transaction refusals and the multi-statement script builder.
Dataset, view, materialized-view, procedure and row-access-policy DDL.
The IAM-shaped GRANT/REVOKE, and that the SQL-privilege form refuses.