Reference for the monetdb SQL dialect.
Source: monetdb
Tests: monetdb
MonetDB is its own independent open-source
column-store analytical database — not a fork of PostgreSQL or MySQL, and not
related to them at the wire-protocol level. Its SQL grammar follows the SQL
standard closely enough, though (double-quoted identifiers, LIMIT n OFFSET m, ILIKE, CREATE SCHEMA, CREATE TABLE IF NOT EXISTS, sequences,
triggers, savepoints and a MERGE statement) that the PostgreSQL builders'
WHERE / ORDER BY / GROUP BY / JOIN / CTE output is also valid MonetDB
SQL. So MonetDBDialect extends PostgresDialect and keeps those
builders — every value still leaves them as a $n placeholder with the value
beside it — while replacing the entire connection layer, since MonetDB speaks
its own MAPI protocol, not PostgreSQL's.
| Property | Value |
|---|---|
| Dialect name | monetdb |
| Extends | PostgresDialect |
| Driver | monetdb (monetdb-nodejs), loaded lazily with require() on first connect |
| Default port | 50000 (MONETDB_DEFAULT_PORT, the monetdbd MAPI port) |
Registered with Prorm | yes, as SqlDialect.MonetDB = 'monetdb' in src/dialects/loaders.ts |
| Cloud SDK | none |
| Local runtime | self-hosted; no image in this repo |
MonetDBDialect is registered with Prorm, so new Prorm({ dialect: 'monetdb', ... }) works. It can also be constructed directly:
import { Prorm } from 'ts-prorm-orm';
const d = new Prorm({
dialect: 'monetdb',
host: 'localhost',
// port defaults to MONETDB_DEFAULT_PORT (50000)
database: 'demo', // defaults to 'demo'
username: 'monetdb',
password: process.env.MONETDB_PASSWORD,
dialectOptions: {
connectOptions: { language: 'sql' }, // passed straight through to `new Connection(...)`
},
});
await d.connect();
d.name === 'monetdb'; d.library === 'monetdb'.
$ npm install ts-prorm-orm monetdb
$ pnpm add ts-prorm-orm monetdb
$ yarn add ts-prorm-orm monetdb
$ bun add ts-prorm-orm monetdb
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'monetdb',
host: 'localhost',
database: 'demo',
username: 'monetdb',
password: process.env.MONETDB_PASSWORD,
});
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'monetdb',
url: 'monetdb://monetdb:monetdb@localhost:50000/demo',
});
A URL is folded into 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) },
});
await db.sync(); // CREATE TABLE — see Generated SQL below
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,
});
Each block is the statement the model call above it hands the dialect's query(), captured with the driver stubbed out. The model layer inlines the values.
db.sync() for the model in the Quick start:
CREATE TABLECREATE TABLE IF NOT EXISTS "users" ("id" INTEGER AUTO_INCREMENT NOT NULL, "email" VARCHAR(255) NOT NULL UNIQUE, "name" VARCHAR(100), "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("id"));
await User.create({ email: 'ada@example.com', name: 'Ada' });
await User.update({ name: 'Grace' }, { where: { email: 'ada@example.com' } });
await User.destroy({ where: { id: 1 } });
-- insert
INSERT INTO "users" ("email", "name") VALUES ('ada@example.com', 'Ada');
-- update
UPDATE "users" SET "name" = 'Grace', "updatedAt" = TIMESTAMP '2026-09-27 05:01:42.307' WHERE "email" = 'ada@example.com';
-- delete
DELETE FROM "users" WHERE "id" = 1;
await User.upsert({ id: 1, email: 'ada@example.com', name: 'Ada Lovelace' });
MERGE statementMERGE INTO "users" AS tgt USING (SELECT 1 AS "id", 'ada@example.com' AS "email", 'Ada Lovelace' AS "name") AS src ON tgt."id" = src."id" WHEN MATCHED THEN UPDATE SET "email" = src."email", "name" = src."name" WHEN NOT MATCHED THEN INSERT ("id", "email", "name") VALUES (src."id", src."email", src."name");
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,
});
SELECT * FROM "users" WHERE "name" LIKE 'A%' ORDER BY "id" ASC LIMIT 20 OFFSET 40;
[!NOTE] No JSON operator in the WHERE builder
DataTypes.JSON()maps toJSON, but this dialect's WHERE builder refusesOp.containson it: MonetDB does not support the @> / <@ containment operators. The condition used a PostgreSQL-only operator; MonetDB reads JSON with json.filter / json.text.
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(() => {});
});
START TRANSACTION;
INSERT INTO "users" ("email", "name") VALUES ('grace@example.com', 'Grace');
SAVEPOINT "prorm_sp_1";
DELETE FROM "users" WHERE "id" = 1;
ROLLBACK TO SAVEPOINT "prorm_sp_1";
RELEASE SAVEPOINT "prorm_sp_1";
COMMIT;
sample on the select builder reads a random fraction of the table, for estimates over large tables:
const sample = db.getDialectInstance()!.buildSelectQuery({
tableName: 'users',
sample: { method: 'BERNOULLI', percent: 10 },
});
await db.query(sample.sql);
SELECT * FROM "users" SAMPLE 0.1;
Type mapping for the portable descriptors: STRING → VARCHAR(255), auto-increment INTEGER → INTEGER, BOOLEAN → BOOLEAN, DATE → TIMESTAMP, UUID → UUID, JSON → JSON. The type comparison lines the major engines up side by side.
| Option | Default | Effect |
|---|---|---|
host | 'localhost' | MAPI host |
port | 50000 | MAPI port |
database | 'demo' | database name |
username | — | passed to new Connection(...) if set |
password | — | passed to new Connection(...) if set |
connectOptions | — | any further monetdb Connection options, spread in last |
connect() calls new Connection(buildConnectOptions()) then await conn.connect(); disconnect() closes it. getConnection() returns the raw
driver connection; isConnected() reflects whether one is open.
Values are bound by the server, not inlined: query() rewrites $n (array
replacements) or :name (object replacements) placeholders in the SQL to
MonetDB's ? markers via toWire(), then:
await conn.execute(sql);await conn.prepare(sql), await stmt.execute(...params), await stmt.release().dialect.buildSelectQuery({ tableName: 'users', where: { name: "O'Brien" }, limit: 5 });
// sql: SELECT * FROM "users" WHERE "name" = $1 LIMIT 5
// after toWire + prepare: 'SELECT * FROM "users" WHERE "name" = ? LIMIT 5', args: ["O'Brien"]
toDriverValue() converts bind values for the wire: Date → 'YYYY-MM-DD HH:MI:SS.fff' (UTC), bigint → string, Buffer → uppercase hex, a plain
object → JSON.stringify, and an array throws (MonetDB has no array type;
bind the elements individually). A result with no columns reports
rowCount from affectedRows; otherwise each row is zipped into an object
keyed by column name.
getDatabaseVersion() reads sys.environment where name = 'monet_version'.
startTransaction() opens a dedicated connection, runs START TRANSACTION on it, and returns a MonetDBTransaction; commit() /
rollback() run COMMIT / ROLLBACK on that connection and close it.
Savepoints are inherited unchanged from PostgresDialect
(createSavepointSQL('sp1') → 'SAVEPOINT sp1') — MonetDB has them.
MonetDB uses optimistic concurrency control: there is no per-transaction
isolation level to pick, so passing isolationLevel throws.
MonetDBDialect extends PostgresDialect (postgres) and
overrides 105 of its methods (Object.getOwnPropertyNames(MonetDBDialect.prototype),
computed by the scratch test); everything else is inherited unchanged.
| Area | Overridden |
|---|---|
| Connection and execution | commitTransaction, connect, disconnect, getConnection, isConnected, listen, notify, query, queryStream, rollbackTransaction, startTransaction, unlisten |
| DML builders | buildDeleteQuery, buildInsertQuery, buildLimitOffset, buildMergeSql, buildSampleClause, buildSelectQuery, buildUpdateQuery, buildUpsertQuery, buildWhereClause, escape, quoteTable, replaceReplacements |
| Types | buildColumnSql, canonicalTypeSql, createEnumType, ensureEnumTypes, getDataTypeSql, mapNativeType, unsupportedType |
| DDL and schema | addColumn, addIndex, addMergePartition, attachPartition, buildAddPartitionSql, buildCreateIndexSql, buildCreateSequenceSql, buildCreateTableSql, buildCreateTriggerSql, buildDropPartitionSql, changeColumn, createDatabaseSQL, createDomain, createExtension, createForeignDataWrapper, createForeignServer, createForeignTable, createFulltextIndex, createGINIndex, createGISTIndex, createIndex, createMaterializedView, createPartition, createPartitionedTable, createPolicy, createProcedure, createSequence, createStoredProcedure, createTable, createTrigger, createVectorIndex, detachPartition, dropDatabaseSQL, dropExtension, dropIndex, dropMergePartition, dropSequence, dropTable, dropTrigger, enableRLS, refreshMaterializedView, removeIndex |
| Introspection | assertIndexOptions, assertNoReturning, describeTable, getDatabaseVersion, getExtensions, hasExtension, hasPolicy, hasSequence, hasTrigger, listSequences, showTables, showViews |
| Other | bound, buildConnectOptions, buildCopyIntoSql, buildCopyToSql, buildMapiUri, connectionFor, copyFrom, copyInto, copyTo, currSequenceValue, delimitersSql, formatDefault, nextSequenceValue, openConnection, pgAdvisoryLock, pgTryAdvisoryLock, quoteString, refuse, toDriverValue, toWire |
escape() doubles backslashes as well as apostrophes — MonetDB reads C-style
backslash escapes inside ordinary '...' literals, so a value like
C:\temp would otherwise arrive with a tab in it:
d().escape("C:\\temp\\it's"); // 'C:\\temp\\it''s'
Dates become typed TIMESTAMP '...' literals in UTC, and Buffers a BLOB '<hex>' literal:
d().escape(new Date(Date.UTC(2024, 4, 6, 7, 8, 9, 10))); // TIMESTAMP '2024-05-06 07:08:09.010'
d().escape(Buffer.from([0xde, 0xad])); // BLOB 'DEAD'
quoteTable(table, schema) never elides the schema — MonetDB's default
schema is sys, not public — so quoteTable('t', 'sys') is "sys"."t".
| ORM | MonetDB |
|---|---|
| STRING(n) / CHAR(n) / TEXT | VARCHAR(n) / CHAR(n) / TEXT (= CLOB) |
| TINYINT / SMALLINT / INTEGER / BIGINT | same names |
| FLOAT / DOUBLE | REAL / DOUBLE |
| DECIMAL(p,s) | DECIMAL(p,s), p ≤ 38 (throws above 38) |
| BOOLEAN | BOOLEAN |
| DATE / DATEONLY / TIME | TIMESTAMP(n) / DATE / TIME(n) |
| UUID / JSON / JSONB / INET | UUID / JSON / JSON / INET |
| BLOB | BLOB |
| ENUM | VARCHAR(longest value) — no enum type |
| GEOMETRY | GEOMETRY (needs the geom module) |
a raw DATETIME(n) type string | rewritten to TIMESTAMP(n) |
ARRAY, HSTORE, VECTOR, RANGE, CIDR, MACADDR, GEOGRAPHY and any other unknown
key throw ("MonetDB does not support the column type '...'").
buildCreateTableSql supports MonetDB's table kinds through kind on
MonetDBTableOptions: MERGE, REPLICA, REMOTE, UNLOGGED, LOCAL TEMPORARY, GLOBAL TEMPORARY (default: an ordinary table).
d().buildCreateTableSql('ev', cols, { kind: 'MERGE', mergePartitionBy: { type: 'RANGE', column: 'd' } });
// CREATE MERGE TABLE "ev" ("id" INTEGER, "d" DATE) PARTITION BY RANGE ON ("d")
d().buildCreateTableSql('r', { id: { type: INT } }, {
kind: 'REMOTE',
remote: { host: 'node2.local', database: 'db', schema: 'sys', table: 'orders', user: 'u', password: "p'w" },
});
// CREATE REMOTE TABLE "r" ("id" INTEGER) ON 'mapi:monetdb://node2.local:50000/db/sys/orders' WITH USER 'u' PASSWORD 'p''w'
d().buildCreateTableSql('tmp', { id: { type: INT } }, { kind: 'LOCAL TEMPORARY' });
// CREATE LOCAL TEMPORARY TABLE "tmp" ("id" INTEGER) ON COMMIT PRESERVE ROWS
buildMapiUri() validates every host/database/schema/table token against
^[A-Za-z0-9._-]+$ before building the mapi:monetdb://host:port/db[/schema/table]
URI. A raw partitionBy throws, pointing at kind: 'MERGE' with
mergePartitionBy instead; mergePartitionBy without kind: 'MERGE',
remote without kind: 'REMOTE', and onCommit on a non-TEMPORARY kind
all throw.
d().buildAddPartitionSql('m', 'p1', { range: { from: 1, to: 100 }, withNullValues: true });
// ALTER TABLE "m" ADD TABLE "p1" AS PARTITION FROM 1 TO 100 WITH NULL VALUES
d().buildAddPartitionSql('m', 'p2', { range: { from: null, to: '2024-01-01' } });
// ALTER TABLE "m" ADD TABLE "p2" AS PARTITION FROM RANGE MINVALUE TO '2024-01-01'
d().buildAddPartitionSql('m', 'p3', { values: ["x'y", 'z'] });
// ALTER TABLE "m" ADD TABLE "p3" AS PARTITION IN ('x''y', 'z')
d().buildDropPartitionSql('m', 'p1');
// ALTER TABLE "m" DROP TABLE "p1"
addMergePartition / dropMergePartition run these. Exactly one of range,
values or forNullValues may be given.
ORDERED and IMPRINTS are MonetDB's column-store index kinds and cover
exactly one column; every index (including these) is treated by MonetDB as
an optimizer hint, not a constraint:
d().buildCreateIndexSql('t', { name: 'ix', fields: ['a'], kind: 'IMPRINTS' });
// CREATE IMPRINTS INDEX "ix" ON "t" ("a")
removeIndex / dropIndex emit DROP INDEX "n" with no ON table — index
names are schema-scoped in MonetDB. addIndex/createIndex refuse using
(access methods — use kind), where (partial indexes), include and
concurrently.
changeColumn retypes with ALTER TABLE t ALTER COLUMN c <type> — no
PostgreSQL TYPE keyword — then SET [NOT] NULL and SET DEFAULT as
separate statements (releases before Dec2023 cannot retype a column at all).
buildLimitOffset validates limit/offset as non-negative integers
(toCount), rejecting e.g. '5 OR 1=1'.
buildSelectQuery adds a trailing SAMPLE, MonetDB's row-level sampling
clause:
d().buildSelectQuery({ tableName: 't', monetdbSample: { rows: 100, seed: 7 } });
// SELECT * FROM "t" SAMPLE 100 SEED 7
d().buildSelectQuery({ tableName: 't', sample: { method: 'BERNOULLI', percent: 25, seed: 3 } });
// SELECT * FROM "t" SAMPLE 0.25 SEED 3 (PostgreSQL TABLESAMPLE is translated: percent/100 -> fraction)
A PostgreSQL sample.method other than 'BERNOULLI' throws (MonetDB
SAMPLE is row-level, not block-level); lock: true (FOR UPDATE/FOR SHARE) throws, since MonetDB uses optimistic concurrency instead of row
locks; sample and monetdbSample together throw; a SAMPLE clause on a
dialect-built UNION throws.
buildWhereClause keeps ILIKE (MonetDB has it) but scans the compiled SQL
(outside string/identifier literals) and throws on PostgreSQL-only operators:
->/->>/#>/#>>, @>/<@, &&, ::, and ~/!~/~*/!~*.
MonetDB's wire does not speak PostgreSQL's COPY FROM STDIN stream, so
copyFrom()/copyTo() both refuse and point at the structured builders
below.
d().buildCopyIntoSql('t', {
from: ['/data/a.csv', "/data/o'b.csv"],
onClient: true,
columns: ['a', 'b'],
records: 1000,
offset: 2,
delimiters: { field: ',', record: '\n', quote: '"' },
nullAs: '',
bestEffort: true,
});
// COPY 1000 RECORDS OFFSET 2 INTO "t" ("a", "b") FROM '/data/a.csv', '/data/o''b.csv' ON CLIENT
// USING DELIMITERS ',', '\n', '"' NULL AS '' BEST EFFORT
d().buildCopyToSql({ select: { tableName: 't', where: { a: "x'y" } }, to: '/tmp/out.csv', delimiters: { field: '|' } });
// COPY SELECT * FROM "t" WHERE "a" = 'x''y' INTO '/tmp/out.csv' ON SERVER USING DELIMITERS '|'
copyInto(table, options) runs buildCopyIntoSql through query().
buildCopyToSql inlines its SELECT's bound values as escaped literals,
because COPY is not a preparable statement. records/offset are
validated as non-negative integers; 'STDIN'/'STDOUT' cannot be combined
with onClient.
MonetDB spells sequences differently from PostgreSQL: NEXT VALUE FOR "s",
not nextval('s').
d().buildCreateSequenceSql({ name: 's', startWith: 10, incrementBy: 5, cycle: true });
// CREATE SEQUENCE "s" START WITH 10 INCREMENT BY 5 CYCLE
OR REPLACE / IF NOT EXISTS / TEMPORARY / ownedBy all throw.
nextSequenceValue() runs SELECT NEXT VALUE FOR "s" AS value;
currSequenceValue() calls get_value_for(CURRENT_SCHEMA, $1);
hasSequence() and listSequences() read sys.sequences.
MonetDB trigger bodies are inline — there is no companion trigger function as
on PostgreSQL — and a trigger fires on exactly one event:
d().buildCreateTriggerSql({
name: 'audit', tableName: 't', timing: 'AFTER', events: ['INSERT'], level: 'ROW',
referencing: { newRow: 'n' }, body: 'INSERT INTO log VALUES (n.id);',
});
// CREATE TRIGGER "audit" AFTER INSERT ON "t" REFERENCING NEW ROW AS "n"
// FOR EACH ROW BEGIN ATOMIC INSERT INTO log VALUES (n.id); END
INSTEAD OF timing, more than one event, and constraint triggers all throw.
dropTrigger / hasTrigger are schema-scoped like indexes and sequences.
MonetDB has no ON CONFLICT and no RETURNING. Single-row upserts compile
to a MERGE statement:
d().buildUpsertQuery('t', { id: 1, v: 'x' }, { conflictFields: ['id'] });
MERGE INTO "t" AS tgt USING (SELECT $1 AS "id", $2 AS "v") AS src ON tgt."id" = src."id"
WHEN MATCHED THEN UPDATE SET "v" = src."v"
WHEN NOT MATCHED THEN INSERT ("id", "v") VALUES (src."id", src."v")
-- values: [1, 'x']
buildInsertQuery routes to the same buildMergeSql when options.upsert
is set; conflictFields is required. buildUpdateQuery/buildDeleteQuery
throw on limit (there is no UPDATE/DELETE ... LIMIT; narrow the
WHERE instead) and on returning. buildDeleteQuery({ truncate: true, restartIdentity: true }) emits TRUNCATE TABLE "t" RESTART IDENTITY.
| Call | Why |
|---|---|
| array bind values / array literals | MonetDB has no array type; bind or escape elements individually. |
INSERT ... RETURNING / upsert RETURNING | No RETURNING; read affected rows back with a SELECT in the same transaction. |
UPDATE ... LIMIT / DELETE ... LIMIT | Narrow the WHERE clause instead. |
SELECT ... FOR UPDATE / FOR SHARE (lock) | MonetDB uses optimistic concurrency: a conflicting transaction fails at COMMIT rather than waiting on a lock. |
startTransaction({ isolationLevel }) | No per-transaction isolation level; every transaction runs under optimistic concurrency control. |
TABLESAMPLE method other than BERNOULLI | MonetDB SAMPLE is row-level. |
PostgreSQL JSON/array/regex operators (->, @>, &&, ::, ~) | MonetDB reads JSON with json.filter/json.text instead. |
addIndex/createIndex with using, where, include, concurrently | Use kind: 'ORDERED'/'IMPRINTS'; index the whole column; a column store reads columns independently; create the index normally. |
dropIndex with ifExists/cascade | Drop the index by name. |
copyFrom / copyTo | MonetDB's wire doesn't speak PostgreSQL's COPY FROM/TO STDIN stream; use copyInto/buildCopyToSql. |
CREATE SEQUENCE ... OR REPLACE/IF NOT EXISTS/TEMPORARY/OWNED BY | Create the sequence plainly; drop it first to replace it. |
Trigger INSTEAD OF, several events, constraint triggers | MonetDB triggers fire BEFORE/AFTER on one event, and are not constraint triggers. |
createDatabaseSQL / dropDatabaseSQL | MonetDB databases are managed with monetdb create <name> / monetdb destroy <name> against monetdbd, not SQL. |
createStoredProcedure/createProcedure | MonetDB procedures use its own SQL/PSM dialect, not generated here. |
createMaterializedView/refreshMaterializedView | No materialized views; use CREATE TABLE ... AS SELECT ... WITH DATA. |
createExtension/dropExtension | Optional modules (e.g. geom) are compiled into the server, not installed as extensions. |
createEnumType | An ENUM column is stored as VARCHAR; add a CHECK constraint. |
createDomain | Declare the type and constraint on each column. |
createPolicy/enableRLS | Restrict rows with views and GRANTs. |
listen/unlisten/notify | No LISTEN/NOTIFY; poll a table, or use a trigger that writes to a queue table. |
pgAdvisoryLock/pgTryAdvisoryLock | MonetDB uses optimistic concurrency, not locks. |
createPartitionedTable/createPartition/attachPartition/detachPartition | Use a MERGE table (kind: 'MERGE', mergePartitionBy) with addMergePartition/dropMergePartition. |
createGINIndex/createGISTIndex/createVectorIndex/createFulltextIndex | MonetDB index kinds are plain, UNIQUE, ORDERED and IMPRINTS only; no pgvector, no tsvector. |
createForeignDataWrapper/createForeignServer/createForeignTable | Read another MonetDB server through a REMOTE table (kind: 'REMOTE'). |
monetdb / monetdb-nodejs) is mocked in the test suite as atests/dialects/monetdb.test.ts); it has not been runmonetdbd in this repo.getExtensions()/hasExtension()/hasPolicy() return []/false ratherqueryStream() fetches the whole result first, then emits it row by row —DECIMAL precision above 38 throws; the comment on getDataTypeSql notesdescribeTable() infers autoIncrement by testing the column default for/next value for/i — a column whose default happens to be a literalNEXT VALUE FOR expression from a different sequence would also read asComputed from the code — capabilitiesOf() (src/transaction-support.ts),
whereSupportOf() (src/query-builders/expression-render.ts),
sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the
builders — by the scratch test described in the
capability matrix.
| Capability | monetdb |
|---|---|
| Transactions atomic | yes |
| Savepoints | yes |
| Isolation | up to SERIALIZABLE |
| Transaction modes | read-only yes; deferrable yes; constraint deferral yes |
| RETURNING | refused — MonetDB does not support RETURNING. Read the affected rows back with a SELECT in the same transaction. |
| Upsert | MERGE INTO … WHEN MATCHED / WHEN NOT MATCHED |
| JSON column | JSON (JSONB → JSON) |
| Vector column | refused — MonetDB does not support the column type 'VECTOR'. Store the vector as JSON or a BLOB. |
| Array column | refused — MonetDB does not support the column type 'ARRAY'. MonetDB has no composite types; store JSON, or use a child table. |
| WHERE builder | 35 operator keys; $and/$or/$not; $exists: no; $in subquery: no; expression operands: no |
| 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 |