Sequelize
Sequelize connects to DB9 using the pg (node-postgres) driver over pgwire. Model definitions with Model.init(), associations (hasMany, belongsTo, belongsToMany), the Op query operators, and managed transactions work out of the box.
DB9 passes 100% of Sequelize compatibility tests (87/87) covering CRUD, associations, transactions, advanced SQL, and vector operations.
Prerequisites
Section titled “Prerequisites”- A DB9 database (create one)
- Node.js 18+
- Sequelize 6.35+
Create a DB9 Database
Section titled “Create a DB9 Database”db9 create --name sequelize-appGet the connection string:
db9 db status sequelize-appProject Setup
Section titled “Project Setup”mkdir sequelize-db9 && cd sequelize-db9npm init -ynpm install sequelize pgnpm install -D typescript @types/nodeConnection
Section titled “Connection”import { Sequelize } from 'sequelize';
const sequelize = new Sequelize({ dialect: 'postgres', host: 'pg.db9.io', port: 5433, username: 'sequelize-app.admin', password: 'YOUR_PASSWORD', database: 'postgres', logging: false, pool: { max: 5, min: 0, acquire: 30000, idle: 10000, },});
export default sequelize;Test the connection:
await sequelize.authenticate();console.log('Connected to DB9');Model Definitions
Section titled “Model Definitions”Sequelize uses Model.init() with column type definitions:
import { Model, DataTypes, Optional } from 'sequelize';import sequelize from '../connection';
interface UserAttributes { id: number; email: string; name: string; age: number; isActive: boolean; bio: string | null; metadata: Record<string, unknown> | null;}
interface UserCreationAttributes extends Optional<UserAttributes, 'id' | 'age' | 'isActive' | 'bio' | 'metadata'> {}
export class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes { declare id: number; declare email: string; declare name: string; declare age: number; declare isActive: boolean; declare bio: string | null; declare metadata: Record<string, unknown> | null; declare createdAt: Date; declare updatedAt: Date; declare posts?: Post[];}
User.init( { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, email: { type: DataTypes.STRING(255), allowNull: false, unique: true }, name: { type: DataTypes.STRING(100), allowNull: false }, age: { type: DataTypes.INTEGER, defaultValue: 0 }, isActive: { type: DataTypes.BOOLEAN, defaultValue: true }, bio: { type: DataTypes.TEXT, allowNull: true }, metadata: { type: DataTypes.JSONB, allowNull: true }, }, { sequelize, tableName: 'users', timestamps: true });import { Model, DataTypes, Optional } from 'sequelize';import sequelize from '../connection';
interface PostAttributes { id: number; title: string; content: string; published: boolean; authorId: number;}
interface PostCreationAttributes extends Optional<PostAttributes, 'id' | 'published'> {}
export class Post extends Model<PostAttributes, PostCreationAttributes> implements PostAttributes { declare id: number; declare title: string; declare content: string; declare published: boolean; declare authorId: number; declare createdAt: Date;}
Post.init( { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, title: { type: DataTypes.STRING(500), allowNull: false }, content: { type: DataTypes.TEXT, allowNull: false }, published: { type: DataTypes.BOOLEAN, defaultValue: false }, authorId: { type: DataTypes.INTEGER, allowNull: false }, }, { sequelize, tableName: 'posts', timestamps: true });import { Model, DataTypes, Optional } from 'sequelize';import sequelize from '../connection';
export class Tag extends Model { declare id: number; declare name: string;}
Tag.init( { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, name: { type: DataTypes.STRING(100), allowNull: false, unique: true }, }, { sequelize, tableName: 'tags', timestamps: false });Associations
Section titled “Associations”Define associations after all models are initialized:
import { User } from './User';import { Post } from './Post';import { Tag } from './Tag';
// One-to-ManyUser.hasMany(Post, { foreignKey: 'authorId', as: 'posts', onDelete: 'CASCADE' });Post.belongsTo(User, { foreignKey: 'authorId', as: 'author' });
// Many-to-ManyPost.belongsToMany(Tag, { through: 'post_tags', as: 'tags' });Tag.belongsToMany(Post, { through: 'post_tags', as: 'posts' });
export { User, Post, Tag };Create Tables
Section titled “Create Tables”Use raw SQL rather than sequelize.sync() for production, as Sequelize’s schema sync relies on information_schema introspection that may not fully work with DB9:
import sequelize from './connection';
async function setup() { await sequelize.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, age INT DEFAULT 0, "isActive" BOOLEAN DEFAULT true, bio TEXT, metadata JSONB, "createdAt" TIMESTAMPTZ DEFAULT now(), "updatedAt" TIMESTAMPTZ DEFAULT now() ) `);
await sequelize.query(` CREATE TABLE IF NOT EXISTS posts ( id SERIAL PRIMARY KEY, title VARCHAR(500) NOT NULL, content TEXT NOT NULL, published BOOLEAN DEFAULT false, "authorId" INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, "createdAt" TIMESTAMPTZ DEFAULT now(), "updatedAt" TIMESTAMPTZ DEFAULT now() ) `);
await sequelize.query(` CREATE TABLE IF NOT EXISTS tags ( id SERIAL PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL ) `);
await sequelize.query(` CREATE TABLE IF NOT EXISTS post_tags ( "postId" INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, "tagId" INT NOT NULL REFERENCES tags(id) ON DELETE CASCADE, "createdAt" TIMESTAMPTZ DEFAULT now(), "updatedAt" TIMESTAMPTZ DEFAULT now(), PRIMARY KEY ("postId", "tagId") ) `);
console.log('Tables created');}
setup();CRUD Operations
Section titled “CRUD Operations”import { Op } from 'sequelize';import { User, Post, Tag } from './models';
// Createconst user = await User.create({ email: 'alice@example.com', name: 'Alice', age: 30, metadata: { role: 'admin' },});
// Bulk createconst users = await User.bulkCreate([ { email: 'bob@example.com', name: 'Bob', age: 25 }, { email: 'carol@example.com', name: 'Carol', age: 35 },]);
// Readconst found = await User.findOne({ where: { email: 'alice@example.com' } });const byId = await User.findByPk(user.id);const all = await User.findAll({ where: { age: { [Op.gte]: 25 } }, order: [['age', 'DESC']], limit: 10, offset: 0,});
// Select specific columnsconst names = await User.findAll({ attributes: ['id', 'name'], where: { isActive: true },});
// Aggregatesconst count = await User.count();const totalAge = await User.sum('age');
// Updateawait User.update({ name: 'Alice Updated' }, { where: { id: user.id } });
// Instance updateconst alice = await User.findByPk(user.id);alice!.age = 31;await alice!.save();
// Upsertconst [upserted, created] = await User.upsert({ email: 'alice@example.com', name: 'Alice V2', age: 32,});
// Deleteawait User.destroy({ where: { id: user.id } });Operator queries
Section titled “Operator queries”import { Op } from 'sequelize';
// Comparisonawait User.findAll({ where: { age: { [Op.gt]: 25 } } });await User.findAll({ where: { age: { [Op.lte]: 30 } } });await User.findAll({ where: { name: { [Op.in]: ['Alice', 'Bob'] } } });
// Pattern matchingawait User.findAll({ where: { name: { [Op.like]: '%li%' } } });await User.findAll({ where: { name: { [Op.iLike]: '%alice%' } } });
// NULL checksawait User.findAll({ where: { bio: { [Op.is]: null } } });
// Logical operatorsawait User.findAll({ where: { [Op.or]: [ { [Op.and]: [{ age: { [Op.gte]: 30 } }, { bio: { [Op.not]: null } }] }, { name: 'Alice' }, ], },});Loading Associations
Section titled “Loading Associations”// Eager load (LEFT JOIN)const userWithPosts = await User.findByPk(1, { include: [{ model: Post, as: 'posts' }],});console.log(userWithPosts!.posts);
// Nested includesconst userFull = await User.findByPk(1, { include: [{ model: Post, as: 'posts', include: [{ model: Tag, as: 'tags' }], }],});
// Filtered includeconst userPublished = await User.findByPk(1, { include: [{ model: Post, as: 'posts', where: { published: true }, }],});
// INNER JOIN (only users with posts)const authors = await User.findAll({ include: [{ model: Post, as: 'posts', required: true }],});
// Many-to-Many: add and remove tagsconst post = await Post.findByPk(1);const tag = await Tag.create({ name: 'db9' });await post!.addTag(tag);await post!.removeTag(tag);Transactions
Section titled “Transactions”Managed transactions (auto-commit/rollback)
Section titled “Managed transactions (auto-commit/rollback)”import sequelize from './connection';
await sequelize.transaction(async (t) => { const user = await User.create( { email: 'txn@example.com', name: 'Txn User', age: 30 }, { transaction: t } );
await Post.create( { title: 'Transactional Post', content: 'Atomic.', authorId: user.id }, { transaction: t } ); // Auto-commits on success, auto-rolls back on error});Unmanaged transactions (manual control)
Section titled “Unmanaged transactions (manual control)”const t = await sequelize.transaction();
try { await User.create( { email: 'manual@example.com', name: 'Manual', age: 28 }, { transaction: t } ); await t.commit();} catch (err) { await t.rollback(); throw err;}Isolation levels
Section titled “Isolation levels”import { Transaction } from 'sequelize';
await sequelize.transaction( { isolationLevel: Transaction.ISOLATION_LEVELS.REPEATABLE_READ }, async (t) => { const users = await User.findAll({ transaction: t }); return users; });Supported: READ_COMMITTED, REPEATABLE_READ. SERIALIZABLE is not implemented — Sequelize will not error, but the transaction is silently downgraded to REPEATABLE_READ with a server WARNING. Set REPEATABLE_READ explicitly so the intent is visible in your code.
Raw SQL for Advanced Features
Section titled “Raw SQL for Advanced Features”import { QueryTypes } from 'sequelize';
// Window functionsconst ranked = await sequelize.query( `SELECT name, age, ROW_NUMBER() OVER (ORDER BY age DESC) AS rank FROM users ORDER BY rank`, { type: QueryTypes.SELECT });
// CTEsconst result = await sequelize.query( `WITH active AS ( SELECT * FROM users WHERE "isActive" = true ) SELECT name, age FROM active ORDER BY age`, { type: QueryTypes.SELECT });
// DISTINCT ONconst firstPerAge = await sequelize.query( `SELECT DISTINCT ON (age) name, age FROM users ORDER BY age, name`, { type: QueryTypes.SELECT });
// Parameterized queriesconst filtered = await sequelize.query( 'SELECT * FROM users WHERE age > $1 AND age < $2', { type: QueryTypes.SELECT, bind: [20, 35] });Vector Search
Section titled “Vector Search”Sequelize does not have a native vector column type. Use raw SQL for vector operations:
// Create vector table and indexawait sequelize.query(` CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS embeddings ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, embedding vector(3) NOT NULL ); CREATE INDEX IF NOT EXISTS idx_embeddings ON embeddings USING hnsw (embedding vector_l2_ops);`);
// Insert vectorsawait sequelize.query( `INSERT INTO embeddings (name, embedding) VALUES ($1, $2)`, { bind: ['doc-1', '[1.0, 2.0, 3.0]'] });
// Cosine distance searchconst results = await sequelize.query( `SELECT name, cosine_distance(embedding, '[1.0, 1.0, 1.0]') AS distance FROM embeddings ORDER BY distance ASC LIMIT 5`, { type: QueryTypes.SELECT });
// L2 distance with operatorconst nearest = await sequelize.query( `SELECT name FROM embeddings WHERE embedding <-> '[1.0, 0.0, 0.0]' < 1.0 ORDER BY embedding <-> '[1.0, 0.0, 0.0]'`, { type: QueryTypes.SELECT });DB9 supports HNSW indexes with vector_l2_ops, vector_cosine_ops, and vector_ip_ops; IVFFlat is not available at all. A CREATE INDEX ... USING hnsw builds a real index over both the wire protocol and the HTTP SQL API. The planner only uses it when the query’s distance metric matches the index’s opclass, the probe is an inlined literal, a LIMIT is present, and the query has no WHERE clause — the examples above build a vector_l2_ops index and then query with cosine_distance or without a LIMIT, so they run as exact scans. See When the index is used. If your code uses CREATE INDEX ... USING ivfflat, change it to USING hnsw.
Schema Changes
Section titled “Schema Changes”Manage migrations with raw SQL rather than Sequelize’s built-in migration tools:
// Add a columnawait sequelize.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS phone TEXT');
// Create an indexawait sequelize.query('CREATE INDEX IF NOT EXISTS idx_users_age ON users (age)');Production Notes
Section titled “Production Notes”- Driver: Sequelize uses
pg(node-postgres). Setdialect: 'postgres'in the configuration. - Connection pooling: Configure
pool.maxbased on your workload. DB9 supports multiple concurrent connections per tenant. - Timestamps: Use
timestamps: truein model options for automaticcreatedAt/updatedAtmanagement. - Upsert behavior: Sequelize’s
upsert()works with DB9, but thecreatedreturn value may benullinstead of a boolean. Check bothtrueandnullif you rely on the return value. - Connection string: Use
sequelize-app.adminas the username, with hostpg.db9.ioand port5433.
Troubleshooting
Section titled “Troubleshooting”sequelize.sync() creates unexpected schema
Section titled “sequelize.sync() creates unexpected schema”DB9’s information_schema support is limited. Sequelize’s schema sync may not detect existing tables correctly. Use raw DDL and avoid sync({ force: true }) in production.
Upsert created flag is null
Section titled “Upsert created flag is null”DB9 may return null for the created flag on upsert instead of true/false. If your logic depends on knowing whether a row was inserted or updated, query the row after upsert.
Connection timeout
Section titled “Connection timeout”Verify the host (pg.db9.io), port (5433), and username format ({database-name}.admin). DB9 routes connections by parsing the tenant from the username.
JSONB queries
Section titled “JSONB queries”Sequelize’s JSONB operators work with DB9. For advanced operations not covered by the ORM API, use sequelize.query() with PostgreSQL’s native JSONB operators (->, ->>, @>).
A bind value works unchanged as the key, path or operand of ->, ->>, #>, #>>, #-,
@> and <@. The key-existence operators ?, ?| and ?&, the - (delete key) operator, and
jsonb_extract_path() / jsonb_extract_path_text() are the exception: DB9 cannot infer a bound
parameter’s type there, and the query fails with
42P18 could not determine data type of parameter $1 (42883 for -). Cast the parameter — the
cast is also valid PostgreSQL. (This applies to bind only — Sequelize’s replacements are
inlined as literals before the query reaches the driver, so they are never bind parameters and
need no cast.)
// Works: ->> and @> infer a bound key or operandawait sequelize.query('SELECT metadata ->> $key FROM users', { bind: { key: 'role' },});await sequelize.query('SELECT id FROM users WHERE metadata @> $filter', { bind: { filter: JSON.stringify({ role: 'admin' }) },});
// Fails: a bound key for the ? operatorawait sequelize.query('SELECT id FROM users WHERE metadata ? $key', { bind: { key: 'role' },});
// Works: cast the parameterawait sequelize.query('SELECT id FROM users WHERE metadata ? $key::text', { bind: { key: 'role' },});Use ::text for ? and -, and ::text[] for ?| and ?& — the full table is in the
compatibility matrix.
The key-existence operators need one more precaution: ? is also Sequelize’s positional
replacement token, so metadata ? ? with a replacements array fails before the query is sent
(Positional replacement (?) 1 has no entry in the replacement map). Use named replacements or
bind, which do not collide:
// Fails: the ? operator is read as a positional replacementawait sequelize.query('SELECT id FROM users WHERE metadata ? ?', { replacements: ['role'],});
// Works: named replacement, no collisionawait sequelize.query('SELECT id FROM users WHERE metadata ? :key', { replacements: { key: 'role' },});Verified Compatibility
Section titled “Verified Compatibility”Tested with Sequelize 6.35+ against DB9. All 87 tests pass covering:
| Category | Status |
|---|---|
| Connection and pooling | Pass |
| CRUD with operator queries | Pass |
| Associations (hasMany, belongsTo, belongsToMany) | Pass |
| Transactions and isolation levels | Pass |
| Advanced SQL (window, CTE, DISTINCT ON) | Pass |
| Vector operations and HNSW indexing | Pass |
| DDL operations | Pass |
Next Pages
Section titled “Next Pages”- TypeORM — TypeORM with DB9
- Prisma — Prisma ORM with DB9
- Connect — connection strings and authentication
- Vector Extension — HNSW indexes and distance operators
- Compatibility Matrix — full PostgreSQL compatibility surface