StrictDB Documentation

Complete API reference for strictdb and strictdb-mcp. One unified API for MongoDB, PostgreSQL, MySQL, MSSQL, SQLite, and Elasticsearch.

Installation

Install the core package and the peer dependency for your database:

# Core package
npm install strictdb

# Add your database driver (pick one or more)
npm install mongodb              # MongoDB
npm install pg                   # PostgreSQL
npm install mysql2               # MySQL
npm install mssql                # MSSQL
npm install better-sqlite3       # SQLite
npm install @elastic/elasticsearch  # Elasticsearch
Zero unused dependencies. All database drivers are peer dependencies — you only install what you actually use.

Quick Start

StrictDB auto-detects your backend from the connection URI. No configuration files, no schema definitions required.

import { StrictDB } from 'strictdb';

// Auto-detects backend from URI
const db = await StrictDB.create({
  uri: 'postgresql://user:pass@localhost:5432/mydb'
});

// Read
const user = await db.queryOne('users', { email: 'alice@example.com' });

// Write — returns a receipt, never void
const receipt = await db.insertOne('users', {
  email: 'bob@example.com',
  name: 'Bob',
  role: 'user'
});
console.log(receipt.insertedCount); // 1

// Update with MongoDB-style operators
await db.updateOne('users',
  { email: 'bob@example.com' },
  { $set: { role: 'admin' }, $inc: { loginCount: 1 } }
);

// Same code works on Mongo, Postgres, MySQL, MSSQL, SQLite, Elasticsearch
await db.close();

URI Auto-Detection

URI PrefixBackendDriver
mongodb:// / mongodb+srv://mongomongodb
postgresql:// / postgres://sqlpg
mysql://sqlmysql2
mssql://sqlmssql
sqlite: / file:sqlbetter-sqlite3
http:// / https://elastic@elastic/elasticsearch

Configuration

const db = await StrictDB.create({
  uri: 'postgresql://localhost:5432/mydb',  // required
  pool: 'standard',        // 'high' | 'standard' | 'low'
  dbName: 'mydb',           // override database name
  label: 'primary',          // logging label
  schema: false,             // enable Zod validation on writes
  sanitize: true,            // input sanitization (default: true)
  guardrails: {              // or just true/false
    limitRequired: true,    // block unbounded queries (default: true)
    emptyFilter: true,      // block empty-filter deletes/updates (default: true)
    nullComparison: true,   // block = NULL in SQL mode (default: true)
  },
  logging: true,             // true | false | 'verbose'
  slowQueryMs: 1000,         // slow query threshold in ms
  timestamps: true,          // auto createdAt/updatedAt injection
  sanitizeRules: [],          // custom SanitizeRule[] array
  reconnect: {                // or just true/false
    enabled: true,
    maxAttempts: 10,
    initialDelayMs: 1000,
    maxDelayMs: 30000,
    backoffMultiplier: 2
  },
  elastic: {                  // Elasticsearch-specific options
    apiKey: '...',
    caFingerprint: '...',
    sniffOnStart: false
  }
});

Read Operations

queryOne<T>(collection, filter, options?)

Find a single document matching the filter. Returns null if not found.

const user = await db.queryOne<User>('users', { email: 'alice@example.com' });
// → User | null

// With projection — only return specific fields
const user = await db.queryOne('users',
  { email: 'alice@example.com' },
  { projection: { name: 1, email: 1 } }
);

queryMany<T>(collection, filter, options?)

Find multiple documents. Limit is required — guardrails block unbounded queries.

const admins = await db.queryMany<User>('users', {
  role: 'admin',
  status: { $in: ['active', 'pending'] },
  age: { $gte: 18 }
}, {
  sort: { createdAt: -1 },
  limit: 50,
  skip: 0
});
// → User[]

queryWithLookup<T>(collection, options)

Join across collections. Uses $lookup on MongoDB, two-query approach on SQL/ES.

const order = await db.queryWithLookup<OrderWithUser>('orders', {
  match: { orderId: 'ORD-001' },
  lookup: {
    from: 'users',          // related collection
    localField: 'userId',    // field in orders
    foreignField: '_id',     // field in users
    as: 'user',              // nest result here
    type: 'left'             // 'left' | 'inner'
  },
  sort: { createdAt: -1 },
  limit: 1
});
// → { orderId: 'ORD-001', userId: '...', user: { name: 'Alice', ... } }
How lookups work per backend: MongoDB uses native $lookup aggregation. SQL backends execute two queries (main + related) and nest the results. Elasticsearch uses the same two-query approach.

count<T>(collection, filter?)

const total = await db.count('users', { role: 'admin' });
// → number

QueryOptions

OptionTypeDescription
sort{ field: 1 | -1 }Sort direction. 1 = ascending, -1 = descending
limitnumberMax documents to return. Required for queryMany
skipnumberNumber of documents to skip (pagination)
projection{ field: 0 | 1 }Include (1) or exclude (0) fields from results

Write Operations

All write operations return an OperationReceipt — never void.

insertOne<T>(collection, doc)

const receipt = await db.insertOne('users', {
  email: 'alice@example.com',
  name: 'Alice',
  role: 'user',
  age: 28
});
// receipt.insertedCount === 1

insertMany<T>(collection, docs)

const receipt = await db.insertMany('users', [
  { email: 'alice@example.com', name: 'Alice', role: 'user' },
  { email: 'bob@example.com', name: 'Bob', role: 'admin' },
]);
// receipt.insertedCount === 2

updateOne<T>(collection, filter, update, upsert?)

Updates a single document. Enforces single-row semantics on all SQL dialects.

await db.updateOne('users',
  { email: 'alice@example.com' },
  { $set: { role: 'admin' }, $inc: { loginCount: 1 } }
);

// Upsert — insert if not found
await db.updateOne('users',
  { email: 'new@example.com' },
  { $set: { name: 'New User', role: 'user' } },
  true  // upsert
);

updateMany<T>(collection, filter, update, options?)

Update all matching documents. Empty filter is blocked by guardrails.

// Update all inactive users
await db.updateMany('users',
  { status: 'inactive', lastLogin: { $lt: cutoffDate } },
  { $set: { archived: true } }
);

// To update ALL documents, you must confirm:
await db.updateMany('users',
  { _id: { $exists: true } },
  { $set: { migrated: true } },
  { confirm: 'UPDATE_ALL' }
);

deleteOne<T>(collection, filter)

await db.deleteOne('users', { email: 'old@example.com' });

deleteMany<T>(collection, filter, options?)

// Delete matching documents
await db.deleteMany('logs', { createdAt: { $lt: thirtyDaysAgo } });

// Delete ALL requires explicit confirmation
await db.deleteMany('temp_data',
  { _id: { $exists: true } },
  { confirm: 'DELETE_ALL' }
);

SQL Mode — Overview & API

Execute SQL on any backend — including MongoDB. On MongoDB, StrictDB parses the SQL, generates aggregate pipelines, and executes them natively. On SQL databases, it routes the query to the native driver. On Elasticsearch, SELECT queries translate to Query DSL.

Two-way query engine. MongoDB-style filters work on SQL databases (auto-translated to WHERE clauses). Native SQL works on MongoDB (auto-translated to aggregate pipelines). Write in whatever you think in, run on whatever you need.
// db.sql() works on ALL backends
const results = await db.sql(`
  SELECT u.name, COUNT(o.id) AS orders
  FROM users u
  JOIN orders o ON u.id = o.user_id
  WHERE u.status = 'active'
  GROUP BY u.name
  HAVING COUNT(o.id) > 5
  ORDER BY orders DESC
  LIMIT 10
`);
// On MongoDB: translates to $lookup, $unwind, $match, $group, $sort, $limit
// On PostgreSQL/MySQL/MSSQL/SQLite: executes SQL natively

Signature

db.sql(sql: string, options?: SqlOptions): Promise<SqlMode2Result | OperationReceipt>

Options

OptionTypeDefaultDescription
paramsunknown[]Parameterized query values (? or $1 placeholders)
dialectstring'mysql'SQL dialect: 'mysql', 'postgresql', 'mariadb', 'sqlite', 'bigquery'
explainbooleanfalseReturn execution plan alongside results
rawbooleanfalseSQL backends only — bypass pipeline translation, pass SQL directly to driver

Return Types

SELECT queries return SqlMode2Result:

{ data: Record<string, unknown>[], plan?: ExplainPlan }

Write queries (INSERT, UPDATE, DELETE) return OperationReceipt:

{ operation, success, matchedCount, modifiedCount, insertedCount, deletedCount, duration, backend }

Supported Features

SQL Mode supports a comprehensive set of SQL features across all backends. On SQL databases, queries execute natively. On MongoDB, every feature is translated to aggregation pipeline stages.

FeatureMongoDBPostgreSQLMySQLMSSQLSQLiteES
SELECT + WHERE
INSERT / UPDATE / DELETE
JOINs (5 types)
Subqueries
CTEs (WITH)
Window Functions
Aggregation (GROUP BY)
UNION / UNION ALL
Parameterized Queries
Transactions
30+ SQL Functions

JOINs

All five standard JOIN types are supported. On MongoDB, JOINs translate to $lookup and $unwind pipeline stages.

JOIN TypeMongoDB Translation
INNER JOIN$lookup + $unwind (preserveNullAndEmptyArrays: false)
LEFT JOIN$lookup + $unwind (preserveNullAndEmptyArrays: true)
RIGHT JOINSwaps main collection, then LEFT JOIN
FULL OUTER JOINTwo parallel pipelines merged with $unionWith
CROSS JOIN$lookup without conditions + $unwind
// INNER JOIN
const results = await db.sql(`
  SELECT u.name, o.total
  FROM users u
  INNER JOIN orders o ON u.id = o.user_id
  WHERE o.total > 100
  LIMIT 20
`);

// FULL OUTER JOIN — runs two pipelines in parallel
const all = await db.sql(`
  SELECT u.name, o.total
  FROM users u
  FULL OUTER JOIN orders o ON u.id = o.user_id
  LIMIT 50
`);

// Multi-condition JOIN
const matched = await db.sql(`
  SELECT a.*, b.score
  FROM students a
  JOIN grades b ON a.id = b.student_id AND a.semester = b.semester
  LIMIT 100
`);

Subqueries

Subqueries are resolved as dependencies before the main query executes. On MongoDB, results are injected into the pipeline via $match with $in, $nin, or $expr.

// WHERE IN (subquery)
const active = await db.sql(`
  SELECT * FROM users
  WHERE id IN (SELECT user_id FROM orders WHERE total > 100)
  LIMIT 50
`);

// WHERE NOT IN
const inactive = await db.sql(`
  SELECT * FROM users
  WHERE id NOT IN (SELECT user_id FROM logins WHERE last_seen > '2025-01-01')
  LIMIT 50
`);

// WHERE EXISTS
const withOrders = await db.sql(`
  SELECT * FROM users u
  WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
  LIMIT 50
`);

// Scalar subquery
const above = await db.sql(`
  SELECT * FROM employees
  WHERE salary > (SELECT AVG(salary) FROM employees)
  LIMIT 50
`);

CTEs (Common Table Expressions)

WITH clauses are resolved as dependencies before the main query. Multiple CTEs can reference each other. WITH RECURSIVE translates to $graphLookup on MongoDB.

// Basic CTE
const result = await db.sql(`
  WITH active_users AS (
    SELECT * FROM users WHERE status = 'active'
  )
  SELECT name, email FROM active_users
  ORDER BY name
  LIMIT 100
`);

// Multiple CTEs with dependencies
const report = await db.sql(`
  WITH
    dept_totals AS (
      SELECT department, SUM(salary) as total
      FROM employees
      GROUP BY department
    ),
    avg_total AS (
      SELECT AVG(total) as avg_dept FROM dept_totals
    )
  SELECT department, total FROM dept_totals
  WHERE total > (SELECT avg_dept FROM avg_total)
  LIMIT 50
`);

// Recursive CTE (translates to $graphLookup on MongoDB)
const tree = await db.sql(`
  WITH RECURSIVE org AS (
    SELECT id, name, manager_id FROM employees WHERE manager_id IS NULL
    UNION ALL
    SELECT e.id, e.name, e.manager_id
    FROM employees e JOIN org o ON e.manager_id = o.id
  )
  SELECT * FROM org LIMIT 500
`);

Window Functions

Window functions translate to $setWindowFields on MongoDB. Supports PARTITION BY and ORDER BY within the window specification.

FunctionDescription
ROW_NUMBER()Sequential row number within partition
RANK()Rank with gaps for ties
DENSE_RANK()Rank without gaps
LAG(field, offset)Access previous row's value
LEAD(field, offset)Access next row's value
// RANK with PARTITION BY
const ranked = await db.sql(`
  SELECT name, department, salary,
    RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
  FROM employees
  LIMIT 100
`);

// LAG — compare to previous row
const deltas = await db.sql(`
  SELECT date, revenue,
    revenue - LAG(revenue, 1) OVER (ORDER BY date) AS daily_change
  FROM daily_stats
  LIMIT 90
`);

Aggregation & GROUP BY

Full aggregation support with GROUP BY, HAVING, and aggregate functions. On MongoDB, these translate to $group and $match stages.

FunctionMongoDB Equivalent
COUNT(*), COUNT(field)$sum: 1 / $sum: $cond
COUNT(DISTINCT field)$addToSet + $size
SUM(field)$sum
AVG(field)$avg
MIN(field), MAX(field)$min, $max
GROUP_CONCAT(field)$push + $reduce
ARRAY_AGG(field)$push
STDDEV(field)$stdDevPop
// GROUP BY with HAVING
const stats = await db.sql(`
  SELECT department, COUNT(*) as headcount, AVG(salary) as avg_salary
  FROM employees
  GROUP BY department
  HAVING COUNT(*) > 5
  ORDER BY avg_salary DESC
  LIMIT 20
`);

// UNION ALL
const combined = await db.sql(`
  SELECT name, 'employee' as type FROM employees
  UNION ALL
  SELECT name, 'contractor' as type FROM contractors
  LIMIT 100
`);

SQL Functions

30+ SQL functions are supported and automatically translated to MongoDB expressions. On SQL backends, functions execute natively.

String Functions

FunctionExample
UPPER(), LOWER()SELECT UPPER(name) FROM users
TRIM(), LTRIM(), RTRIM()SELECT TRIM(email) FROM users
CONCAT(), CONCAT_WS()SELECT CONCAT(first, ' ', last) FROM users
LENGTH(), CHAR_LENGTH()SELECT LENGTH(name) FROM users
SUBSTRING(), LEFT(), RIGHT()SELECT SUBSTRING(name, 1, 3) FROM users
REPLACE(), REVERSE()SELECT REPLACE(email, '@old', '@new') FROM users
POSITION(), LOCATE(), INSTR()SELECT POSITION('@' IN email) FROM users

Numeric Functions

FunctionExample
ROUND(), ABS()SELECT ROUND(price, 2) FROM products
CEIL(), FLOOR(), TRUNC()SELECT CEIL(rating) FROM reviews
POWER(), SQRT(), EXP()SELECT SQRT(area) FROM plots
LOG(), LN(), LOG10()SELECT LOG10(views) FROM posts
MOD(), RAND()SELECT MOD(id, 10) FROM users
GREATEST(), LEAST()SELECT GREATEST(a, b, c) FROM scores

Date Functions

FunctionExample
NOW(), CURRENT_TIMESTAMP()SELECT NOW() as current_time
EXTRACT(part FROM date)SELECT EXTRACT(YEAR FROM created_at) FROM users
DATEDIFF(), DATE_ADD(), DATE_SUB()SELECT DATEDIFF(NOW(), created_at) FROM users
DATE_FORMAT(), TO_CHAR()SELECT DATE_FORMAT(date, '%Y-%m') FROM events
DATE_TRUNC()SELECT DATE_TRUNC('month', created_at) FROM users

Conditional Functions

FunctionExample
CASE WHENSELECT CASE WHEN age > 18 THEN 'adult' ELSE 'minor' END FROM users
COALESCE()SELECT COALESCE(nickname, name) FROM users
NULLIF()SELECT NULLIF(status, 'unknown') FROM users
IF(), IIF()SELECT IF(active, 'yes', 'no') FROM users

Type Conversion

FunctionExample
CAST(expr AS type)SELECT CAST(price AS INTEGER) FROM products
CONVERT(expr, type)SELECT CONVERT(date_str, DATE) FROM events

Parameterized Queries

Prevent SQL injection with parameterized placeholders. Both MySQL-style (?) and PostgreSQL-style ($1) are supported on all backends.

// MySQL-style placeholders
const users = await db.sql(
  'SELECT * FROM users WHERE email = ? AND status = ? LIMIT 10',
  { params: ['alice@example.com', 'active'] }
);

// PostgreSQL-style placeholders
const orders = await db.sql(
  'SELECT * FROM orders WHERE user_id = $1 AND total > $2 LIMIT 20',
  { params: [42, 100] }
);

Transactions in SQL Mode

Execute multi-statement transactions via SQL or the programmatic API. Supported on all backends except Elasticsearch.

// SQL transaction block
await db.sql(`
  BEGIN;
  INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
  UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
  COMMIT;
`);

// Programmatic transaction (auto-commits on success, rolls back on error)
await db.withTransaction(async (tx) => {
  await tx.insertOne('users', { name: 'Alice' });
  await tx.updateOne('accounts', { userId: 1 }, { $inc: { balance: -100 } });
});
BackendTransaction Support
MongoDB✓ Session-based (requires replica set)
PostgreSQL✓ Native
MySQL✓ Native
MSSQL✓ Native
SQLite✓ Native
Elasticsearch— Not supported

Explain Mode

Pass { explain: true } to see the full execution plan alongside results. Useful for debugging and understanding how SQL translates to MongoDB pipelines.

const result = await db.sql(`
  SELECT department, AVG(salary) as avg_sal
  FROM employees
  GROUP BY department
  HAVING AVG(salary) > 50000
  LIMIT 10
`, { explain: true });

// result.data → [{ department: 'Engineering', avg_sal: 95000 }, ...]
// result.plan → {
//   phases: 1,
//   dependencies: [],
//   pipelines: [{ collection: 'employees', stages: [
//     { $group: { _id: '$department', avg_sal: { $avg: '$salary' } } },
//     { $match: { avg_sal: { $gt: 50000 } } },
//     { $limit: 10 }
//   ]}],
//   parallel: false,
//   durationMs: 23
// }

Explain Output Fields

FieldTypeDescription
phasesnumber1 (no dependencies) or 2 (has subqueries/CTEs)
dependenciesarrayResolved subqueries and CTEs with their pipelines
pipelinesarrayMongoDB aggregation stages that were executed
parallelbooleanTrue if FULL OUTER JOIN or independent pipelines ran in parallel
durationMsnumberTotal execution time in milliseconds

Guardrails in SQL Mode

The same safety guardrails that protect MongoDB-style queries also apply to SQL Mode.

GuardrailBlocked QueryFix
Unbounded SELECTSELECT * FROM usersAdd LIMIT
Empty-filter UPDATEUPDATE users SET active = falseAdd WHERE clause
Empty-filter DELETEDELETE FROM logsAdd WHERE clause
NULL comparisonWHERE field = NULLUse IS NULL
// This is blocked (no LIMIT):
await db.sql('SELECT * FROM users');
// Error: LIMIT_REQUIRED — Add a LIMIT clause

// This is allowed:
await db.sql('SELECT * FROM users LIMIT 100');

Cross-Dialect Normalization

StrictDB automatically normalizes dialect-specific SQL so queries work across all backends. Write in the dialect you know — StrictDB handles the rest.

MSSQL Normalizations

MSSQL SyntaxNormalized To
SELECT TOP 10 *SELECT * ... LIMIT 10
DATEPART(year, date)EXTRACT(YEAR FROM date)
GETDATE()NOW()
ISNULL(a, b)COALESCE(a, b)
LEN(x)LENGTH(x)
CHARINDEX(sub, str)POSITION(sub IN str)

MySQL Normalizations

MySQL SyntaxNormalized To
IFNULL(a, b)COALESCE(a, b)
IF(cond, a, b)CASE WHEN cond THEN a ELSE b END
Write any dialect, run anywhere. You can write T-SQL with TOP and GETDATE(), and it will execute correctly on MongoDB, PostgreSQL, or any other backend.

db.aggregate(collection, pipeline, options?)

Run a raw aggregation pipeline directly against MongoDB. Bypasses filter translation — you write the pipeline yourself. On SQL and Elasticsearch backends, this method throws UNSUPPORTED_OPERATION.

// Raw MongoDB aggregate pipeline
const stats = await db.aggregate('orders', [
  { $match: { status: 'completed' } },
  {
    $group: {
      _id: '$userId',
      totalSpent: { $sum: '$amount' },
      orderCount: { $sum: 1 }
    }
  },
  { $sort: { totalSpent: -1 } },
  { $limit: 10 }
]);
// → [{ _id: 'user-1', totalSpent: 450, orderCount: 9 }, ...]

// With explain
const plan = await db.aggregate('orders', pipeline, { explain: true });
When to use aggregate() vs sql(): Use aggregate() when you want to write raw MongoDB pipelines. Use sql() when you want to write SQL and have StrictDB generate the pipeline for you.

db.bulkWrite(collection, operations, options?)

Execute multiple write operations in a single round-trip. More efficient than calling individual methods in a loop. Returns a combined OperationReceipt.

const receipt = await db.bulkWrite('products', [
  {
    insertOne: {
      doc: { sku: 'WIDGET-1', name: 'Widget', price: 9.99, stock: 100 }
    }
  },
  {
    updateOne: {
      filter: { sku: 'GADGET-5' },
      update: { $inc: { stock: -10 } }
    }
  },
  {
    updateMany: {
      filter: { price: { $lt: 1 } },
      update: { $set: { discontinued: true } }
    }
  },
  {
    deleteOne: {
      filter: { sku: 'OLD-SKU' }
    }
  }
]);

// receipt.insertedCount  → 1
// receipt.modifiedCount  → number of rows/docs updated
// receipt.deletedCount   → 1
// receipt.duration       → total ms for the batch

bulkWrite() Supported Operations

OperationFieldsDescription
insertOne{ doc }Insert a single document
updateOne{ filter, update, upsert? }Update first matching document
updateMany{ filter, update }Update all matching documents
deleteOne{ filter }Delete first matching document
deleteMany{ filter }Delete all matching documents
Guardrails apply. Empty-filter deleteMany and updateMany inside bulkWrite are still blocked unless you pass confirm: 'DELETE_ALL' or 'UPDATE_ALL' in the operation.

Operation Receipts

Every write operation returns a structured receipt:

{
  operation: 'insertOne',     // operation name
  collection: 'users',        // target collection/table
  success: true,              // did it succeed?
  matchedCount: 0,            // documents matched by filter
  modifiedCount: 0,           // documents actually modified
  insertedCount: 1,           // documents inserted
  insertedId: 'abc123',       // ID of inserted doc (insertOne only)
  insertedIds: ['a', 'b'],    // IDs inserted (insertMany only)
  upsertedId: 'xyz789',       // ID of upserted doc (upsert only)
  deletedCount: 0,            // documents deleted
  duration: 12,               // execution time in ms
  backend: 'sql'              // 'mongo' | 'sql' | 'elastic'
}

Transactions

Full transaction support across all backends:

await db.withTransaction(async (tx) => {
  // All operations inside use the same transaction
  await tx.insertOne('orders', {
    userId: 'user-1',
    item: 'widget',
    qty: 5
  });

  await tx.updateOne('inventory',
    { sku: 'W1' },
    { $inc: { stock: -5 } }
  );

  // If anything throws, entire transaction rolls back
});
BackendTransaction SupportNotes
MongoDBYesRequires replica set
PostgreSQLYesBEGIN / COMMIT / ROLLBACK
MySQLYesSTART TRANSACTION / COMMIT / ROLLBACK
MSSQLYesBEGIN TRANSACTION / COMMIT / ROLLBACK
SQLiteYesBEGIN / COMMIT / ROLLBACK
ElasticsearchNoThrows UNSUPPORTED_OPERATION

Batch Operations

const receipt = await db.batch([
  { operation: 'insertOne', collection: 'orders',
    doc: { item: 'widget', qty: 5, userId: 'u1' } },
  { operation: 'updateOne', collection: 'inventory',
    filter: { sku: 'W1' },
    update: { $inc: { stock: -5 } } },
  { operation: 'deleteOne', collection: 'cart_items',
    filter: { userId: 'u1', sku: 'W1' } }
]);

Filter Reference — Comparison Operators

StrictDB uses MongoDB-style filters across all backends. Filters are automatically translated to SQL WHERE clauses or Elasticsearch Query DSL.

$eq — Equality

Implicit when you pass a value directly. These are identical:

{ role: 'admin' }                   // shorthand
{ role: { $eq: 'admin' } }          // explicit
PostgreSQL
"role" = $1        -- params: ['admin']
Elasticsearch
{ term: { role: 'admin' } }

$ne — Not Equal

{ status: { $ne: 'banned' } }
SQL
"status" != $1
Elasticsearch
{ bool: { must_not: { term: { status: 'banned' } } } }

$gt, $gte, $lt, $lte — Range

{ age: { $gte: 18, $lt: 65 } }
SQL
"age" >= $1 AND "age" < $2
-- params: [18, 65]
Elasticsearch
{ range: { age: { gte: 18, lt: 65 } } }

null — Null Equality

{ email: null }
SQL
"email" IS NULL
Elasticsearch
{ bool: { must_not: { exists: { field: 'email' } } } }

Filter Reference — Array & Pattern Operators

$in — Match Any Value in Array

{ role: { $in: ['admin', 'mod'] } }
SQL
"role" IN ($1, $2)
-- params: ['admin', 'mod']
Elasticsearch
{ terms: { role: ['admin', 'mod'] } }

$nin — Not In Array

{ role: { $nin: ['banned', 'suspended'] } }
SQL
"role" NOT IN ($1, $2)
Elasticsearch
{ bool: { must_not: { terms: { role: ['banned', 'suspended'] } } } }

$exists — Field Existence

{ email: { $exists: true } }   // field IS NOT NULL
{ email: { $exists: false } }  // field IS NULL
SQL ($exists: true)
"email" IS NOT NULL
ES ($exists: true)
{ exists: { field: 'email' } }

$regex — Pattern Matching

{ name: { $regex: '^Tim' } }
DialectSQL Output
PostgreSQL"name" ~ $1
MySQL"name" REGEXP ?
MSSQL"name" LIKE @p1
SQLite"name" LIKE ?
Elasticsearch{ regexp: { name: '^Tim' } }

$size — Array Length

{ tags: { $size: 3 } }
DialectTranslation
PostgreSQLjsonb_array_length("tags") = $1
MySQLJSON_LENGTH("tags") = ?
SQLitejson_array_length("tags") = ?
ElasticsearchScript query on array length

Filter Reference — Logical Operators

$and — All Conditions Must Match

{ $and: [{ age: { $gte: 18 } }, { role: 'admin' }] }
SQL
(("age" >= $1) AND ("role" = $2))
Elasticsearch
{ bool: { must: [
  { range: { age: { gte: 18 } } },
  { term: { role: 'admin' } }
] } }

Multiple top-level fields are implicitly $and:

{ role: 'admin', age: { $gte: 18 } }
// SQL: "role" = $1 AND "age" >= $2

$or — Any Condition Matches

{ $or: [{ role: 'admin' }, { role: 'mod' }] }
SQL
(("role" = $1) OR ("role" = $2))
Elasticsearch
{ bool: { should: [
  { term: { role: 'admin' } },
  { term: { role: 'mod' } }
], minimum_should_match: 1 } }

$nor — No Conditions Match

{ $nor: [{ role: 'banned' }, { status: 'inactive' }] }
SQL
NOT (("role" = $1) OR ("status" = $2))
Elasticsearch
{ bool: { must_not: [
  { term: { role: 'banned' } },
  { term: { status: 'inactive' } }
] } }

$not — Negate a Condition

{ age: { $not: { $gt: 18 } } }
SQL
NOT ("age" > $1)
Elasticsearch
{ bool: { must_not: [
  { range: { age: { gt: 18 } } }
] } }

Full SQL Translation Examples

Complete before/after examples showing how MongoDB-style filters become native SQL across all four dialects.

Real-World Query: Active Admin Users

// StrictDB filter (works on any backend)
const filter = {
  status: 'active',
  age: { $gte: 18 },
  role: { $in: ['admin', 'mod'] }
};
DialectGenerated SQL
PostgreSQLWHERE "status" = $1 AND "age" >= $2 AND "role" IN ($3, $4)
MySQLWHERE "status" = ? AND "age" >= ? AND "role" IN (?, ?)
MSSQLWHERE "status" = @p1 AND "age" >= @p2 AND "role" IN (@p3, @p4)
SQLiteWHERE "status" = ? AND "age" >= ? AND "role" IN (?, ?)

Complex Nested Query

{
  $or: [
    { role: 'admin' },
    { $and: [
      { age: { $gte: 18 } },
      { status: { $ne: 'banned' } },
      { email: { $exists: true } }
    ]}
  ]
}
-- PostgreSQL output:
(("role" = $1) OR (("age" >= $2) AND ("status" != $3) AND ("email" IS NOT NULL)))

Full SELECT Statements

OperationSQL Output (PostgreSQL)
Basic selectSELECT * FROM "users" WHERE "role" = $1
With sort + limitSELECT * FROM "users" ORDER BY "name" ASC LIMIT 50
With paginationSELECT * FROM "users" LIMIT 10 OFFSET 20
InsertINSERT INTO "users" ("name", "age") VALUES ($1, $2)
UpdateUPDATE "users" SET "name" = $1 WHERE "id" = $2
DeleteDELETE FROM "users" WHERE "id" = $1
CountSELECT COUNT(*) as count FROM "users" WHERE "role" = $1

Elasticsearch Translation Examples

Complex Query to ES Query DSL

// StrictDB filter
{
  status: 'active',
  age: { $gte: 18, $lt: 65 },
  role: { $in: ['admin', 'mod'] },
  email: { $exists: true }
}

// Elasticsearch Query DSL output:
{
  bool: {
    must: [
      { term: { status: 'active' } },
      { range: { age: { gte: 18, lt: 65 } } },
      { terms: { role: ['admin', 'mod'] } },
      { exists: { field: 'email' } }
    ]
  }
}

Update Operators — $set, $inc, $unset

$set — Set Field Values

{ $set: { name: 'Bob', role: 'admin' } }
SQL
SET "name" = $1, "role" = $2
ES Painless
ctx._source.name = params.set_name;
ctx._source.role = params.set_role

$inc — Increment Numeric Fields

{ $inc: { loginCount: 1, score: -5 } }
SQL
SET "loginCount" = "loginCount" + $1,
    "score" = "score" + $2
ES Painless
ctx._source.loginCount += params.inc_loginCount;
ctx._source.score += params.inc_score

$unset — Remove Fields

{ $unset: { tempField: true } }
SQL
SET "tempField" = NULL
ES Painless
ctx._source.remove('tempField')

Combined Update

{ $set: { name: 'Bob' }, $inc: { count: 1 } }
// SQL: SET "name" = $1, "count" = "count" + $2
// ES:  ctx._source.name = params.set_name; ctx._source.count += params.inc_count

Update Operators — $push, $pull

$push — Append to Array

{ $push: { tags: 'new-tag' } }
BackendBehavior
MongoDBNative $push
Elasticsearchctx._source.tags.add(params.push_tags)
SQLJSON array manipulation (backend-specific)

$pull — Remove from Array

{ $pull: { tags: 'old-tag' } }
BackendBehavior
MongoDBNative $pull
Elasticsearchctx._source.tags.removeIf(e -> e == params.pull_tags)
SQLJSON array manipulation (backend-specific)

SQL Dialects — Parameter Styles

DialectPlaceholderExample
PostgreSQL$1, $2, $3...WHERE "age" = $1 AND "role" = $2
MySQL?, ?, ?...WHERE "age" = ? AND "role" = ?
SQLite?, ?, ?...WHERE "age" = ? AND "role" = ?
MSSQL@p1, @p2, @p3...WHERE "age" = @p1 AND "role" = @p2

SQL Dialects — Pagination

Each SQL dialect handles LIMIT/OFFSET differently. StrictDB generates the correct syntax automatically.

ScenarioPG / MySQL / SQLiteMSSQL
Limit onlyLIMIT 10SELECT TOP(10) *
Skip + LimitLIMIT 10 OFFSET 20ORDER BY (SELECT NULL) OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
Skip onlyOFFSET 20ORDER BY (SELECT NULL) OFFSET 20 ROWS
Sort + Skip + LimitORDER BY "name" ASC LIMIT 10 OFFSET 5ORDER BY "name" ASC OFFSET 5 ROWS FETCH NEXT 10 ROWS ONLY
MSSQL requires ORDER BY for OFFSET. When no sort is specified, StrictDB automatically inserts ORDER BY (SELECT NULL) as a fallback.

SQL Dialects — Single-Row Semantics

updateOne and deleteOne enforce exactly one row across all dialects:

DialectupdateOne TechniquedeleteOne Technique
PostgreSQLSubquery with ctidSubquery with ctid
MySQLLIMIT 1LIMIT 1
SQLiteSubquery with rowidLIMIT 1
MSSQLUPDATE TOP(1)DELETE TOP(1)

SQL Dialects — Upsert

Pass upsert: true to updateOne. StrictDB uses an UPDATE-then-INSERT pattern that works identically across all SQL dialects:

  1. Try UPDATE (limited to 1 row)
  2. If zero rows matched, INSERT with filter equality fields merged with $set fields
await db.updateOne('users',
  { email: 'test@test.com' },                     // filter
  { $set: { name: 'Test', role: 'user' } },       // update
  true                                              // upsert
);
// If no match → INSERT INTO "users" ("email", "name", "role") VALUES ($1, $2, $3)

SQL Dialects — Projection

Inclusion (select specific columns):

{ projection: { name: 1, email: 1 } }
// → SELECT "name", "email" FROM "users" ...

Exclusion (hide columns — SQL doesn't natively support this, so StrictDB uses post-processing):

{ projection: { password: 0, secret: 0 } }
// → SELECT * FROM "users" ...  (then strips password, secret from results)

Schema & Validation — Zod Registration

import { z } from 'zod';

const userSchema = z.object({
  email: z.string().max(255),
  name: z.string(),
  age: z.number().int().optional(),
  role: z.enum(['admin', 'user', 'mod']),
  active: z.boolean(),
  createdAt: z.date(),
  tags: z.array(z.string()).optional(),
  metadata: z.object({}).optional(),
});

db.registerCollection({ name: 'users', schema: userSchema });

// Auto-create tables/indexes
await db.ensureCollections();
await db.ensureIndexes();

Schema — SQL DDL Generation

StrictDB introspects Zod schemas and generates CREATE TABLE statements for each dialect:

PostgreSQL

CREATE TABLE IF NOT EXISTS "users" (
  "email" VARCHAR(255) NOT NULL,
  "name" TEXT NOT NULL,
  "age" INTEGER,
  "role" TEXT NOT NULL CHECK(role IN ('admin', 'user', 'mod')),
  "active" BOOLEAN NOT NULL,
  "createdAt" TIMESTAMPTZ NOT NULL,
  "tags" JSONB,
  "metadata" JSONB
)

Type Mapping

Zod TypePGMySQLMSSQLSQLite
z.string().max(N)VARCHAR(N)VARCHAR(N)NVARCHAR(N)TEXT
z.string()TEXTTEXTNVARCHAR(MAX)TEXT
z.number().int()INTEGERINTINTINTEGER
z.number()DOUBLE PRECISIONDOUBLEFLOATREAL
z.boolean()BOOLEANTINYINT(1)BITINTEGER
z.date()TIMESTAMPTZDATETIMEDATETIME2TEXT
z.enum([...])TEXT + CHECKTEXTNVARCHAR(255)TEXT
z.array(...)JSONBJSONNVARCHAR(MAX)TEXT

Schema — Elasticsearch Mapping Generation

Zod TypeES Mapping
z.string().max(256){ type: 'keyword' }
z.string(){ type: 'text', fields: { keyword: { type: 'keyword', ignore_above: 256 } } }
z.number().int(){ type: 'integer' }
z.number(){ type: 'double' }
z.boolean(){ type: 'boolean' }
z.date(){ type: 'date' }
z.enum([...]){ type: 'keyword' }
z.array(...){ type: 'nested' }

Schema — Runtime Validation

With schema: true in config, all writes are validated before execution:

const db = await StrictDB.create({ uri, schema: true });

try {
  await db.insertOne('users', { name: '', email: 'bad' });
} catch (err) {
  err.code  // 'VALIDATION_ERROR'
  err.fix   // "Field 'name' must be at least 1 character. Field 'email' must be a valid email."
}

Guardrails

Enabled by default. These are hard blocks, not warnings.

OperationBlocked WhenGuardrailOverride
deleteMany({})Empty filteremptyFilter{ confirm: 'DELETE_ALL' } + non-empty filter
updateMany({})Empty filteremptyFilter{ confirm: 'UPDATE_ALL' } + non-empty filter
deleteOne({})Empty filteremptyFilterAlways requires a non-empty filter
queryMany()No limitlimitRequiredInclude { limit: N }
SELECT * FROM tNo LIMITlimitRequiredAdd LIMIT clause
DELETE FROM tNo WHEREemptyFilterAdd WHERE clause
WHERE x = NULL= NULL comparisonnullComparisonUse IS NULL

Error code: GUARDRAIL_BLOCKED. Emits guardrail-blocked event.

Granular Configuration

Pass true/false to enable/disable all guardrails, or an object for granular control:

const db = await StrictDB.create({
  uri,
  guardrails: {
    limitRequired: false,    // allow unbounded queries
    emptyFilter: true,       // still block empty-filter deletes/updates
    nullComparison: true,    // still block = NULL in SQL mode
  }
});
OptionTypeDefaultDescription
limitRequiredbooleantrueBlock queryMany() without limit and SELECT without LIMIT
emptyFilterbooleantrueBlock deleteMany, deleteOne, updateMany with empty filters, and DELETE/UPDATE without WHERE
nullComparisonbooleantrueBlock = NULL and != NULL in SQL mode (should use IS NULL / IS NOT NULL)

Sanitization

Enabled by default. Prevents injection and validates field names.

Custom Sanitize Rules

const db = await StrictDB.create({
  uri,
  sanitizeRules: [
    // Trim all string fields
    { transform: (v) => typeof v === 'string' ? v.trim() : v },

    // Lowercase emails
    { field: 'email', transform: (v) => v.toLowerCase() },

    // Strip HTML from specific fields
    { field: ['name', 'bio'],
      transform: (v) => v.replace(/<[^>]*>/g, '') },

    // Apply to ALL string fields
    { field: '*',
      transform: (v) => typeof v === 'string' ? v.trim() : v },
  ]
});

SanitizeRule Interface

PropertyTypeDescription
fieldstring | string[] | '*'Which fields to apply to. Omit or '*' for all fields.
transform(value, field, collection) => valueTransform function. Receives value, field name, and collection name.
Rules never mutate input. A new object is always returned. Rules are applied in order, and missing fields are skipped.

AI Integration — Why AI-First?

Traditional database drivers give AI agents raw access with no guardrails. StrictDB provides four tools that make AI database interactions safe and self-correcting:

ProblemWithout StrictDBWith StrictDB
AI hallucinates column namesQuery fails with cryptic errordescribe() returns real schema
AI writes bad queryExecutes and corrupts datavalidate() catches before execution
AI doesn't understand translationNo visibility into native queryexplain() shows exact SQL/ES
AI gets an errorParses stack trace (unreliable)Reads .fix field (exact action)
AI runs DELETE FROM usersAll data deletedBlocked by guardrails

describe(collection) — Schema Discovery

const schema = await db.describe('users');
// {
//   name: 'users',
//   backend: 'sql',
//   fields: [
//     { name: 'id', type: 'integer', required: true },
//     { name: 'email', type: 'string', required: true },
//     { name: 'role', type: 'string', required: true, enum: ['admin', 'user', 'mod'] },
//     { name: 'age', type: 'integer', required: false },
//   ],
//   indexes: [{ fields: { email: 1 }, unique: true }],
//   documentCount: 12847,
//   exampleFilter: { role: 'admin' }
// }

validate(collection, operation) — Dry-Run

const check = await db.validate('users', {
  filter: { rol: 'admin' },              // typo!
  doc: { email: 'test@test.com' }
});
// { valid: false, errors: [
//   { field: 'rol', message: 'Unknown field', expected: 'role' }
// ]}

explain(collection, operation) — Query Translation

const plan = await db.explain('users', {
  filter: { role: 'admin', age: { $gte: 18 } },
  sort: { createdAt: -1 },
  limit: 50
});
// {
//   backend: 'sql',
//   native: 'SELECT * FROM "users" WHERE "role" = $1 AND "age" >= $2 ORDER BY "createdAt" DESC LIMIT 50'
// }

Self-Correcting Errors

Every StrictDBError includes a .fix field — a plain-English corrective action:

try {
  await db.insertOne('users', { email: 'alice@example.com' });
} catch (err) {
  err.code       // 'DUPLICATE_KEY'
  err.fix        // 'Use updateOne() instead or check with queryOne() first'
  err.retryable  // false
  err.backend    // 'sql'
  err.collection // 'users'
  err.operation  // 'insertOne'
}

Fuzzy Collection Matching

await db.queryOne('usres', { id: 1 });  // typo!
// Error fix: 'Did you mean "users"? Registered collections: users, orders, products'

Method Suggestions

// If an AI tries to use MongoDB-native method names:
db.find(...)       // → fix: 'Use queryMany() instead'
db.findOne(...)    // → fix: 'Use queryOne() instead'
db.bulkWrite(...)  // → fix: 'Use batch() instead'

MCP Server — Setup

# Install
npm install -g strictdb-mcp

# Run (reads STRICTDB_URI from environment)
STRICTDB_URI="postgresql://user:pass@localhost:5432/mydb" npx strictdb-mcp

MCP Server — All 14 Tools

ToolTypeParameters
strictdb_describediscovercollection
strictdb_validatevalidatecollection, filter?, update?, doc?
strictdb_explainexplaincollection, filter?, sort?, limit?
strictdb_query_onereadcollection, filter, sort?
strictdb_query_manyreadcollection, filter, sort?, limit (required), skip?
strictdb_countreadcollection, filter?
strictdb_insert_onewritecollection, doc
strictdb_insert_manywritecollection, docs[]
strictdb_update_onewritecollection, filter, update {$set?, $inc?, $unset?}, upsert?
strictdb_update_manywritecollection, filter, update {$set?, $inc?, $unset?}
strictdb_delete_onewritecollection, filter
strictdb_delete_manywritecollection, filter
strictdb_batchbatchoperations[]
strictdb_statusutilnone

MCP Server — Claude Desktop Configuration

{
  "mcpServers": {
    "strictdb": {
      "command": "npx",
      "args": ["strictdb-mcp"],
      "env": {
        "STRICTDB_URI": "postgresql://user:pass@localhost:5432/mydb"
      }
    }
  }
}

Claude Code Configuration

{
  "mcpServers": {
    "strictdb": {
      "type": "stdio",
      "command": "npx",
      "args": ["strictdb-mcp"],
      "env": {
        "STRICTDB_URI": "postgresql://user:pass@localhost:5432/mydb"
      }
    }
  }
}

Events

db.on('connected', ({ backend, dbName, label }) => { ... });
db.on('disconnected', ({ backend, reason, timestamp }) => { ... });
db.on('reconnecting', ({ backend, attempt, maxAttempts, delayMs }) => { ... });
db.on('reconnected', ({ backend, attempt, downtimeMs }) => { ... });
db.on('error', ({ code, message, fix, backend }) => { ... });
db.on('operation', ({ collection, operation, durationMs, receipt }) => { ... });
db.on('slow-query', ({ collection, operation, durationMs, threshold }) => { ... });
db.on('guardrail-blocked', ({ collection, operation, reason }) => { ... });
db.on('pool-status', ({ active, idle, waiting, max }) => { ... });
db.on('shutdown', ({ exitCode }) => { ... });

Reconnection

Exponential backoff with jitter to prevent thundering herd:

// Delay formula:
// baseDelay = initialDelayMs * (backoffMultiplier ^ (attempt - 1))
// cappedDelay = min(baseDelay, maxDelayMs)
// actualDelay = cappedDelay * (0.75 + random * 0.5)   ← jitter ±25%

// Example with defaults (1s initial, 2x multiplier, 30s max):
// Attempt 1: ~1s    (750ms – 1250ms)
// Attempt 2: ~2s    (1500ms – 2500ms)
// Attempt 3: ~4s    (3000ms – 5000ms)
// Attempt 4: ~8s    (6000ms – 10000ms)
// Attempt 5: ~16s   (12000ms – 20000ms)
// Attempt 6+: ~30s  (capped at maxDelayMs)

Auto Timestamps

// Enable with defaults (created_at, updated_at)
const db = await StrictDB.create({ uri, timestamps: true });

// Or customize field names
const db = await StrictDB.create({
  uri,
  timestamps: { createdAt: 'born', updatedAt: 'modified' }
});

Escape Hatch — db.raw()

Access the underlying native driver when you need backend-specific features:

const native = db.raw();

// MongoDB → returns MongoClient
// PostgreSQL → returns pg.Pool
// MySQL → returns mysql2 connection
// MSSQL → returns mssql connection pool
// SQLite → returns better-sqlite3 Database
// Elasticsearch → returns @elastic/elasticsearch Client
Warning: Operations through raw() bypass guardrails, sanitization, receipts, and event logging. Use only when StrictDB's unified API doesn't cover your use case.

Error Code Reference

CodeRetryable.fix Message
CONNECTION_FAILEDYesCheck connection URI and ensure database server is running
CONNECTION_LOSTYesConnection dropped — auto-reconnect will attempt recovery
AUTHENTICATION_FAILEDNoCheck username/password in connection URI
TIMEOUTYesAdd a filter to narrow results, add an index, or increase timeout
POOL_EXHAUSTEDYesConnection pool full — reduce concurrency or increase pool size
DUPLICATE_KEYNoUse updateOne() instead or check with queryOne() first
VALIDATION_ERRORNoDocument doesn't match schema — see error details for field-specific fixes
COLLECTION_NOT_FOUNDNoDid you mean "X"? Run ensureCollections() to create tables
QUERY_ERRORNoCheck field names and filter operators
GUARDRAIL_BLOCKEDNoProvide a non-empty filter or use confirm option
UNKNOWN_OPERATORNoUse supported filter syntax: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $regex
SCHEMA_MISMATCHNoDocument structure doesn't match registered schema
UNSUPPORTED_OPERATIONNoOperation not available for this backend (e.g., transactions on Elasticsearch)
INTERNAL_ERRORNoUnexpected backend error — see originalError for details

TypeScript Types

import type {
  Backend,              // 'mongo' | 'sql' | 'elastic'
  Driver,               // 'mongodb' | 'pg' | 'mysql2' | 'mssql' | 'sqlite' | 'elasticsearch'
  SqlDialect,           // 'pg' | 'mysql2' | 'mssql' | 'sqlite'
  StrictFilter,         // MongoDB-style filter object
  UpdateOperators,      // { $set?, $inc?, $unset?, $push?, $pull? }
  QueryOptions,         // { sort?, limit?, skip?, projection? }
  LookupOptions,        // { match, lookup, unwind?, sort?, limit? }
  OperationReceipt,     // Write operation result
  StrictDBConfig,       // create() configuration
  StrictDBEvents,       // Typed event map
  SanitizeRule,         // Custom sanitization rule
  ReconnectConfig,      // Reconnection options
  IndexDefinition,      // Index schema
  CollectionSchema,     // Collection + Zod schema registration
  StrictDBError,        // Error class with .fix, .code, .retryable
} from 'strictdb';

Using Generics

interface User {
  email: string;
  name: string;
  role: 'admin' | 'user' | 'mod';
  age?: number;
}

// Fully typed — result is User | null
const user = await db.queryOne<User>('users', { email: 'alice@example.com' });

// Filter autocomplete works with the generic
const admins = await db.queryMany<User>('users',
  { role: 'admin' },
  { sort: { name: 1 }, limit: 50 }
);