DataFn
Database

Database Adapters

Connect DataFn to your database.

DataFn uses database adapters to interface with databases on the server. The adapter provides a uniform API for CRUD operations, transactions, and schema management across different database backends.

Adapter Interface

The TypeScript adapter uses the @superfunctions/db adapter system:

interface Adapter {
  readonly id: string;
  readonly name: string;
  readonly version: string;
  readonly capabilities: AdapterCapabilities;

  create<T>(params: CreateParams): Promise<T>;
  findOne<T>(params: FindOneParams): Promise<T | null>;
  findMany<T>(params: FindManyParams): Promise<T[]>;
  update<T>(params: UpdateParams): Promise<T>;
  delete(params: DeleteParams): Promise<void>;

  createMany<T>(params: CreateManyParams): Promise<T[]>;
  updateMany(params: UpdateManyParams): Promise<number>;
  deleteMany(params: DeleteManyParams): Promise<number>;

  upsert<T>(params: UpsertParams): Promise<T>;
  count(params: CountParams): Promise<number>;

  transaction<R>(callback: (trx: TransactionAdapter) => Promise<R>): Promise<R>;

  initialize(): Promise<void>;
  isHealthy(): Promise<HealthStatus>;
  close(): Promise<void>;

  getSchemaVersion(namespace: string): Promise<number>;
  setSchemaVersion(namespace: string, version: number): Promise<void>;
  validateSchema(schema: TableSchema): Promise<ValidationResult>;

  readonly internal: InternalCrud;
}

The Python adapter uses a Protocol-based interface:

from typing import Any, Dict, List, Optional, AsyncContextManager, Protocol

class Adapter(Protocol):
    async def find_many(
        self,
        model: str,
        where: List[Dict[str, Any]],
        limit: Optional[int] = None,
        sort: Optional[List[str]] = None,
        cursor: Optional[Dict[str, Any]] = None,
        namespace: str = "datafn",
    ) -> List[Dict[str, Any]]:
        ...

    async def find_one(
        self,
        model: str,
        where: List[Dict[str, Any]],
        namespace: str = "datafn",
    ) -> Optional[Dict[str, Any]]:
        ...

    async def create(
        self,
        model: str,
        data: Dict[str, Any],
        namespace: str = "datafn",
    ) -> None:
        ...

    async def update(
        self,
        model: str,
        where: List[Dict[str, Any]],
        data: Dict[str, Any],
        namespace: str = "datafn",
    ) -> None:
        ...

    async def delete(
        self,
        model: str,
        where: List[Dict[str, Any]],
        namespace: str = "datafn",
    ) -> None:
        ...

    def transaction(self) -> AsyncContextManager["Adapter"]:
        ...

Python Method Parameters

ParameterTypeDescription
modelstrResource (table) name.
wherelist[dict]Filter conditions as a list of {field, op, value} dicts.
limitint | NoneMaximum records to return.
sortlist[str] | NoneSort fields, e.g. ["+createdAt", "-title"].
cursordict | NoneCursor-based pagination state.
namespacestrNamespace for multi-tenant isolation. Defaults to "datafn".
datadictRecord data for create/update operations.

Namespace Support

All CRUD operations accept an optional namespace parameter for row-level isolation. When row-level namespacing is enabled, the adapter automatically filters queries by a discriminator column (default: __ns):

await adapter.findMany({ model: "todos", where: [], namespace: "tenant-1" });
// Internally: SELECT * FROM todos WHERE __ns = 'tenant-1'

Namespace is always server-derived. The client never supplies the namespace directly.

Operation Parameters

// Create
await adapter.create({
  model: "todos",
  data: { id: "t1", title: "Buy milk" },
  namespace: "tenant-1",
});

// Find
const todo = await adapter.findOne({
  model: "todos",
  where: [{ field: "id", operator: "eq", value: "t1" }],
  namespace: "tenant-1",
});

const todos = await adapter.findMany({
  model: "todos",
  where: [{ field: "status", operator: "eq", value: "active" }],
  orderBy: [{ field: "createdAt", direction: "desc" }],
  limit: 50,
  namespace: "tenant-1",
});

// Update and Delete
await adapter.update({
  model: "todos",
  where: [{ field: "id", operator: "eq", value: "t1" }],
  data: { status: "done" },
  namespace: "tenant-1",
});

await adapter.delete({
  model: "todos",
  where: [{ field: "id", operator: "eq", value: "t1" }],
  namespace: "tenant-1",
});
# Create
await adapter.create("todos", {"id": "t1", "title": "Buy milk"}, namespace="tenant-1")

# Find
todo = await adapter.find_one("todos", [{"field": "id", "op": "eq", "value": "t1"}], namespace="tenant-1")

todos = await adapter.find_many(
    "todos",
    [{"field": "status", "op": "eq", "value": "active"}],
    sort=["+createdAt"],
    limit=50,
    namespace="tenant-1",
)

# Update and Delete
await adapter.update(
    "todos",
    [{"field": "id", "op": "eq", "value": "t1"}],
    {"status": "done"},
    namespace="tenant-1",
)

await adapter.delete(
    "todos",
    [{"field": "id", "op": "eq", "value": "t1"}],
    namespace="tenant-1",
)

Transactions

await adapter.transaction(async (trx) => {
  await trx.create({ model: "todos", data: { id: "t1", title: "Buy milk" } });
  await trx.update({
    model: "todos",
    where: [{ field: "id", operator: "eq", value: "t2" }],
    data: { completed: true },
  });
});

The transaction() method returns an async context manager. Operations within the context use a single database transaction:

async with adapter.transaction() as tx:
    await tx.create("todos", {"id": "t1", "title": "Buy milk"})
    await tx.update("todos", [{"field": "id", "op": "eq", "value": "t2"}], {"completed": True})
    # Commits on successful exit, rolls back on exception.

Where Clause Operators

OperatorDescription
eqEqual to
neNot equal to
gtGreater than
gteGreater than or equal to
ltLess than
lteLess than or equal to
inIn array
not_inNot in array
containsString contains
starts_withString starts with
ends_withString ends with

Internal Tables

DataFn uses several internal tables managed via the adapter.internal API:

TablePurpose
__datafn_metaStores next_server_seq per namespace
__datafn_changesChange log entries for incremental sync
__datafn_idempotencyMutation deduplication records
__datafn_seedSeed tracking metadata

Internal tables are created automatically on first use via CREATE TABLE IF NOT EXISTS.

Available Adapters

AdapterPackageLanguageDatabases
Drizzle@superfunctions/dbTypeScriptPostgreSQL, MySQL, SQLite
Memory@superfunctions/dbTypeScriptIn-memory (testing)
CustomPythonAny (implement the Adapter protocol)

Row-Level Namespace Configuration

const adapter = drizzleAdapter({
  db: drizzleInstance,
  dialect: "postgres",
  rowLevelNamespace: {
    enabled: true,
    columnName: "__ns",    // Default
    mandatory: true,       // Default
  },
});