Databases
Databases back stores. Define one in your project, map it to a path alias in
tsconfig.json, and import it wherever you need it.
Available databases
| Database | Storage | Type |
|---|---|---|
| MemoryDB | in-memory | relational |
| SQLite | in-memory/file | relational |
| MySQL | DBMS | relational |
| PostgreSQL | DBMS | relational |
| OracleDB | DBMS | relational |
| JSONDB | JSON files | document |
| MongoDB | DBMS | document |
MemoryDB
For development, tests, or ephemeral use, Primate ships a built-in in-memory database:
import memorydb from "primate/memorydb";
const db = memorydb();MemoryDB requires no configuration and holds no state between instances. It is particularly useful in test suites where you want a clean database per test.
Setting up a database
Create a file for your database configuration and map it to a path alias in
tsconfig.json:
// config/db.ts
import postgresql from "@primate/postgresql";
export default postgresql({
host: "localhost",
port: 5432,
database: "myapp",
username: "myapp",
password: "myapp",
});
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/config/db": ["./config/db"]
}
}
}The @/config/db alias is conventional but not required — @db, @/db, or any other
alias you prefer works equally well.
If you plan to use your database or stores outside of Primate, make sure to add
a package.json import as well.
// package.json
{
"imports": {
"@/config/db": "./config/db.ts"
}
}
Using a database in a store
Import your database and pass it explicitly to the store:
import db from "@/config/db";
import store from "primate/store";
import p from "pema";
export default store({
table: "post",
db,
schema: {
id: store.key.primary(p.u32),
title: p.string,
},
});See the stores page for the full store API. See Pema and Stores for which schemas can represent persistent fields.
Connection pools
PostgreSQL, MySQL, and MongoDB accept the same optional pool controls:
const db = postgresql({
database: "myapp",
pool: {
max: 10,
connectTimeout: 5_000,
idleTimeout: 60_000,
},
});Timeouts are in milliseconds. connectTimeout limits establishing a new
network connection; it does not limit how long a query can wait for pool
capacity. Omitted values preserve each driver's defaults. See the driver page
for exact defaults and idleTimeout: 0 behavior.
Use fork() when work needs capacity and lifecycle independent from the Store
pool, such as a long transaction:
const dedicated = db.fork({ pool: { max: 2 } });
try {
// dedicated.client, dedicated.sql(), and Store operations use another pool
} finally {
await dedicated.close();
}The fork privately inherits the endpoint, credentials, and omitted pool
controls. Closing it does not close the parent, and closing the parent does not
close the fork. Call close() on every fork you create.
Raw SQL
For queries that don't fit the store API — complex joins, aggregates,
database-specific functions, or raw DDL — all four SQL drivers (SQLite, MySQL,
PostgreSQL, and OracleDB) expose db.sql for immediate execution:
import db from "@/config/db";
import p from "pema";
const results = await db.sql({
query: "SELECT name FROM users WHERE age > :age",
params: { age: 18 },
output: p.array({ name: p.string }),
});Named placeholders such as :age map to params keys and are always passed
through the driver's parameter-binding mechanism rather than interpolated into
the query. TypeScript enforces that every placeholder has a matching parameter
and that no unrelated parameters are supplied. An output schema validates the
database result and supplies its inferred return type. The output may use any
suitable validation schema, including Pema collections.
Both params and output are optional. A write-only query needs no output and
returns void:
await db.sql({
query: "INSERT INTO users (name, age) VALUES (:name, :age)",
params: { name: "John", age: 30 },
});A query with no parameters omits params:
const results = await db.sql({
query: "SELECT name FROM users",
output: p.array({ name: p.string }),
});The string shorthand executes static SQL and deliberately discards its result:
await db.sql("CREATE INDEX idx_users_age ON users (age)");Each db.sql call is a standalone operation. Writes commit before the returned
promise resolves; db.sql is not an explicit transaction API. If you manage a
driver transaction through db.client, use that client consistently for every
statement in the transaction rather than mixing in db.sql calls.
Placeholder translation is handled automatically per driver — SQLite uses
$name, while MySQL, PostgreSQL, and OracleDB use positional parameters. You
always write :name and the driver does the rest. Parameter values are raw
driver values; use explicit SQL conversion functions or db.client for
driver-specific representations and behavior.
Typical project layout
config/
db.ts # database configuration
stores/
Post.ts # store using @/config/db