SOURCEs, is transformed by VIEWs, and is kept continuously up toMATERIALIZED VIEWs and INDEXes that are recomputed incrementally asMaterializeDialect extends PostgresDialect and reuses the pg driver,$n parameter binding, double-quoted identifier escaping,findAll-shaped reads compile through the inheritedbuildSelectQuery with every value bound, so reading a materialized view| Property | Value |
|---|---|
| Dialect name | materialize |
| Extends | PostgresDialect |
| Driver | pg |
| Default port | 6875 (not 5432) |
| Cloud SDK | none |
| Local runtime | self-hosted; no image in this repo |
$ 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: 'materialize',
host: 'abc123.us-east-1.aws.materialize.cloud',
database: 'materialize',
username: 'you@example.com',
password: process.env.MZ_APP_PASSWORD,
dialectOptions: {
ssl: { rejectUnauthorized: true },
},
});import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'materialize',
url: 'materialize://you%40example.com:secret@abc123.us-east-1.aws.materialize.cloud:6875/materialize',
dialectOptions: {
ssl: { rejectUnauthorized: true },
},
});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(), allowNull: false },
email: { type: DataTypes.STRING(255), allowNull: false },
name: { type: DataTypes.STRING(100) },
meta: { type: DataTypes.JSONB() },
});
await db.sync(); // CREATE TABLE — see Generated SQL belowemail is not declared unique: the engine has no enforced UNIQUE constraint. No primary key: the engine's tables have none. meta is JSONB, which the containment query below needs.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,
});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({ id: 1, email: 'ada@example.com', name: 'Ada' });
await User.update({ name: 'Grace' }, { where: { email: 'ada@example.com' } });
await User.destroy({ where: { id: 1 } });[!WARNING] No upsert
Materialize has no ON CONFLICT clause — a table has no unique constraint for a conflict to be detected against. Upsert belongs on the way in: a source declared with ENVELOPE UPSERT keys the stream and keeps the latest value per key. For a table written by hand, DELETE the key and INSERT inside one transaction.
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({ id: 2, 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 materialize has none. Run the work in a single transaction, or restructure it so nothing nests.
STRING → VARCHAR(255), auto-increment INTEGER → refused, BOOLEAN → BOOLEAN, DATE → TIMESTAMP, UUID → UUID, JSON → JSONB. The type comparison lines the major engines up side by side.import { Prorm } from 'ts-prorm-orm';
const db = new Prorm({
dialect: 'materialize',
host: 'xxx.materialize.cloud',
port: 6875, // the default; no override needed
username: 'you@example.com',
password: process.env.MZ_APP_PASSWORD,
database: 'materialize',
dialectOptions: { ssl: { rejectUnauthorized: true } }, // Materialize Cloud requires TLS
});password, andMaterializeDialect extends PostgresDialect (postgres) andObject.getOwnPropertyNames(MaterializeDialect.prototype),| Area | Overridden |
|---|---|
| Connection and execution | listen, notify, startTransaction, unlisten |
| DML builders | buildInsertQuery, buildUpsertQuery, formatDefaultValue |
| Types | binaryTypeSql, buildColumnSql, canonicalTypeSql, collectionTypeSql, createEnumType, documentTypeSql, enumTypeSql, extensionTypeSql, getDataTypeSql, integerTypeSql, legacyInvalidTypeNames, legacyTypeReplaced, namedEnumTypes, networkTypeSql, numericTypeSql, rangeTypeSql, spatialTypeSql, temporalTypeSql, vectorTypeSql |
| DDL and schema | addColumn, addForeignKey, addIndex, alterRefusal, attachPartition, buildCreateIndexSql, buildCreateMaterializedViewSql, buildCreateSinkSql, buildCreateSourceSql, buildCreateTableSql, changeColumn, createCluster, createDomain, createExtension, createIndex, createMaterializeIndex, createMaterializedView, createPartition, createPartitionedTable, createPolicy, createSavepointSQL, createSequence, createSink, createSource, createStoredProcedure, createTable, createTrigger, detachPartition, disableRLS, dropCluster, dropExtension, dropPartition, dropPolicy, dropSequence, dropStoredProcedure, dropTrigger, enableRLS, refreshMaterializedView, removeColumn, renameColumn |
| Introspection | assertConstraintsSupported, assertIndexOptionsSupported, getExtensions, hasExtension, hasPolicy, hasSequence, hasStoredProcedure, hasTrigger, resolveMzTimestamp |
| Other | buildSubscribeQuery, nextSequenceValue, partitionRefusal, releaseSavepointSQL, rlsRefusal, rollbackToSavepointSQL, savepointRefusal, sequenceRefusal, upsertRefusal |
TABLEs exist and INSERT/UPDATE/DELETE work on them, but they are forREFRESH MATERIALIZED VIEW does not existREFRESH MATERIALIZED VIEW, a syntax error here, and theMaterialize has no REFRESH MATERIALIZED VIEW ('mv'). A Materialize materialized
view is recomputed incrementally as its inputs change and is always current —
there is no stale state to refresh away. If the goal is to read it at a
specific logical time, use `AS OF` (or SUBSCRIBE) instead.PRIMARY KEY, no UNIQUE, no FOREIGN KEY, no CHECK, and no SERIALawait db.dialect.createTable('t', {
id: { type: DataTypes.BIGINT, allowNull: false },
doc: { type: DataTypes.JSON },
});
// CREATE TABLE "t" ("id" BIGINT NOT NULL, "doc" JSONB)primaryKey: true column throws and says where the invariant belongs:Materialize tables have no PRIMARY KEY (column 'id'). Tables are unconstrained
inputs to a dataflow; deduplicate in the view that reads them, or let the
source apply ENVELOPE UPSERT on the way in.ALTER TABLE cannot reshape a relationaddColumn, removeColumn, changeColumn and renameColumn all throw.ALTER covers RENAME, OWNER TO, SET CLUSTER and the| Asked for | What happens |
|---|---|
refreshMaterializedView() | Refused — a Materialize MV is always current |
Column primaryKey / unique / references / autoIncrement | Refused, each naming the alternative |
| Any table constraint | Refused |
uniqueKeys, partitionBy on createTable | Refused |
addColumn / removeColumn / changeColumn / renameColumn | Refused |
addForeignKey() | Refused |
buildUpsertQuery() / { upsert: true } | Refused — points at ENVELOPE UPSERT |
LISTEN / UNLISTEN / NOTIFY | Refused — points at SUBSCRIBE and SINK |
Sequences (createSequence, nextSequenceValue, …) | Refused; hasSequence() returns false |
| Triggers, policies/RLS, stored procedures | Refused; the has* probes return false |
| Extensions | Refused; getExtensions() returns [] |
| All five partitioning entry points | Refused |
createEnumType(), createDomain() | Refused |
HSTORE, VECTOR, PostGIS, CITEXT column types | Refused — no extensions to provide them |
UNIQUE / USING / partial / INCLUDE / CONCURRENTLY index options | Refused |
| PostgreSQL isolation levels | Refused — see below |
SAVEPOINT (all three builders) | Refused |
WITH NO DATA materialized view | Refused — not a state a Materialize view has |
Materialize has no ON CONFLICT clause — a table has no unique constraint for a
conflict to be detected against. Upsert belongs on the way *in*: a source
declared with ENVELOPE UPSERT keys the stream and keeps the latest value per
key. For a table written by hand, DELETE the key and INSERT inside one
transaction.STRICT SERIALIZABLE (the default), SERIALIZABLE, andSTRONG SESSION SERIALIZABLE. Asking for READ COMMITTED throws rather thanSUBSCRIBE — the streaming readSUBSCRIBE streams the changes to a query: each row carries mz_timestampmz_diff (+1 for an insert, -1 for a retraction) alongside the query'sbuildSubscribeQuery compiles one from the same findAll-shapedbuildSelectQuery takes, with every value bound:db.dialect.buildSubscribeQuery({
tableName: 'orders',
where: { status: 'open' },
progress: true,
envelopeUpsertKey: ['id'],
});snapshot, progress, envelopeUpsertKey, asOf and upTo are all$n values have to reach thequery()replacements before sending.MaterializeStore.subscribe() drives the cursorpg client with the values bound.db.dialect.buildCreateSourceSql({
name: 'kafka_src',
cluster: 'ingest',
from: "FROM KAFKA CONNECTION kc (TOPIC 'events') FORMAT JSON",
});
// CREATE SOURCE "kafka_src" IN CLUSTER "ingest" FROM KAFKA CONNECTION kc (TOPIC 'events') FORMAT JSONCONNECTION object rather thanbuildCreateSinkSql({ name, from, into, format, envelope }) is the mirrordb.dialect.buildCreateIndexSql('orders_view', {
name: 'idx',
cluster: 'serving',
fields: ['id'],
});
// CREATE INDEX "idx" IN CLUSTER "serving" ON "orders_view" ("id")
db.dialect.buildCreateIndexSql('orders_view');
// CREATE DEFAULT INDEX ON "orders_view"db.dialect.buildCreateMaterializedViewSql({
name: 'mv',
query: 'SELECT a, b FROM t',
columns: ['a', 'b'],
cluster: 'compute',
assertNotNull: ['a'],
retainHistoryFor: '1 hour',
});createCluster(name, { size, replicationFactor, introspectionInterval }) anddropCluster(name, { ifExists, cascade }) manage the unit of compute an object| Model type | Materialize | Note |
|---|---|---|
JSON | JSONB | Materialize implements jsonb only; a json column is a parse error |
ENUM | TEXT | CREATE TYPE ... AS ENUM is not implemented |
HSTORE, VECTOR, PostGIS, CITEXT | throws | Extension types, and there are no extensions |
anything autoIncrement | throws | No sequences, so no SERIAL |
capabilitiesOf() (src/transaction-support.ts),whereSupportOf() (src/query-builders/expression-render.ts),sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the| Capability | materialize |
|---|---|
| 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 | refused — Materialize has no ON CONFLICT clause — a table has no unique constraint for a conflict to be detected against. Upsert belongs on the way in: a source declare |
| JSON column | JSONB (JSONB → JSONB) |
| Vector column | refused — Materialize does not support pgvector types (it has no extensions) (column type 'VECTOR'). Transform the value before it reaches Materialize, or keep the column |
| Array column | INTEGER[] |
| WHERE builder | 62 operator keys; $and/$or/$not; $exists: yes; $in subquery: yes; expression operands: yes |
| SQL objects | family materialize: triggers no; procedures no; functions no; materialized views native; sequences no; enums check; RLS no |
| Streaming | queryStream() present; LISTEN present; createExtension() present |
tests/dialects/materialize.test.ts exercises the dialect with a mocked driver, so nothing touches a server.MaterializeStore — the connection-level store,find/count/insert/update/destroy/createSource/subscribe.