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 DatabaseA 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.
- Like every live object, it also has hydrate(), refresh() and raw.
| id | str | |
| name | str | |
| engine | str |
|
| status | str |
|
| size_bytes | int | None | |
| table_count | int | None | |
| parent_id | str | None | The database a branch was made from (None if it isn't one). |
| used_by | list[dict[str, Any]] | Who uses it: apps that bind it ( |
| region | str | None | The region it keeps its data in ( |
| user_id | str | None | |
| workflow | dict[str, Any] | None |
|
| created_at | Any | |
| console_url | str | None | This database in the developer console (None off the hosted API). |
only
db.only(*operations) -> DatabaseOperationsA 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=...).
A ref to just those operations, to list in tools=; bind(...) fixes arguments.
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
) -> DatabaseCreate 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.
'sqlite'NoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The new database, ready and empty.
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
) -> DatabaseA 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.
False'sqlite'NoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)A database that loads (or is created) the first time it's used. Nothing is sent yet.
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=...) -> DatabaseA lazy reference to a database by id.
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
A database that loads the first time you read a field. Nothing is sent yet.
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) -> DatabaseLoad a database by id now; an unknown id raises noclick.NotFound.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The database, loaded: its engine, size, table count and status.
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.
NoneReturn at most this many.
None)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.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Every matching database, newest first, fetched page by page.
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
) -> PageOne page of databases; pass next_cursor back as cursor.
The next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)NoneWhich users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)A page whose data holds the databases and whose next_cursor is None on the last page.
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) -> QueryResultRun 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).
NoneThe rows as dicts, in rows or by iterating the result, with columns, rows_read
and duration_ms.
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) -> QueryResultRun 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.
NoneWhat it did: rows_written, last_row_id for an insert on SQLite, and the rows
a returning clause gave back.
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) # 1batchasync
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.
One QueryResult per statement, in order.
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) -> MigrationResultApply 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).
FalseNoneThe migrations applied now, and those still pending (all the new ones after a dry run).
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 newOr 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.
Each applied migration with when it ran, oldest first.
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.
Every table by name, each with its columns in order and its row_count.
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) -> OperationPut 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.
An operation whose wait() gives this database, restored.
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) # readybranchasync
db.branch(name, *, at=None, engine=None) -> OperationCopy 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.
NoneNoneA 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.
Each branch as a Database, newest first.
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) -> strA 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.
3600FalseA postgresql:// URL with its short-lived credentials inside; treat it as a secret.
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() -> OperationExport 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.
An operation whose wait() gives an ExportBundle: a zip at url holding the dump.
db = noclick.Database.from_name("booking-db")
bundle = db.export().wait()
print(bundle.url, bundle.expires_at)deleteasync
db.delete() -> TrashItemDelete 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.
db = noclick.Database.from_name("booking-db-preview")
db.delete()DatabaseOperations
class DatabaseOperationsSome of a database's operations as tools (db.only(...)), with the
arguments bind(...) fixed.
| database | Database | |
| operations | list[str] | |
| bindings | dict[str, Any] | The arguments NoClick fills on every call, out of the model's reach. |
bind
database_operations.bind(**arguments) -> DatabaseOperationsFix 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.
A new ref with these arguments bound on top of any bound before.
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 QueryResultWhat 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).
| columns | list[str] | |
| rows | list[dict[str, Any]] | |
| rows_read | int | |
| rows_written | int | |
| last_row_id | int | None | |
| duration_ms | float | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
Migration
class MigrationA 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.
| name | str | |
| applied_at | Any | |
| checksum | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
MigrationResult
class MigrationResultWhat 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.
| applied | list[str] | |
| pending | list[str] | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
Table
class TableA table: its name, its columns in order, and row_count (exact on
SQLite, the planner's estimate on Postgres).
| name | str | |
| columns | list[Column] | |
| row_count | int | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
Column
class ColumnOne 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).
| name | str | |
| type | str | |
| nullable | bool | |
| primary_key | bool | |
| default | str | None |