Database

Databases: SQL for an app or an agent, SQLite by default and Postgres when you outgrow it.

db = noclick.Database.from_name("orders", create_if_missing=True)
db.migrate("migrations/")
db.execute("insert into orders (id, item, qty) values (?, ?, ?)", ["o1", "bun", 3])
for row in db.query("select item, qty from orders where qty > ?", [2]):
    print(row["item"], row["qty"])

The same object serves an app (bound to a project) and an agent keeping its own state. Placeholders are ? with a list, or :name with a dict, on either engine.

class Database

A SQL database: SQLite (engine="sqlite", the default, cheap while idle) or Postgres. query reads, execute and batch write, migrate applies your schema. status is ready, or restoring / branching while an operation works on it. In an agent's or a toolkit's tools= it is three tools, {name}__query, {name}__execute and {name}__schema; only(...) picks some of them.

Attributes
idstr
namestr
enginestr

"sqlite" or "postgres".

statusstr

ready, restoring or branching.

size_bytesint | None
table_countint | None
parent_idstr | None

The database a branch was made from (None if it isn't one).

used_bylist[dict[str, Any]]

Who uses it: apps that bind it (kind project, with the binding name) and agents with it in their tools (kind agent), each with its id and name.

regionstr | None

The region it keeps its data in (noclick.regions()), fixed when it was created; a branch is in its source's.

user_idstr | None
workflowdict[str, Any] | None

{id, name} of the automation whose tables it holds (NoClick makes one for each automation that keeps tables; None for every other database).

created_atAny
console_urlstr | None

This database in the developer console (None off the hosted API).

only

db.only(*operations) -> DatabaseOperations

A tool ref to some of its operations, for tools=: "query" (a read-only statement), "execute" (a statement that writes) and "schema" (its tables and columns). The database itself in tools= gives all three. "upsert" writes one row of one table, inserting it or updating the row with its primary key; its parameters are that table's columns, so the model fills a form rather than writing SQL. Bind its table with .bind(table=...).

Parameters
*operationsstr
Returns
DatabaseOperations

A ref to just those operations, to list in tools=; bind(...) fixes arguments.

Usage

Let an agent read the bookings, but never change them:

bookings = noclick.Database.from_name("booking-db")
concierge = noclick.Agent.from_name(
    "concierge", create_if_missing=True,
    instructions="You book tables. Check the bookings before you promise one.",
    tools=[bookings.only("query", "schema"), "search"],
)

Or let it save one row at a time to one table, and nothing else:

leads = noclick.Database.from_name("leads")
researcher = noclick.Agent.from_name(
    "researcher", create_if_missing=True,
    instructions="You research each company you're given and save what you find.",
    tools=["exa.search", leads.only("upsert").bind(table="enriched")],
)

createclassmethodasync

noclick.Database.create(
    name, *, engine='sqlite', region=None, user=..., client=None
) -> Database

Create a database called name, unique to its user (a taken name raises noclick.Conflict with existing_id). region (noclick.regions()) is where its data is kept, for good; without one, your account's default region, else us-east. A SQLite database outside eu is placed in its region as capacity allows (us-east and us-west keep it in the United States). An agent or app in the same region reaches it fastest.

Parameters
namestrrequired
engineEnginedefault 'sqlite'
regionstr | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Database

The new database, ready and empty.

Usage

SQLite unless you ask for Postgres:

orders = noclick.Database.create("orders")
analytics = noclick.Database.create("analytics", engine="postgres")

Created inside as_user, it belongs to (and bills) that user:

with noclick.as_user("user_42"):
    notes = noclick.Database.create("notes")

Beside the agent that queries it:

concierge = noclick.Agent.from_name("concierge")
bookings = noclick.Database.create("bookings", region=concierge.region)

from_nameclassmethod

noclick.Database.from_name(
    name, *, create_if_missing=False, engine='sqlite', region=None, branch_data=None,
    user=..., client=None
) -> Database

A lazy reference to the database called name in the current (or given) user. With create_if_missing=True it's created on first use with engine in region (as create); one that exists is used as it is, whatever its engine and region.

Inside a deployed app (signed in by its deployment) the name is looked up in the app's own namespace for the environment the code runs in, and one made there is the app's: a name bound to the app by hand wins, and in a preview of an app whose preview_resources is branch the branch gets its own copy at its first write, reading production's as it is until then, so a preview never changes production's data. A Postgres copy is a copy-on-write branch of production's data. A SQLite copy holds the schema and migrations only (migrate's seed fills it), unless branch_data="copy" asks for production's rows too: they're read at one moment while production keeps serving, from a database of up to 200,000 rows and 16 MB. Asked after the branch's copy was made without them, that copy goes to the trash and the next write makes one with them. branch_data changes nothing outside a preview.

Parameters
namestrrequired
create_if_missingbooldefault False
engineEnginedefault 'sqlite'
regionstr | Nonedefault None
branch_dataBranchData | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Database

A database that loads (or is created) the first time it's used. Nothing is sent yet.

Usage

Make your code the definition, so the first run creates it:

db = noclick.Database.from_name("orders", create_if_missing=True)
db.migrate("migrations/")
print(db.query("select count(*) as n from orders").rows[0]["n"])

An agent's own state, one database per user:

with noclick.as_user("user_42"):
    memory = noclick.Database.from_name("assistant-state", create_if_missing=True)
    memory.execute("create table if not exists notes (id integer primary key, body text)")

A preview that tries a change against production's rows, never touching them:

db = noclick.Database.from_name("orders", create_if_missing=True, branch_data="copy")

from_idclassmethod

noclick.Database.from_id(database_id, *, client=None, user=...) -> Database

A lazy reference to a database by id.

Parameters
database_idstrrequired
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Database

A database that loads the first time you read a field. Nothing is sent yet.

Usage
db = noclick.Database.from_id("6c2e9a41-7d3b-4f15-8e0a-2b9c4d7f1e63")
print(db.query("select count(*) as n from bookings").rows[0]["n"])

getclassmethodasync

noclick.Database.get(database_id, *, user=..., client=None) -> Database

Load a database by id now; an unknown id raises noclick.NotFound.

Parameters
database_idstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Database

The database, loaded: its engine, size, table count and status.

Usage
db = noclick.Database.get("6c2e9a41-7d3b-4f15-8e0a-2b9c4d7f1e63")
print(db.name, db.engine, db.size_bytes, db.status)

listclassmethodasync

noclick.Database.list(
    *, engine=None, limit=None, user_filter=None, user=..., client=None
) -> list[Database]

The current user's databases (the account's own outside as_user, or those user_filter covers), branches included.

Parameters
engineEngine | Nonedefault None
limitint | None

Return at most this many.

(default None)
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
list[Database]

Every matching database, newest first, fetched page by page.

Usage

Your account's databases:

for db in noclick.Database.list():
    print(db.name, db.engine, db.size_bytes)

Every user's Postgres databases:

big = noclick.Database.list(engine="postgres", user_filter=noclick.ALL_USERS)

list_pageclassmethodasync

noclick.Database.list_page(
    *, cursor=None, limit=None, engine=None, user_filter=None, user=..., client=None
) -> Page

One page of databases; pass next_cursor back as cursor.

Parameters
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
engineEngine | Nonedefault None
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Page

A page whose data holds the databases and whose next_cursor is None on the last page.

Usage
page = noclick.Database.list_page(limit=50, user_filter=noclick.ALL_USERS)
while True:
    for db in page.data:
        print(db.user_id, db.name, db.size_bytes)
    if page.next_cursor is None:
        break
    page = noclick.Database.list_page(limit=50, user_filter=noclick.ALL_USERS, cursor=page.next_cursor)

queryasync

db.query(sql, params=None) -> QueryResult

Run a read: one select (or with ... select) that changes nothing. It runs read-only, so a statement that writes raises noclick.InvalidRequest; use execute for those. A result is at most 10 MB and a statement stops after 30 seconds. On Postgres, noclick.embed(:q, model) turns a parameter or a quoted text into a halfvec before the statement runs (up to 100 distinct texts a request; not over connection_url), so a read, an agent's query tool among them, can search by meaning without your code embedding first (billed as AI to the database's user).

Parameters
sqlstrrequired
paramsParamsdefault None
Returns
QueryResult

The rows as dicts, in rows or by iterating the result, with columns, rows_read and duration_ms.

Usage

Pass values as parameters, never in the SQL string:

db = noclick.Database.from_name("orders")
for row in db.query("select id, item, qty from orders where qty > ?", [2]):
    print(row["id"], row["item"], row["qty"])

Named parameters take a dict:

db = noclick.Database.from_name("booking-db")
result = db.query(
    "select * from bookings where venue = :venue and starts_at >= :day",
    {"venue": "Harbor Room", "day": "2026-10-01"},
)
print(result.columns, len(result), result.rows_read)

Search by meaning in one statement, embedding the question in SQL:

kb = noclick.Database.from_name("help-center")
for row in kb.query(
    "select id, title from articles "
    "order by embedding <=> noclick.embed(:q, 'openrouter/openai/text-embedding-3-small') limit 5",
    {"q": "how long do refunds take"},
):
    print(row["id"], row["title"])

executeasync

db.execute(sql, params=None) -> QueryResult

Run one statement that writes: insert, update, delete or DDL (several go in a batch). A SQLite database that would pass 10 GB raises noclick.QuotaExceeded, whose next step is branching into Postgres. SQLite refuses virtual tables, so every database exports as plain SQL.

Parameters
sqlstrrequired
paramsParamsdefault None
Returns
QueryResult

What it did: rows_written, last_row_id for an insert on SQLite, and the rows a returning clause gave back.

Usage
db = noclick.Database.from_name("orders", create_if_missing=True)
db.execute("create table if not exists orders (id text primary key, item text, qty integer)")
done = db.execute("insert into orders (id, item, qty) values (?, ?, ?)", ["o1", "bun", 3])
print(done.rows_written)   # 1

batchasync

db.batch(statements) -> list[QueryResult]

Run up to 100 statements in one transaction: all of them apply or none do. Each is the SQL alone or a (sql, params) pair.

Parameters
statementsSequence[Statement]required
Returns
list[QueryResult]

One QueryResult per statement, in order.

Usage
db = noclick.Database.from_name("booking-db")
moved, freed = db.batch([
    ("update bookings set slot = ? where id = ?", ["18:30", "b_41"]),
    ("update slots set taken = 0 where time = ?", ["18:00"]),
])
print(moved.rows_written, freed.rows_written)

migrateasync

db.migrate(migrations, *, dry_run=False, seed=None) -> MigrationResult

Apply the migrations the database hasn't run yet, in name order: migrations is a local directory (its *.sql files, read here) or a {name: sql} dict. They run in one transaction, so a failing migration applies none of them and raises noclick.InvalidRequest naming it. A name that already ran with different SQL raises noclick.Conflict. dry_run=True reports what would run and changes nothing.

seed is a SQL file that runs once, after the migrations and in their transaction, on a preview branch's database that holds none of production's data (a SQLite copy made without its rows, or one only the branch has), so the preview has something to show; it shows in applied and the ledger as (seed). Production never runs it. Without seed, a seed.sql beside the migrations directory is the seed (supabase/seed.sql beside supabase/migrations/ is).

Parameters
migrationsstr | os.PathLike[str] | Mapping[str, str]required
dry_runbooldefault False
seedstr | os.PathLike[str] | Nonedefault None
Returns
MigrationResult

The migrations applied now, and those still pending (all the new ones after a dry run).

Usage

Keep migrations next to your code, named so they sort in order (0001_init.sql, 0002_add_slots.sql), and run them on every deploy:

db = noclick.Database.from_name("booking-db", create_if_missing=True)
result = db.migrate("migrations/")
print(result.applied)   # ['0002_add_slots.sql'], or [] when there was nothing new

Or pass them inline, and check first:

db = noclick.Database.from_name("orders", create_if_missing=True)
plan = db.migrate({"0001_init.sql": "create table orders (id text primary key, qty integer)"}, dry_run=True)
print(plan.pending)

Sample rows for every preview, from db/seed.sql beside db/migrations/:

db.migrate("db/migrations/")

migrationsasync

db.migrations() -> list[Migration]

The migrations this database has applied.

Returns
list[Migration]

Each applied migration with when it ran, oldest first.

Usage
db = noclick.Database.from_name("booking-db")
for migration in db.migrations():
    print(migration.name, migration.applied_at)

schemaasync

db.schema() -> list[Table]

The database's tables, their columns and row counts.

Returns
list[Table]

Every table by name, each with its columns in order and its row_count.

Usage
db = noclick.Database.from_name("booking-db")
for table in db.schema():
    print(table.name, table.row_count, [column.name for column in table.columns])

restoreasync

db.restore(*, at) -> Operation

Put the database back as it was at at (a timezone-aware datetime), in place: anything written since is gone. at must be within its history: 30 days on SQLite, 7 on Postgres. While it runs, status is restoring and queries raise noclick.Conflict. To look at the past without losing the present, branch at that time instead.

Parameters
atdatetime | strrequired
Returns
Operation

An operation whose wait() gives this database, restored.

Usage
from datetime import datetime, timedelta, timezone

db = noclick.Database.from_name("booking-db")
before = datetime.now(timezone.utc) - timedelta(hours=2)
restored = db.restore(at=before).wait()
print(restored.status)   # ready

branchasync

db.branch(name, *, at=None, engine=None) -> Operation

Copy the database into a new one called name: as it is now, or as it was at at (within its history: 30 days on SQLite, 7 on Postgres). engine="postgres" branches a SQLite database into Postgres, the way past SQLite's 10 GB. The branch is a database of its own (parent_id names this one) for the same user. While a SQLite database is copied, its status is branching and queries raise noclick.Conflict.

Parameters
namestrrequired
atdatetime | str | Nonedefault None
engineEngine | Nonedefault None
Returns
Operation

An operation whose wait() gives the new Database.

Usage

A copy to test a migration against before it touches production:

db = noclick.Database.from_name("booking-db")
preview = db.branch("booking-db-preview").wait()
preview.migrate("migrations/")

Move to Postgres when SQLite is too small; point your app at the new one once it's ready:

db = noclick.Database.from_name("booking-db")
bigger = db.branch("booking-pg", engine="postgres").wait()
print(bigger.engine, bigger.connection_url(ttl_s=600))

branchesasync

db.branches() -> list[Database]

The databases branched from this one.

Returns
list[Database]

Each branch as a Database, newest first.

Usage
db = noclick.Database.from_name("booking-db")
for branch in db.branches():
    print(branch.name, branch.engine, branch.created_at)

connection_urlasync

db.connection_url(*, ttl_s=3600, read_only=False) -> str

A Postgres connection string for tools that speak the wire protocol (psql, an ORM, a BI tool), with a password that stops working after ttl_s seconds (60 to 86,400), when any session still open on it ends. read_only=True connects as a role that can only read. A SQLite database has no wire protocol and raises noclick.NotAvailable.

Parameters
ttl_sintdefault 3600
read_onlybooldefault False
Returns
str

A postgresql:// URL with its short-lived credentials inside; treat it as a secret.

Usage
import psycopg

db = noclick.Database.from_name("analytics")
with psycopg.connect(db.connection_url(ttl_s=600, read_only=True)) as conn:
    print(conn.execute("select count(*) from events").fetchone())

exportasync

db.export() -> Operation

Export the database as SQL that recreates its schema and rows in any database of its engine: sqlite3 booking.db < databases/booking-db.sql, or a pg_dump script for psql (NoClick's own noclick schema, owners and grants left out). Exporting a Postgres database wakes it, and its compute is billed as any use is.

Returns
Operation

An operation whose wait() gives an ExportBundle: a zip at url holding the dump.

Usage
db = noclick.Database.from_name("booking-db")
bundle = db.export().wait()
print(bundle.url, bundle.expires_at)

deleteasync

db.delete() -> TrashItem

Delete the database and its data: it goes to the trash, restorable for 30 days (its storage billed until it's purged), and its connection URLs stop working. One with branches raises noclick.Conflict naming them until you delete them, as does one an app still binds or an agent has in its tools.

Returns
TrashItem

Its TrashItem: restore() puts it back, tables and rows with it.

Usage
db = noclick.Database.from_name("booking-db-preview")
db.delete()

DatabaseOperations

class DatabaseOperations

Some of a database's operations as tools (db.only(...)), with the arguments bind(...) fixed.

Attributes
databaseDatabase
operationslist[str]
bindingsdict[str, Any]

The arguments NoClick fills on every call, out of the model's reach.

bind

database_operations.bind(**arguments) -> DatabaseOperations

Fix arguments of these operations, as ConnectionOperations.bind does: a bound parameter leaves the tool's schema and NoClick fills it on every call, from {{user.id}}, {{user.email}}, {{user.name}} or {{user.metadata.<key>}} where a value names them. upsert needs table bound; bind a column too and every row it writes carries that value. Sends nothing.

Parameters
**argumentsAny
Returns
DatabaseOperations

A new ref with these arguments bound on top of any bound before.

Usage

One tool that writes one enriched company at a time, keyed by its primary key:

db = noclick.Database.from_name("leads")
researcher = noclick.Agent.from_name(
    "researcher",
    create_if_missing=True,
    instructions="You research each company you're given and save what you find.",
    tools=["exa.search", db.only("upsert").bind(table="enriched")],
)

Every row a user's agent writes is stamped with that user:

db = noclick.Database.from_name("notes")
notes = db.only("upsert").bind(table="notes", owner="{{user.id}}")

QueryResult

class QueryResult

What one statement returned. rows holds a dict per row, keyed by the columns in order; iterating the result yields the rows. rows_read and rows_written are what the statement touched: on SQLite the rows it read and wrote, what it's billed on; on Postgres the rows it returned and changed, not billed. last_row_id is the rowid an insert made on SQLite (None on Postgres, where returning gives it).

Attributes
columnslist[str]
rowslist[dict[str, Any]]
rows_readint
rows_writtenint
last_row_idint | None
duration_msfloat | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

Migration

class Migration

A migration the database has applied: its name (the file name), when (applied_at), and the checksum of the SQL that ran. A later migrate that sends the same name with different SQL is refused.

Attributes
namestr
applied_atAny
checksumstr | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

MigrationResult

class MigrationResult

What migrate did: applied names the migrations it ran now, in order; pending names those still to run (empty after a real run, everything new after a dry_run). Migrations applied before are in neither.

Attributes
appliedlist[str]
pendinglist[str]
rawdict[str, Any]

The response JSON exactly as the API sent it.

Table

class Table

A table: its name, its columns in order, and row_count (exact on SQLite, the planner's estimate on Postgres).

Attributes
namestr
columnslist[Column]
row_countint | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

Column

class Column

One column of a table: its name, its type as the engine spells it, whether it's nullable and part of the primary_key, and its default expression (None when it has none).

Attributes
namestr
typestr
nullablebool
primary_keybool
defaultstr | None