L7 - Adapters
Translate VibORM's database-agnostic operations into dialect-specific SQL
Location: src/adapters/
Why This Layer Exists
Databases have different syntax for the same operations:
| Operation | PostgreSQL | MySQL | SQLite |
|---|---|---|---|
| Identifier quote | "column" |
`column` |
"column" |
| String concat | || |
CONCAT() |
|| |
| JSON aggregation | json_agg() |
JSON_ARRAYAGG() |
json_group_array() |
| Low-level upsert primitive | ON CONFLICT ... DO UPDATE |
ON DUPLICATE KEY UPDATE |
ON CONFLICT ... DO UPDATE |
| Boolean | true/false |
1/0 |
1/0 |
| RETURNING | RETURNING * |
Requires separate SELECT | RETURNING * |
The adapter pattern lets VibORM support multiple databases without scattering conditionals throughout the codebase.
MySQL’s low-level primitive reacts to any unique collision, so VibORM does not use it for public non-returning upserts. The query engine selects and locks the requested target, chooses the create/update branch, and refetches inside one transaction. This preserves the same target-specific public behavior as PostgreSQL and SQLite.
MySQL duplicate skipping likewise avoids INSERT IGNORE, which suppresses
unrelated failures. Top-level bulk creates execute row-level inserts inside one
transaction and recover only mapped duplicate-key errors; nested idempotent
inserts use a duplicate-key no-op update. Both paths surface foreign-key,
NOT NULL, conversion, truncation, and other integrity failures.
Adapter Interface
Every adapter implements DatabaseAdapter (see src/adapters/database-adapter.ts) — a large interface organized into ~25 namespaced groups of pure, composable SQL-fragment functions:
interface DatabaseAdapter {
raw: (sqlString: string) => Sql; // escape hatch (use sparingly)
identifiers: { escape, column, table, aliased }; // "name" vs `name`
literals: { value, null, ... }; // parameterized values
operators: { eq, neq, lt, gt, in, like, ... }; // comparison operators
expressions: { concat, coalesce, cast, ... }; // scalar expressions
aggregates: { count, sum, avg, min, max, ... }; // aggregate functions
json: { ... }; // JSON build/extract/agg
arrays: { ... }; // array ops (native or JSON)
orderBy: { ... }; // ORDER BY fragments
clauses: { ... }; // WHERE/LIMIT/... clauses
set: { ... }; // UPDATE SET fragments
filters: { ... }; // filter helpers
subqueries: { ... }; // correlated subqueries
assemble: { ... }; // final query assembly
cte: { ... }; // common table expressions
mutations: { ... }; // INSERT/UPDATE/DELETE/upsert
assertions: { ... }; // runtime SQL assertions
joins: { ... }; // JOIN fragments
setOperations: { ... }; // UNION and friends
vector: { ... }; // vector distance (pgvector, ...)
geospatial: { ... }; // point/geometry support
result: { ... }; // result-shape hints
lastInsertId: () => Sql; // lastval()/LAST_INSERT_ID()/...
batchRefs: BatchReferenceSqlAdapter; // atomic-batch temp refs
capabilities: {
supportsReturning: boolean;
supportsLateralJoins: boolean;
supportsVector: boolean;
supportsUpsertWhere: boolean;
supportsMutationTargetInSubquery: boolean;
// ...
};
}
Each group takes database-agnostic inputs and returns dialect-specific Sql fragments. The query engine composes fragments; it never inspects them.
Available Adapters
| Adapter | Database |
|---|---|
PostgresAdapter |
PostgreSQL |
MySQLAdapter |
MySQL 8.0+ |
SQLiteAdapter |
SQLite 3.35+ |
Database Capabilities
The capabilities group lets the query engine branch on database features instead of dialect names. Examples:
| Capability | PostgreSQL | MySQL | SQLite |
|---|---|---|---|
supportsReturning |
✅ | ❌ | ✅ |
supportsLateralJoins |
✅ | ✅ | ❌ |
supportsVector |
with pgvector | ❌ | ❌ |
supportsUpsertWhere |
✅ | ❌ | ✅ |
supportsMutationTargetInSubquery |
✅ | ❌ | ✅ |
When a capability is false the query engine picks a fallback strategy (e.g. correlated subqueries instead of lateral joins, or a transactional upsert instead of ON CONFLICT ... WHERE).
How Adapters Are Used
Query engine calls adapter methods instead of generating SQL directly:
// In query engine
const aggregated = ctx.adapter.json.agg(subquery);
// PostgreSQL adapter returns
sql`COALESCE(json_agg(row_to_json(${subquery})), '[]'::json)`
// MySQL adapter returns
sql`JSON_ARRAYAGG(JSON_OBJECT(${fields}))`
The query engine doesn’t know which SQL was generated - it just gets a composable fragment.
Connection to Other Layers
- L6 (Query Engine): Query engine delegates all SQL syntax to adapters
- L8 (Drivers): Adapters produce SQL that drivers execute
- L12 (Migrations): Migration drivers, not query adapters, generate database-specific DDL