Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

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, but it does not inspect their rendered SQL to recover semantic facts.

If a later compiler fold needs to know a fact such as the target table, whether an insert skips duplicates, or whether an identity is database-assigned, L6 carries that fact beside the Sql fragment. It does not recognize PostgreSQL, MySQL, or SQLite tokens. Provider error codes and message recognition belong to driver error mapping.

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 capability-specific path (for example, 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 knows the operation meaning and receives a composable fragment; the adapter alone owns its dialect spelling.

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

Was this page helpful?