pg_searchpg driver, so ParadeDBDialect extends PostgresDialect andpg_search brings.| Property | Value |
|---|---|
| Dialect name | paradedb |
| Extends | PostgresDialect |
| Driver | pg |
| Default port | 5432 |
| Extension | pg_search |
| Cloud SDK | none |
| Local runtime | self-hosted; no image in this repo |
|||, &&&, ===, ### and @@@ operators with pdb.* functions and castparadedb.* function API is slated for removal upstream$ 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: 'paradedb',
host: 'localhost',
database: 'app',
username: 'postgres',
password: process.env.DB_PASSWORD,
});import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'paradedb',
url: 'paradedb://postgres:secret@localhost: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,
});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 { ParadeDBDialect } from 'prorm/dialects/paradedb';
const dialect = new ParadeDBDialect({
host: 'localhost',
username: 'postgres',
password: process.env.PG_PASSWORD,
database: 'app',
});
await dialect.query(dialect.createExtensionSQL()); // CREATE EXTENSION IF NOT EXISTS pg_searchParadeDBDialect extends PostgresDialect (postgres) andObject.getOwnPropertyNames(ParadeDBDialect.prototype),| Area | Overridden |
|---|---|
| DML builders | buildSearchCondition, buildSearchQuery, buildWhereClause |
| DDL and schema | addIndex, buildCreateSearchIndexSql, createExtensionSQL, createSearchIndex |
| Other | buildScoreExpression, buildSnippetExpression, buildTokenizerSql, compileSearch, fuzzySql, numberSql, operandSql, partialPredicate, slopSql |
dialect.buildCreateSearchIndexSql('mock_items', {
name: 'search_idx',
keyField: 'id',
fields: [
{ column: 'description', tokenizer: { name: 'simple', options: { stemmer: 'english' } } },
'category',
'rating',
],
});keyField is required, emitted first, and must be untokenized (aaccessMethod: 'paradedb' emits USING paradedb (0.25.0+); the defaultbm25 is a supported alias on every v2 release.concurrently: true (ParadeDB's recommended rebuild path), ifNotExists,where object are supported.{ name, min, max, pattern, language, options } renders as a pdb.* type:| Spec | SQL |
|---|---|
{ name: 'literal' } | pdb.literal |
{ name: 'ngram', min: 3, max: 3 } | pdb.ngram(3, 3) |
{ name: 'edge_ngram', min: 2, max: 5 } | pdb.edge_ngram(2, 5) |
{ name: 'lindera', language: 'japanese' } | pdb.lindera(japanese) |
{ name: 'regex_pattern', pattern: '\\w+' } | pdb.regex_pattern('\w+') |
{ name: 'simple', options: { k1: 0.9, b: 0.3 } } | pdb.simple('k1=0.9', 'b=0.3') |
unicode_words (the default), simple, whitespace, icu,literal, literal_normalized, source_code, chinese_compatible, jieba,lindera, ngram, edge_ngram, regex_pattern. Option values are escaped;paradedbSearch and compiled by the dialect —$n parameter:| Builder | SQL |
|---|---|
match('description', 'running shoes') | "description" ||| $1::text |
match(..., { mode: 'all' }) | "description" &&& $1::text |
match(..., ['running', 'shoes']) | ... ||| $1::text[] (exact tokens) |
match(..., { fuzzy: 2 }) | ... ||| $1::pdb.fuzzy(2) |
match(..., { fuzzy: 2, boost: 2 }) | ... $1::pdb.fuzzy(2)::pdb.boost(2) |
match(..., { constScore: 1 }) | ... $1::pdb.const(1) |
match(..., { tokenizer: { name: 'whitespace' } }) | ... $1::pdb.whitespace |
term('category', 'footwear') | "category" === $1::text |
term(..., 'rann', { fuzzy: { distance: 1, prefix: true } }) | === $1::pdb.fuzzy(1, t) |
phrase('description', 'shoes running', { slop: 2 }) | ### $1::pdb.slop(2) |
regex('description', 'key.*') | @@@ pdb.regex($1::text) |
parse('id', 'description:(sleek shoes) AND rating:>3', { lenient: true }) | "id" @@@ pdb.parse($1::text, lenient => true) |
all('id') | "id" @@@ pdb.all() |
and(...), or(...), not(...) | parenthesised combinations |
transpositionCostOne renders pdb.fuzzy(d, f, t).PARADEDB_SEARCH symbol and everyfindAll's SELECT, UPDATE and DELETE — compiles itimport { paradedbSearch as s, PARADEDB_SEARCH } from 'prorm/dialects/paradedb';
dialect.buildSelectQuery({
tableName: 'mock_items',
where: {
in_stock: true,
rating: { [Op.gte]: 4 },
[PARADEDB_SEARCH]: s.match('description', 'running shoes', { fuzzy: 1 }),
},
limit: 5,
});s.or(...)/s.and(...), not inside $or.buildSearchCondition(search, startAt) returns the bare { sql, values }.dialect.buildSearchQuery({
tableName: 'mock_items',
attributes: ['id', 'description'],
search: s.match('description', 'shoes'),
score: { keyField: 'id' },
snippets: [{ column: 'description', startTag: '<i>', endTag: '</i>' }],
orderByScore: true,
order: [['id', 'ASC']], // tie-breaker: equal scores are otherwise unordered
limit: 5,
});buildScoreExpression(key) and buildSnippetExpression(col, opts) return theaddIndex / createIndex behave as in PostgreSQL (partial indexes included).addIndex for using: 'bm25' throws and points atcreateSearchIndex, because that index cannot exist without a key_field.capabilitiesOf() (src/transaction-support.ts),whereSupportOf() (src/query-builders/expression-render.ts),sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the| Capability | paradedb |
|---|---|
| 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 | 35 operator keys; $and/$or/$not; $exists: no; $in subquery: no; expression operands: no |
| 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 |
tests/dialects/paradedb.test.ts exercises the dialect with a mocked driver, so nothing touches a server.