This is the full developer documentation for DB9 # DB9 Documentation > PostgreSQL-compatible distributed SQL database built on TiKV. Instant provisioning, multi-tenant isolation, and built-in extensions for AI agents. DB9 gives every agent, user, or CI run its own Postgres-compatible database in under a second. Use the sections below to find your starting point — whether you’re evaluating DB9, building agent workflows, setting up production infrastructure, or integrating with your existing stack. ## Start Here [Section titled “Start Here”](#start-here) What is DB9? What DB9 is, who it’s for, and how it compares to other serverless Postgres options. [Read the overview →](/docs/overview/) Quick Start Install the CLI or SDK and create your first database in under a minute. [Get started →](/docs/quickstart/) Why DB9 for AI Agents Built-in embeddings, file system, HTTP from SQL, branching, and instant provisioning — why agents choose DB9. [Read more →](/docs/why-db9-for-ai-agents/) Connect Connection strings, psql, ORMs, drivers, TLS, and authentication — everything you need to connect. [Connect →](/docs/connect/) Architecture TiKV storage, pgwire protocol, SQL execution pipeline, multi-tenant isolation, and the extension system. [Architecture →](/docs/architecture/) Production Checklist Authentication, secrets, connection management, branching strategy, observability, and recovery. [Production checklist →](/docs/production-checklist/) ## Build [Section titled “Build”](#build) Guides for agent workflows, embeddings, file ingestion, HTTP calls, branching, and scheduling. Agent Workflows The full agent lifecycle — provisioning, embeddings, file ingestion, HTTP calls, branching, and scheduling through SQL. [Agent workflows →](/docs/agent-workflows/overview/) Vector Search Built-in embeddings with `embedding()`, HNSW indexes, and semantic search — no external embedding API needed. [Vector search →](/docs/extensions/vector/) File System (fs9) Store and query files, logs, and artifacts through SQL using the fs9 extension. [fs9 reference →](/docs/extensions/fs9/) HTTP from SQL Call external APIs directly from SQL — webhooks, LLM calls, and data enrichment without leaving the database. [HTTP extension →](/docs/extensions/http/) Scheduled Jobs Run periodic tasks with pg\_cron — maintenance, data pipelines, and automated workflows. [pg\_cron reference →](/docs/extensions/pg-cron/) ## Platform [Section titled “Platform”](#platform) How DB9 provisions, isolates, and manages databases at scale. Provisioning Create, manage, and delete databases programmatically — CLI, SDK, or REST API. Fleet patterns and lifecycle states. [Provisioning →](/docs/platform/provisioning/) Multi-Tenant Patterns Database-per-user, database-per-app, ephemeral-per-task, and branch-per-preview — choose the right isolation model. [Multi-tenant patterns →](/docs/platform/multi-tenant-patterns/) ## Reference [Section titled “Reference”](#reference) CLI Reference Full reference for the `db9` command-line tool — auth, databases, SQL, filesystem, branching, and cron. [CLI docs →](/docs/cli/) TypeScript SDK `get-db9` package — `instantDatabase()`, full client API, credential management, and TypeScript types. [SDK docs →](/docs/sdk/) SQL Reference Data types, DDL/DML, built-in functions, transactions, system catalog, and advanced SQL features. [SQL reference →](/docs/sql/) Extensions fs9 file system, HTTP client, pg\_cron, vector search, full-text search, hstore, Parquet import, and more. [Extensions →](/docs/extensions/) # DB9 with Claude Code > Give Claude Code full database capabilities — create, query, branch, and manage DB9 databases from natural language prompts. The DB9 skill for Claude Code teaches the agent how to use the `db9` CLI and SQL to create databases, run queries, manage files, create branches, and schedule jobs — all from natural language prompts. ## What the Integration Provides [Section titled “What the Integration Provides”](#what-the-integration-provides) Once the DB9 skill is installed, Claude Code can: * Create and manage databases (`db9 create`, `db9 list`, `db9 delete`) * Run SQL queries (`db9 db sql`) * Upload and query files via the filesystem (`db9 fs`) * Create database branches for safe experimentation (`db9 branch`) * Schedule recurring SQL jobs (`db9 db cron`) * Use built-in extensions: vector search, embeddings, HTTP from SQL, full-text search * Manage authentication and tokens The skill file is a Markdown document installed to Claude Code’s `skills/` directory. Claude Code automatically reads files from `~/.claude/skills/` (user scope) and `./.claude/skills/` (project scope) as context. The DB9 skill includes CLI syntax, SQL patterns, extension usage, and security guidelines. It also instructs Claude Code to periodically re-read the latest version from `https://db9.ai/skill.md`. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and working * The `db9` CLI installed: Terminal ```bash curl -fsSL https://db9.ai/install | sh ``` You do **not** need to be logged in to DB9 before onboarding. The skill installation does not send any tokens or credentials. ## Install the DB9 Skill [Section titled “Install the DB9 Skill”](#install-the-db9-skill) The recommended way to install the skill is via `db9 onboard`: Terminal ```bash # Interactive — auto-detects Claude Code if installed db9 onboard # Non-interactive, Claude Code only db9 onboard --yes --agent claude # Install for both user and project scope db9 onboard --yes --agent claude --scope both ``` ### Scope options [Section titled “Scope options”](#scope-options) | Scope | Install location | When to use | | ------------------------ | ------------------------------- | ----------------------------------- | | `--scope user` (default) | `~/.claude/skills/db9/SKILL.md` | Skill available in all projects | | `--scope project` | `./.claude/skills/db9/SKILL.md` | Skill scoped to this project only | | `--scope both` | Both locations | Recommended for active DB9 projects | ### Safety checks [Section titled “Safety checks”](#safety-checks) Before making any changes, you can preview what `db9 onboard` will do: Terminal ```bash # Show where files would be written (zero changes) db9 onboard --dry-run --agent claude # Show resolved target paths db9 onboard --print-locations --agent claude ``` The onboard command: * Never sends tokens or credentials anywhere * Backs up existing skill files before overwriting * Skips installation if the local version is already newer (use `--force` to override) * Uses atomic file writes to avoid partial installs ## Verify the Installation [Section titled “Verify the Installation”](#verify-the-installation) After installing, verify Claude Code can see the skill: Terminal ```bash # Check the skill file exists cat ~/.claude/skills/db9/SKILL.md | head -5 ``` Expected output: Output ```text --- name: db9 version: 1.0.0 description: Serverless Postgres for AI agents... --- ``` Then start Claude Code and try a prompt: ```plaintext Create a DB9 database called "test-app" and show me the connection string. ``` Claude Code should run `db9 create --name test-app --show-secrets` and return the connection details. ## Example Prompts [Section titled “Example Prompts”](#example-prompts) Once the skill is active, try these prompts with Claude Code: ### Database management [Section titled “Database management”](#database-management) ```plaintext Create a DB9 database for my project and set it as the default. ``` ```plaintext List all my DB9 databases and show which one is the default. ``` ### Schema and queries [Section titled “Schema and queries”](#schema-and-queries) ```plaintext Create a users table with id, email, name, and created_at columns in my DB9 database. Then insert three sample users and query them back. ``` ```plaintext Show me the schema of all tables in my DB9 database. ``` ### Vector search and embeddings [Section titled “Vector search and embeddings”](#vector-search-and-embeddings) ```plaintext Create a documents table with a text column and vector embeddings. Insert some sample documents and run a semantic similarity search. ``` ### File operations [Section titled “File operations”](#file-operations) ```plaintext Upload the README.md file to my DB9 database filesystem, then query its contents using SQL. ``` ### Branching [Section titled “Branching”](#branching) ```plaintext Create a branch of my database called "experiment", add a new column to the users table on the branch, then delete the branch. ``` ### Scheduled jobs [Section titled “Scheduled jobs”](#scheduled-jobs) ```plaintext Set up a pg_cron job that runs VACUUM on my database every night at 3am. ``` ## Update the Skill [Section titled “Update the Skill”](#update-the-skill) The skill file is versioned. To update to the latest version: Terminal ```bash db9 onboard --yes --agent claude ``` If your local version is newer than the remote (e.g., you customized it), the update is skipped. Use `--force` to overwrite: Terminal ```bash db9 onboard --yes --agent claude --force ``` You can also point to a custom skill source: Terminal ```bash # From a URL db9 onboard --agent claude --skill-url https://example.com/custom-skill.md # From a local file db9 onboard --agent claude --skill-path ./my-custom-skill.md ``` ## Current Limitations [Section titled “Current Limitations”](#current-limitations) * **No direct database connection from Claude Code** — Claude Code uses the `db9` CLI to interact with databases, not a direct pgwire connection. All operations go through CLI commands or the REST API. * **CLI must be authenticated** — Before Claude Code can create or manage databases, you need to have run `db9 login` or `db9 create` (which auto-registers an anonymous account) at least once. * **Anonymous account limits** — Anonymous accounts are limited to 5 active databases. Run `db9 claim` to upgrade to a verified account and remove this limit. * **Skill is read-only context** — The skill file provides instructions to Claude Code but does not execute anything. All actual operations run through the `db9` CLI with your normal permissions. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Agent Workflows Overview](/docs/agent-workflows/overview/) — how DB9 fits into agent pipelines * [CLI Reference](/docs/cli/) — complete `db9` command reference * [Quick Start](/docs/quickstart/) — get started with DB9 in two minutes * [Extensions](/docs/extensions/) — vector search, HTTP, fs9, pg\_cron, and more * [Connect](/docs/connect/) — connection strings for ORMs and drivers # Install DB9 Skills > Use db9 onboard to install the DB9 skill into Claude Code, OpenAI Codex, OpenCode, and other coding agents. `db9 onboard` installs or updates the DB9 skill file into supported coding agents. The skill teaches agents how to use the `db9` CLI and SQL for database management, queries, file operations, branching, and scheduling. ## How It Works [Section titled “How It Works”](#how-it-works) 1. The CLI detects which agents are installed on your system 2. It downloads the latest skill file from `https://db9.ai/skill.md` (or uses an embedded fallback if the download fails) 3. It compares versions and writes `SKILL.md` to each agent’s skills directory 4. The agent reads the skill file as context on future sessions The onboard command **never sends tokens or credentials**. It only writes a Markdown file to your local filesystem. ## Quick Start [Section titled “Quick Start”](#quick-start) Terminal ```bash # Interactive — auto-detects installed agents and prompts db9 onboard # Non-interactive, install for all detected agents db9 onboard --yes --all # Install for specific agents db9 onboard --yes --agent claude --agent codex ``` ## Supported Agents [Section titled “Supported Agents”](#supported-agents) | Agent | `--agent` value | User scope path | Project scope path | Supports `--scope project`? | | -------------- | --------------- | ---------------------------------------- | --------------------------------- | --------------------------- | | Claude Code | `claude` | `~/.claude/skills/db9/SKILL.md` | `./.claude/skills/db9/SKILL.md` | Yes | | OpenAI Codex | `codex` | `~/.codex/skills/db9/SKILL.md` | — | No (user only) | | OpenCode | `opencode` | `~/.config/opencode/skills/db9/SKILL.md` | `./.opencode/skills/db9/SKILL.md` | Yes | | Generic agents | `agents` | `~/.agents/skills/db9/SKILL.md` | `./.agents/skills/db9/SKILL.md` | Yes | * **Codex** respects the `CODEX_HOME` environment variable. If set, the skill installs to `$CODEX_HOME/skills/db9/SKILL.md` instead of `~/.codex/...`. * **Generic agents** (`--agent agents`) targets a shared `~/.agents/` directory for agent frameworks that follow this convention. ## Scope [Section titled “Scope”](#scope) The `--scope` flag controls where the skill file is written: | Scope | Behavior | | ------------------------ | ------------------------------------------------------------------------------------ | | `--scope user` (default) | Installs to the user’s home directory. Skill is available in all projects. | | `--scope project` | Installs relative to the current working directory. Skill is scoped to this project. | | `--scope both` | Installs to both locations (when supported by the agent). | Terminal ```bash # User scope (default) db9 onboard --yes --agent claude # Project scope db9 onboard --yes --agent claude --scope project # Both scopes db9 onboard --yes --agent claude --scope both ``` > **Codex limitation:** OpenAI Codex currently supports only `--scope user`. Requesting `--scope project --agent codex` explicitly will produce an error. When using `--all --scope project`, Codex is automatically skipped with a warning. ## Auto-Detection [Section titled “Auto-Detection”](#auto-detection) When you run `db9 onboard` without `--agent` or `--all`, the CLI detects which agents are installed: | Agent | Detection method | | -------------- | ------------------------------------------------------------------- | | Claude Code | `claude` binary on PATH or `~/.claude/` directory exists | | OpenAI Codex | `codex` binary on PATH, `$CODEX_HOME` exists, or `~/.codex/` exists | | OpenCode | `opencode` binary on PATH or `~/.config/opencode/` exists | | Generic agents | `~/.agents/` or `./.agents/` directory exists | If no agents are detected, the CLI prints a tip suggesting explicit `--agent` flags. ## Safety and Introspection [Section titled “Safety and Introspection”](#safety-and-introspection) ### Dry run [Section titled “Dry run”](#dry-run) Preview all actions without writing anything: Terminal ```bash db9 onboard --dry-run ``` Output shows each target with its planned action (`install`, `update`, or `skip`) and the reason. ### Print locations [Section titled “Print locations”](#print-locations) Show resolved file paths without downloading or writing: Terminal ```bash db9 onboard --print-locations db9 onboard --print-locations --agent claude --scope both ``` ### Version comparison [Section titled “Version comparison”](#version-comparison) The skill file uses semantic versioning in its YAML frontmatter (`version: 1.0.0`). On update: * If the local version **matches** the remote, the file is skipped (up-to-date) * If the local version is **older**, the file is updated * If the local version is **newer** (e.g., you customized it), the update is skipped — use `--force` to override ### Backup [Section titled “Backup”](#backup) When updating an existing skill file, the CLI creates a timestamped backup (e.g., `SKILL.md.bak.1710000000000`) before overwriting. ### Atomic writes [Section titled “Atomic writes”](#atomic-writes) File writes use a temporary file and atomic rename to prevent partial installs. ## Update and Force [Section titled “Update and Force”](#update-and-force) Terminal ```bash # Update to the latest skill version db9 onboard --yes --all # Force overwrite even if local version is newer db9 onboard --yes --agent claude --force ``` ## Custom Skill Source [Section titled “Custom Skill Source”](#custom-skill-source) Point to a custom skill file instead of the default `https://db9.ai/skill.md`: Terminal ```bash # From a URL db9 onboard --agent claude --skill-url https://example.com/custom-skill.md # From a local file db9 onboard --agent claude --skill-path ./my-custom-skill.md ``` If the remote URL download fails and the default URL was used, the CLI falls back to an embedded copy of the skill bundled into the binary. ## All Flags Reference [Section titled “All Flags Reference”](#all-flags-reference) | Flag | Description | | --------------------- | ----------------------------------------------------------------------------------- | | `--agent ` | Agent to install for (repeatable). Values: `codex`, `claude`, `opencode`, `agents`. | | `--all` | Install for all supported agents. | | `--scope ` | Install scope: `user` (default), `project`, or `both`. | | `--yes` / `-y` | Skip prompts and choose safe defaults. | | `--dry-run` | Print intended actions and make zero filesystem changes. | | `--force` | Overwrite existing skill even when local version is newer. | | `--print-locations` | Print resolved target locations without writing. | | `--skill-url ` | Skill source URL (default: `https://db9.ai/skill.md`). | | `--skill-path ` | Install from a local skill file instead of downloading. | ## What the Skill Teaches [Section titled “What the Skill Teaches”](#what-the-skill-teaches) The skill file instructs agents on: * Database operations (create, list, connect, delete, branch) * SQL execution and schema management * Filesystem operations (fs cp, fs sh, fs mount, fs ls) * User and token management * Migration and cron job management * Authentication flows (login, claim, anonymous, adopt) * Browser SDK integration and publishable key management The skill content is updated with each CLI release. Run `db9 onboard --yes --all` to get the latest version. ## Current Limitations [Section titled “Current Limitations”](#current-limitations) * **Codex is user-scope only** — project-scope support may be added in a future Codex release. * **No automatic updates** — the skill file includes an instruction for agents to re-read from the URL periodically, but `db9 onboard` itself must be re-run to update the on-disk file. * **Agent-specific skill directories are a convention** — if an agent framework changes its skills directory path, `db9 onboard` needs a CLI update to match. ## Next Pages [Section titled “Next Pages”](#next-pages) * [DB9 with Claude Code](/docs/agent-workflows/claude-code/) — Claude Code-specific setup and example prompts * [DB9 with OpenAI Codex](/docs/agent-workflows/openai-codex/) — Codex-specific setup and scope constraints * [Agent Workflows Overview](/docs/agent-workflows/overview/) — how DB9 fits into agent pipelines * [CLI Reference](/docs/cli/) — complete command reference including `db9 onboard` # DB9 with OpenAI Codex > Give OpenAI Codex CLI full database capabilities — create, query, branch, and manage DB9 databases from natural language prompts. The DB9 skill for OpenAI Codex teaches the agent how to use the `db9` CLI and SQL to create databases, run queries, manage files, create branches, and schedule jobs — all from natural language prompts. ## What the Integration Provides [Section titled “What the Integration Provides”](#what-the-integration-provides) Once the DB9 skill is installed, Codex can: * Create and manage databases (`db9 create`, `db9 list`, `db9 delete`) * Run SQL queries (`db9 db sql`) * Upload and query files via the filesystem (`db9 fs`) * Create database branches for safe experimentation (`db9 branch`) * Schedule recurring SQL jobs (`db9 db cron`) * Use built-in extensions: vector search, embeddings, HTTP from SQL, full-text search * Manage authentication and tokens The skill file is a Markdown document installed to Codex’s `skills/` directory. It includes CLI syntax, SQL patterns, extension usage, and security guidelines. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * [OpenAI Codex CLI](https://github.com/openai/codex) installed and working * The `db9` CLI installed: Terminal ```bash curl -fsSL https://db9.ai/install | sh ``` You do **not** need to be logged in to DB9 before onboarding. The skill installation does not send any tokens or credentials. ## Install the DB9 Skill [Section titled “Install the DB9 Skill”](#install-the-db9-skill) The recommended way to install the skill is via `db9 onboard`: Terminal ```bash # Interactive — auto-detects Codex if installed db9 onboard # Non-interactive, Codex only db9 onboard --yes --agent codex # Install for all detected agents at once db9 onboard --yes --all ``` ### Scope [Section titled “Scope”](#scope) Codex currently supports **user scope only**. The skill is installed to: ```plaintext ~/.codex/skills/db9/SKILL.md ``` If the `CODEX_HOME` environment variable is set, the skill installs to `$CODEX_HOME/skills/db9/SKILL.md` instead. > **Note:** `--scope project` is not supported for Codex. If you pass `--scope project --agent codex`, the CLI will return an error. When using `--all --scope project`, Codex is automatically skipped with a warning. ### Safety checks [Section titled “Safety checks”](#safety-checks) Before making any changes, you can preview what `db9 onboard` will do: Terminal ```bash # Show where files would be written (zero changes) db9 onboard --dry-run --agent codex # Show resolved target paths db9 onboard --print-locations --agent codex ``` The onboard command: * Never sends tokens or credentials anywhere * Backs up existing skill files before overwriting * Skips installation if the local version is already newer (use `--force` to override) * Uses atomic file writes to avoid partial installs ## Verify the Installation [Section titled “Verify the Installation”](#verify-the-installation) After installing, verify the skill file exists: Terminal ```bash cat ~/.codex/skills/db9/SKILL.md | head -5 ``` Expected output: Output ```text --- name: db9 version: 1.0.0 description: Serverless Postgres for AI agents... --- ``` Then start Codex and try a prompt: ```plaintext Create a DB9 database called "test-app" and show me the connection string. ``` Codex should run `db9 create --name test-app --show-secrets` and return the connection details. ## Example Prompts [Section titled “Example Prompts”](#example-prompts) Once the skill is active, try these prompts with Codex: ### Database management [Section titled “Database management”](#database-management) ```plaintext Create a DB9 database for my project and set it as the default. ``` ```plaintext List all my DB9 databases and delete any that start with "test-". ``` ### Schema and queries [Section titled “Schema and queries”](#schema-and-queries) ```plaintext Create a users table with id, email, name, and created_at columns in my DB9 database. Then insert three sample users and query them back. ``` ### Vector search and embeddings [Section titled “Vector search and embeddings”](#vector-search-and-embeddings) ```plaintext Create a documents table with a text column and vector embeddings. Insert some sample documents and run a semantic similarity search. ``` ### File operations [Section titled “File operations”](#file-operations) ```plaintext Upload the README.md file to my DB9 database filesystem, then query its contents using SQL. ``` ### Branching [Section titled “Branching”](#branching) ```plaintext Create a branch of my database called "experiment", add a new column to the users table on the branch, then delete the branch. ``` ## Update the Skill [Section titled “Update the Skill”](#update-the-skill) The skill file is versioned. To update to the latest version: Terminal ```bash db9 onboard --yes --agent codex ``` If your local version is newer than the remote (e.g., you customized it), the update is skipped. Use `--force` to overwrite: Terminal ```bash db9 onboard --yes --agent codex --force ``` ## Current Limitations [Section titled “Current Limitations”](#current-limitations) * **User scope only** — Codex does not currently support project-scoped skills. The skill is always installed to `~/.codex/skills/db9/SKILL.md` (or `$CODEX_HOME`). * **No direct database connection** — Codex uses the `db9` CLI, not a direct pgwire connection. All operations go through CLI commands or the REST API. * **CLI must be authenticated** — Before Codex can create or manage databases, you need to have run `db9 login` or `db9 create` (which auto-registers an anonymous account) at least once. * **Anonymous account limits** — Anonymous accounts are limited to 5 active databases. Run `db9 claim` to upgrade to a verified account and remove this limit. * **Skill is read-only context** — The skill file provides instructions to Codex but does not execute anything. All actual operations run through the `db9` CLI with your normal permissions. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Agent Workflows Overview](/docs/agent-workflows/overview/) — how DB9 fits into agent pipelines * [DB9 with Claude Code](/docs/agent-workflows/claude-code/) — same integration for Claude Code * [CLI Reference](/docs/cli/) — complete `db9` command reference * [Quick Start](/docs/quickstart/) — get started with DB9 in two minutes * [Extensions](/docs/extensions/) — vector search, HTTP, fs9, pg\_cron, and more # Agent Workflows > How AI agents use DB9 as a programmable backend — provisioning, storing, searching, calling APIs, branching, and scheduling through standard SQL. DB9 gives AI agents a complete backend through the PostgreSQL wire protocol. Instead of stitching together separate services for storage, embeddings, files, HTTP, and scheduling, agents do all of it in SQL. This page is the starting point for building agent workflows on DB9. It maps common agent tasks to the DB9 capabilities that solve them, shows how those capabilities compose, and links to deeper guides. ## Who should read this [Section titled “Who should read this”](#who-should-read-this) * **Agent developers** building assistants, copilots, or autonomous agents that need persistent state. * **Platform engineers** creating agent infrastructure where each agent, user, or task gets its own database. * **Teams already using DB9** who want to move beyond basic storage into full agent automation. If you’re still deciding whether DB9 is the right fit, start with [Why DB9 for AI Agents](/docs/why-db9-for-ai-agents/). ## The agent lifecycle in DB9 [Section titled “The agent lifecycle in DB9”](#the-agent-lifecycle-in-db9) Most agent workflows follow a predictable lifecycle. DB9 has a built-in primitive for each stage: | Agent lifecycle stage | DB9 primitive | How to access | | ------------------------------- | ---------------------------------------- | ------------------- | | **Provision** a workspace | `instantDatabase()` or `db9 create` | SDK, CLI | | **Store** structured state | Standard SQL tables | Any Postgres client | | **Search** semantically | `embedding()` + pgvector operators | SQL | | **Ingest** files and artifacts | fs9 functions and table source | SQL, CLI, SDK | | **Call** external APIs | `http_get()`, `http_post()`, and friends | SQL | | **Branch** for safe experiments | `db9 branch create` | CLI, SDK | | **Schedule** recurring work | pg\_cron | SQL, CLI | | **Onboard** agent tooling | `db9 onboard` | CLI | Each primitive is accessible through standard SQL or the PostgreSQL wire protocol. Agents don’t need a proprietary SDK to use them — any Postgres driver works. ## Provision: one database per agent, user, or task [Section titled “Provision: one database per agent, user, or task”](#provision-one-database-per-agent-user-or-task) Agent systems typically need to create databases on demand. No signup required for agents DB9 provisions a database in under a second with no signup required for anonymous use. Agents can create databases autonomously without any pre-configured credentials. **From the TypeScript SDK:** TypeScript ```typescript import { instantDatabase } from 'get-db9'; const db = await instantDatabase({ name: 'agent-session-42', seed: ` CREATE TABLE memory (id SERIAL, key TEXT, value JSONB); CREATE TABLE artifacts (id SERIAL, path TEXT, content TEXT); `, }); // db.connectionString → ready for any Postgres client ``` ▶ Run `instantDatabase()` is idempotent — if a database with that name already exists, it returns the existing one. This makes agent restarts safe. **From the CLI:** Terminal ```bash db9 create --name agent-session-42 ``` ▶ Run For fleet operations, the SDK client also exposes `databases.create()`, `databases.list()`, `databases.delete()`, and `databases.credentials()` for full programmatic lifecycle management. **Common patterns:** * **Database-per-agent** — each agent instance gets its own database for complete isolation. * **Database-per-user** — multi-tenant applications give each end user a dedicated database. * **Database-per-task** — disposable databases for one-shot jobs, deleted when the task completes. → *Deeper guide: [Provisioning](/docs/platform/provisioning/)* ## Store: standard SQL for structured state [Section titled “Store: standard SQL for structured state”](#store-standard-sql-for-structured-state) Agents store context, conversation history, tool outputs, and intermediate results in regular Postgres tables. DB9 supports the full range of SQL data types, DDL, DML, transactions, and indexes. SQL ```sql -- Agent stores a tool result INSERT INTO memory (key, value) VALUES ('search_result', '{"query": "quarterly revenue", "hits": 14}'); -- Agent retrieves its context SELECT key, value FROM memory ORDER BY id DESC LIMIT 10; ``` Because DB9 speaks the PostgreSQL wire protocol, agents can use any Postgres-compatible ORM or driver: Prisma, Drizzle, SQLAlchemy, TypeORM, or raw `pg` connections. → *Reference: [SQL](/docs/sql/), [Connect](/docs/connect/)* ## Search: built-in embeddings and vector queries [Section titled “Search: built-in embeddings and vector queries”](#search-built-in-embeddings-and-vector-queries) DB9 includes a server-side `embedding()` function that generates vectors without a separate embedding service. Combined with pgvector-compatible operators and HNSW indexes, agents can build semantic search in pure SQL. SQL ```sql -- Enable the extension (once per database) CREATE EXTENSION IF NOT EXISTS embedding; -- Store content with its embedding INSERT INTO knowledge (content, vec) VALUES ( 'DB9 supports branching for safe experiments', embedding('DB9 supports branching for safe experiments')::vector(1024) ); -- Semantic search SELECT content, vec <-> embedding('how do I test safely?')::vector(1024) AS distance FROM knowledge ORDER BY distance LIMIT 5; ``` The `embedding()` function accepts an optional model name and dimensions parameter: SQL ```sql -- Use a specific model SELECT embedding('hello world', 'bedrock/amazon-titan-v2', 1024); ``` Embeddings are generated server-side and cached within each statement execution. The per-statement limit is 100 embedding calls (configurable via the `embedding.max_calls` session parameter). → *Guide: [RAG with Built-in Embeddings](/docs/guides/rag-with-built-in-embeddings/) (coming soon) · Reference: [Vector Search](/docs/extensions/vector/)* ## Ingest: query files from SQL with fs9 [Section titled “Ingest: query files from SQL with fs9”](#ingest-query-files-from-sql-with-fs9) Agents produce and consume files — logs, CSVs, JSON exports, Parquet snapshots. fs9 makes these queryable without loading them into tables first. **Read files as tables:** SQL ```sql -- Query a CSV directly SELECT * FROM extensions.fs9('/data/results.csv'); -- Query a Parquet file (read one file at a time; globs do not decode Parquet) SELECT * FROM extensions.fs9('/exports/january.parquet'); ``` **Manage files from SQL:** SQL ```sql -- Write a result file SELECT fs9_write('/output/summary.json', '{"status": "complete", "rows": 1024}'); -- Read a file SELECT fs9_read('/output/summary.json'); -- List directory contents SELECT * FROM extensions.fs9('/output/', recursive := true); ``` **Manage files from the CLI or SDK:** Terminal ```bash # Copy a local file into the database db9 fs cp ./data.csv mydb:/data/data.csv # Interactive file shell db9 fs sh mydb # FUSE mount (access db files like a local directory) db9 fs mount mydb ./mnt ``` The SDK provides a full file API (`fs.read`, `fs.write`, `fs.list`, `fs.stat`, etc.) over WebSocket for programmatic access. File limits: 100 MB per file, 128 MB concurrent read budget, 10,000 files per glob query. → *Guide: [Analyze Agent Logs with fs9](/docs/guides/analyze-agent-logs-with-fs9/) (coming soon) · Reference: [fs9](/docs/extensions/fs9/)* ## Call: HTTP requests from SQL [Section titled “Call: HTTP requests from SQL”](#call-http-requests-from-sql) Agents often need to call external services — webhooks, LLM APIs, enrichment endpoints. DB9’s HTTP extension makes this possible from SQL without leaving the database: SQL ```sql -- GET request SELECT status, content FROM http_get('https://api.example.com/status'); -- POST with JSON body SELECT status, content FROM http_post( 'https://hooks.slack.com/services/T.../B.../xxx', '{"text": "Agent task complete"}', 'application/json' ); ``` Safety boundaries are enforced: * HTTPS only (port 443) — no plaintext HTTP by default * Private and loopback IPs are blocked (SSRF protection) * 100 requests per SQL statement, 20 concurrent per tenant * 1 MB max response body, 256 KB max request body * 5-second request timeout, 1-second connect timeout → *Guide: [HTTP from SQL](/docs/guides/http-from-sql/) (coming soon) · Reference: [HTTP Extension](/docs/extensions/http/)* ## Branch: safe experiments and rollback [Section titled “Branch: safe experiments and rollback”](#branch-safe-experiments-and-rollback) Agents can fork a database to try a risky operation, validate the result, and discard the branch if it fails: Terminal ```bash # Create a branch db9 branch create mydb --name experiment-v2 # Agent works on the branch using its own connection string... # If the experiment failed, delete the branch db9 branch delete # If it succeeded, promote the result (or just keep using the branch) ``` Branches are independent database copies created from the parent’s schema at the time of branching. Each branch gets its own connection string, credentials, and isolated storage — writes to the branch do not affect the parent. The SDK also supports branching programmatically: TypeScript ```typescript const branch = await client.databases.branch(parentId, { name: 'experiment-v2' }); // branch.connectionString → isolated workspace ``` **Common branch patterns:** * **Preview environments** — branch per pull request for isolated testing. * **Schema experiments** — try a migration on a branch before applying to production. * **Task isolation** — each agent task gets a branch, merged or discarded on completion. → *Guide: [Branching Workflows](/docs/guides/branching-workflows/) (coming soon)* ## Schedule: recurring jobs with pg\_cron [Section titled “Schedule: recurring jobs with pg\_cron”](#schedule-recurring-jobs-with-pg_cron) Agents can schedule periodic work — cache refreshes, log cleanup, periodic API polling — directly in the database: SQL ```sql -- Schedule a cleanup job every 6 hours SELECT cron.schedule( 'cleanup-old-context', '0 */6 * * *', $$DELETE FROM memory WHERE created_at < now() - interval '7 days'$$ ); -- Check execution history SELECT * FROM cron.job_run_details ORDER BY runid DESC LIMIT 5; ``` The CLI provides full cron management: Terminal ```bash db9 db cron mydb create '*/30 * * * *' "SELECT * FROM http_get('https://api.example.com/poll')" db9 db cron mydb list db9 db cron mydb history --limit 10 db9 db cron mydb status ``` Jobs run inside the database engine with no external scheduler. You can enable, disable, and delete jobs through SQL or the CLI. → *Guide: [Scheduled Jobs with pg\_cron](/docs/guides/scheduled-jobs-with-pg-cron/) (coming soon) · Reference: [pg\_cron](/docs/extensions/pg-cron/)* ## Onboard: install DB9 as an agent skill [Section titled “Onboard: install DB9 as an agent skill”](#onboard-install-db9-as-an-agent-skill) The `db9 onboard` command installs a DB9 skill file into your AI coding agent’s skills directory. Once installed, the agent can use DB9 commands as part of its normal workflow. Terminal ```bash # Auto-detect installed agents and install db9 onboard # Target a specific agent db9 onboard --agent claude db9 onboard --agent codex db9 onboard --agent opencode # Install for all detected agents db9 onboard --all # Preview what would be installed db9 onboard --dry-run ``` **Supported agents:** | Agent | User scope | Project scope | | -------------- | ---------------------------------------- | --------------------------------- | | Claude Code | `~/.claude/skills/db9/SKILL.md` | `./.claude/skills/db9/SKILL.md` | | OpenAI Codex | `~/.codex/skills/db9/SKILL.md` | — (user scope only) | | OpenCode | `~/.config/opencode/skills/db9/SKILL.md` | `./.opencode/skills/db9/SKILL.md` | | Generic agents | `~/.agents/skills/db9/SKILL.md` | `./.agents/skills/db9/SKILL.md` | Use `--scope user`, `--scope project`, or `--scope both` to control where skills are installed. Skill files include a semver version in their frontmatter — `db9 onboard` only updates when a newer version is available (use `--force` to override). → *Guide: [Install DB9 Skills](/docs/agent-workflows/install-db9-skills/) (coming soon) · [Claude Code](/docs/agent-workflows/claude-code/) (coming soon) · [OpenAI Codex](/docs/agent-workflows/openai-codex/) (coming soon)* ## Deploy: serverless functions for data processing [Section titled “Deploy: serverless functions for data processing”](#deploy-serverless-functions-for-data-processing) When a workflow requires logic beyond what a single SQL query can express, agents can deploy [Serverless Functions](/docs/functions/) — JavaScript/TypeScript code that runs with native SQL and `ctx.fs9` filesystem access. Functions are useful for data transformation pipelines, webhook handlers, and any task where application code is cleaner than SQL. ## Composing capabilities [Section titled “Composing capabilities”](#composing-capabilities) The real power of DB9 for agents is that these primitives compose through SQL. A single agent workflow can: 1. **Create** a database with `instantDatabase()` 2. **Ingest** a CSV with `SELECT * FROM extensions.fs9('/data/input.csv')` 3. **Embed** and index the content with `embedding()` 4. **Search** semantically with pgvector operators 5. **Call** an external API with `http_post()` to deliver results 6. **Schedule** a follow-up job with `cron.schedule()` 7. **Branch** to try an alternative approach All of this happens inside the database, in SQL, through a standard Postgres connection. No orchestrator, no sidecar, no message queue. ## Next steps [Section titled “Next steps”](#next-steps) * [DB9 with Claude Code](/docs/agent-workflows/claude-code/) — install the DB9 skill and start using databases from Claude Code * [DB9 with OpenAI Codex](/docs/agent-workflows/openai-codex/) — install the DB9 skill for OpenAI Codex CLI * [Quick Start](/docs/quickstart/) — create your first database in under a minute * [Connect](/docs/connect/) — connection strings, drivers, and authentication * [Why DB9 for AI Agents](/docs/why-db9-for-ai-agents/) — the positioning case for using DB9 in agent systems * [CLI Reference](/docs/cli/) — full command reference for `db9 create`, `db9 onboard`, `db9 branch`, and more * [TypeScript SDK](/docs/sdk/) — `instantDatabase()`, client API, and programmatic database management * [RAG with Built-in Embeddings](/docs/guides/rag-with-built-in-embeddings/) — build a retrieval pipeline using DB9-native embeddings * [Branching Workflows](/docs/guides/branching-workflows/) — preview environments, task isolation, and safe migrations using branches * [Extensions](/docs/extensions/) — deep dives into fs9, HTTP, embeddings, vector search, and pg\_cron # REST API Reference > Complete reference for the DB9 Customer REST API — authentication, databases, SQL execution, filesystem, branching, users, tokens, and more. The DB9 REST API lets you manage databases, execute SQL, and interact with the platform programmatically. All customer endpoints live under `https://api.db9.ai/customer`. For most use cases, the [CLI](/docs/cli/) or [TypeScript SDK](/docs/sdk/) provide a more ergonomic interface. The REST API is ideal for custom integrations, CI/CD pipelines, or languages without an SDK. ## Authentication [Section titled “Authentication”](#authentication) Most requests require a `Bearer` token in the `Authorization` header: Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" https://api.db9.ai/customer/databases ``` ▶ Run **Exceptions:** `POST /customer/anonymous-register` requires no credentials. `POST /customer/anonymous-refresh` authenticates using `anonymous_id` + `anonymous_secret` in the request body (no Bearer token). Obtain tokens via: * `db9 token create` (CLI) * `db9 login --api-key ` (verifies and saves an existing API key locally) * Anonymous bootstrap (`/customer/anonymous-register`) ## Error Responses [Section titled “Error Responses”](#error-responses) All errors return a consistent JSON shape: Error Response Shape ```json { "message": "human-readable message describing what went wrong" } ``` | Status | Meaning | | ------ | --------------------------------------------- | | `400` | Bad request — missing or invalid fields | | `401` | Unauthorized — missing or invalid token | | `403` | Forbidden — token lacks required scope | | `404` | Not found — resource does not exist | | `409` | Conflict — resource already exists | | `422` | Unprocessable — valid JSON but semantic error | | `429` | Rate limited — slow down and retry | | `500` | Internal server error | ## Account & Sessions [Section titled “Account & Sessions”](#account--sessions) GET `/customer/me` Return the current customer's identity and profile. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/me ``` ▶ Run Response ```json { "id": "cus_01h9xxxxxxxxxxxxxxxx", "email": "user@example.com", "created_at": "2026-01-15T10:00:00Z", "status": "active" } ``` POST `/customer/anonymous-register` Create an anonymous account and return a session token. No credentials required. Terminal ```bash curl -X POST https://api.db9.ai/customer/anonymous-register ``` ▶ Run Response ```json { "anonymous_id": "anon_01h9xxxxxxxxxxxxxxxx", "anonymous_secret": "sk_anon_xxxxxxxxxxxxxxxxxxxx", "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_at": "2027-01-15T10:00:00Z", "is_anonymous": true } ``` POST `/customer/anonymous-refresh` Refresh an anonymous session token using the customer ID and secret. | Field | Type | Required | Description | | ------------------ | ------ | -------- | ------------------------ | | `anonymous_id` | string | yes | Anonymous customer ID | | `anonymous_secret` | string | yes | Anonymous account secret | Terminal ```bash curl -X POST \ -H "Content-Type: application/json" \ -d '{"anonymous_id": "anon_01h9xx", "anonymous_secret": "sk_anon_xx"}' \ https://api.db9.ai/customer/anonymous-refresh ``` Request ```json { "anonymous_id": "anon_01h9xxxxxxxxxxxxxxxx", "anonymous_secret": "sk_anon_xxxxxxxxxxxxxxxxxxxx" } ``` Response ```json { "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_at": "2027-02-15T10:00:00Z" } ``` POST `/customer/claim` Upgrade an anonymous account to a verified identity using an Auth0 id\_token. Requires a Bearer token. Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"id_token": ""}' \ https://api.db9.ai/customer/claim ``` Request ```json { "id_token": "" } ``` Response ```json { "id": "cus_01h9xxxxxxxxxxxxxxxx", "email": "user@example.com", "claimed": true } ``` POST `/customer/adopt-anonymous-databases/preflight` Pre-check eligibility and quotas before adopting databases from an anonymous account. | Field | Type | Required | Description | | ----------------------- | ------ | -------- | ------------------------------------ | | `anonymous_customer_id` | string | yes | Customer ID of the anonymous account | | `anonymous_secret` | string | yes | Secret for the anonymous account | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"anonymous_customer_id": "anon_01h9xx", "anonymous_secret": "sk_anon_xx"}' \ https://api.db9.ai/customer/adopt-anonymous-databases/preflight ``` Request ```json { "anonymous_customer_id": "anon_01h9xxxxxxxxxxxxxxxx", "anonymous_secret": "sk_anon_xxxxxxxxxxxxxxxxxxxx" } ``` Response ```json { "databases": [ { "id": "db_01h9xxxxxxxxxxxxxxxx", "name": "myapp", "state": "ACTIVE" } ], "quota_ok": true, "current_count": 1, "database_limit": 10 } ``` POST `/customer/adopt-anonymous-databases` Transfer databases from an anonymous account to your authenticated account. | Field | Type | Required | Description | | ----------------------- | --------- | -------- | --------------------------------------- | | `anonymous_customer_id` | string | yes | Customer ID of the anonymous account | | `anonymous_secret` | string | yes | Secret for the anonymous account | | `database_ids` | string\[] | yes | List of database IDs to adopt (max 100) | | `idempotency_key` | string | yes | Unique key for idempotent retries | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"anonymous_customer_id": "anon_01h9xx", "anonymous_secret": "sk_anon_xx", "database_ids": ["db_01h9xx"], "idempotency_key": "adopt-2026-01-15"}' \ https://api.db9.ai/customer/adopt-anonymous-databases ``` Request ```json { "anonymous_customer_id": "anon_01h9xxxxxxxxxxxxxxxx", "anonymous_secret": "sk_anon_xxxxxxxxxxxxxxxxxxxx", "database_ids": ["db_01h9xxxxxxxxxxxxxxxx"], "idempotency_key": "adopt-2026-01-15" } ``` Response ```json { "transferred": ["db_01h9xxxxxxxxxxxxxxxx"], "skipped": [], "total_transferred": 1 } ``` ## Databases [Section titled “Databases”](#databases) POST `/customer/databases` Create a new database. Returns credentials and connection string immediately. | Field | Type | Required | Description | | ---------------- | ------ | -------- | ------------------------------------------ | | `name` | string | yes | Database name | | `region` | string | no | Deployment region | | `admin_password` | string | no | Admin password (auto-generated if omitted) | | `project_id` | string | no | Project ID to group databases under | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "myapp", "region": "us-east-1"}' \ https://api.db9.ai/customer/databases ``` ▶ Run Request ```json { "name": "myapp", "region": "us-east-1" } ``` Response ```json { "id": "db_01h9xxxxxxxxxxxxxxxx", "name": "myapp", "region": "us-east-1", "state": "ready", "connection_string": "postgresql://admin:password@db-01h9xx.db9.ai:5433/myapp", "admin_user": "admin", "admin_password": "generated-password", "created_at": "2026-01-15T10:00:00Z", "last_active_at": null, "last_modified_at": null } ``` GET `/customer/databases` List all databases in your account. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases ``` ▶ Run Response ```json [ { "id": "db_01h9xxxxxxxxxxxxxxxx", "name": "myapp", "region": "us-east-1", "state": "ready", "created_at": "2026-01-15T10:00:00Z", "last_active_at": null, "last_modified_at": null } ] ``` GET `/customer/databases/{database_id}` Get database details including connection string and current state. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx ``` ▶ Run Response ```json { "id": "db_01h9xxxxxxxxxxxxxxxx", "name": "myapp", "region": "us-east-1", "state": "ready", "connection_string": "postgresql://admin:password@db-01h9xx.db9.ai:5433/myapp", "created_at": "2026-01-15T10:00:00Z", "last_active_at": null, "last_modified_at": null } ``` DELETE `/customer/databases/{database_id}` Permanently delete a database. This action is irreversible. Terminal ```bash curl -X DELETE -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx ``` Response ```json { "message": "Database disabled" } ``` POST `/customer/databases/{database_id}/reset-password` Reset the admin password for a database and return the new credentials. Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/reset-password ``` ▶ Run Response ```json { "admin_user": "admin", "admin_password": "new-generated-password", "connection_string": "postgresql://admin:new-generated-password@db-01h9xx.db9.ai:5433/postgres" } ``` GET `/customer/databases/{database_id}/credentials` Retrieve stored admin credentials for a database. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/credentials ``` ▶ Run Response ```json { "admin_user": "admin", "admin_password": "stored-password", "connection_string": "postgresql://admin:stored-password@db-01h9xx.db9.ai:5432/myapp" } ``` GET `/customer/databases/{database_id}/schema` Get the database schema — tables, columns, and types. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/schema ``` ▶ Run Response ```json { "tables": [ { "schema": "public", "name": "users", "columns": [ { "name": "id", "type": "integer", "nullable": false }, { "name": "email", "type": "text", "nullable": false }, { "name": "created_at", "type": "timestamp", "nullable": true } ] } ], "views": [ { "name": "active_users", "schema": "public" } ] } ``` GET `/customer/databases/{database_id}/observability` Get database metrics and slow query samples. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/observability ``` ▶ Run Response ```json { "summary": { "tps": 42.5, "latency_avg_ms": 3.2, "latency_p99_ms": 18.7, "active_connections": 3, "database_storage_bytes": 10485760, "fs_logical_bytes": 2097152 }, "samples": [ { "query": "SELECT * FROM events WHERE user_id = $1", "sample_count": 120, "error_count": 0, "latency_avg_ms": 245.0, "latency_p99_ms": 480.0, "latency_max_ms": 612.0, "last_seen_ms_ago": 5000 } ] } ``` ## Batch Create [Section titled “Batch Create”](#batch-create) POST `/customer/databases/batch` Create multiple databases in a single request. Each item in the array is created independently — partial success is possible. | Field | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------- | | `databases` | array | yes | Array of `{ name, region?, admin_password? }` items | | `project_id` | string | no | Project ID to assign all databases to | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"databases": [{"name": "shard-1"}, {"name": "shard-2", "region": "us-east-1"}], "project_id": "proj-abc"}' \ https://api.db9.ai/customer/databases/batch ``` Request ```json { "databases": [ { "name": "shard-1" }, { "name": "shard-2", "region": "us-east-1" } ], "project_id": "proj-abc" } ``` Response ```json { "created": [ { "id": "db_01...", "name": "shard-1", "state": "ready", ... }, { "id": "db_02...", "name": "shard-2", "state": "ready", ... } ], "failed": [], "total_requested": 2, "total_created": 2 } ``` ## SQL Execution [Section titled “SQL Execution”](#sql-execution) POST `/customer/databases/{database_id}/sql` Execute a SQL query and return results as column names + row arrays. | Field | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------------ | | `query` | string | no | Inline SQL query | | `file_content` | string | no | SQL content (alternative to `query`) | Provide either `query` or `file_content`, not both. Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "SELECT id, email FROM users LIMIT 2"}' \ https://api.db9.ai/customer/databases/db_01h9xx/sql ``` ▶ Run Request ```json { "query": "SELECT id, email FROM users LIMIT 2" } ``` Response ```json { "columns": [ { "name": "id", "data_type": "integer" }, { "name": "email", "data_type": "text" } ], "rows": [ [1, "alice@example.com"], [2, "bob@example.com"] ], "row_count": 2, "command": "SELECT" } ``` POST `/customer/databases/{database_id}/dump` Export schema and data as SQL. Pass {"ddl\_only": true} for schema-only export. Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"ddl_only": false}' \ https://api.db9.ai/customer/databases/db_01h9xx/dump ``` ▶ Run Request ```json { "ddl_only": false } ``` Response ```json { "sql": "-- DB9 dump\nCREATE TABLE users (...);\nINSERT INTO users VALUES (...);", "object_count": 3 } ``` ## Connect Tokens & Keys [Section titled “Connect Tokens & Keys”](#connect-tokens--keys) POST `/customer/databases/{database_id}/connect-token` Mint a short-lived JWT connect token. Pass {"role": "admin"} in the body. Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"role": "admin"}' \ https://api.db9.ai/customer/databases/db_01h9xx/connect-token ``` ▶ Run Request ```json { "role": "admin" } ``` Response ```json { "host": "db-01h9xx.db9.ai", "port": 5433, "database": "postgres", "user": "tenant_id.admin", "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "fs_websocket_url": "wss://fs.db9.ai/ws", "expires_in_seconds": 900, "expires_at": "2026-01-15T11:00:00Z", "tls": "not_enforced" } ``` POST `/customer/databases/{database_id}/connect-keys` Create a long-lived DB Connect Key. The key value is only returned once. Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"role": "admin", "scopes": []}' \ https://api.db9.ai/customer/databases/db_01h9xx/connect-keys ``` ▶ Run Request ```json { "role": "admin", "scopes": [] } ``` Response ```json { "id": "ck_01h9xxxxxxxxxxxxxxxx", "name": "ci", "connect_key": "db9ck_xxxxxxxxxxxxxxxxxxxx", "role": "admin", "scopes": [], "created_at": "2026-01-15T10:00:00Z" } ``` GET `/customer/databases/{database_id}/connect-keys` List all DB Connect Keys (metadata only — key values are never returned). Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/connect-keys ``` ▶ Run Response ```json [ { "id": "ck_01h9xxxxxxxxxxxxxxxx", "name": "ci", "role": "admin", "scopes": [], "created_at": "2026-01-15T10:00:00Z", "expires_at": "2027-01-15T10:00:00Z" } ] ``` DELETE `/customer/databases/{database_id}/connect-keys/{key_id}` Revoke a DB Connect Key immediately. Terminal ```bash curl -X DELETE -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/connect-keys/ck_01h9xx ``` Response ```json { "message": "Connect key revoked" } ``` ## Auth Configuration [Section titled “Auth Configuration”](#auth-configuration) GET `/customer/databases/{database_id}/auth-config` Get the browser data-plane authentication configuration. Default auth\_mode is byo\_jwt. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/auth-config ``` ▶ Run Response ```json { "auth_mode": "byo_jwt", "byo_jwt": { "jwks_url": "https://example.com/.well-known/jwks.json", "audience": "my-app", "issuer": "https://example.com", "subject_claim": "sub", "claims_allowlist": [], "claims_max_bytes": 4096 }, "created_at": "2026-01-15T10:00:00Z", "updated_at": "2026-01-15T10:00:00Z" } ``` PUT `/customer/databases/{database_id}/auth-config` Create or update the browser data-plane authentication configuration. | Field | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------ | | `auth_mode` | string | no | Authentication mode (e.g. `byo_jwt`) | | `byo_jwt` | object | no | Bring-your-own JWT configuration | Terminal ```bash curl -X PUT -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"auth_mode": "byo_jwt", "byo_jwt": {"jwks_url": "https://example.com/.well-known/jwks.json", "audience": "my-app", "issuer": "https://example.com"}}' \ https://api.db9.ai/customer/databases/db_01h9xx/auth-config ``` Request ```json { "auth_mode": "byo_jwt", "byo_jwt": { "jwks_url": "https://example.com/.well-known/jwks.json", "audience": "my-app", "issuer": "https://example.com", "subject_claim": "sub", "claims_allowlist": [], "claims_max_bytes": 4096 } } ``` Response ```json { "auth_mode": "byo_jwt", "byo_jwt": { "jwks_url": "https://example.com/.well-known/jwks.json", "audience": "my-app", "issuer": "https://example.com", "subject_claim": "sub", "claims_allowlist": [], "claims_max_bytes": 4096 }, "created_at": "2026-01-15T10:00:00Z", "updated_at": "2026-01-15T10:00:00Z" } ``` ## Publishable Keys [Section titled “Publishable Keys”](#publishable-keys) POST `/customer/databases/{database_id}/publishable-keys` Create a publishable key for browser/client-side data access. Key value returned once only. | Field | Type | Required | Description | | ----------------- | --------- | -------- | ------------------------------- | | `name` | string | no | Key name | | `allowed_origins` | string\[] | no | CORS allowed origins | | `exposed_schemas` | string\[] | no | Schemas accessible via this key | | `exposed_tables` | string\[] | no | Tables accessible via this key | | `rate_limit` | object | no | Rate limiting (`rps`, `burst`) | | `expires_in_days` | number | no | Key expiration in days | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "web-app", "allowed_origins": ["https://example.com"], "exposed_schemas": ["public"]}' \ https://api.db9.ai/customer/databases/db_01h9xx/publishable-keys ``` ▶ Run Request ```json { "name": "web-app", "allowed_origins": ["https://example.com"], "exposed_schemas": ["public"], "exposed_tables": ["posts", "comments"], "rate_limit": { "rps": 100, "burst": 200 }, "expires_in_days": 90 } ``` Response ```json { "id": "pk_01h9xxxxxxxxxxxxxxxx", "publishable_key": "db9pk_xxxxxxxxxxxxxxxxxxxx", "name": "web-app", "allowed_origins": ["https://example.com"], "exposed_schemas": ["public"], "exposed_tables": ["posts", "comments"], "expires_at": "2026-04-15T10:00:00Z", "created_at": "2026-01-15T10:00:00Z" } ``` GET `/customer/databases/{database_id}/publishable-keys` List all publishable keys (metadata only — key values are never returned). Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/publishable-keys ``` ▶ Run Response ```json [ { "id": "pk_01h9xxxxxxxxxxxxxxxx", "name": "web-app", "allowed_origins": ["https://example.com"], "exposed_schemas": ["public"], "exposed_tables": ["posts", "comments"], "expires_at": "2026-04-15T10:00:00Z", "created_at": "2026-01-15T10:00:00Z" } ] ``` DELETE `/customer/databases/{database_id}/publishable-keys/{key_id}` Revoke a publishable key immediately. Terminal ```bash curl -X DELETE -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/publishable-keys/ck_01h9xx ``` Response ```json { "message": "Publishable key revoked" } ``` ## Service Keys [Section titled “Service Keys”](#service-keys) POST `/customer/databases/{database_id}/service-keys` Create a service key for server-side operations (CRUD only — consumption API not yet implemented). Key value returned once only. Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/service-keys ``` ▶ Run Response ```json { "id": "sk_01h9xxxxxxxxxxxxxxxx", "name": "service-key", "service_key": "db9sk_xxxxxxxxxxxxxxxxxxxx", "created_at": "2026-01-15T10:00:00Z" } ``` GET `/customer/databases/{database_id}/service-keys` List all service keys (metadata only). Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/service-keys ``` ▶ Run Response ```json [ { "id": "sk_01h9xxxxxxxxxxxxxxxx", "name": "service-key", "created_at": "2026-01-15T10:00:00Z", "expires_at": "2027-01-15T10:00:00Z" } ] ``` DELETE `/customer/databases/{database_id}/service-keys/{key_id}` Revoke a service key immediately. Terminal ```bash curl -X DELETE -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/service-keys/ck_01h9xx ``` Response ```json { "message": "Service key revoked" } ``` ## Database Users [Section titled “Database Users”](#database-users) GET `/customer/databases/{database_id}/users` List all PostgreSQL users in the database. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/users ``` ▶ Run Response ```json [ { "name": "admin", "is_superuser": true, "can_login": true, "can_create_db": true, "can_create_role": true }, { "name": "app_user", "is_superuser": false, "can_login": true, "can_create_db": false, "can_create_role": false } ] ``` POST `/customer/databases/{database_id}/users` Create a new PostgreSQL user. Returns the generated password (returned once only). Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"username": "app_user", "password": "playground123"}' \ https://api.db9.ai/customer/databases/db_01h9xx/users ``` ▶ Run Request ```json { "username": "app_user" } ``` Response ```json { "message": "User 'app_user' created", "username": "app_user", "password": "generated-password", "connection_string": "postgresql://tenant_id.app_user@db-01h9xx.db9.ai:5433/postgres", "connection_string_with_password": "postgresql://tenant_id.app_user:generated-password@db-01h9xx.db9.ai:5433/postgres" } ``` DELETE `/customer/databases/{database_id}/users/{username}` Delete a PostgreSQL user from the database. Terminal ```bash curl -X DELETE -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/users/app_user ``` Response ```json { "message": "User 'app_user' deleted" } ``` ## Migrations [Section titled “Migrations”](#migrations) GET `/customer/databases/{database_id}/migrations` List all applied migrations with names, checksums, and timestamps. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/migrations ``` ▶ Run Response ```json [ { "name": "add_users_table", "checksum": "sha256:abc123", "applied_at": "2026-01-15T10:00:00Z", "sql_preview": "CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL);" } ] ``` POST `/customer/databases/{database_id}/migrations` Apply a migration. Migrations are idempotent by name — re-applying the same name returns 409. | Field | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------------- | | `name` | string | yes | Migration name (must be unique) | | `sql` | string | yes | SQL to execute | | `checksum` | string | no | Content digest for integrity verification | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "add_users_table", "sql": "CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL);"}' \ https://api.db9.ai/customer/databases/db_01h9xx/migrations ``` ▶ Run Request ```json { "name": "add_users_table", "sql": "CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL);" } ``` Response ```json { "status": "applied", "name": "add_users_table" } ``` ## Branching [Section titled “Branching”](#branching) POST `/customer/databases/{database_id}/branch` Create a branch — a full copy of the database schema (and optionally data) at a point in time. | Field | Type | Required | Description | | ------------- | ------ | -------- | -------------------------------------------------------------------------------------- | | `name` | string | yes | Branch name | | `snapshot_at` | string | no | RFC 3339 UTC timestamp for point-in-time branching (omit to branch from current state) | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "feature-branch"}' \ https://api.db9.ai/customer/databases/db_01h9xx/branch ``` ▶ Run Request ```json { "name": "feature-branch", "snapshot_at": "2026-03-22T06:00:00Z" } ``` Response ```json { "id": "db_01h9yyyyyyyyyyyyyyyy", "name": "feature-branch", "parent_database_id": "db_01h9xxxxxxxxxxxxxxxx", "region": "us-east-1", "state": "ready", "connection_string": "postgresql://admin:password@db-01h9yy.db9.ai:5433/feature-branch", "created_at": "2026-01-15T10:00:00Z", "last_active_at": null, "last_modified_at": null } ``` ## Backups & Restores [Section titled “Backups & Restores”](#backups--restores) Not Yet Generally Available The backup and restore API requires backend infrastructure configuration (`backup_s3_bucket`). On the shared production environment, these endpoints currently return **503 — Backup service is not configured**. Contact the DB9 team if you need access to backups. POST `/customer/databases/{database_id}/backups` Create a backup of a database. | Field | Type | Required | Description | | ------- | ------ | -------- | ----------------------------------- | | `label` | string | no | Human-readable label for the backup | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"label": "pre-migration-snap"}' \ https://api.db9.ai/customer/databases/db_01h9xx/backups ``` Request ```json { "label": "pre-migration-snap" } ``` Response ```json { "id": "bkp_01...", "database_id": "db_01h9xxxxxxxxxxxxxxxx", "state": "creating", "label": "pre-migration-snap", "created_at": "2026-04-09T10:00:00Z" } ``` GET `/customer/databases/{database_id}/backups` List all backups for a database. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/backups ``` GET `/customer/databases/{database_id}/backups/{backup_id}` Get details of a specific backup. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/backups/bkp_01h9xx ``` DELETE `/customer/databases/{database_id}/backups/{backup_id}` Delete a backup. Terminal ```bash curl -X DELETE -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/backups/bkp_01h9xx ``` POST `/customer/restores` Restore a database from a backup. Creates a new database with the restored data. | Field | Type | Required | Description | | ----------------- | ------ | -------- | -------------------------------- | | `backup_id` | string | yes | ID of the backup to restore from | | `target_name` | string | yes | Name for the restored database | | `idempotency_key` | string | no | Idempotency key for safe retries | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"backup_id": "bkp_01h9xx", "target_name": "myapp-restored"}' \ https://api.db9.ai/customer/restores ``` Request ```json { "backup_id": "bkp_01...", "target_name": "myapp-restored" } ``` GET `/customer/restores/{restore_id}` Check the status of a restore operation. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/restores/rst_01h9xx ``` ## Filesystem [Section titled “Filesystem”](#filesystem) POST `/customer/databases/{database_id}/fs-connect` Obtain database credentials and filesystem WebSocket URL for fs9 access. Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/fs-connect ``` ▶ Run Response ```json { "database_id": "db_01h9xxxxxxxxxxxxxxxx", "database_name": "myapp", "admin_user": "admin", "admin_password": "stored-password", "connection_string": "postgresql://tenant_id.admin@db-01h9xx.db9.ai:5433/postgres", "fs_websocket_url": "wss://fs.db9.ai/ws", "connection_string_with_password": "postgresql://tenant_id.admin:stored-password@db-01h9xx.db9.ai:5433/postgres" } ``` ## Functions [Section titled “Functions”](#functions) POST `/customer/databases/{database_id}/functions` Deploy a serverless function. Functions run in an isolated environment with optional database access, secrets, and filesystem scope. | Field | Type | Required | Description | | ------------------- | --------- | -------- | ------------------------------------------------------------------ | | `name` | string | yes | Function name | | `entrypoint` | string | yes | Function entrypoint (e.g. `index.handler`) | | `bundle_ref` | string | yes | Bundle storage reference | | `bundle_digest` | string | no | Bundle content digest for integrity | | `run_as` | string | no | Database role to execute as | | `bypass_rls` | boolean | no | Bypass Row-Level Security | | `limits` | object | no | Execution limits | | `network_allowlist` | string\[] | no | Allowed outbound network destinations | | `fs9_scope` | object | no | Filesystem access scope | | `secret_bindings` | string\[] | no | Secrets to bind to the function | | `cron_schedule` | string | no | Cron expression for scheduled execution | | `visibility` | string | no | `"public"` (callable via publishable key) or `"private"` (default) | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "my-function", "entrypoint": "index.handler", "bundle_ref": "s3://bucket/bundle.zip"}' \ https://api.db9.ai/customer/databases/db_01h9xx/functions ``` Request ```json { "name": "my-function", "entrypoint": "index.handler", "bundle_ref": "s3://bucket/bundle.zip", "bundle_digest": "sha256:abc123", "run_as": "app_user", "bypass_rls": false, "limits": { "timeout_ms": 30000 }, "network_allowlist": ["https://api.example.com"], "fs9_scope": { "read": ["/data"], "write": [] }, "secret_bindings": ["API_KEY"], "cron_schedule": "*/5 * * * *" } ``` Response ```json { "function": { "id": "fn_01h9xxxxxxxxxxxxxxxx", "name": "my-function", "enabled": true, "visibility": "private", "created_at": "2026-01-15T10:00:00Z", "updated_at": "2026-01-15T10:00:00Z" }, "version": { "id": "ver_01h9xxxxxxxxxxxxxxxx", "version": 1, "entrypoint": "index.handler", "bundle_ref": "s3://bucket/bundle.zip", "bundle_digest": "sha256:abc123", "run_as": "app_user", "bypass_rls": false, "created_at": "2026-01-15T10:00:00Z" } } ``` GET `/customer/databases/{database_id}/functions` List all deployed functions. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/functions ``` ▶ Run Response ```json [ { "id": "fn_01h9xxxxxxxxxxxxxxxx", "name": "my-function", "enabled": true, "visibility": "private", "created_at": "2026-01-15T10:00:00Z", "updated_at": "2026-01-15T10:00:00Z" } ] ``` POST `/customer/databases/{database_id}/functions/{function_id}/invoke` Invoke a function synchronously and return the output. | Field | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------- | | `input` | object | no | Function input payload | | `idempotency_key` | string | no | Unique key for idempotent invocations | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"input": {"user_id": "u_123"}}' \ https://api.db9.ai/customer/databases/db_01h9xx/functions/fn_01h9xx/invoke ``` ▶ Run Request ```json { "input": { "user_id": "u_123" } } ``` Response ```json { "run_id": "run_01h9xxxxxxxxxxxxxxxx", "function_id": "fn_01h9xxxxxxxxxxxxxxxx", "version_id": "ver_01h9xxxxxxxxxxxxxxxx", "status": "completed", "result_json": "{\"result\":\"ok\"}" } ``` GET `/customer/databases/{database_id}/functions/{function_id}/runs` List all runs for a function, ordered by most recent first. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/functions/fn_01h9xx/runs ``` ▶ Run Response ```json [ { "id": "run_01h9xxxxxxxxxxxxxxxx", "function_id": "fn_01h9xxxxxxxxxxxxxxxx", "version_id": "ver_01h9xxxxxxxxxxxxxxxx", "trigger_type": "invoke", "status": "completed", "run_as": "app_user", "attempt": 1, "created_at": "2026-01-15T10:00:00Z" } ] ``` GET `/customer/databases/{database_id}/functions/{function_id}/runs/{run_id}` Get details for a specific function run including output and timing. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/functions/fn_01h9xx/runs/run_01h9xx ``` ▶ Run Response ```json { "id": "run_01h9xxxxxxxxxxxxxxxx", "function_id": "fn_01h9xxxxxxxxxxxxxxxx", "version_id": "ver_01h9xxxxxxxxxxxxxxxx", "trigger_type": "invoke", "status": "completed", "run_as": "app_user", "attempt": 1, "created_at": "2026-01-15T10:00:00Z" } ``` GET `/customer/databases/{database_id}/functions/{function_id}/runs/{run_id}/logs` Get structured log output for a specific function run. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/functions/fn_01h9xx/runs/run_01h9xx/logs ``` ▶ Run Response ```json { "run_id": "run_01h9xxxxxxxxxxxxxxxx", "logs": "2026-01-15T10:00:00.010Z [info] Function started\n2026-01-15T10:00:00.140Z [info] Function completed" } ``` ## Secrets [Section titled “Secrets”](#secrets) GET `/customer/databases/{database_id}/secrets` List all secret names and timestamps. Secret values are never returned. Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/secrets ``` ▶ Run Response ```json [ { "name": "API_KEY", "created_at": "2026-01-15T10:00:00Z", "updated_at": "2026-01-20T08:00:00Z" } ] ``` POST `/customer/databases/{database_id}/secrets` Create a secret. The value is encrypted at rest and never returned after creation. | Field | Type | Required | Description | | ------- | ------ | -------- | ------------------------------------------ | | `name` | string | yes | Secret name (alphanumeric and underscores) | | `value` | string | yes | Secret value (encrypted at rest) | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "API_KEY", "value": "sk-live-xxxxxxxxxxxx"}' \ https://api.db9.ai/customer/databases/db_01h9xx/secrets ``` ▶ Run Request ```json { "name": "API_KEY", "value": "sk-live-xxxxxxxxxxxx" } ``` Response ```json { "name": "API_KEY", "created_at": "2026-01-15T10:00:00Z" } ``` PUT `/customer/databases/{database_id}/secrets/{secret_name}` Update an existing secret's value. | Field | Type | Required | Description | | ------- | ------ | -------- | ---------------- | | `value` | string | yes | New secret value | Terminal ```bash curl -X PUT -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"value": "sk-live-new-xxxxxxxxxxxx"}' \ https://api.db9.ai/customer/databases/db_01h9xx/secrets/API_KEY ``` ▶ Run Request ```json { "value": "sk-live-new-xxxxxxxxxxxx" } ``` Response ```json { "name": "API_KEY", "updated_at": "2026-02-01T09:00:00Z" } ``` DELETE `/customer/databases/{database_id}/secrets/{secret_name}` Delete a secret permanently. Terminal ```bash curl -X DELETE -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/databases/db_01h9xx/secrets/API_KEY ``` **Response:** `204 No Content` — empty body. ## API Tokens [Section titled “API Tokens”](#api-tokens) POST `/customer/tokens` Create an API token. The token value is returned once only — store it securely. | Field | Type | Required | Description | | ----------------- | ------- | -------- | --------------------------------------------------------------------------------------------------- | | `name` | string | no | Token name | | `expires_in_days` | integer | no | Expiry in days (default: 365) | | `scope_json` | string | no | JSON string scoping token to specific databases, e.g. `{"databases":[{"id":"","access":"ro"}]}` | Terminal ```bash curl -X POST -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "ci-token", "expires_in_days": 365}' \ https://api.db9.ai/customer/tokens ``` ▶ Run Request ```json { "name": "ci-token", "expires_in_days": 365 } ``` Response ```json { "id": "tok_01h9xxxxxxxxxxxxxxxx", "name": "ci-token", "token": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "expires_at": "2027-01-15T10:00:00Z", "created_at": "2026-01-15T10:00:00Z" } ``` GET `/customer/tokens` List all API tokens (metadata only — token values are never returned). Terminal ```bash curl -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/tokens ``` ▶ Run Response ```json [ { "id": "tok_01h9xxxxxxxxxxxxxxxx", "name": "ci-token", "expires_at": "2027-01-15T10:00:00Z", "created_at": "2026-01-15T10:00:00Z" } ] ``` DELETE `/customer/tokens/{token_id}` Revoke an API token immediately. All requests using this token will be rejected. Terminal ```bash curl -X DELETE -H "Authorization: Bearer $DB9_API_KEY" \ https://api.db9.ai/customer/tokens/tok_01h9xx ``` Response ```json { "message": "Token revoked" } ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * [Error Codes](/docs/error-codes/) — Full HTTP status code reference with causes and resolution * [CLI Reference](/docs/cli/) — Command-line interface * [TypeScript SDK](/docs/sdk/) — Server-side SDK wrapping this API * [Browser SDK](/docs/sdk-browser/) — Client-side data access * [Connect](/docs/connect/) — Connection strings and drivers # Architecture > How DB9 works — the control plane, data plane, TiKV storage, pgwire protocol, SQL execution pipeline, multi-tenant isolation, and extension system. DB9 is a PostgreSQL-compatible distributed SQL database built on [TiKV](https://tikv.org/). It separates a **control plane** (API server, CLI, SDK) from a **data plane** (the SQL engine that speaks the PostgreSQL wire protocol and stores data in TiKV). This page explains how the pieces fit together, how queries flow from client to storage, and where the architectural boundaries are. ## Mental model [Section titled “Mental model”](#mental-model) Output ```text +-------------------------------------------------+ | Clients | | psql ORMs drivers SDK CLI Browser | +------------------------+------------------------+ | +-----------+-----------+ | | v v +-------------------+ +-------------------+ | Control Plane | | Data Plane | | (db9-backend) | | (db9-server) | | | | | | REST API | | pgwire protocol | | Auth & tokens | | SQL engine | | DB lifecycle | | Extensions | | Branching | | Worker engine | | Observability | | | +---------+---------+ +---------+---------+ | | +-----------+-----------+ v +-----------------+ | TiKV | | (distributed | | KV storage) | +-----------------+ ``` **Control plane** — manages database lifecycle, authentication, tokens, branching, and observability. You interact with it through the `db9` CLI, the TypeScript SDK, or the REST API. **Data plane** — runs SQL. The `db9-server` process accepts PostgreSQL wire protocol connections, parses and optimizes SQL, executes queries, and reads/writes data in TiKV. Extensions like fs9, HTTP, embedding, vector search, and pg\_cron run here as compiled-in capabilities. **TiKV** — the storage layer. A distributed key-value store with Raft consensus and ACID transactions. Each DB9 database gets its own isolated keyspace in TiKV, so tenants share infrastructure but never data. ## Core components [Section titled “Core components”](#core-components) ### db9-server (data plane) [Section titled “db9-server (data plane)”](#db9-server-data-plane) The SQL engine. A single async Rust process running on the Tokio runtime. | Component | Role | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **pgwire listener** | Accepts PostgreSQL wire protocol connections on port `5433`. Handles startup, TLS, authentication, and the Simple Query / Extended Query protocols. | | **SQL parser** | Parses SQL text into an AST using [sqlparser-rs](https://github.com/sqlparser-rs/sqlparser-rs). | | **Analyzer** | Single-pass name resolution and type checking. Resolves column references, infers types, applies coercions, and tracks subquery scopes. | | **Optimizer** | Cost-based query optimization. Converts analyzed queries into logical plans, applies rewrite rules (predicate pushdown, join reordering via DPccp), then selects physical operators. Uses table statistics from `ANALYZE` when available. | | **Executor** | Volcano-model pull-based iterator pipeline. Operators include table scan, index range scan, HNSW k-NN scan, hash join, nested-loop join, sort, aggregate, and window functions. | | **TiKV store** | Facade over the TiKV client. Manages transactions (pessimistic locking, snapshot isolation), key encoding, and per-keyspace connection pooling. | | **RLS engine** | Row-Level Security enforcement. Injects policy predicates into query plans, validates DML operations against WITH CHECK clauses, and manages bypass logic for superusers and BYPASSRLS roles. | | **Extension runtime** | Compiled-in extensions registered at startup. Each extension can add SQL functions, types, operators, and background workers. | | **Worker engine** | Unified async task queue stored in TiKV. Drives pg\_cron schedules, async triggers, auto-analyze, HNSW index background merges, and [Serverless Functions](/docs/functions/) execution. | ### db9-backend (control plane) [Section titled “db9-backend (control plane)”](#db9-backend-control-plane) The API server. An [Axum](https://github.com/tokio-rs/axum) web application backed by a metadata database (SQLite or PostgreSQL). | Component | Role | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **REST API** | Database CRUD, user management, token lifecycle, branching, migrations, observability, and SQL execution via HTTP. | | **Public Data API** | REST endpoint (`/public/v1/databases/:id/query`) for browser SDK queries. Validates publishable keys, optional BYO JWT tokens, and enforces schema/table access. Issues connect tokens under the resolved role for RLS enforcement. | | **Auth service** | Anonymous trial accounts, Auth0 SSO, API tokens, publishable keys, BYO JWT validation, and connect tokens (short-lived pgwire credentials). | | **PD client** | Manages TiKV keyspaces — creates and deletes the isolated storage namespaces that back each database. | | **PG client** | Connects to db9-server to execute tenant setup SQL (roles, default extensions, seed data). | | **Reconciler** | Background process that detects and recovers failed provisioning or deletion operations. | ### db9 CLI and TypeScript SDK [Section titled “db9 CLI and TypeScript SDK”](#db9-cli-and-typescript-sdk) Client-side tools that talk to both planes: * **Control plane** — `db9 create`, `db9 list`, `db9 token create`, `db9 branch create`, and other management commands go through the REST API. * **Data plane** — `db9 sql`, `db9 db sql --direct`, and `db9 db connect` (which mints a short-lived JWT) connect directly to db9-server over pgwire. The SDK does not yet publish a native `connectToken()` equivalent. ## Query execution flow [Section titled “Query execution flow”](#query-execution-flow) When a client sends a SQL query, it follows this path through db9-server: Output ```text SQL text │ ├─ Parse (sqlparser-rs → AST) │ ├─ Dispatch │ ├─ Data statements (SELECT, INSERT, UPDATE, DELETE) → Analyzer │ ├─ DDL / utility statements → direct handler │ └─ Transaction control (BEGIN, COMMIT, SAVEPOINT) → session state │ ├─ Analyze (name resolution, type checking, scope tracking) │ ├─ Optimize │ ├─ Logical plan (AnalyzedQuery → LogicalPlan) │ ├─ Rewrite rules (predicate pushdown, join reordering, subquery decorrelation) │ └─ Physical plan (scan selection, join selection, aggregate strategy) │ ├─ Execute (Volcano iterator pipeline) │ ├─ BEGIN transaction (pessimistic) │ ├─ Pull rows through operator tree │ ├─ Read/write TiKV (get, scan, put, delete) │ └─ COMMIT or ROLLBACK │ └─ Respond (pgwire RowDescription + DataRow messages) ``` ### Scan strategies [Section titled “Scan strategies”](#scan-strategies) The physical planner selects from several scan methods: | Scan type | When used | | -------------------- | ------------------------------------------------------------------------------------------------------------------------ | | **Table scan** | No matching index, or large result sets where a sequential scan is cheaper. | | **Index range scan** | BTree index matches the predicate (equality, range, IN-list). Supports expression indexes and partial indexes. | | **HNSW k-NN scan** | `ORDER BY vec <-> query LIMIT k` with a vector index. Uses the [usearch](https://github.com/unum-cloud/usearch) library. | ### Transaction model [Section titled “Transaction model”](#transaction-model) * **Default isolation**: `READ COMMITTED`, with PostgreSQL’s statement-level snapshot semantics — each statement in a transaction sees rows committed by other sessions before it started. * **REPEATABLE READ**: Supported, backed by a single TiKV MVCC snapshot held for the transaction’s lifetime. * **Serializable**: Not implemented. TiKV provides snapshot isolation, not PostgreSQL-style serializable snapshot isolation (SSI). DB9 Difference: SERIALIZABLE is downgraded, not rejected On the PostgreSQL wire protocol, `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE` succeeds with `WARNING: TiKV provides snapshot isolation; SERIALIZABLE has been downgraded to REPEATABLE READ`. The transaction then runs at REPEATABLE READ, so anomalies only SSI prevents — write skew in particular — are not detected. `SHOW transaction_isolation` reports the level actually applied (`repeatable read`), which is how you can detect the downgrade at runtime. Over the HTTP SQL API the same request returns an error instead. * **Savepoints**: Full PostgreSQL-style `SAVEPOINT` / `ROLLBACK TO` with undo records tracked in TiKV. * **Autocommit**: Statements outside an explicit transaction run in autocommit mode. ## Multi-tenant isolation [Section titled “Multi-tenant isolation”](#multi-tenant-isolation) DB9 isolates tenants at the storage layer using TiKV keyspaces. Output ```text TiKV Cluster ├─ Keyspace A (tenant_a) │ ├─ System metadata (tables, schemas, types, extensions, roles) │ ├─ Table data (rows keyed by table ID + primary key) │ ├─ Index data (secondary index entries) │ └─ Worker state (cron jobs, task queue) │ ├─ Keyspace B (tenant_b) │ └─ ... (completely independent copy) │ └─ Keyspace C (tenant_c) └─ ... ``` **How it works:** 1. The client connects with username `tenant_id.role` (e.g., `a1b2c3d4e5f6.admin`). 2. db9-server parses the username to extract the tenant ID and look up the keyspace. 3. A `TenantHandle` is acquired from the connection pool, binding all storage operations to that keyspace. 4. All key encoding includes the keyspace prefix, so reads and writes are physically separated in TiKV. Each tenant gets independent: * Table and schema namespaces * Roles and privileges * Extension installation state * Worker queue and cron schedules * Memory accounting (`DB9_TENANT_MEMORY_QUOTA_BYTES`) There is no cross-tenant query path. A connection can only access its own keyspace. ## Extension system [Section titled “Extension system”](#extension-system) Extensions are compiled into the db9-server binary and registered at startup. They are not dynamically loaded plugins. Each extension adds some combination of: * **SQL functions** (e.g., `http_get()`, `fs9_read()`, `embedding()`) * **Types** (e.g., `vector(384)`) * **Operators** (e.g., `<->` for vector distance) * **Index methods** (e.g., HNSW for vector search) * **Background workers** (e.g., pg\_cron scheduler) Extensions are installed per-tenant with `CREATE EXTENSION`: SQL ```sql CREATE EXTENSION http; CREATE EXTENSION fs9; CREATE EXTENSION vector; CREATE EXTENSION embedding; CREATE EXTENSION pg_cron; ``` Installation state is stored in TiKV under the tenant’s keyspace, so each database independently controls which extensions are active. Some extensions are enabled by default when a database is created (http, pg\_cron). Others require explicit installation. For the full list, see the [Extensions overview](/docs/extensions/). ## Connection handling [Section titled “Connection handling”](#connection-handling) db9-server uses a semaphore-based admission control model: * **Max connections**: 1000 by default (configurable). * **Per-connection**: Each client connection gets its own async Tokio task with session-local state (transaction, settings, search path). * **Connection pool**: `TikvClientPool` caches per-keyspace `TikvStore` instances. Idle tenants are evicted after 300 seconds by default. * **TLS**: Supported via cert/key configuration. SCRAM-SHA-256 is the default authentication method. For connection string format, authentication options, and driver compatibility, see [Connect to DB9](/docs/connect/). ## Constraints and boundaries [Section titled “Constraints and boundaries”](#constraints-and-boundaries) | Area | Current state | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Execution model** | Single-process, single-node. No distributed query execution across multiple db9-server instances. | | **Plan cache** | Session-local only. No shared prepared statement cache across connections. | | **GIN indexes** | Functional — the planner generates GIN scan plans and the executor reads the index (`EXPLAIN ANALYZE` shows `KV Table Scan Pairs: 0`). `jsonb_ops` containment on highly distinct values still reads much of the index, so it gains less than array or full-text predicates. | | **Parallel query** | Not supported. Queries execute on a single Tokio task. | | **Foreign data wrappers** | Not supported. Use the HTTP extension for external data access. | | **Logical replication** | Not supported. Use branching or the REST API for data movement. | These are architectural boundaries, not bugs. They reflect the current stage of the system. Check the [SQL limits](/docs/sql/limits/) page for detailed compatibility notes. ## When this architecture fits well [Section titled “When this architecture fits well”](#when-this-architecture-fits-well) * **Agent and automation workloads** — instant provisioning, per-task isolation, and built-in capabilities (embeddings, files, HTTP, cron) reduce the number of external services. * **Multi-tenant SaaS** — keyspace isolation provides strong data separation without managing separate database clusters. * **Development and CI** — branching and disposable databases are first-class operations, not workarounds. * **Workloads that fit in a single node** — the SQL engine is mature and the optimizer is cost-based, but there is no distributed execution layer today. ## When to consider alternatives [Section titled “When to consider alternatives”](#when-to-consider-alternatives) * You need distributed query execution across multiple nodes for analytical workloads. * You depend on PostgreSQL extensions that DB9 does not support — check [Extensions](/docs/extensions/) and [SQL limits](/docs/sql/limits/). * You need logical replication, foreign data wrappers, or custom C extensions. ## Next steps [Section titled “Next steps”](#next-steps) * [Production Checklist](/docs/production-checklist/) — authentication, observability, recovery, and operational limits * [Connect to DB9](/docs/connect/) — connection strings, drivers, and authentication * [Why DB9 for AI Agents](/docs/why-db9-for-ai-agents/) — how the architecture enables agent workflows * [Extensions](/docs/extensions/) — fs9, HTTP, vector search, pg\_cron, and more * [SQL Reference](/docs/sql/) — supported SQL syntax and compatibility notes * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full PostgreSQL compatibility surface * [CLI Reference](/docs/cli/) — full command reference for the control plane # CLI Reference > Complete reference for the db9 command-line tool — installation, authentication, database management, SQL, filesystem, filesystem watch, branching, serverless functions, and agent onboarding. The `db9` CLI is the primary interface for creating databases, running SQL, managing files, branching, and onboarding AI agents. This page is a complete command reference. ## Common Journeys [Section titled “Common Journeys”](#common-journeys) | I want to… | Start here | | ------------------------------------------ | ----------------------------------------------------------- | | Try DB9 in under two minutes | [Quick Start](/docs/quickstart/) | | Connect an ORM or driver | [Connect](/docs/connect/) | | Understand the architecture | [Architecture](/docs/architecture/) | | Set up DB9 for Claude Code or Codex | [Agent Workflows](/docs/agent-workflows/overview/) | | Create databases programmatically | [Provisioning](/docs/platform/provisioning/) | | Inspect query performance | [Inspect & Observability](#inspect-and-observability) below | | Run SQL from the terminal | [SQL Execution](#sql-execution) below | | Upload files to a database | [Filesystem](#filesystem) below | | Create a branch for testing | [Branching](#branching) below | | Schedule recurring SQL jobs | [Cron Jobs](#cron-jobs) below | | Watch filesystem changes in real time | [Filesystem Watch](#filesystem-watch) below | | Deploy serverless functions (experimental) | [Functions](#functions) below | | Prepare for production | [Production Checklist](/docs/production-checklist/) | ## Installation [Section titled “Installation”](#installation) Install with the official script (Linux/macOS, amd64/arm64): Terminal ```bash curl -fsSL https://db9.ai/install | sh ``` To control where binaries land, set `DB9_INSTALL_DIR`: Terminal ```bash DB9_INSTALL_DIR="$HOME/.local/bin" curl -fsSL https://db9.ai/install | sh ``` Verify: Terminal ```bash db9 --version db9 status ``` ▶ Run The installer places `db9` (and optionally `db9-fuse` for FUSE mounts) into your chosen directory. ### macOS prerequisite for `db9 fs mount` [Section titled “macOS prerequisite for db9 fs mount”](#macos-prerequisite-for-db9-fs-mount) `db9 fs mount` on macOS currently depends on the macFUSE kernel backend. Terminal ```bash brew install --cask macfuse ``` After installation, macOS may require manual system extension approval in **System Settings > Privacy & Security** before FUSE mounts are allowed. ## Global Flags [Section titled “Global Flags”](#global-flags) These flags apply to every `db9` command: | Flag | Env Variable | Default | Description | | --------------------- | ------------------ | -------------------- | --------------------------------------------------------------------------------------------------------- | | `--api-url ` | `DB9_API_URL` | `https://api.db9.ai` | API base URL | | `--output ` | — | `table` | Output format: `table`, `json`, `csv`, `raw` (CSV without headers), or `quiet` (minimal machine-readable) | | `--json` | — | — | Shorthand for `--output json` | | `--quiet` | — | — | Suppress non-essential output (only print machine-readable values) | | `--insecure` | `DB9_INSECURE` | — | Skip TLS certificate verification | | `--experimental` | `DB9_EXPERIMENTAL` | — | Enable experimental features (e.g. `functions`) | | `-d, --database ` | `DB9_DATABASE` | — | Override the default database for this command | ## Authentication [Section titled “Authentication”](#authentication) DB9 supports three auth paths: **anonymous trial**, **SSO login**, and **API key** (CI/agents). Tokens are stored in `~/.db9/credentials`. > **Security:** Treat `DB9_API_KEY` as a secret. Never send your token to any domain other than `api.db9.ai`. Terminal ```bash # Zero-setup trial (auto creates anonymous account + token) db9 create --name quickstart # Upgrade anonymous account to verified SSO identity db9 claim db9 claim --id-token # Human operator login (browser-based) db9 login # API key login (CI/CD, agents) db9 login --api-key # Agent runtime export DB9_API_KEY= ``` See [Token Management](#token-management) for creating automation tokens. ### Adopt Anonymous Databases [Section titled “Adopt Anonymous Databases”](#adopt-anonymous-databases) Transfer databases created under an anonymous trial account into your verified account: Terminal ```bash db9 adopt ``` This launches an interactive flow that verifies ownership of the anonymous account (via `anonymous_secret`) and migrates selected databases into your current account. For details, see [Anonymous & Claimed Databases](/docs/platform/anonymous-and-claimed-databases/). ## Setup & Utilities [Section titled “Setup & Utilities”](#setup--utilities) Terminal ```bash # Guided setup: login and create your first database db9 init # Set or show default database (omit DB to show current) db9 use db9 use --clear # Remove stored credentials db9 logout # Update db9 (and db9-fuse) to the latest version db9 update # Generate shell completion scripts db9 completion bash db9 completion zsh # Show / set CLI configuration db9 config show db9 config set api_url https://api.db9.ai ``` ## Database Lifecycle [Section titled “Database Lifecycle”](#database-lifecycle) Database commands accept `` as a database name (preferred) or ID. When omitted, the default database set by `db9 use` is used. These are **top-level commands** — use `db9 create`, not `db9 db create`. Terminal ```bash # Create (name is optional; will auto-generate if omitted) db9 create --name myapp db9 create --name myapp --region us-west db9 create --name myapp --project # assign to a project db9 create --name myapp --password # set the admin password (default: random) db9 create --name myapp --show-secrets # print password & connection string # List db9 list # List with database storage sizes db9 list --size # Filter by project db9 list --project # Check login status db9 status # Show database status db9 db status # Print passwordless connection info (psql-compatible) db9 db connect # Output connection as DATABASE_URL=... format (for .env files) db9 db connect --env # Create a short-lived connect token (10-min TTL, for psql/ORM usage) — same as `db9 db connect`, use --user for a specific role db9 db connect --user app_user # Reset admin password db9 db reset-password # Delete (prompts unless you pass --yes) db9 delete --yes ``` > **Deprecated forms:** `db9 db create`, `db9 db list`, `db9 db delete`, `db9 db branch`, and `db9 db connect-token` still work but print a deprecation warning. Use the top-level forms instead (`db9 db connect-token` → `db9 db connect`). Note the deprecated `db9 db connect-token` takes `--role`, while `db9 db connect` takes `--user`; neither accepts the other’s flag. #### `db9 list` output [Section titled “db9 list output”](#db9-list-output) The table form prints a leading unlabeled column that marks the default database with `*`, followed by: | Column | Shown by default | Notes | | --------------- | ------------------ | --------------------------------------- | | `ID` | yes | 12-character database ID | | `NAME` | yes | | | `STATE` | yes | Uppercase, e.g. `ACTIVE` | | `REGION` | yes | | | `PROJECT` | yes | Project UUID | | `CREATED` | yes | | | `LAST ACTIVE` | yes | `-` until activity is observed | | `SIZE` | only with `--size` | Requires an extra API call per database | | `LAST MODIFIED` | only with `--size` | | `db9 list --json` returns an array of objects with nine fields: `id`, `name`, `state`, `region`, `project_id`, `created_at`, `last_active_at`, `last_modified_at`, and `default`. The two activity fields are RFC 3339 timestamps, or `null` when no activity has been observed yet; `db9 db status --json` carries them too. For programmatic database creation patterns, see [Provisioning](/docs/platform/provisioning/). ## Shorthand Aliases [Section titled “Shorthand Aliases”](#shorthand-aliases) Most `db9 db ` subcommands have top-level aliases so you can type less: | Shorthand | Equivalent | | ---------------------------- | ------------------------- | | `db9 sql ` | `db9 db sql ` | | `db9 connect ` | `db9 db connect ` | | `db9 inspect ` | `db9 db inspect ` | | `db9 users ` | `db9 db users ` | | `db9 seed ` | `db9 db seed ` | | `db9 dump ` | `db9 db dump ` | | `db9 cron ` | `db9 db cron ` | | `db9 cat :/path` | Read a remote file | | `db9 rm :/path` | Remove a remote file | | `db9 mv :/src :/dst` | Move/rename a remote file | | `db9 sh ` | `db9 fs sh ` | | `db9 cp ` | `db9 fs cp ` | | `db9 ls :/path` | `db9 fs ls :/path` | | `db9 mount ` | `db9 fs mount ` | The `cat`, `rm`, and `mv` shorthands operate on the database filesystem directly — no need for `db9 fs sh`. ## Inspect and Observability [Section titled “Inspect and Observability”](#inspect-and-observability) | Subcommand | Description | | ----------------------------- | ---------------------------------- | | `db9 db inspect` | Overview metrics dashboard | | `db9 db inspect queries` | Query samples and performance | | `db9 db inspect report` | Combined summary + queries report | | `db9 db inspect schemas` | List database schemas | | `db9 db inspect tables` | List database tables | | `db9 db inspect indexes` | List database indexes | | `db9 db inspect slow-queries` | Slow queries sorted by p99 latency | Terminal ```bash # Overview metrics db9 db inspect # Subcommands db9 db inspect queries # query samples and performance db9 db inspect report # combined summary + queries report db9 db inspect schemas # list database schemas db9 db inspect tables # list database tables db9 db inspect indexes # list database indexes db9 db inspect slow-queries # slow queries sorted by p99 latency ``` For production monitoring guidance, see [Production Checklist](/docs/production-checklist/). ## Database Users [Section titled “Database Users”](#database-users) Terminal ```bash # List database users db9 db users list # Create a new database user (password auto-generated if omitted) db9 db users create --username app_user --password 'SecurePass1' # Delete a database user db9 db users delete --username app_user ``` ## SQL Execution [Section titled “SQL Execution”](#sql-execution) Run SQL via inline query (`-q` / `--query`), file (`-f` / `--file`), stdin, or an interactive REPL. The `` argument is optional — if omitted, the CLI auto-selects the default database or prompts interactively. The interactive REPL defaults to **direct pgwire mode** (faster, real PostgreSQL error messages) with automatic API fallback. Use `--api` to force HTTP API mode, or `--direct` / `-D` to explicitly request pgwire. Terminal ```bash # Inline db9 db sql --query "SELECT now()" # From file db9 db sql --file ./query.sql # From stdin cat ./query.sql | db9 db sql # Interactive REPL (TTY only — defaults to direct pgwire) db9 db sql # Force HTTP API mode (disables auto-direct in REPL) db9 db sql --api # Explicit direct pgwire mode db9 db sql --direct db9 db sql --direct --dsn "postgresql://..." # Execute a seed SQL file db9 db seed ./seed.sql # Export database schema (and optionally data) as SQL db9 db dump db9 db dump --ddl-only db9 db dump --output-file ./backup.sql ``` ## Filesystem [Section titled “Filesystem”](#filesystem) Each database has a remote filesystem. Use `db9 fs` for shell access, file copy, and (optionally) FUSE mounts. Terminal ```bash # List remote files and directories db9 fs ls :/path db9 fs ls --long :/path # long format (permissions, size, mtime) db9 fs ls --recursive :/path # recursive listing # Interactive filesystem shell db9 fs sh # Run a single command (bash -c style) db9 fs sh --command "ls -la" # Quick file operations (top-level shortcuts) db9 cat :/path/to/file # Print file contents db9 rm :/path/to/file # Remove a file db9 mv :/old/path :/new/path # Move or rename db9 tailf :/logs/app.log # Follow remote file appends # Upload / download (scp-like) db9 fs cp ./local.txt :/remote/path/local.txt db9 fs cp :/remote/path/remote.txt ./remote.txt # Recursive upload / download (directories) db9 fs cp --recursive ./imports :/data/imports # Glob upload / multi-file upload db9 fs cp ./*.go :/remote/ db9 fs cp a.go b.go :/remote/ # Mount via FUSE (requires db9-fuse and system FUSE support) db9 fs mount "$HOME/mnt/mydb" db9 fs mount "$HOME/mnt/mydb" --read-only db9 fs mount "$HOME/mnt/mydb" --cache-ttl 30 db9 fs mount "$HOME/mnt/mydb" --multipart-threshold 16777216 # 16 MB db9 fs mount "$HOME/mnt/mydb" -f # foreground db9 fs mount "$HOME/mnt/mydb" --write-async # client async writeback (alias: --writeback); close() seals to local journal, drains opportunistically — not a remote-commit promise db9 fs mount "$HOME/mnt/mydb" --include '*.md' # sync only .md files db9 fs mount "$HOME/mnt/mydb" --include '*.rs' --exclude 'target/**' db9 fs mount "$HOME/mnt/mydb" --no-ignore # disable .fuseignore ``` ### Filesystem Watch [Section titled “Filesystem Watch”](#filesystem-watch) Monitor filesystem events in real time. The `watch` command uses WebSocket push when the server advertises the `watch` capability, and falls back to polling the `fs9_events()` table function when it does not. Terminal ```bash # Watch all events on a database db9 fs watch :/ # Watch events under a specific path db9 fs watch :/data/ # Poll every 5 seconds (default: 1) — applies only to the polling fallback db9 fs watch :/ --interval 5 # Output JSON lines (for piping to other tools) db9 fs watch :/ --json ``` Supported event types: `CREATE`, `WRITE`, `DELETE`, `RENAME`, `MKDIR`. Events include the path, timestamp, file size, and (for renames) the old path. Plain-text output example: Output ```text 10:23:45.123 CREATE /data/file.txt 10:23:46.456 RENAME /data/old.txt -> /data/new.txt 10:23:47.789 DELETE /data/file.txt ``` If the internal event ring overflows (events arrive faster than the polling interval), the CLI prints a warning so you can increase the polling frequency or narrow the path prefix. ### File Tail (follow) [Section titled “File Tail (follow)”](#file-tail-follow) Follow a remote file and print appended content, similar to `tail -f`: Terminal ```bash # Follow a log file (prints last 10 lines, then streams new content) db9 fs tailf :/logs/app.log # Show last 50 lines before following db9 fs tailf :/logs/app.log --lines 50 # Custom polling interval (default: 1 second) db9 fs tailf :/logs/app.log --interval 2 ``` ### Tee (stdin to remote file) [Section titled “Tee (stdin to remote file)”](#tee-stdin-to-remote-file) Read stdin, echo it to stdout, and write it to a remote file — like Unix `tee` but targeting the database filesystem: Terminal ```bash # Pipe command output to a remote file echo "hello" | db9 fs tee :/logs/out.txt # Append instead of overwriting cat local.txt | db9 fs tee -a :/logs/out.txt ``` | Flag | Default | Description | | -------------- | ------- | ----------------------------------------- | | `-a, --append` | off | Append to the file instead of overwriting | Advanced filesystem flags (all `fs` subcommands): `--ws-url ` (direct WebSocket URL), `--ws-port ` (default: 5480). ### Filesystem Shell Builtins [Section titled “Filesystem Shell Builtins”](#filesystem-shell-builtins) The interactive shell (`db9 fs sh`) supports a POSIX-like command set: | Category | Commands | | --------------- | --------------------------------------------------------------------------------------------------- | | File ops | `ls`, `cat`, `cp`, `mv`, `rm`, `touch`, `mkdir`, `stat`, `find`, `tree`, `diff`, `patch` | | Text processing | `grep`, `head`, `tail`, `sort`, `uniq`, `cut`, `wc`, `tr`, `rev`, `tee`, `jq` | | Navigation | `cd`, `pwd`, `basename`, `dirname` | | Shell | `echo`, `printf`, `read`, `export`, `set`, `test`, `alias`, `source`, `env`, `date`, `sleep`, `seq` | | Control flow | `if`/`then`/`fi`, `for`/`do`/`done`, `while`/`do`/`done`, `case`/`esac`, pipes, redirections | Use `help ` inside the shell for per-command usage. For querying files with SQL, see [fs9 extension](/docs/extensions/fs9/). ## Migrations [Section titled “Migrations”](#migrations) Manage local migration files and apply them to a database. Default directory is `./migrations`. Terminal ```bash # Create a new migration file db9 migration new create_users # List local migration files db9 migration list # See applied vs pending db9 migration status # Apply pending migrations db9 migration up ``` All migration subcommands accept `--dir ` to override the default `./migrations` directory. ## Branching [Section titled “Branching”](#branching) Branches are schema copies used for safe experimentation. Branch commands are **top-level** under `db9 branch`. Terminal ```bash # Create a branch from an existing database db9 branch create --name feature1 db9 branch create --name feature1 --show-secrets # print password + connection string db9 branch create --name feature1 --show-password # print password only db9 branch create --name feature1 --show-connection-string # print connection string only # Point-in-time branch (PITR) — branch from a specific past timestamp db9 branch create --name rollback --snapshot-at 2026-03-22T06:00:00Z # List branches of a database db9 branch list # Delete a branch database db9 branch delete ``` The `--snapshot-at` flag accepts an RFC 3339 UTC timestamp (e.g. `2026-03-22T06:00:00Z`). When TiKV snapshot restore is available, the branch will contain the parent’s data as of that timestamp rather than the current state. For branch-based workflows (preview environments, rollback, task isolation), see [Multi-Tenant Patterns](/docs/platform/multi-tenant-patterns/). ## Cron Jobs [Section titled “Cron Jobs”](#cron-jobs) Schedule SQL with pg\_cron (extension required). Cron commands live under `db9 db cron`. | Subcommand | Description | | --------------------- | -------------------------- | | `db9 db cron list` | List all cron jobs | | `db9 db cron create` | Create a cron job | | `db9 db cron delete` | Delete a job by ID or name | | `db9 db cron history` | Show job execution history | | `db9 db cron enable` | Enable a disabled job | | `db9 db cron disable` | Disable a job | | `db9 db cron status` | Show job status | Terminal ```bash # List jobs db9 db cron list # Create (schedule + SQL command) db9 db cron create "*/5 * * * *" "SELECT 1" # Create from file (optional name enables upsert semantics) db9 db cron create "0 * * * *" --name hourly_job -f ./job.sql # History / status db9 db cron history --job --limit 20 db9 db cron status # Enable / disable / delete by job id or job name db9 db cron disable db9 db cron enable db9 db cron delete ``` For scheduling patterns and operational guidance, see [pg\_cron extension](/docs/extensions/pg-cron/). ## Functions [Section titled “Functions”](#functions) Serverless functions let you deploy and run JavaScript/TypeScript code that executes against your database. | Subcommand | Description | | ------------------------------ | --------------------------------------------------- | | `db9 functions list` | List functions in a database | | `db9 functions create` | Create a new function | | `db9 functions update` | Update an existing function by name or ID | | `db9 functions invoke` | Invoke a function | | `db9 functions history` | List recent runs, or show details of a specific run | | `db9 functions logs` | Show logs for a specific run | | `db9 functions secrets list` | List all secrets (names only) | | `db9 functions secrets set` | Create or update a secret | | `db9 functions secrets delete` | Delete a secret | Terminal ```bash # List functions (this subcommand takes --database or a positional DB, not --db) db9 functions list --database myapp # Create a function (reads index.js or main.ts from current directory) db9 functions create my-func --db myapp # Create with TypeScript source db9 functions create my-func --db myapp --ts # Create with execution limits db9 functions create my-func --db myapp \ --limits-json '{"timeout_ms":60000,"memory_mb":128}' db9 functions create my-func --db myapp --limits-file ./limits.json db9 functions create my-func --db myapp --timeout 30000 # Bind secrets to a function db9 functions create my-func --db myapp --secret API_KEY=my_api_key # Grant filesystem access to a function db9 functions create my-func --db myapp --fs9-scope /data:ro --fs9-scope /output:rw # Create a public function (callable via publishable key) db9 functions create my-func --db myapp --visibility public # Update an existing function db9 functions update my-func --db myapp -f ./updated.js db9 functions update my-func --db myapp --timeout 60000 --secret NEW_KEY=my_new_key db9 functions update my-func --db myapp --visibility private # Invoke a function db9 functions invoke my-func --db myapp db9 functions invoke my-func --db myapp --payload '{"key":"value"}' # View run history and logs db9 functions history my-func --db myapp -n 50 db9 functions history my-func --db myapp db9 functions logs my-func --db myapp # Manage secrets db9 functions secrets list --db myapp db9 functions secrets set MY_SECRET --db myapp --value "secret-value" db9 functions secrets set MY_SECRET --db myapp --value-stdin db9 functions secrets delete MY_SECRET --db myapp ``` The `--db` flag is optional when a default database is set via `db9 use`. The `--timeout` convenience flag overrides any `timeout_ms` value in the limits JSON. ## Sharing [Section titled “Sharing”](#sharing) `db9 share` is a shorthand for creating a database-scoped token. It creates the token and prints a ready-to-use connection string. Terminal ```bash # Read-only share (default), 7-day expiry db9 share my-app # Read-write share db9 share my-app --rw # Custom expiry (30 days) db9 share my-app --expires 30 # Custom token name db9 share my-app --name team-token ``` | Flag | Default | Description | | ------------------ | --------------------- | -------------------------------------------- | | `--rw` | off | Grant read-write access (default: read-only) | | `--expires ` | `7` | Token expiry in days | | `--name ` | `share--YYYYMMDD` | Custom token name | Under the hood this calls `POST /customer/tokens` with the appropriate `scope_json`. For more control (multi-database scopes, custom names), use `db9 token create --scope`. ## Explorer [Section titled “Explorer”](#explorer) `db9 explore` launches a browser-based file and SQL explorer. It downloads a pre-built SPA to `~/.db9/explorer/` and serves it locally with API proxying. Terminal ```bash # Open explorer for a database db9 explore mydb # Custom port db9 explore mydb --port 8080 # Don't auto-open the browser db9 explore mydb --no-open # Use cached assets without checking for updates db9 explore --no-download # Force re-download even if cached db9 explore mydb --force-download ``` | Flag | Default | Description | | ------------------ | ------- | ------------------------------------- | | `--port ` | `7979` | Local port (env: `DB9_EXPLORER_PORT`) | | `--no-open` | off | Skip automatic browser open | | `--no-download` | off | Use cached assets, skip update check | | `--force-download` | off | Re-download assets even if cached | ## Token Management [Section titled “Token Management”](#token-management) Create and manage API tokens for CI/CD pipelines, automation, and agent workflows. Terminal ```bash # Show the current raw token (for use with DB9_API_KEY) db9 token show # Create a new API token (default: 365-day expiry) db9 token create --name my-agent db9 token create --name ci-token --expires-in-days 90 # Create a database-scoped token (read-only or read-write) db9 token create --name readonly-token --scope mydb:ro db9 token create --name multi-db --scope app:rw --scope analytics:ro # List your API tokens db9 token list # Revoke a token db9 token revoke ``` Store created tokens in a secret manager. See [Production Checklist](/docs/production-checklist/) for token security guidance. ## Type Generation [Section titled “Type Generation”](#type-generation) Generate types from the live schema (prints to stdout). Available languages: TypeScript and Python. Terminal ```bash # TypeScript db9 gen types --lang typescript --schema public > db9.types.ts # Python db9 gen types --lang python --schema public > db9_types.py ``` ## Agent Onboarding [Section titled “Agent Onboarding”](#agent-onboarding) `db9 onboard` installs or updates the DB9 skill into supported local coding agents. It does not require login and does not send tokens anywhere. Terminal ```bash # Interactive wizard db9 onboard # Non-interactive db9 onboard --yes --all db9 onboard --yes --agent codex --agent claude --scope user # Safety / introspection (zero filesystem changes) db9 onboard --dry-run db9 onboard --print-locations # Advanced: custom skill source db9 onboard --skill-url https://db9.ai/skill.md db9 onboard --skill-path ./SKILL.md db9 onboard --force ``` Supported agents and default install locations: | Agent | `--scope user` | `--scope project` | Notes | | ---------- | --------------------------------------------------- | --------------------------------- | ---------------------------------------- | | `codex` | `~/.codex/skills/db9/SKILL.md` (or `$CODEX_HOME/…`) | — | Codex currently supports user scope only | | `claude` | `~/.claude/skills/db9/SKILL.md` | `./.claude/skills/db9/SKILL.md` | `--scope both` installs both | | `opencode` | `~/.config/opencode/skills/db9/SKILL.md` | `./.opencode/skills/db9/SKILL.md` | `--scope both` installs both | | `agents` | `~/.agents/skills/db9/SKILL.md` | `./.agents/skills/db9/SKILL.md` | Generic agent-compatible directory | For agent workflow patterns, see [Agent Workflows](/docs/agent-workflows/overview/). ## Error Messages [Section titled “Error Messages”](#error-messages) Common errors and how to resolve them. | Error | Cause | Resolution | | ----------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------- | | `not authenticated` | No token found in `~/.db9/credentials` | Run `db9 login` or set `DB9_API_KEY` env var | | `database not found` | Wrong name/ID, or database was deleted | Run `db9 list` to see available databases | | `permission denied` | Token lacks the required scope | Create a new token: `db9 token create` | | `connection refused` | API endpoint unreachable | Check network; verify `--api-url` flag or `DB9_API_URL` env var | | `rate limit exceeded` | Too many requests in a short window | Wait and retry; implement exponential backoff in scripts | | `name already exists` | Duplicate database name | Choose a different name, or delete the existing database first | | `invalid token` | Token has expired or been revoked | Run `db9 login` or create a new token | | `database is not active` | Database is in `CREATING`, `CLONING`, or `DISABLED` state | Check state with `db9 db status `; wait for `ACTIVE` | | `no databases found` | Account has no databases yet | Run `db9 create --name ` to create one | | `WebSocket connection failed` | Filesystem (`fs9`) WebSocket unavailable | Verify the database is active; check `fs_websocket_url` with `db9 db status ` | ## Exit Codes [Section titled “Exit Codes”](#exit-codes) `db9` follows standard Unix exit code conventions for use in shell scripts and CI/CD pipelines. | Code | Meaning | Example trigger | | ---- | ------------- | ----------------------------------------------------------------------------------------------------- | | `0` | Success | Command completed without errors (including `--help` and `--version`) | | `1` | Runtime error | The command parsed, but failed — database not found, SQL error, API error. See stderr | | `2` | Usage error | The command did not parse — unknown subcommand or flag, missing required argument, invalid flag value | Check for **any** non-zero code rather than testing for `1` specifically: a typo in a flag name exits `2`, not `1`. **Usage in shell scripts:** Terminal ```bash db9 create --name mydb if [ $? -ne 0 ]; then echo "Database creation failed" >&2 exit 1 fi # Or inline db9 create --name mydb || { echo "Failed"; exit 1; } ``` **Capturing error output:** Terminal ```bash output=$(db9 db sql mydb -q "SELECT 1" 2>&1) if [ $? -ne 0 ]; then echo "Error: $output" >&2 exit 1 fi ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Authentication failures [Section titled “Authentication failures”](#authentication-failures) **Symptom:** `not authenticated` or `invalid token` errors. Terminal ```bash # Check current auth status db9 status # Re-authenticate interactively db9 login # Use an API key directly (CI/CD) export DB9_API_KEY=your-token db9 list # View the credentials file cat ~/.db9/credentials # List all tokens db9 token list ``` ### Network errors and connection refused [Section titled “Network errors and connection refused”](#network-errors-and-connection-refused) **Symptom:** `connection refused` or timeouts when running any command. Terminal ```bash # Check which API endpoint is configured db9 status # Override the API URL for a single command db9 --api-url https://api.db9.ai list # Set permanently via environment export DB9_API_URL=https://api.db9.ai ``` If behind a corporate proxy, ensure `HTTPS_PROXY` or `https_proxy` is set in your environment before running `db9` commands. ### Permission denied [Section titled “Permission denied”](#permission-denied) **Symptom:** Commands fail with `permission denied` after authentication succeeds. API tokens can be created with limited scopes. Create a new full-access token and switch to it: Terminal ```bash db9 token create --name full-access export DB9_API_KEY= ``` ### Database stuck in non-ACTIVE state [Section titled “Database stuck in non-ACTIVE state”](#database-stuck-in-non-active-state) **Symptom:** `database is not active` when running SQL or filesystem commands. Terminal ```bash # Check current state db9 db status mydb ``` States and meanings: | State | Description | | --------------- | ------------------------------------------------ | | `CREATING` | Initial provisioning in progress (usually < 30s) | | `CLONING` | Branch copy in progress | | `ACTIVE` | Ready for connections | | `DISABLING` | Deletion in progress | | `DISABLED` | Deleted or suspended | | `CREATE_FAILED` | Provisioning failed — delete and recreate | For branch databases, poll until active: Terminal ```bash while [ "$(db9 db status mydb --json 2>/dev/null | jq -r .state)" != "ACTIVE" ]; do echo "Waiting for ACTIVE state…" sleep 2 done ``` ### Filesystem (fs9) WebSocket errors [Section titled “Filesystem (fs9) WebSocket errors”](#filesystem-fs9-websocket-errors) **Symptom:** `WebSocket connection failed` when using `db9 fs` commands. Terminal ```bash # Verify the database is ACTIVE db9 db status mydb # Check the WebSocket URL is present db9 db status mydb --json | jq .fs_websocket_url ``` If using the TypeScript SDK on Node.js 18–20, install the `ws` package: Terminal ```bash npm install ws ``` Then pass it as the `WebSocket` option when creating the client — see [Filesystem (client.fs)](/docs/sdk/#filesystem-clientfs). ### Verbose debug output [Section titled “Verbose debug output”](#verbose-debug-output) Set `DB9_DEBUG=1` to enable verbose logging for any command: Terminal ```bash DB9_DEBUG=1 db9 list DB9_DEBUG=1 db9 db sql mydb -q "SELECT 1" ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * [TypeScript SDK](/docs/sdk/) — Server-side database management with get-db9 * [Browser SDK](/docs/sdk-browser/) — Client-side data access with @db9/browser * [SQL Reference](/docs/sql/) — SQL engine compatibility and features * [Extensions](/docs/extensions/) — fs9, HTTP, vector, pg\_cron, and more * [Connect](/docs/connect/) — Connection strings, TLS, and driver configuration # DB9 vs Neon > A fair comparison of DB9 and Neon — when each is the better choice for your PostgreSQL workload, and where they differ on architecture, branching, extensions, and developer experience. DB9 and Neon are both serverless PostgreSQL-compatible databases, but they solve different problems. Neon optimizes for infrastructure flexibility — autoscaling, scale-to-zero, and efficient branching. DB9 optimizes for instant provisioning, AI agent integration, and built-in application-layer extensions. This page is for developers evaluating both platforms. It covers the key differences honestly so you can choose the right one. ## At a Glance [Section titled “At a Glance”](#at-a-glance) | Capability | DB9 | Neon | | ------------------------- | ----------------------------------------------------------------------------- | --------------------------------------- | | **Provisioning** | Synchronous, under 1 second | Seconds (project + endpoint startup) | | **Autoscaling** | No | Yes (0.25–56 CU) | | **Scale-to-zero** | No (always on) | Yes (5-minute idle default) | | **Branching** | Full data copy, async | Copy-on-write, instant | | **Read replicas** | No | Yes (zero storage overhead) | | **Connection pooling** | No built-in pooler | Built-in PgBouncer | | **Serverless driver** | Yes (`@db9/browser` over HTTP) | Yes (`@neondatabase/serverless`) | | **Extensions** | 9 built-in | 40+ community | | **HTTP from SQL** | Yes (built-in `http` extension — GET, POST, PUT, DELETE, PATCH, HEAD) | No | | **File system in SQL** | Yes (built-in `fs9` extension) | No | | **FUSE filesystem mount** | Yes (`db9 fs mount`) | No | | **Native embeddings** | Yes (built-in `embedding()` function) | No (pgvector only, external embeddings) | | **Document chunking** | Yes (built-in `CHUNK_TEXT()` for RAG) | No | | **Full-text search** | Yes (English, Chinese/jieba, ngram tokenizers) | Yes (standard PostgreSQL FTS) | | **Parquet import** | Yes (built-in `read_parquet()` + COPY FORMAT parquet) | No | | **Agent onboarding** | Yes (`db9 onboard` for Claude, Codex, etc.) | No | | **Anonymous access** | Yes (no signup required for up to 5 databases) | No (account required) | | **CLI observability** | Yes (`db9 db inspect` — queries, slow-queries, schemas, indexes) | No | | **Logical replication** | No | Yes | | **Row-level security** | Yes | Yes | | **LISTEN/NOTIFY** | Yes (pgwire sessions; HTTP API can `NOTIFY` only) | Yes | | **Transaction isolation** | READ COMMITTED / REPEATABLE READ (SERIALIZABLE downgraded to REPEATABLE READ) | Full SERIALIZABLE | | **Storage engine** | TiKV (distributed KV) | Custom (Pageserver + Safekeeper) | | **Wire protocol** | pgwire v3 | pgwire v3 | ## When should I use DB9 instead of Neon? **Instant database provisioning at scale.** DB9 creates databases synchronously in under a second. This makes it practical to provision a database per user, per agent task, or per CI run without waiting. The SDK’s `instantDatabase()` creates and returns a ready-to-use connection in a single call. **AI agent workflows.** DB9 is designed for AI agents. The `db9 onboard` CLI installs skills for Claude Code, OpenAI Codex, Opencode, and other agents. Anonymous accounts let agents create databases without signup flows. Built-in `embedding()` generates vector embeddings from text directly in SQL — no external embedding API needed. **Application-layer extensions.** DB9 includes extensions that run application logic inside the database: * `http` — make HTTP requests from SQL (GET, POST, PUT, DELETE, PATCH, HEAD) * `fs9` — read and write files from SQL, query file contents as tables; FUSE mount for local filesystem access * `embedding()` — generate text embeddings natively (default model: text-embedding-v4, 1024 dimensions) * `CHUNK_TEXT()` — split documents into overlapping chunks for RAG pipelines * `read_parquet()` — import Parquet files directly in SQL * `pg_cron` — schedule recurring SQL jobs * Full-text search with Chinese tokenizer support (jieba/zhparser) These are compiled into DB9 and available without installation. Neon does not offer equivalent built-in HTTP, file system, native embedding, document chunking, or Parquet import capabilities. **No-signup developer experience.** Developers can install the CLI and create a database without creating an account. Anonymous accounts support up to 5 databases and can be upgraded later with `db9 claim`. This reduces friction for prototyping, tutorials, and agent-driven workflows. ## When should I use Neon instead of DB9? **Variable or unpredictable workloads.** Neon autoscales compute between a minimum and maximum (up to 56 CU) based on load. When idle, compute scales to zero — you pay nothing for unused databases. DB9 databases are always on with fixed compute. **Cost-sensitive development environments.** Neon’s free tier includes 100 projects with scale-to-zero. Development and staging databases that sit idle most of the time cost nothing. DB9 does not scale to zero, so idle databases still consume resources. **Development branching.** Neon branches are copy-on-write and instant regardless of database size. A 100 GB production database branches in milliseconds with no additional storage until writes diverge. DB9 branches are full data copies — creation is asynchronous and takes seconds to minutes depending on size, with each branch consuming its own storage. **Read-heavy workloads.** Neon supports read replicas that share the same storage backend. You can scale read capacity independently without data duplication. DB9 does not support read replicas. **Broad extension ecosystem.** Neon supports 40+ PostgreSQL extensions including PostGIS, pg\_trgm, pgcrypto, ltree, and pg\_partman. DB9 has 9 built-in extensions and cannot install community extensions. If your application depends on PostGIS or other specialized extensions, Neon is the better fit. **Serverless and edge deployments.** Neon’s `@neondatabase/serverless` and DB9’s `@db9/browser` both enable SQL queries over HTTP from edge runtimes (Cloudflare Workers, Vercel Edge Functions) where TCP connections are not available. DB9’s standard pgwire path remains TCP-based for traditional PostgreSQL drivers. **Full PostgreSQL feature parity.** Neon supports table partitioning, logical replication, and SERIALIZABLE isolation. DB9 does not support table partitioning or logical replication, advisory locks are node-local rather than cross-process/global, and `LISTEN` requires a direct pgwire connection (the HTTP SQL API can `NOTIFY` but not subscribe). Choose Neon if you need these PostgreSQL features If your application depends on table partitioning, logical replication, community extensions (PostGIS, pg\_trgm, pgcrypto), or true SERIALIZABLE isolation, Neon is the better choice for your current requirements. ## Architecture Differences [Section titled “Architecture Differences”](#architecture-differences) ### Storage [Section titled “Storage”](#storage) **DB9** uses TiKV, a distributed key-value store, as its storage engine. Each database gets an isolated TiKV keyspace. This architecture enables instant provisioning (creating a keyspace is fast) but means branching requires a full data copy. **Neon** uses a custom storage layer (Pageserver + Safekeeper) that separates compute from storage. This enables copy-on-write branching, point-in-time recovery, and read replicas that share storage. Compute endpoints connect to storage over the network and can scale independently. ### Compute [Section titled “Compute”](#compute) **DB9** runs a custom SQL engine that parses, optimizes, and executes queries against TiKV. Each database has dedicated compute that is always running. There is no autoscaling or scale-to-zero. **Neon** runs standard PostgreSQL (with custom storage hooks). Compute endpoints autoscale between configured CU ranges and suspend after idle timeout. Resumed endpoints reconnect to persistent storage. ### Branching [Section titled “Branching”](#branching) **DB9** branches by copying all data from the parent database into a new TiKV keyspace. This is asynchronous — state progresses from CLONING to ACTIVE. Maximum 2 concurrent branch creations. Each branch is a fully independent database. **Neon** branches by creating a new compute endpoint pointing to the same storage at a specific point in time. No data is copied at branch time. Writes to the branch are stored as deltas. This is instant and storage-efficient. ## Extension Comparison [Section titled “Extension Comparison”](#extension-comparison) | Extension / Capability | DB9 | Neon | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | | pgvector (vector type + HNSW) | Built-in | Available | | Native embedding generation | Built-in (`embedding()`) | Not available | | Document chunking for RAG | Built-in (`CHUNK_TEXT()`) | Not available | | HTTP requests from SQL | Built-in (`http` — 6 methods) | Not available | | File system access from SQL | Built-in (`fs9` extension) | Not available | | FUSE filesystem mount | Built-in (`db9 fs mount`) | Not available | | Parquet import | Built-in (`read_parquet()` + COPY) | Not available | | Full-text search (Chinese/jieba) | Built-in (zhparser extension) | Standard PostgreSQL FTS | | pg\_cron (job scheduling) | Built-in | Available | | uuid-ossp | Built-in | Available | | hstore | Built-in | Available | | PostGIS | Not available | Available | | pg\_trgm (trigram search) | Not available | Available | | pgcrypto | Metadata shim only — `CREATE EXTENSION` succeeds but registers no functions; `gen_random_uuid()` and `digest()` are built-ins, while `crypt()`/`gen_salt()`/`hmac()` are unavailable | Available | | ltree | Not available | Available | | pg\_partman | Not available | Available | | Custom / community extensions | Not supported | 40+ supported | ## ORM and Driver Compatibility [Section titled “ORM and Driver Compatibility”](#orm-and-driver-compatibility) Both platforms support standard PostgreSQL ORMs and drivers. DB9 has tested compatibility with: | ORM / Driver | DB9 Test Results | Neon | | ------------ | -------------------- | --------- | | Prisma | 89/89 (100%) | Supported | | Drizzle | 75/75 (100%) | Supported | | SQLAlchemy | Tested (smoke + e2e) | Supported | | TypeORM | 147/150 (98%) | Supported | | Sequelize | 87/87 (100%) | Supported | | Knex | 97/97 (100%) | Supported | | GORM | Tested (smoke + e2e) | Supported | Both platforms use pgwire v3 and work with any PostgreSQL-compatible driver (node-postgres, psycopg, pgx, JDBC). ## Migration Between Platforms [Section titled “Migration Between Platforms”](#migration-between-platforms) Moving from Neon to DB9 or vice versa uses standard `pg_dump` / `psql` workflows. See the [Migrate from Neon](/docs/migrations/from-neon/) guide for step-by-step instructions. Key migration considerations: * DB9 does not use Neon’s serverless driver — switch to `@db9/browser` (HTTP) or a standard PostgreSQL driver * DB9 does not support logical replication * DB9 connection strings use a different format (`tenant_id.role@host:5433/postgres`) * Extensions not in DB9’s built-in set will not be available ## Summary [Section titled “Summary”](#summary) **DB9 is the better choice when** you need instant database provisioning, AI agent integration, or application-layer SQL extensions (HTTP, files, native embeddings). It excels at multi-tenant patterns where each user or agent gets their own database. **Neon is the better choice when** you need autoscaling, scale-to-zero, efficient branching, read replicas, or broad extension support. It excels at variable workloads where cost optimization and PostgreSQL feature completeness matter. Both platforms are PostgreSQL-compatible and work with the same ORMs, drivers, and SQL. The right choice depends on which capabilities matter most for your workload. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Migrate from Neon](/docs/migrations/from-neon/) — step-by-step migration guide * [DB9 vs Supabase](/docs/comparisons/db9-vs-supabase/) — comparison with Supabase * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — detailed feature support * [Overview](/docs/overview/) — what DB9 is and who it is for * [Why DB9 for AI Agents](/docs/why-db9-for-ai-agents/) — the agent-native positioning # DB9 vs PlanetScale > A fair comparison of DB9 and PlanetScale Postgres — when each is the better choice for your PostgreSQL workload, and where they differ on architecture, extensions, and developer experience. DB9 and PlanetScale are both managed PostgreSQL-compatible databases, but they solve different problems. PlanetScale optimizes for operational reliability — high availability, read replicas, and connection pooling. db9 database optimizes for instant provisioning, AI agent integration, and built-in application-layer extensions. This page is for developers evaluating both platforms. It covers the key differences honestly so you can choose the right one. ## At a Glance [Section titled “At a Glance”](#at-a-glance) | Capability | DB9 | PlanetScale Postgres | | ------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------- | | **PostgreSQL version** | Custom engine (pgwire v3) | Standard PostgreSQL v17 | | **Storage engine** | TiKV (distributed KV) | EBS or NVMe Metal | | **Provisioning** | Synchronous, under 1 second | Not documented | | **Scale-to-zero** | No (always on) | No (always on) | | **High availability** | No | Yes (1 primary + 2 replicas, 3 AZs) | | **Connection pooling** | No built-in pooler | Yes (PgBouncer add-on) | | **Read replicas** | No | Yes (add-on) | | **Branching** | Full data copy, async | Schema-only copy from backup | | **Free tier** | Yes (no signup, up to 5 databases) | No (14-day trial, $5/mo minimum) | | **Native embeddings** | Yes (built-in `embedding()` function) | No | | **Document chunking** | Yes (built-in `CHUNK_TEXT()` for RAG) | No | | **HTTP from SQL** | Yes (built-in `http` extension — GET, POST, PUT, DELETE, PATCH, HEAD) | No | | **File system in SQL** | Yes (built-in `fs9` extension) | No | | **Parquet import** | Yes (built-in `read_parquet()` + COPY FORMAT parquet) | No | | **Agent onboarding** | Yes (`db9 onboard` for Claude, Codex, etc.) | No (MCP server available) | | **MCP integration** | No | Yes (16 tools, hosted) | | **Extensions** | 9 built-in | Curated (vectorscale, TimescaleDB, pg\_strict) | | **LISTEN/NOTIFY** | Yes (pgwire sessions; HTTP API can `NOTIFY` only) | Yes (standard PostgreSQL) | | **Transaction isolation** | READ COMMITTED / REPEATABLE READ (SERIALIZABLE downgraded to REPEATABLE READ) | Full SERIALIZABLE | ## When should I use DB9 instead of PlanetScale? **Instant database provisioning at scale.** DB9 creates databases synchronously in under a second. This makes it practical to provision a database per user, per agent task, or per CI run without waiting. The SDK’s `instantDatabase()` creates and returns a ready-to-use connection in a single call. **AI agent workflows.** DB9 is designed for AI agents. The `db9 onboard` CLI installs skills for Claude Code, OpenAI Codex, Opencode, and other agents. Anonymous accounts let agents create databases without signup flows. Built-in `embedding()` generates vector embeddings from text directly in SQL — no external embedding API needed. **Application-layer extensions.** DB9 includes extensions that run application logic inside the database: * `http` — make HTTP requests from SQL (GET, POST, PUT, DELETE, PATCH, HEAD) * `fs9` — read and write files from SQL, query file contents as tables; FUSE mount for local filesystem access * `embedding()` — generate text embeddings natively (default model: text-embedding-v4, 1024 dimensions) * `CHUNK_TEXT()` — split documents into overlapping chunks for RAG pipelines * `read_parquet()` — import Parquet files directly in SQL * `pg_cron` — schedule recurring SQL jobs * Full-text search with Chinese tokenizer support (jieba/zhparser) These are compiled into DB9 and available without installation. PlanetScale does not offer equivalent built-in HTTP, file system, native embedding, document chunking, or Parquet import capabilities. **No-signup developer experience.** Developers can install the CLI and create a database without creating an account. Anonymous accounts support up to 5 databases and can be upgraded later with `db9 claim`. PlanetScale requires a paid account ($5/mo minimum) after a 14-day trial — there is no permanent free tier. ## When should I use PlanetScale instead of DB9? **Production workloads requiring high availability.** PlanetScale deploys every database with one primary and two replicas across three availability zones by default. Automatic failover handles node failures without application changes. DB9 does not offer built-in multi-AZ replication or automatic failover. **Read-heavy workloads.** PlanetScale supports read replicas as an add-on, letting you scale read capacity independently. DB9 does not support read replicas. **Connection-heavy applications.** PlanetScale offers a PgBouncer-based connection pooling add-on for applications that open many short-lived connections (serverless functions, high-concurrency web apps). DB9 does not include a built-in connection pooler. **MCP-first agent integration.** PlanetScale provides a hosted MCP server with 16 tools for database management, schema exploration, and query execution. If your agent workflow is built around MCP rather than CLI onboarding, PlanetScale has a more mature MCP integration. DB9 focuses on CLI-based agent onboarding via `db9 onboard`. **Standard PostgreSQL compatibility.** PlanetScale runs standard PostgreSQL v17 with full support for SERIALIZABLE isolation and a curated set of extensions including vectorscale, TimescaleDB, and pg\_strict. DB9 does not support SERIALIZABLE isolation, its extension set is limited to 9 built-in options, and `LISTEN` requires a direct pgwire connection. Choose PlanetScale if you need these PostgreSQL features If your application depends on SERIALIZABLE isolation, high availability with automatic failover, connection pooling, or read replicas, PlanetScale is the better choice for your current requirements. ## Architecture Differences [Section titled “Architecture Differences”](#architecture-differences) ### Storage [Section titled “Storage”](#storage) **DB9** uses TiKV, a distributed key-value store, as its storage engine. Each database gets an isolated TiKV keyspace. This architecture enables instant provisioning (creating a keyspace is fast) but means branching requires a full data copy. **PlanetScale** uses either EBS or NVMe Metal storage depending on the plan. Storage is tightly coupled to compute nodes. The three-node default deployment (one primary, two replicas) provides durability through synchronous replication across availability zones. ### Compute [Section titled “Compute”](#compute) **DB9** runs a custom SQL engine that parses, optimizes, and executes queries against TiKV. Each database has dedicated compute that is always running. There is no autoscaling or scale-to-zero. **PlanetScale** runs standard PostgreSQL v17. Each database has fixed compute with always-on nodes. There is no autoscaling or scale-to-zero. The focus is on operational reliability through multi-AZ replication rather than elastic scaling. ### Branching [Section titled “Branching”](#branching) **DB9** branches by copying all data from the parent database into a new TiKV keyspace. This is asynchronous — state progresses from CLONING to ACTIVE. Maximum 2 concurrent branch creations. Each branch is a fully independent database with a complete data copy. **PlanetScale** branches by creating a schema-only copy from a backup of the source database. Data is not included — the branch starts with an empty dataset matching the source schema. This is lightweight but means branches cannot be used for data-inclusive testing without additional data loading. ## Extension Comparison [Section titled “Extension Comparison”](#extension-comparison) | Extension / Capability | DB9 | PlanetScale Postgres | | -------------------------------- | ---------------------------------- | --------------------------- | | pgvector (vector type + HNSW) | Built-in | Available (via vectorscale) | | Native embedding generation | Built-in (`embedding()`) | Not available | | Document chunking for RAG | Built-in (`CHUNK_TEXT()`) | Not available | | HTTP requests from SQL | Built-in (`http` — 6 methods) | Not available | | File system access from SQL | Built-in (`fs9` extension) | Not available | | FUSE filesystem mount | Built-in (`db9 fs mount`) | Not available | | Parquet import | Built-in (`read_parquet()` + COPY) | Not available | | Full-text search (Chinese/jieba) | Built-in (zhparser extension) | Standard PostgreSQL FTS | | pg\_cron (job scheduling) | Built-in | Not documented | | TimescaleDB | Not available | Available | | vectorscale | Not available | Available | | pg\_strict | Not available | Available | | Custom / community extensions | Not supported | Curated set supported | ## ORM and Driver Compatibility [Section titled “ORM and Driver Compatibility”](#orm-and-driver-compatibility) Both platforms support standard PostgreSQL ORMs and drivers. DB9 has tested compatibility with: | ORM / Driver | DB9 Test Results | PlanetScale Postgres | | ------------ | -------------------- | -------------------- | | Prisma | 89/89 (100%) | Supported | | Drizzle | 75/75 (100%) | Supported | | SQLAlchemy | Tested (smoke + e2e) | Supported | | TypeORM | 147/150 (98%) | Supported | | Sequelize | 87/87 (100%) | Supported | | Knex | 97/97 (100%) | Supported | | GORM | Tested (smoke + e2e) | Supported | Both platforms use pgwire v3 and work with any PostgreSQL-compatible driver (node-postgres, psycopg, pgx, JDBC). ## Migration Between Platforms [Section titled “Migration Between Platforms”](#migration-between-platforms) Moving from PlanetScale to DB9 or vice versa uses standard `pg_dump` / `psql` workflows. See the [Migrate from PlanetScale](/docs/migrations/from-planetscale/) guide for step-by-step instructions. Key migration considerations: * PlanetScale runs standard PostgreSQL v17; DB9 runs a custom engine — test query compatibility before migrating * DB9 does not support SERIALIZABLE isolation * DB9 connection strings use a different format (`tenant_id.role@host:5433/postgres`) * Extensions not in DB9’s built-in set will not be available * PlanetScale does not include DB9’s application-layer extensions (HTTP, file system, native embeddings) ## Summary [Section titled “Summary”](#summary) **DB9 is the better choice when** you need instant database provisioning, AI agent integration, or application-layer SQL extensions (HTTP, files, native embeddings). It excels at multi-tenant patterns where each user or agent gets their own database, and its free anonymous tier removes friction for prototyping. **PlanetScale is the better choice when** you need production-grade high availability, read replicas, connection pooling, or full PostgreSQL feature compatibility. It excels at operational reliability with its three-AZ default deployment and standard PostgreSQL v17 engine. Both platforms are PostgreSQL-compatible and work with standard ORMs, drivers, and SQL. Neither supports scale-to-zero. The right choice depends on whether your priority is developer experience and AI-native features (DB9) or operational reliability and PostgreSQL completeness (PlanetScale). ## Next Pages [Section titled “Next Pages”](#next-pages) * [DB9 vs Neon](/docs/comparisons/db9-vs-neon/) — comparison with Neon * [DB9 vs Supabase](/docs/comparisons/db9-vs-supabase/) — comparison with Supabase * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — detailed feature support * [Overview](/docs/overview/) — what DB9 is and who it is for * [Why DB9 for AI Agents](/docs/why-db9-for-ai-agents/) — the agent-native positioning # DB9 vs Supabase > A fair comparison of DB9 and Supabase — database-only vs full application platform, and when each is the right choice. DB9 is a serverless PostgreSQL-compatible database. Supabase is a full application platform — PostgreSQL database plus authentication, file storage, realtime subscriptions, edge functions, and an auto-generated REST API. They are not direct substitutes. DB9 competes with Supabase’s database layer. Everything else in Supabase (Auth, Storage, Realtime, Edge Functions, PostgREST) has no equivalent in DB9. This page helps you understand which approach fits your project. ## At a Glance [Section titled “At a Glance”](#at-a-glance) | Capability | DB9 | Supabase | | --------------------------- | ----------------------------------------------------------------------------- | --------------------------------------- | | **Product scope** | Database only | Full application platform | | **PostgreSQL database** | Yes | Yes (standard PostgreSQL) | | **Authentication** | No | Yes (GoTrue) | | **File storage** | fs9 extension (SQL-based) | Yes (S3-backed, CDN) | | **Realtime subscriptions** | No | Yes (CDC + LISTEN/NOTIFY) | | **Auto-generated REST API** | No | Yes (PostgREST) | | **Edge Functions** | No | Yes (Deno) | | **Row-level security** | Yes | Yes | | **Provisioning speed** | Synchronous, under 1 second | Seconds to minutes (full project setup) | | **Agent onboarding** | Yes (`db9 onboard`) | No | | **Anonymous access** | Yes (no signup, up to 5 databases) | No (account required) | | **Native embeddings** | Yes (`embedding()` function) | No (pgvector only, external embeddings) | | **Document chunking** | Yes (built-in `CHUNK_TEXT()` for RAG) | No | | **Full-text search** | Yes (English, Chinese/jieba, ngram tokenizers) | Yes (standard PostgreSQL FTS) | | **Parquet import** | Yes (built-in `read_parquet()` + COPY) | No | | **FUSE filesystem mount** | Yes (`db9 fs mount`) | No | | **HTTP from SQL** | Yes (built-in `http` — 6 methods) | Yes (pg\_net extension) | | **CLI observability** | Yes (`db9 db inspect`) | No (dashboard-based) | | **Connection pooling** | No built-in pooler | Yes (Supavisor) | | **Extensions** | 9 built-in | 40+ (PostGIS, pg\_trgm, pgcrypto, etc.) | | **LISTEN/NOTIFY** | Yes (pgwire sessions; HTTP API can `NOTIFY` only) | Yes | | **Logical replication** | No | Yes | | **Transaction isolation** | READ COMMITTED / REPEATABLE READ (SERIALIZABLE downgraded to REPEATABLE READ) | Full SERIALIZABLE | | **Wire protocol** | pgwire v3 | pgwire v3 | ## When should I use DB9 instead of Supabase? **You want a database, not a platform.** If your application already has its own auth, storage, and API layer — or you prefer to compose these from separate services — DB9 gives you a PostgreSQL-compatible database without bundled platform services. There is no auth system to configure, no storage policies to manage, and no PostgREST to learn. **Instant database provisioning at scale.** DB9 creates databases synchronously in under a second. The SDK’s `instantDatabase()` returns a ready-to-use connection in a single call. This makes database-per-user, database-per-agent, and database-per-CI-run patterns practical without orchestration. **AI agent workflows.** DB9 is built for AI agents. The `db9 onboard` CLI installs skills for Claude Code, OpenAI Codex, Opencode, and other agents. Anonymous accounts let agents create databases without signup flows. Built-in `embedding()` generates vector embeddings from text directly in SQL — no external embedding API or infrastructure needed. **Application-layer SQL extensions.** DB9 includes extensions that bring application logic into SQL: * `http` — make HTTP requests from SQL (GET, POST, PUT, DELETE, PATCH, HEAD) * `fs9` — read, write, and query files from SQL; parse JSONL/CSV/TSV as tables; FUSE mount for local access * `embedding()` — generate text embeddings natively (default model: text-embedding-v4, 1024 dimensions) * `CHUNK_TEXT()` — split documents into overlapping chunks for RAG pipelines * `read_parquet()` — import Parquet files directly in SQL * `pg_cron` — schedule recurring SQL jobs * Full-text search with Chinese tokenizer support (jieba/zhparser) These are compiled into DB9 and always available. Supabase has pg\_net for outbound HTTP webhooks and pg\_cron, but does not offer built-in file system access, native embedding generation, document chunking, or Parquet import. **No-signup developer experience.** Install the CLI and create a database immediately — no account creation, no email verification. Anonymous accounts support up to 5 databases and can be upgraded later with `db9 claim`. ## When should I use Supabase instead of DB9? **You want a full backend, not just a database.** Supabase bundles authentication (GoTrue), file storage (S3-backed with CDN), realtime subscriptions (CDC), edge functions (Deno), and an auto-generated REST/GraphQL API. If your project needs most of these, Supabase provides them as a single integrated platform. Building the same stack from separate services takes more effort. **Client-side database access.** Supabase’s auto-generated PostgREST API and client libraries (`@supabase/supabase-js`) let frontend applications query the database directly with row-level security enforcing access control. DB9 provides the [Browser SDK](/docs/sdk-browser/) (`@db9/browser`) with a chainable query builder for client-side access, backed by publishable keys and RLS. **Row-level security.** Supabase projects heavily use RLS to enforce per-user data access at the database level. This is core to Supabase’s security model for client-side access. DB9 supports RLS with CREATE POLICY, ENABLE/FORCE ROW LEVEL SECURITY, and per-operation policies (SELECT, INSERT, UPDATE, DELETE). The [Browser SDK](/docs/sdk-browser/) uses RLS as its primary access control mechanism. **Realtime features.** Supabase Realtime combines PostgreSQL’s LISTEN/NOTIFY with change data capture to push database changes to connected clients. DB9 supports LISTEN/NOTIFY over pgwire, but has no change data capture — you emit events yourself with triggers and fan them out from your own service. If your application needs live updates delivered to browser clients out of the box, Supabase handles this natively. Choose Supabase if you need these features If your app needs LISTEN/NOTIFY realtime subscriptions, Auth (GoTrue), S3-backed file storage, Edge Functions, or community extensions like PostGIS or pgcrypto, Supabase is a better fit for those requirements today. **Broad extension ecosystem.** Supabase supports 40+ PostgreSQL extensions including PostGIS, pg\_trgm, pgcrypto, ltree, and pg\_graphql. DB9 has 9 built-in extensions and cannot install community extensions. **Standard PostgreSQL.** Supabase runs standard PostgreSQL with full feature parity — SERIALIZABLE isolation, table partitioning, table inheritance, advisory locks, logical replication, and all PL/pgSQL capabilities. DB9’s custom SQL engine has compatibility gaps in these areas. **Dashboard and visual tools.** Supabase includes a web dashboard with a SQL editor, table viewer, auth management, storage browser, and logs. DB9 is CLI-first with no web dashboard. ## Architecture Differences [Section titled “Architecture Differences”](#architecture-differences) ### Scope [Section titled “Scope”](#scope) **DB9** is a database service. You get a PostgreSQL-compatible database with built-in extensions. Everything else — auth, file storage, APIs, realtime — is your responsibility. **Supabase** is an application platform. Each project includes a PostgreSQL database, GoTrue auth server, S3-compatible storage, Realtime server, PostgREST API, and Deno edge function runtime. These services are pre-integrated and share the same project. ### Storage engine [Section titled “Storage engine”](#storage-engine) **DB9** uses TiKV, a distributed key-value store. Each database gets an isolated keyspace. This enables instant provisioning but means branching requires full data copies. **Supabase** uses standard PostgreSQL with its default storage engine. Compute and storage are not separated — scaling requires upgrading the instance. ### Multi-tenancy [Section titled “Multi-tenancy”](#multi-tenancy) **DB9** supports database-per-tenant natively. Creating thousands of databases is practical because provisioning is synchronous and sub-second. The SDK’s `instantDatabase()` is idempotent and handles the full lifecycle. **Supabase** is designed around one project per application (or a small number of projects). Multi-tenancy is typically handled within a single database using schemas or RLS policies, not by creating separate projects per tenant. ## Extension Comparison [Section titled “Extension Comparison”](#extension-comparison) | Extension / Capability | DB9 | Supabase | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ | | pgvector (vector type + HNSW) | Built-in | Available | | Native embedding generation | Built-in (`embedding()`) | Not available | | Document chunking for RAG | Built-in (`CHUNK_TEXT()`) | Not available | | HTTP requests from SQL | Built-in (`http` — 6 methods) | pg\_net (async webhooks) | | File system access from SQL | Built-in (`fs9` extension) | Not available | | FUSE filesystem mount | Built-in (`db9 fs mount`) | Not available | | Parquet import | Built-in (`read_parquet()` + COPY) | Not available | | Full-text search (Chinese/jieba) | Built-in (zhparser extension) | Standard PostgreSQL FTS | | pg\_cron (job scheduling) | Built-in | Available | | uuid-ossp | Built-in | Available | | hstore | Built-in | Available | | PostGIS | Not available | Available | | pg\_trgm (trigram search) | Not available | Available | | pgcrypto | Metadata shim only — `CREATE EXTENSION` succeeds but registers no functions; `gen_random_uuid()` and `digest()` are built-ins, while `crypt()`/`gen_salt()`/`hmac()` are unavailable | Available | | pg\_graphql | Not available | Available | | pgsodium (encryption) | Not available | Available | | Custom / community extensions | Not supported | 40+ supported | ## ORM and Driver Compatibility [Section titled “ORM and Driver Compatibility”](#orm-and-driver-compatibility) Both platforms work with standard PostgreSQL ORMs and drivers. DB9 has tested compatibility with: | ORM / Driver | DB9 Test Results | Supabase | | ------------ | -------------------- | --------- | | Prisma | 89/89 (100%) | Supported | | Drizzle | 75/75 (100%) | Supported | | SQLAlchemy | Tested (smoke + e2e) | Supported | | TypeORM | 147/150 (98%) | Supported | | Sequelize | 87/87 (100%) | Supported | | Knex | 97/97 (100%) | Supported | | GORM | Tested (smoke + e2e) | Supported | Supabase additionally provides its own client library (`@supabase/supabase-js`) that wraps PostgREST. DB9 does not have an equivalent — use standard PostgreSQL drivers or ORMs. ## Migration Between Platforms [Section titled “Migration Between Platforms”](#migration-between-platforms) Moving from Supabase to DB9 uses standard `pg_dump` with schema exclusions for Supabase’s internal schemas. See the [Migrate from Supabase](/docs/migrations/from-supabase/) guide for step-by-step instructions. Key considerations: * DB9 replaces only the database — plan replacements for Auth, Storage, Realtime, and Edge Functions * RLS policies transfer directly — DB9 supports CREATE POLICY with the same syntax * The Supabase client library must be replaced with a standard PostgreSQL driver or ORM * DB9 connection strings use a different format (`tenant_id.role@host:5433/postgres`) ## Summary [Section titled “Summary”](#summary) **DB9 is the better choice when** you want a standalone PostgreSQL-compatible database with instant provisioning, AI agent integration, and built-in SQL extensions for HTTP, files, and embeddings. It fits teams that compose their own backend from separate services. **Supabase is the better choice when** you want a full application backend — database, auth, storage, realtime, and API — in one integrated platform. It fits teams that want to ship quickly with batteries included, especially for client-side-heavy applications that rely on RLS and PostgREST. The decision is architectural: do you want a database or a platform? If you already have (or prefer to build) your own auth, storage, and API layers, DB9 gives you a fast, agent-friendly database. If you want those services bundled and integrated, Supabase provides them. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Migrate from Supabase](/docs/migrations/from-supabase/) — step-by-step migration guide * [DB9 vs Neon](/docs/comparisons/db9-vs-neon/) — comparison with Neon (database vs database) * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — detailed feature support * [Overview](/docs/overview/) — what DB9 is and who it is for * [Why DB9 for AI Agents](/docs/why-db9-for-ai-agents/) — the agent-native positioning # DB9 vs Turso > A fair comparison of DB9 and Turso — when each is the better choice, and where they differ on SQL dialect, architecture, edge replication, and developer experience. DB9 and Turso are both serverless databases designed for modern applications, but they are built on fundamentally different foundations. db9 database is PostgreSQL-compatible, backed by TiKV. Turso is built on libSQL, a fork of SQLite, with per-database file storage and edge replication. This is not just a feature comparison — it is a SQL dialect decision. Choosing between PostgreSQL and SQLite/libSQL affects your schema design, query patterns, ORM compatibility, and migration path. This page helps you evaluate both platforms honestly. ## At a Glance [Section titled “At a Glance”](#at-a-glance) | Capability | DB9 | Turso | | ------------------------- | ---------------------------------------------------------------- | --------------------------------------- | | **SQL dialect** | PostgreSQL-compatible | SQLite/libSQL | | **Wire protocol** | pgwire v3 | HTTP/WebSocket (libSQL protocol) | | **Storage engine** | TiKV (distributed KV) | SQLite B-tree (per-database file) | | **Provisioning** | Synchronous, under 1 second | Instant (no specific SLA) | | **Edge replication** | No | Yes (multi-region groups) | | **Embedded replicas** | No | Yes (local SQLite file sync) | | **Vector search** | Yes (pgvector built-in, HNSW) | Yes (native, no extension) | | **Native embeddings** | Yes (built-in `embedding()` function) | No | | **Full-text search** | Yes (English, Chinese/jieba, ngram tokenizers) | Yes (Tantivy-powered) | | **HTTP from SQL** | Yes (built-in `http` — 6 methods) | No | | **File system in SQL** | Yes (built-in `fs9` extension) | No | | **FUSE filesystem mount** | Yes (`db9 fs mount`) | No | | **Document chunking** | Yes (built-in `CHUNK_TEXT()` for RAG) | No | | **Parquet import** | Yes (built-in `read_parquet()` + COPY) | No | | **Agent onboarding** | Yes (`db9 onboard` for Claude, Codex, etc.) | AgentFS (sandbox + COW overlays) | | **Anonymous access** | Yes (no signup, up to 5 databases) | No (account required) | | **Free tier** | Yes (no signup, 5 databases) | Yes (100 databases, 5 GB) | | **SDKs** | TypeScript, Browser | 10 languages | | **Concurrent writes** | Yes | Yes (libSQL multi-writer) | | **Extension ecosystem** | 9 built-in PostgreSQL extensions | SQLite extensions (different ecosystem) | | **ORM compatibility** | PostgreSQL ORMs (Prisma, Drizzle, SQLAlchemy, etc.) | SQLite ORMs and Drizzle (libSQL driver) | | **CLI observability** | Yes (`db9 db inspect` — queries, slow-queries, schemas, indexes) | No | | **Row-level security** | Yes | No (application-level auth) | ## When should I use DB9 instead of Turso? **You need PostgreSQL compatibility.** DB9 speaks pgwire v3 and works with the entire PostgreSQL ecosystem — Prisma, Drizzle, SQLAlchemy, TypeORM, Sequelize, Knex, GORM, and any PostgreSQL driver. If your application already uses PostgreSQL, or you need PostgreSQL-specific features like row-level security, `jsonb` operators, CTEs with writeable expressions, or window functions, DB9 is a direct fit. Turso uses SQLite SQL, which has a different type system, different ALTER TABLE behavior, and different ORM driver requirements. **AI agent workflows.** db9 database is designed for AI agents. The `db9 onboard` CLI installs skills for Claude Code, OpenAI Codex, Opencode, and other agents. Anonymous accounts let agents create databases without signup flows. Built-in `embedding()` generates vector embeddings from text directly in SQL — no external embedding API needed. Turso offers AgentFS with sandbox environments and copy-on-write overlays, but does not include native embedding generation. **Application-layer extensions.** DB9 includes extensions that run application logic inside the database: * `http` — make HTTP requests from SQL (GET, POST, PUT, DELETE, PATCH, HEAD) * `fs9` — read and write files from SQL, query file contents as tables; FUSE mount for local filesystem access * `embedding()` — generate text embeddings natively (default model: text-embedding-v4, 1024 dimensions) * `CHUNK_TEXT()` — split documents into overlapping chunks for RAG pipelines * `read_parquet()` — import Parquet files directly in SQL * `pg_cron` — schedule recurring SQL jobs * Full-text search with Chinese tokenizer support (jieba/zhparser) These are compiled into DB9 and available without installation. Turso does not offer equivalent HTTP-from-SQL, file system access, native embedding generation, document chunking, or Parquet import capabilities. **No-signup developer experience.** Developers can install the CLI and create a database without creating an account. Anonymous accounts support up to 5 databases and can be upgraded later with `db9 claim`. This reduces friction for prototyping, tutorials, and agent-driven workflows. **Instant provisioning with guarantees.** DB9 creates databases synchronously in under a second with a deterministic SLA. The SDK’s `instantDatabase()` creates and returns a ready-to-use connection in a single call. This makes database-per-user, database-per-agent, and database-per-CI-run patterns practical without orchestration. ## When should I use Turso instead of DB9? **Edge-first, latency-sensitive workloads.** Turso replicates databases to edge locations using multi-region groups. Reads are served from the nearest replica. If your application runs globally and read latency is critical, Turso’s edge replication reduces round-trips to the database. DB9 does not replicate to edge locations. **Embedded replicas for offline or local-first apps.** Turso syncs a full SQLite replica to the application process as a local file. Reads hit the local file with zero network latency. Writes sync back to the primary. This is powerful for mobile apps, desktop apps, and CLI tools that need to work offline or with intermittent connectivity. DB9 does not support embedded replicas. **SQLite simplicity.** If your application already uses SQLite, or you want the simplicity of SQLite’s type system and file-based model, Turso extends that foundation with server-side features. SQLite’s single-file mental model, zero-configuration local development, and broad language support make it a natural fit for lightweight applications. **Broad SDK coverage.** Turso provides official SDKs in 10 languages including TypeScript, Python, Go, Rust, Java, PHP, Ruby, Swift, Kotlin, and .NET. DB9 provides TypeScript and Browser SDKs. For non-TypeScript backends, DB9 works through standard PostgreSQL drivers, but Turso offers purpose-built libSQL clients with features like embedded replicas built in. **High database count on free tier.** Turso’s free tier includes 100 databases and 5 GB of storage. DB9’s free tier provides 5 databases without signup. If you need many lightweight databases for free, Turso offers a higher ceiling. Choose Turso if you need these capabilities If your application depends on edge replication, embedded local replicas, offline-first sync, or native SDKs beyond TypeScript, Turso is the better choice for those requirements today. ## Architecture Differences [Section titled “Architecture Differences”](#architecture-differences) ### Storage [Section titled “Storage”](#storage) **DB9** uses TiKV, a distributed key-value store, as its storage engine. Each database gets an isolated TiKV keyspace. This architecture enables instant provisioning (creating a keyspace is fast) and supports PostgreSQL-compatible transaction semantics at READ COMMITTED and REPEATABLE READ. **Turso** stores each database as a SQLite file using SQLite’s B-tree storage format. The primary database lives in one region, and replicas are full copies of the SQLite file synced to edge locations. libSQL extends SQLite with server-side write-ahead log (WAL) replication for multi-region consistency. ### Compute [Section titled “Compute”](#compute) **DB9** runs a custom SQL engine that parses, optimizes, and executes PostgreSQL-compatible queries against TiKV. Each database has dedicated compute that is always running. **Turso** runs libSQL (a fork of SQLite) with extensions for server-side operation. Compute is co-located with the SQLite file at each replica location. The libSQL engine handles both local reads and forwarded writes to the primary. ### Replication [Section titled “Replication”](#replication) **DB9** does not replicate databases across regions. All reads and writes go to the same location. **Turso** replicates databases across configurable region groups. Reads are served locally from the nearest replica. Writes are forwarded to the primary region and then propagated to replicas. Embedded replicas extend this model to the application process itself, syncing a local SQLite file. ## SQL Dialect Differences [Section titled “SQL Dialect Differences”](#sql-dialect-differences) This is the most significant technical difference between the two platforms. DB9 and Turso use different SQL dialects, which affects schema design, queries, and tooling. ### Type System [Section titled “Type System”](#type-system) **DB9 (PostgreSQL):** Rich type system with `integer`, `bigint`, `numeric`, `text`, `varchar(n)`, `boolean`, `timestamp`, `timestamptz`, `jsonb`, `uuid`, `bytea`, arrays, composite types, and more. Strict type checking at insert time. **Turso (SQLite):** Dynamic type system with type affinities — `INTEGER`, `REAL`, `TEXT`, `BLOB`, `NUMERIC`. Any column can store any type. Type enforcement is application-side or via `STRICT` tables (libSQL extension). ### Schema Changes [Section titled “Schema Changes”](#schema-changes) **DB9 (PostgreSQL):** Full `ALTER TABLE` support — add/drop/rename columns, change types, add/drop constraints, rename tables, all within transactions. **Turso (SQLite):** Limited `ALTER TABLE` — add columns and rename columns/tables. Dropping columns requires SQLite 3.35.0+. Changing column types or adding constraints to existing columns requires creating a new table, copying data, and renaming. ### Queries [Section titled “Queries”](#queries) **DB9 (PostgreSQL):** Supports CTEs (`WITH`), window functions (`ROW_NUMBER()`, `RANK()`, `LAG()`), `LATERAL` joins, `jsonb` operators (`->`, `->>`, `@>`, `?`), `RETURNING` clauses, upsert with `ON CONFLICT`, and `GENERATE_SERIES()`. **Turso (SQLite):** Supports CTEs and window functions. Does not support `LATERAL` joins. JSON functions use `json_extract()` instead of operator syntax. Supports upsert with `ON CONFLICT`. `RETURNING` is available in newer SQLite/libSQL versions. ### ORM Compatibility [Section titled “ORM Compatibility”](#orm-compatibility) **DB9** works with PostgreSQL-native ORMs: Prisma (postgresql provider), Drizzle (pg driver), SQLAlchemy (postgresql dialect), TypeORM, Sequelize, Knex, and GORM. **Turso** works with SQLite-compatible ORMs: Prisma (sqlite provider + libSQL adapter), Drizzle (libSQL driver), and others that support SQLite. Switching from PostgreSQL ORMs to SQLite ORMs requires schema and query changes. ## ORM and Driver Compatibility [Section titled “ORM and Driver Compatibility”](#orm-and-driver-compatibility) Because DB9 and Turso use different SQL dialects, they work with different ORM configurations: | ORM / Driver | DB9 (PostgreSQL) | Turso (SQLite/libSQL) | | ------------ | ------------------------------------ | -------------------------------------------- | | Prisma | `postgresql` provider — 89/89 (100%) | `sqlite` provider + `@prisma/adapter-libsql` | | Drizzle | `pg` driver — 75/75 (100%) | `libsql` driver | | SQLAlchemy | `postgresql` dialect — Tested | Not supported (SQLite dialect differs) | | TypeORM | PostgreSQL driver — 147/150 (98%) | SQLite driver (different feature set) | | Sequelize | PostgreSQL dialect — 87/87 (100%) | SQLite dialect | | Knex | PostgreSQL client — 97/97 (100%) | SQLite3 client | | GORM | PostgreSQL driver — Tested | SQLite driver | Switching between platforms requires changing ORM provider/dialect settings, not just connection strings. ## Migration [Section titled “Migration”](#migration) Migration between DB9 and Turso is not a simple dump-and-restore because they use different SQL dialects. Moving between PostgreSQL and SQLite requires schema translation. See the [Migrate from Turso](/docs/migrations/from-turso/) guide for PostgreSQL-specific migration steps. Key migration considerations: * **Type mapping:** PostgreSQL types like `jsonb`, `uuid`, `timestamptz`, and arrays have no direct SQLite equivalents. These must be mapped to `TEXT` or `BLOB` with application-level parsing. * **Schema changes:** PostgreSQL `ALTER TABLE` operations may need to be rewritten as create-copy-rename sequences for SQLite. * **Query rewriting:** PostgreSQL-specific syntax (`jsonb` operators, `LATERAL` joins, `GENERATE_SERIES()`) must be rewritten for SQLite. * **Driver changes:** PostgreSQL drivers (node-postgres, psycopg, pgx) must be replaced with libSQL clients, or vice versa. * **ORM reconfiguration:** ORM provider/dialect settings and type mappings need to change. Tools like `pgloader` or custom ETL scripts can help, but expect a non-trivial migration effort in either direction. ## Summary [Section titled “Summary”](#summary) **DB9 is the better choice when** you need PostgreSQL compatibility, AI agent integration, or application-layer SQL extensions (HTTP, files, native embeddings). It excels at multi-tenant patterns where each user or agent gets their own database, and when your stack already uses PostgreSQL ORMs and drivers. **Turso is the better choice when** you need edge replication, embedded local replicas, offline-first sync, or SQLite simplicity. It excels at globally distributed read-heavy workloads and applications that benefit from a local SQLite file synced to the server. The fundamental choice is between PostgreSQL and SQLite. If your team, tooling, and application are built around PostgreSQL, DB9 keeps you in that ecosystem with added agent and extension capabilities. If you prefer SQLite’s lightweight model and need edge replication, Turso extends SQLite into a distributed database. ## Next Pages [Section titled “Next Pages”](#next-pages) * [DB9 vs Neon](/docs/comparisons/db9-vs-neon/) — comparison with Neon (PostgreSQL vs PostgreSQL) * [DB9 vs Supabase](/docs/comparisons/db9-vs-supabase/) — comparison with Supabase (database vs platform) * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — detailed feature support * [Overview](/docs/overview/) — what DB9 is and who it is for * [Why DB9 for AI Agents](/docs/why-db9-for-ai-agents/) — the agent-native positioning # Connect to DB9 > How to connect to a DB9 database — connection strings, psql, ORMs, drivers, the TypeScript SDK, authentication options, and TLS. DB9 speaks the PostgreSQL wire protocol on port `5433`. Any client that connects to Postgres — `psql`, a Node.js driver, Prisma, SQLAlchemy, or a raw `libpq` binding — connects to DB9 with no adapter or proxy needed. ## Connection string format [Section titled “Connection string format”](#connection-string-format) Every DB9 connection string follows this pattern: Output ```text postgresql://.@:/postgres ``` | Component | Value | Notes | | ------------ | -------------------------------- | ---------------------------------------------------------------------------- | | **Scheme** | `postgresql://` or `postgres://` | Both work. | | **Username** | `.` | The tenant ID is assigned at database creation. The default role is `admin`. | | **Host** | `pg.db9.io` | The public endpoint for hosted DB9. | | **Port** | `5433` | Default PostgreSQL wire protocol port for DB9. | | **Database** | `postgres` | All DB9 databases use the `postgres` database name. | Example: Output ```text postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres ``` ## Get your connection string [Section titled “Get your connection string”](#get-your-connection-string) ### Using the CLI [Section titled “Using the CLI”](#using-the-cli) After creating a database, use `db9 db connect` to retrieve connection details: Terminal ```bash # Create a database db9 create --name myapp # Show connection info db9 db connect myapp ``` ▶ Run `db9 db connect` mints a short-lived token by default (recommended for production and automation). It prints a ready-to-use connection string with the token already embedded as the password, plus the matching `psql` command: Output ```text Temporary connection string (10min): postgresql://a1b2c3d4e5f6.admin:ey...@pg.db9.io:5433/postgres User: a1b2c3d4e5f6.admin Expires: 10min (at 2026-03-12T10:30:00Z) psql Command: psql "postgresql://a1b2c3d4e5f6.admin:ey...@pg.db9.io:5433/postgres" Long-lived DSN: db9 db users myapp create --username --password db9 db reset-password myapp ``` The token is embedded in the connection string rather than printed as a separate field. Use `--json` for machine-readable output (`connection_string`, `host`, `port`, `user`, `database`, `expires_at`, `expires_in`, `expires_in_seconds`). To reset a static password instead: Terminal ```bash db9 db reset-password myapp ``` ### Using the TypeScript SDK [Section titled “Using the TypeScript SDK”](#using-the-typescript-sdk) The SDK returns a connection string automatically when you create or connect to a database: TypeScript ```typescript import { instantDatabase } from 'get-db9'; const db = await instantDatabase({ name: 'myapp' }); console.log(db.connectionString); // postgresql://a1b2c3d4e5f6.admin:password@pg.db9.io:5433/postgres ``` ▶ Run Or call the connect-token REST endpoint directly to get a short-lived token — the SDK does not yet publish a native `connectToken()` method: TypeScript ```typescript const res = await fetch(`https://api.db9.ai/customer/databases/${databaseId}/connect-token`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DB9_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ role: 'admin' }), }); const { host, port, database, user, token, expires_at } = await res.json(); ``` ## Connect with psql [Section titled “Connect with psql”](#connect-with-psql) Use the connection string directly or let `db9 db connect` generate the command for you: Terminal ```bash # Using a connect token (recommended) PGPASSWORD='' psql "postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres" # Using a static password PGPASSWORD='' psql "postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres" ``` You can also use a `.pgpass` file: Terminal ```bash echo "pg.db9.io:5433:postgres:a1b2c3d4e5f6.admin:" >> ~/.pgpass chmod 600 ~/.pgpass psql "postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres" ``` Or use the built-in DB9 SQL REPL, which handles authentication automatically: Terminal ```bash db9 db sql myapp -q "SELECT version()" ``` ▶ Run ## Connect with ORMs and drivers [Section titled “Connect with ORMs and drivers”](#connect-with-orms-and-drivers) DB9 works with any PostgreSQL-compatible driver or ORM. Set the connection string as your database URL. * Node.js (pg) TypeScript ```typescript import pg from 'pg'; const client = new pg.Client({ connectionString: 'postgresql://a1b2c3d4e5f6.admin:password@pg.db9.io:5433/postgres', }); await client.connect(); const res = await client.query('SELECT NOW()'); console.log(res.rows[0]); await client.end(); ``` * Prisma Output ```text # .env DATABASE_URL="postgresql://a1b2c3d4e5f6.admin:password@pg.db9.io:5433/postgres" ``` prisma ```prisma // schema.prisma datasource db { provider = "postgresql" url = env("DATABASE_URL") } ``` * Drizzle TypeScript ```typescript import { drizzle } from 'drizzle-orm/node-postgres'; const db = drizzle('postgresql://a1b2c3d4e5f6.admin:password@pg.db9.io:5433/postgres'); ``` * Python (SQLAlchemy) Python ```python from sqlalchemy import create_engine # With psycopg2 (default driver) engine = create_engine( "postgresql+psycopg2://a1b2c3d4e5f6.admin:password@pg.db9.io:5433/postgres" ) ``` * Go (pgx) Go ```go conn, err := pgx.Connect(context.Background(), "postgresql://a1b2c3d4e5f6.admin:password@pg.db9.io:5433/postgres") ``` For detailed setup guides with migrations, schema patterns, and troubleshooting, see the integration guides for [Prisma](/docs/guides/prisma/), [Drizzle](/docs/guides/drizzle/), [SQLAlchemy](/docs/guides/python-sqlalchemy/), [TypeORM](/docs/guides/typeorm/), [Sequelize](/docs/guides/sequelize/), [Knex](/docs/guides/knex/), and [GORM](/docs/guides/gorm/). ## Authentication [Section titled “Authentication”](#authentication) DB9 supports three ways to authenticate connections: ### Static password [Section titled “Static password”](#static-password) Set at database creation or reset with `db9 db reset-password`. The password is embedded in the connection string or passed via `PGPASSWORD`. Simple, but the credential does not expire. ### Short-lived connect token [Section titled “Short-lived connect token”](#short-lived-connect-token) Generated with `db9 db connect`. The token is used as the password and expires automatically. Recommended for production and CI/CD. The TypeScript SDK does not yet publish a native `connectToken()` method — see [API Reference — Connect Tokens & Keys](/docs/api/#connect-tokens--keys) for the REST equivalent. Terminal ```bash # Generate a token for a specific role db9 db connect myapp --user admin ``` ### API token (for SDK and REST API) [Section titled “API token (for SDK and REST API)”](#api-token-for-sdk-and-rest-api) Named tokens created with `db9 token create` are used for the DB9 REST API and SDK, not for direct pgwire connections. Use them when your application manages databases programmatically rather than connecting with SQL. Terminal ```bash db9 token create --name ci-agent --expires-in-days 90 export DB9_API_KEY= ``` See the [TypeScript SDK](/docs/sdk/) docs for the full auth lifecycle, including anonymous trial accounts and Auth0 SSO. ### Publishable key (for browser access) [Section titled “Publishable key (for browser access)”](#publishable-key-for-browser-access) For client-side applications, use a publishable key (`db9pk_...`) with the [Browser SDK](/docs/sdk-browser/). Publishable keys are scoped to specific schemas and tables and enforce [Row-Level Security](/docs/sql/rls/) policies. TypeScript ```typescript import { createDb9BrowserClient } from '@db9/browser'; const db9 = createDb9BrowserClient({ apiUrl: 'https://api.db9.ai', databaseId: 'your-database-id', anonKey: 'db9pk_your_publishable_key', }); const { data } = await db9.from('todos').select('*'); ``` Publishable keys are created via the REST API. See [Security & Auth — Publishable Keys](/docs/platform/security-and-auth/#publishable-keys) for setup. ## TLS [Section titled “TLS”](#tls) DB9’s hosted service supports TLS connections. The server uses SCRAM-SHA-256 authentication by default. Most PostgreSQL clients default to `sslmode=prefer`, which will use TLS when available. For stricter security: Terminal ```bash psql "postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=require" ``` Or in a connection string for an ORM: Output ```text DATABASE_URL="postgresql://a1b2c3d4e5f6.admin:password@pg.db9.io:5433/postgres?sslmode=require" ``` ## Multi-tenant username format [Section titled “Multi-tenant username format”](#multi-tenant-username-format) DB9 uses the username field to route connections to the correct tenant. The format is: Output ```text . ``` * The **tenant ID** is a 12-character alphanumeric string assigned when the database is created. * The **role** defaults to `admin`. You can create additional roles with `db9 db users create`. This encoding means your username in connection strings will look different from a typical Postgres setup, but it works transparently with all standard PostgreSQL drivers. ## Connection checklist [Section titled “Connection checklist”](#connection-checklist) | Check | Details | | ----------------- | ----------------------------------------------------------------------------------- | | **Port** | `5433`, not the default PostgreSQL `5432` | | **Database name** | Always `postgres` | | **Username** | Must include the tenant ID prefix: `.` | | **Password** | Static password or connect token — both go in `PGPASSWORD` or the connection string | | **TLS** | Use `sslmode=require` or `sslmode=prefer` | | **Protocol** | Standard PostgreSQL wire protocol — no special drivers needed | ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) **“password authentication failed”** * Verify your tenant ID and role are correct in the username field. * If using a connect token, check that it has not expired. * Run `db9 db connect myapp` to generate a fresh token. **“connection refused” on port 5432** * DB9 uses port `5433`, not the PostgreSQL default `5432`. **“SSL SYSCALL error” or TLS handshake failures** * Ensure your client supports TLS. Try adding `?sslmode=require` to the connection string. * Some older clients may need updated CA certificates. **ORM connection timeouts** * Check that your ORM’s connection string uses the correct port (`5433`) and database name (`postgres`). * Verify your network allows outbound connections on port `5433`. ## Next steps [Section titled “Next steps”](#next-steps) * [Quick Start](/docs/quickstart/) — create your first database and run SQL * [CLI Reference](/docs/cli/) — full command reference including `db connect` and `db reset-password` * [TypeScript SDK](/docs/sdk/) — `instantDatabase()`, credential management, and programmatic access * [Browser SDK](/docs/sdk-browser/) — client-side data access with publishable keys and RLS * [Architecture](/docs/architecture/) — how DB9 routes connections through the pgwire protocol to TiKV * [Extensions](/docs/extensions/) — enable vector search, fs9, HTTP from SQL, and pg\_cron after connecting # Error Code Reference > Complete reference for DB9 REST API HTTP status codes — error shapes, common causes, and resolution steps for every status code. All DB9 REST API errors return a consistent JSON shape with a human-readable message: Error Response Shape ```json { "message": "human-readable message describing what went wrong" } ``` Use the HTTP status code for programmatic error handling. Use `message` for displaying messages to users or for debugging. ## Status Codes [Section titled “Status Codes”](#status-codes) ### 200 OK [Section titled “200 OK”](#200-ok) The request succeeded. The response body contains the requested resource or operation result. Example ```json { "id": "db_01h9xxxxxxxxxxxxxxxx", "name": "myapp", "state": "ready" } ``` ### 400 Bad Request [Section titled “400 Bad Request”](#400-bad-request) The request body is missing required fields, contains invalid values, or is malformed JSON. Example ```json { "message": "field 'name' is required" } ``` | Common cause | Resolution | | ---------------------------------------------------------- | ---------------------------------------------------------------- | | Missing required field | Check the endpoint’s field table and include all required fields | | Invalid field type (e.g. string where array expected) | Verify your request body matches the documented types | | Providing both `query` and `file_content` in SQL execution | Provide only one | | Invalid `scope_json` format | Ensure the value is a valid JSON string, not an object | ### 401 Unauthorized [Section titled “401 Unauthorized”](#401-unauthorized) The request has no `Authorization` header, the token is expired, or the token is invalid. Example ```json { "message": "invalid or expired token" } ``` | Common cause | Resolution | | ---------------------------------------------------------- | --------------------------------------------------------------------- | | Missing `Authorization: Bearer ` header | Add the header to your request | | Token has expired | Create a new token with `db9 token create` or `POST /customer/tokens` | | Wrong token (e.g. publishable key used for management API) | Use your API token (`DB9_API_KEY`), not a publishable or service key | | Anonymous secret mismatch | Verify `anonymous_secret` matches the value returned at registration | ### 403 Forbidden [Section titled “403 Forbidden”](#403-forbidden) The token is valid but lacks the scope required to perform the operation. Example ```json { "message": "token does not have access to this database" } ``` | Common cause | Resolution | | ----------------------------------------------------- | --------------------------------------------------------------------------------- | | Token scoped to specific databases, accessing another | Check `scope_json` on the token — create a new token with broader scope if needed | | Read-only token attempting a write operation | Create a token without the `ro` access restriction | ### 404 Not Found [Section titled “404 Not Found”](#404-not-found) The requested resource does not exist, or the authenticated customer does not own it. Example ```json { "message": "database not found" } ``` | Common cause | Resolution | | --------------------------------------- | ------------------------------------------------------- | | Database ID typo or copy-paste error | Double-check the `database_id` in your request path | | Database was deleted | Check your database list with `GET /customer/databases` | | Resource belongs to a different account | Ensure you are using the correct API token | | Key or run ID from a different database | Verify all IDs are from the same database | ### 409 Conflict [Section titled “409 Conflict”](#409-conflict) The operation conflicts with existing state — typically a duplicate name or a state precondition. Example ```json { "message": "database with this name already exists" } ``` | Common cause | Resolution | | ---------------------------------------------------------- | ----------------------------------------------------------------------- | | Creating a database/branch with a name that already exists | Choose a different name or delete the existing resource first | | Applying a migration that was already applied | Check applied migrations with `GET /customer/databases/{id}/migrations` | | Creating a secret that already exists | Use `PUT` to update an existing secret, not `POST` | | Adopting a database already owned by your account | The database is already in your account — no action needed | ### 422 Unprocessable Entity [Section titled “422 Unprocessable Entity”](#422-unprocessable-entity) The request is syntactically valid but semantically incorrect — the server understands the request but cannot process it. Example ```json { "message": "SQL syntax error at position 14: unexpected token 'FORM'" } ``` | Common cause | Resolution | | -------------------------------------------------------- | ------------------------------------------------------------------------ | | SQL syntax error in `POST /sql` or `POST /migrations` | Fix the SQL — the `error` field contains PostgreSQL’s error message | | Invalid cron expression in function deploy | Validate the cron string (e.g. use [crontab.guru](https://crontab.guru)) | | Quota exceeded (databases, branches, keys) | Check your plan limits or delete unused resources | | Invalid JWKS URL in auth config | Ensure the URL returns a valid JWKS JSON document | | `snapshot_at` timestamp outside available history window | Use a more recent timestamp for point-in-time branching | ### 429 Too Many Requests [Section titled “429 Too Many Requests”](#429-too-many-requests) The request was rate limited. Slow down and retry after the indicated delay. Example ```json { "message": "rate limit exceeded, retry after 30 seconds" } ``` | Common cause | Resolution | | ---------------------------------------------------- | -------------------------------------------------------- | | Too many anonymous account registrations from one IP | Wait before registering more anonymous accounts | | Function invocation rate limit | Add a delay between invocations or reduce call frequency | | Burst of management API requests | Implement exponential backoff with jitter | The `Retry-After` response header (when present) indicates the number of seconds to wait before retrying. ### 500 Internal Server Error [Section titled “500 Internal Server Error”](#500-internal-server-error) An unexpected error occurred on the DB9 servers. Example ```json { "message": "internal server error" } ``` | Common cause | Resolution | | -------------------------------------- | ------------------------------------------------------------------------------------- | | Transient server error | Retry the request with exponential backoff | | Persistent 500 on a specific operation | Contact support via [Discord](https://discord.gg/5P3RuyZCgs) with the request details | ## Handling Errors in Code [Section titled “Handling Errors in Code”](#handling-errors-in-code) ### TypeScript [Section titled “TypeScript”](#typescript) TypeScript ```typescript import { createDb9Client } from 'get-db9'; const client = createDb9Client(); try { const db = await client.databases.create({ name: 'myapp' }); } catch (err) { if (err.status === 409) { // Database already exists — retrieve it instead const list = await client.databases.list(); const existing = list.databases.find(d => d.name === 'myapp'); } else if (err.status === 429) { // Rate limited — implement retry logic console.error('Rate limited:', err.message); } else { throw err; } } ``` ### curl [Section titled “curl”](#curl) Terminal ```bash response=$(curl -s -w "\n%{http_code}" \ -H "Authorization: Bearer $DB9_API_KEY" \ -X POST https://api.db9.ai/customer/databases \ -H "Content-Type: application/json" \ -d '{"name": "myapp"}') body=$(echo "$response" | head -1) status=$(echo "$response" | tail -1) if [ "$status" = "409" ]; then echo "Database already exists" elif [ "$status" = "201" ] || [ "$status" = "200" ]; then echo "Created: $body" else echo "Error $status: $body" fi ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * [REST API Reference](/docs/api/) — all endpoints with request/response examples * [TypeScript SDK](/docs/sdk/) — typed client with built-in error handling * [CLI Reference](/docs/cli/) — command-line interface # Extensions > DB9 extensions grouped by task — files, search, integration, automation, and data import. DB9 ships all extensions compiled into the server binary. There is no runtime dynamic loading — every extension is available immediately, and `CREATE EXTENSION` records install metadata per database. ## Extensions by Task [Section titled “Extensions by Task”](#extensions-by-task) ### Files and Data [Section titled “Files and Data”](#files-and-data) | Extension | What it does | Docs | | ----------- | ------------------------------------------------------------------- | ------------------------------------------- | | **fs9** | Query, read, and write the database filesystem from SQL | [fs9 reference](/docs/extensions/fs9/) | | **parquet** | Read Parquet files from the filesystem or HTTP URLs into SQL tables | [Parquet import](/docs/extensions/parquet/) | ### Search [Section titled “Search”](#search) | Extension | What it does | Docs | | -------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------- | | **vector** | pgvector-compatible similarity search (HNSW index building disabled this release) | [Vector Search](/docs/extensions/vector/) | | **embedding** | Server-side text embedding via `EMBEDDING()` and `EMBED_TEXT()` — no external API needed | [Vector Search](/docs/extensions/vector/) | | **Full-Text Search** | PostgreSQL-compatible full-text search with language tokenizers | [Full-Text Search](/docs/extensions/fts/) | | **zhparser** | Chinese full-text search tokenizer (jieba-rs, always available) | [Full-Text Search](/docs/extensions/fts/) | ### Integration and Automation [Section titled “Integration and Automation”](#integration-and-automation) | Extension | What it does | Docs | | ------------ | ----------------------------------------------------------------------- | ------------------------------------- | | **http** | Make HTTP requests from SQL (GET, POST, PUT, DELETE as table functions) | [HTTP client](/docs/extensions/http/) | | **pg\_cron** | Schedule SQL statements to run on a cron expression | [pg\_cron](/docs/extensions/pg-cron/) | ### Compatibility [Section titled “Compatibility”](#compatibility) | Extension | What it does | Docs | | ------------- | ------------------------------------------------------ | ----------------------------------- | | **uuid-ossp** | UUID generation functions (`uuid_generate_v4()`, etc.) | [uuid-ossp](/docs/extensions/uuid/) | | **hstore** | PostgreSQL-compatible key-value store type | [hstore](/docs/extensions/hstore/) | ## Enabling Extensions [Section titled “Enabling Extensions”](#enabling-extensions) Most extensions require `CREATE EXTENSION` before use. A few are pre-enabled or always available. ### Pre-enabled (available on every new database) [Section titled “Pre-enabled (available on every new database)”](#pre-enabled-available-on-every-new-database) These extensions are installed automatically when a database is created: SQL ```sql -- Already enabled — no CREATE EXTENSION needed SELECT http_get('https://httpbin.org/get'); SELECT count(*) FROM cron.job; ``` Note `http_get` and the other HTTP functions return `JSONB` — call them as scalar functions (`SELECT http_get(...)`), not as table-valued functions. See [HTTP client](/docs/extensions/http/) for the full reference. pg\_cron job execution is unavailable in the current release The `cron` schema functions are installed and callable immediately, but `cron.schedule(...)` currently returns an error — job scheduling/execution requires the worker subsystem, which is disabled for this release. See [pg\_cron](/docs/extensions/pg-cron/) for details. * **http** — HTTP client functions available immediately * **pg\_cron** — Extension pre-installed; catalog and management functions (`cron.job`, `cron.unschedule`, etc.) are available immediately, but job scheduling/execution is unavailable in the current release ### Require CREATE EXTENSION [Section titled “Require CREATE EXTENSION”](#require-create-extension) These extensions must be explicitly enabled before their functions are available: SQL ```sql CREATE EXTENSION IF NOT EXISTS fs9; CREATE EXTENSION IF NOT EXISTS embedding; CREATE EXTENSION IF NOT EXISTS parquet; ``` ▶ Run ### Metadata shims (built-in, CREATE EXTENSION optional) [Section titled “Metadata shims (built-in, CREATE EXTENSION optional)”](#metadata-shims-built-in-create-extension-optional) These capabilities work without `CREATE EXTENSION`, but running it records metadata in `pg_extension` for ORM and tool compatibility: Run CREATE EXTENSION for ORM compatibility Even though vector, uuid-ossp, and hstore work without `CREATE EXTENSION`, running it registers the extension in `pg_extension`. ORMs like Prisma and Drizzle check this table to detect capabilities — running `CREATE EXTENSION` prevents false-negative compatibility errors. The same applies to `pgcrypto` and `plpgsql`, which exist only as `pg_extension` entries. * **vector** — The `vector` type and operators (`<->`, `<#>`, `<=>`) work as built-ins. HNSW index building is disabled in the current release, so similarity search runs as an exact scan — see [Vector Search](/docs/extensions/vector/#hnsw-indexes). `CREATE EXTENSION vector` is recommended so ORMs that check `pg_extension` see it as installed. * **uuid-ossp** — UUID functions (`uuid_generate_v4()`, etc.) are built-in. `CREATE EXTENSION "uuid-ossp"` records metadata for `pg_dump`/`pg_restore` compatibility. * **hstore** — Extension metadata is recorded for client compatibility. DB9 does not currently implement full hstore semantics. * **pgcrypto** — Metadata only, so Supabase and ORM bootstrap scripts that run `CREATE EXTENSION pgcrypto` succeed unchanged. It registers no functions of its own: `gen_random_uuid()` and `digest()` are DB9 built-ins that work with or without it. The rest of pgcrypto is **not** implemented — see the caution below. * **plpgsql** — PL/pgSQL is compiled in. `CREATE EXTENSION plpgsql` records metadata so restored dumps (which almost always contain it) do not fail. `CREATE EXTENSION pgcrypto` succeeds, but the crypto functions do not exist Running `CREATE EXTENSION pgcrypto` returns `CREATE EXTENSION` and adds a row to `pg_extension`, so a schema restore will not fail on it. It does **not** make pgcrypto’s functions available. `crypt()`, `gen_salt()`, `hmac()`, `encrypt()`/`decrypt()`, `pgp_sym_encrypt()` and `gen_random_bytes()` all raise `42883 function ... does not exist` when called. Only `gen_random_uuid()` and `digest()` work, and both are DB9 built-ins available on a database with no extensions installed. Hash a value with `digest(data, 'sha256')`, `sha256()` or `md5()`; move password hashing and symmetric encryption into your application layer. ### Always available (no-op) [Section titled “Always available (no-op)”](#always-available-no-op) * **zhparser** — The Chinese full-text tokenizer is compiled in and always active. `CREATE EXTENSION zhparser` is accepted but has no effect. ## Extension Schemas [Section titled “Extension Schemas”](#extension-schemas) Extensions install their functions into specific schemas: | Extension | Default schema | Access pattern | | --------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | http | `extensions` | `http_get(...)` for the scalar JSONB form; `SELECT * FROM extensions.http_get(...)` for the table form. The qualifier is **not** interchangeable — see [http](/docs/extensions/http/) | | fs9 | `extensions` | `fs9_read(...)` or `extensions.fs9_read(...)` — both resolve on the default `search_path` | | embedding | `extensions` | `embedding(...)` — the bare name; `extensions.embedding(...)` does not resolve | | parquet | `extensions` | `SELECT * FROM extensions.read_parquet(...)` — table-valued, `FROM` position only | | pg\_cron | `cron` | `cron.schedule(...)`, `cron.unschedule(...)` | | vector | `public` | Operators and type available directly | | uuid-ossp | `public` | `uuid_generate_v4()` available directly | | hstore | `public` | Type available directly | | pgcrypto | `extensions` | Metadata only — registers no functions (`gen_random_uuid()`, `digest()` are built-ins) | | plpgsql | `public` | Metadata only — PL/pgSQL is compiled in | ## Choosing the Right Extension [Section titled “Choosing the Right Extension”](#choosing-the-right-extension) | I want to… | Use | | ------------------------------------------ | -------------------------------------------------------------------------------------------------- | | Store and search vector embeddings | [vector](/docs/extensions/vector/) + [embedding](/docs/extensions/vector/) | | Build a RAG pipeline without external APIs | [embedding](/docs/extensions/vector/) + [fs9](/docs/extensions/fs9/) | | Call an external API from SQL | [http](/docs/extensions/http/) | | Query files on the database filesystem | [fs9](/docs/extensions/fs9/) | | Watch filesystem changes in real time | [fs9 events](/docs/extensions/fs9/#event-notifications) + [CLI watch](/docs/cli/#filesystem-watch) | | Import Parquet data | [parquet](/docs/extensions/parquet/) | | Schedule a recurring cleanup or report | [pg\_cron](/docs/extensions/pg-cron/) | | Search text content | [Full-Text Search](/docs/extensions/fts/) | | Generate UUIDs | [uuid-ossp](/docs/extensions/uuid/) | ## Next Steps [Section titled “Next Steps”](#next-steps) * [Architecture](/docs/architecture/) — How extensions fit into the DB9 server * [SQL Reference](/docs/sql/) — SQL engine compatibility and features * [RAG with Built-in Embeddings](/docs/guides/rag-with-built-in-embeddings/) — End-to-end tutorial using embedding() and vector search * [Analyze Agent Logs with fs9](/docs/guides/analyze-agent-logs-with-fs9/) — Tutorial: write and query agent logs from SQL * [HTTP from SQL](/docs/guides/http-from-sql/) — Tutorial: call APIs, webhooks, and enrich data from SQL * [Scheduled Jobs with pg\_cron](/docs/guides/scheduled-jobs-with-pg-cron/) — Tutorial: periodic tasks, monitoring, and job management * [Agent Workflows](/docs/agent-workflows/overview/) — Extensions in agent pipelines * [CLI Reference](/docs/cli/) — db9 db sql for running extension commands # CHUNK_TEXT — Document Chunking > Split documents into overlapping chunks for RAG pipelines using the CHUNK_TEXT table-valued function — with smart markdown-aware splitting. `CHUNK_TEXT` is a built-in table-valued function that splits text into overlapping chunks suitable for embedding and retrieval-augmented generation (RAG) pipelines. It is markdown-aware — it prefers to break at paragraph boundaries, headings, and list items rather than mid-sentence. ## Syntax [Section titled “Syntax”](#syntax) SQL ```sql SELECT chunk_index, chunk_text, chunk_pos FROM CHUNK_TEXT( content, [max_chars], [overlap_chars], [title] ); ``` All parameters except `content` are optional and can be passed positionally or as named arguments. | Parameter | Type | Default | Description | | --------------- | ------ | ------- | -------------------------------------------------------------------------------------- | | `content` | `TEXT` | — | The document text to chunk (max 1 MB). | | `max_chars` | `INT` | `3600` | Target maximum chunk size in characters (\~900 tokens). | | `overlap_chars` | `INT` | `540` | Number of characters to overlap between adjacent chunks (default: 15% of `max_chars`). | | `title` | `TEXT` | `NULL` | Document title prepended to each chunk. | ## Output Columns [Section titled “Output Columns”](#output-columns) | Column | Type | Description | | ------------- | ------ | -------------------------------------------------------- | | `chunk_index` | `INT` | Zero-based chunk number. | | `chunk_text` | `TEXT` | The chunk content (with title prefix if provided). | | `chunk_pos` | `INT` | Character offset of this chunk in the original document. | ## Basic Usage [Section titled “Basic Usage”](#basic-usage) SQL ```sql SELECT chunk_index, length(chunk_text) AS len, chunk_pos FROM CHUNK_TEXT( content => 'PostgreSQL is a powerful database system... (long document text)', max_chars => 200, overlap_chars => 30 ); ``` ▶ Run Output ```text chunk_index | len | chunk_pos -------------+-----+----------- 0 | 127 | 0 1 | 181 | 97 2 | 153 | 248 3 | 151 | 371 ``` ## With Title [Section titled “With Title”](#with-title) When `title` is provided, it is prepended to each chunk so that embedding models have document context: SQL ```sql SELECT chunk_index, chunk_text FROM CHUNK_TEXT( 'Hello world. This is the document body.', title => 'My Document' ); ``` ▶ Run Output ```text chunk_index | chunk_text -------------+--------------------------------------------------- 0 | title: My Document | text: Hello world. This is... ``` ## Smart Splitting [Section titled “Smart Splitting”](#smart-splitting) `CHUNK_TEXT` identifies natural break points in the text and assigns them a score based on proximity to the target chunk boundary: * **Paragraph breaks** (double newlines) — strongest break point * **Markdown headings** (`## ...`) — strong break point * **List items** (`- ...`, `1. ...`) — moderate break point * **Sentence endings** — fallback break point Code fences (triple backticks) are treated as no-break zones — the function will not split inside a fenced code block. ## RAG Pipeline Example [Section titled “RAG Pipeline Example”](#rag-pipeline-example) Combine `CHUNK_TEXT` with `embedding()` and HNSW indexes for a complete RAG pipeline: Step 4 is a no-op in the current release HNSW index building is disabled server-side — over the wire protocol the `CREATE INDEX` in step 4 is rejected with `55000` (`feature "hnsw_index" is unavailable`), and over the HTTP SQL API it reports `CREATE INDEX` and appears in `pg_indexes` but is never used by the planner. The pipeline still works end to end: semantic search falls back to exact scan and returns correct results. See [HNSW Indexes](/docs/extensions/vector/#hnsw-indexes). SQL ```sql -- 1. Create the documents and chunks tables CREATE TABLE rag_documents ( id SERIAL PRIMARY KEY, title TEXT, content TEXT ); CREATE TABLE rag_chunks ( id SERIAL PRIMARY KEY, doc_id INT REFERENCES rag_documents(id), chunk_index INT, chunk_text TEXT, -- Must match the embedding model's output size: the default -- text-embedding-v4 model returns 1024 dimensions. embedding VECTOR(1024) ); -- 2. Add a document to chunk INSERT INTO rag_documents (title, content) VALUES ('Branching', repeat('DB9 branching copies the full dataset asynchronously. ', 80)); -- 3. Chunk and embed that document INSERT INTO rag_chunks (doc_id, chunk_index, chunk_text, embedding) SELECT 1, c.chunk_index, c.chunk_text, embedding(c.chunk_text) FROM CHUNK_TEXT( content => (SELECT content FROM rag_documents WHERE id = 1), max_chars => 1500, overlap_chars => 200, title => (SELECT title FROM rag_documents WHERE id = 1) ) AS c; -- 4. Create an HNSW index for fast search CREATE INDEX ON rag_chunks USING hnsw (embedding vector_cosine_ops); -- 5. Semantic search SELECT chunk_text, embedding <=> embedding('How does branching work?') AS distance FROM rag_chunks ORDER BY distance LIMIT 5; ``` ▶ Run ## Use in CTEs [Section titled “Use in CTEs”](#use-in-ctes) `CHUNK_TEXT` works in `WITH` clauses for multi-step processing: SQL ```sql WITH raw_chunks AS ( SELECT chunk_index, chunk_text, chunk_pos FROM CHUNK_TEXT( content => 'Long document...', max_chars => 2000 ) ) SELECT chunk_index, length(chunk_text) AS size FROM raw_chunks WHERE length(chunk_text) > 100; ``` ▶ Run ## Edge Cases [Section titled “Edge Cases”](#edge-cases) | Input | Result | | -------------------------------- | ----------------------------------- | | `NULL` content | Zero rows returned. | | Empty string `''` | Zero rows returned. | | Content shorter than `max_chars` | Single chunk with the full content. | | Content exceeding 1 MB | Error. | ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ----------------------- | ------------------------------ | | Max content size | 1 MB | | Default `max_chars` | 3,600 chars (\~900 tokens) | | Default `overlap_chars` | 540 chars (15% of `max_chars`) | | Min `max_chars` | 1 | | Min `overlap_chars` | 0 | See [Limits and Quotas](/docs/platform/limits-and-quotas/) for the complete list. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Vector Search](/docs/extensions/vector/) — Embedding and HNSW indexes * [Built-in Functions](/docs/sql/functions/) — All SQL functions * [fs9 — File System](/docs/extensions/fs9/) — Ingest files to chunk from the filesystem # fs9 — File System > Read, write, query, and watch files from SQL using the fs9 extension — with directory listing, glob matching, format auto-detection, event notifications, and a WebSocket API for programmatic access. fs9 is a TiKV-backed file system accessible from SQL. It provides scalar functions for reading and writing files, a table function for querying file contents as rows, and a WebSocket API for programmatic access from the TypeScript SDK. Files are stored in TiKV using page-based storage (16 KB pages) with inode metadata — not on a local disk. Each database has its own isolated file system. ## Installation [Section titled “Installation”](#installation) SQL ```sql CREATE EXTENSION IF NOT EXISTS fs9; ``` ▶ Run Superuser required All fs9 functions require superuser privileges. Only the default `admin` role can use fs9 from SQL. Regular database users created via `db9 db users create` will get a permission denied error. ## FUSE Mount on macOS [Section titled “FUSE Mount on macOS”](#fuse-mount-on-macos) If you use `db9 fs mount` on macOS, install macFUSE first: Terminal ```bash brew install --cask macfuse ``` Then approve the macFUSE system extension in **System Settings > Privacy & Security** if prompted. After approval, retry the mount command. Notes: * This prerequisite applies to `db9 fs mount` only. * Other `db9 fs` commands such as `fs cp`, `fs ls`, and `fs sh` do not require macFUSE. * See the [CLI Reference](/docs/cli/#macos-prerequisite-for-db9-fs-mount) for the command-level mount docs. ## Scalar Functions [Section titled “Scalar Functions”](#scalar-functions) ### fs9\_read [Section titled “fs9\_read”](#fs9_read) Read an entire file as text. SQL ```sql SELECT extensions.fs9_read('/logs/app.log'); ``` Returns `TEXT` (UTF-8, lossy conversion for non-UTF-8 bytes). Returns `NULL` if the path argument is `NULL`. ### fs9\_read\_bytea [Section titled “fs9\_read\_bytea”](#fs9_read_bytea) Read an entire file as binary. SQL ```sql SELECT extensions.fs9_read_bytea('/data/image.png'); ``` Returns `BYTEA`. Use this instead of `fs9_read` when the file contains non-UTF-8 binary data. ### fs9\_write [Section titled “fs9\_write”](#fs9_write) Write content to a file, creating it if it does not exist and overwriting if it does. Returns the number of bytes written. SQL ```sql SELECT extensions.fs9_write('/data/output.txt', 'hello world'); -- Returns: 11 ``` Accepts both `TEXT` and `BYTEA` content. Parent directory must already exist `fs9_write` (and `fs9_append`) do not create parent directories automatically. Writing to a path whose parent directory does not exist yet fails with `58P01` — `fs9 resource was not found`, which names no path. Call `extensions.fs9_mkdir('/data', true)` first if the directory may not exist. ### fs9\_append [Section titled “fs9\_append”](#fs9_append) Append content to the end of a file. Creates the file if it does not exist. Returns the number of bytes appended. SQL ```sql SELECT extensions.fs9_append('/logs/events.jsonl', '{"event":"click","ts":"2026-03-12T10:00:00Z"}' || E'\n'); ``` Append does not insert a line separator `fs9_append` writes the bytes exactly as given. If the existing file does not already end with a newline, the appended record is joined onto the last line. For JSONL that produces one malformed line, which is [silently skipped](#file-reading) when the file is read as JSON — so **both** the previous record and the new one disappear from query results, with no error. The bytes are still on disk and remain visible with `format => 'text'`. Always terminate appended records with `E'\n'` (as above), and make sure the file’s existing final line ends with one. ### fs9\_read\_at [Section titled “fs9\_read\_at”](#fs9_read_at) Read a range of bytes from a file. Returns `TEXT`. SQL ```sql SELECT extensions.fs9_read_at('/data/large.bin', 0, 1024); -- Reads first 1024 bytes ``` Both offset and length must be non-negative. Reading beyond the end of the file returns whatever bytes are available. ### fs9\_read\_at\_bytea [Section titled “fs9\_read\_at\_bytea”](#fs9_read_at_bytea) Read a range of bytes from a file. Returns `BYTEA`. SQL ```sql SELECT extensions.fs9_read_at_bytea('/data/large.bin', 0, 1024); -- Reads first 1024 bytes as binary ``` Same semantics as `fs9_read_at`, but returns raw bytes instead of text. ### fs9\_write\_at [Section titled “fs9\_write\_at”](#fs9_write_at) Write data at a specific offset in a file. Creates the file if it does not exist. Fills any gap between the current end and the offset with null bytes. Returns the number of bytes written. SQL ```sql SELECT extensions.fs9_write_at('/data/file.bin', 100, 'data at offset 100'); ``` ### fs9\_truncate [Section titled “fs9\_truncate”](#fs9_truncate) Truncate a file to a specific size. If the new size is larger than the current size, the file is padded with null bytes. Returns `TRUE` on success. SQL ```sql SELECT extensions.fs9_truncate('/logs/rolling.log', 0); -- Empties the file ``` ### fs9\_exists [Section titled “fs9\_exists”](#fs9_exists) Check whether a file or directory exists. SQL ```sql SELECT extensions.fs9_exists('/data/config.json'); -- Returns: true or false ``` ### fs9\_size [Section titled “fs9\_size”](#fs9_size) Return the size of a file in bytes. SQL ```sql SELECT extensions.fs9_size('/data/export.csv'); -- Returns: 48271 ``` ### fs9\_mtime [Section titled “fs9\_mtime”](#fs9_mtime) Return the last modification time of a file as an RFC 3339 timestamp (UTC). SQL ```sql SELECT extensions.fs9_mtime('/data/export.csv'); -- Returns: '2026-03-12T15:30:45Z' ``` ### fs9\_remove [Section titled “fs9\_remove”](#fs9_remove) Remove a file or directory. Returns the number of items removed. Supports glob patterns. SQL ```sql -- Remove a single file SELECT extensions.fs9_remove('/tmp/scratch.txt'); -- Remove a directory and its contents SELECT extensions.fs9_remove('/tmp/work/', true); -- Remove files matching a glob SELECT extensions.fs9_remove('/logs/2026-01-*.jsonl'); ``` The second argument (`recursive`) defaults to `false`. A non-empty directory requires `recursive = true`. ### fs9\_mkdir [Section titled “fs9\_mkdir”](#fs9_mkdir) Create a directory. Returns `TRUE` on success. SQL ```sql -- Create a single directory SELECT extensions.fs9_mkdir('/data/exports'); -- Create nested directories SELECT extensions.fs9_mkdir('/data/exports/2026/03', true); ``` The second argument (`recursive`) defaults to `false`. With `recursive = true`, parent directories are created as needed. ### fs9\_storage\_stats [Section titled “fs9\_storage\_stats”](#fs9_storage_stats) Table-valued function that returns aggregated filesystem storage statistics. SQL ```sql SELECT total_files, total_directories, total_logical_bytes FROM extensions.fs9_storage_stats(); ``` | Column | Type | Description | | --------------------- | ----- | ---------------------------------------- | | `total_files` | INT64 | Total number of files | | `total_directories` | INT64 | Total number of directories | | `total_logical_bytes` | INT64 | Total logical size of all files in bytes | Always returns a single row. Useful for monitoring filesystem usage from SQL or building storage dashboards. Storage stats do not currently track writes accurately `fs9_storage_stats()` does not reliably reflect the current filesystem contents — it can return `0` for all columns on a database with existing files, or a stale count that does not update after new writes. Do not rely on it for accurate monitoring yet; enumerate files directly with `extensions.fs9('/path/')` per directory if you need an accurate count. ## Table Function [Section titled “Table Function”](#table-function) The `extensions.fs9()` table function reads files and directories as SQL rows. It operates in three modes depending on the path. Use named parameters, not positional arguments Optional arguments — `recursive`, `exclude`, `format`, `delimiter`, `header` — must be passed by name (`format => 'csv'`). A positional second argument is rejected: ```plaintext ERROR: fs9: unexpected positional argument (use named parameters like format => 'csv') ``` ### Directory listing [Section titled “Directory listing”](#directory-listing) When the path ends with `/`, fs9 returns directory entries: SQL ```sql SELECT path, type, size, mtime FROM extensions.fs9('/logs/') ORDER BY path; ``` ▶ Run | Column | Type | Description | | ------- | ----- | --------------------------------- | | `path` | TEXT | Full path | | `type` | TEXT | `"file"` or `"dir"` | | `size` | INT64 | Size in bytes (0 for directories) | | `mode` | INT64 | Unix permission mode | | `mtime` | TEXT | Last modified (RFC 3339 UTC) | Use `recursive => true` to walk subdirectories: SQL ```sql SELECT path, size FROM extensions.fs9('/data/', recursive => true) WHERE type = 'file' ORDER BY size DESC; ``` Use `exclude` to skip files matching a pattern: SQL ```sql SELECT * FROM extensions.fs9('/logs/', exclude => '*.tmp'); ``` ### File reading [Section titled “File reading”](#file-reading) When the path points to a file, fs9 reads its contents as rows. The format is auto-detected from the file extension: | Extension | Format | Row schema | | ------------------- | ---------- | ----------------------------------------------------- | | `.csv` | CSV | `_line_number INT`, columns from header, `_path TEXT` | | `.tsv` | TSV | `_line_number INT`, columns from header, `_path TEXT` | | `.jsonl`, `.ndjson` | JSON Lines | `_line_number INT`, `line JSONB`, `_path TEXT` | | `.parquet` | Parquet | Schema from file metadata | | Other | Plain text | `_line_number INT`, `line TEXT`, `_path TEXT` | SQL ```sql -- CSV with auto-detected columns SELECT * FROM extensions.fs9('/data/users.csv'); -- JSONL as queryable JSONB SELECT line->>'level' AS level, count(*) FROM extensions.fs9('/logs/app.jsonl') GROUP BY 1; ``` ▶ Run Override the format or delimiter with named parameters: SQL ```sql -- Force CSV format on a .dat file SELECT * FROM extensions.fs9('/data/raw.dat', format => 'csv'); -- Pipe-delimited file without a header row SELECT col_0, col_1 FROM extensions.fs9('/data/raw.dat', format => 'csv', delimiter => '|', header => false); ``` For CSV/TSV without a header, columns are named `col_0`, `col_1`, etc. Invalid JSON lines in JSONL files are silently skipped. ### Glob matching [Section titled “Glob matching”](#glob-matching) When the path contains `*`, `?`, or `[`, fs9 expands the glob and reads all matching files: SQL ```sql -- All CSV files in a directory SELECT * FROM extensions.fs9('/data/sales/*.csv'); -- Recursive match across subdirectories SELECT _path, line->>'event' AS event FROM extensions.fs9('/logs/**/*.jsonl') ORDER BY _path, _line_number; ``` ▶ Run The schema is determined by the first matching file. All files use the same format. The `_path` column identifies which file each row came from. Glob parameters: * `format`, `delimiter`, `header` — same as file reading * `exclude` — glob pattern to skip (e.g., `*.tmp`) Hidden files (dotfiles) are excluded unless the pattern explicitly starts with `.` or contains `/.`. Globs do not decode Parquet Glob expansion decodes CSV/TSV and JSONL only. A `.parquet` file matched by a glob falls back to the plain-text reader, so a pattern like `extensions.fs9('/data/*.parquet')` returns **zero rows** with the raw-text schema (`_line_number`, `line`, `_path`) instead of the file’s real columns — it does not raise an error. Read Parquet a file at a time, where the schema is taken from the file metadata: SQL ```sql SELECT * FROM extensions.fs9('/data/export.parquet'); ``` To load several Parquet files, `COPY` each one — see [Parquet Import](/docs/extensions/parquet/). Glob read budget If total bytes read across all matching files exceeds 100 MB, fs9 stops reading and returns partial results with a warning. Use `exclude` patterns or more targeted paths for large directories. ## Event Notifications [Section titled “Event Notifications”](#event-notifications) `db9 fs watch` is the recommended way to consume events `fs9_events()` can be queried directly from SQL (see below), but `db9 fs watch` from the CLI handles cursor-based polling for you and is the recommended interface for most applications. `fs9_events()` must be called **schema-qualified** — `SELECT * FROM extensions.fs9_events()`. The unqualified name is not registered as a table-valued function and fails with `ERROR: function fs9_events() does not exist` (`42883`). The `fs9_events()` table function returns a stream of filesystem mutation events. It powers `db9 fs watch` and can be queried directly from SQL for custom change-tracking. Start your cursor at `'0'`, not at an arbitrary timestamp Events are delivered — a `fs9_write` shows up as a `CREATE`/`WRITE` row immediately. But a `since_id` older than the stream’s retained range returns **zero rows instead of the full backlog**: SQL ```sql SELECT count(*) FROM extensions.fs9_events(); -- 17 SELECT count(*) FROM extensions.fs9_events('0'); -- 17 SELECT count(*) FROM extensions.fs9_events('1750000000000-0'); -- 0 (cursor too old) ``` Seed a new consumer with `'0'`, then advance it using `stream_id` values you actually read back. Do not synthesize a cursor from a wall-clock timestamp. SQL ```sql -- All events since the beginning SELECT * FROM extensions.fs9_events(); -- Events after a stream ID you previously read back (cursor-based polling) SELECT * FROM extensions.fs9_events('1786979518971-0'); -- Events under a specific path prefix SELECT * FROM extensions.fs9_events('0', '/data/'); -- Events with a custom limit (default: 10,000) SELECT * FROM extensions.fs9_events('0', '/logs/', 5000); ``` ### Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | Default | Description | | ------------- | ----- | ------- | ------------------------------------------------------------------------------------------------------- | | `since_id` | TEXT | `'0'` | Return events with stream ID greater than this value (Redis Stream ID format, e.g. `'1711324800000-0'`) | | `path_prefix` | TEXT | `NULL` | Filter to events where path starts with this prefix | | `limit` | INT64 | `10000` | Maximum number of event rows to return | ### Return columns [Section titled “Return columns”](#return-columns) | Column | Type | Description | | ------------ | ----------- | ------------------------------------------------- | | `stream_id` | TEXT | Redis Stream ID (e.g. `'1711324800000-0'`) | | `event_type` | TEXT | `CREATE`, `WRITE`, `DELETE`, `RENAME`, or `MKDIR` | | `path` | TEXT | Affected path (post-mutation) | | `old_path` | TEXT | Previous path (only set for `RENAME` events) | | `inode` | INT64 | Inode ID | | `generation` | INT64 | Post-mutation generation | | `is_dir` | BOOLEAN | Whether the target is a directory | | `size` | INT64 | Post-mutation file size (0 for directories) | | `timestamp` | TIMESTAMPTZ | Event timestamp | > **Note:** `since_id` also accepts integer values for backwards compatibility — they are converted to string internally. ### Cursor-based polling pattern [Section titled “Cursor-based polling pattern”](#cursor-based-polling-pattern) SQL ```sql -- Initial read: get the latest stream_id SELECT stream_id FROM extensions.fs9_events('0') ORDER BY stream_id DESC LIMIT 1; -- Subsequent polls: pass the last seen stream_id as since_id SELECT stream_id, event_type, path, old_path, size, timestamp FROM extensions.fs9_events(:last_stream_id) ORDER BY stream_id; ``` For CLI-based watching, use `db9 fs watch` which handles the polling loop, overflow detection, and formatting automatically. See the [CLI Reference](/docs/cli/#filesystem-watch). ## Symbolic Links [Section titled “Symbolic Links”](#symbolic-links) fs9 supports symbolic links through the CLI FUSE mount and WebSocket API. ### CLI (FUSE Mount) [Section titled “CLI (FUSE Mount)”](#cli-fuse-mount) Terminal ```bash # Create a symlink ln -s /data/report.csv /data/latest.csv # Read the symlink target readlink /data/latest.csv ``` ### WebSocket API [Section titled “WebSocket API”](#websocket-api) The `symlink` and `readlink` operations are available over the WebSocket protocol: * `symlink(path, target)` — create a symbolic link at `path` pointing to `target` * `readlink(path)` — return the target of a symbolic link Symlink properties: * Mode: `0o777` (all permissions) * Type: reported as `"symlink"` in `stat` responses * Maximum target length: 4096 bytes * Writing to a symlink directly is not allowed — write to the target path instead ## File Permissions (Mode Bits) [Section titled “File Permissions (Mode Bits)”](#file-permissions-mode-bits) When creating files and directories, fs9 preserves Unix permission bits: | Operation | Default Mode | | ---------------- | -------------------- | | File create | `0644` (`rw-r--r--`) | | Directory create | `0755` (`rwxr-xr-x`) | | Symlink | `0777` (`rwxrwxrwx`) | Executables uploaded with `db9 fs cp` or written via FUSE retain their original mode bits (e.g., `0755` for shell scripts). The `mode` field is visible in `stat` responses and `db9 fs ls -l` output. ## JSONL Support in fssh [Section titled “JSONL Support in fssh”](#jsonl-support-in-fssh) The `jq` builtin in `db9 fs sh` supports JSON Lines (JSONL) format — when input contains multiple JSON objects (one per line), the filter is applied to each object independently: Terminal ```bash db9 fs sh mydb -c "cat /logs/events.jsonl | jq '.event'" ``` Output ```text "click" "pageview" "submit" ``` ## Practical Patterns [Section titled “Practical Patterns”](#practical-patterns) ### Append-only log ingestion [Section titled “Append-only log ingestion”](#append-only-log-ingestion) SQL ```sql -- Write a log entry SELECT extensions.fs9_append( '/logs/agent.jsonl', '{"ts":"2026-03-12T10:00:00Z","action":"search","query":"revenue"}' || E'\n' ); -- Query log entries SELECT line->>'action' AS action, count(*) FROM extensions.fs9('/logs/agent.jsonl') GROUP BY 1 ORDER BY 2 DESC; ``` ▶ Run ### Periodic report generation with pg\_cron [Section titled “Periodic report generation with pg\_cron”](#periodic-report-generation-with-pg_cron) SQL ```sql SELECT cron.schedule('daily-report', '0 6 * * *', $$ SELECT extensions.fs9_write( '/reports/daily-' || to_char(now(), 'YYYY-MM-DD') || '.csv', (SELECT string_agg(id || ',' || name || ',' || total, E'\n') FROM (SELECT id::text, name, sum(amount)::text AS total FROM orders WHERE created_at > now() - interval '1 day' GROUP BY id, name) t) ) $$); ``` ### Upload via HTTP and store in fs9 [Section titled “Upload via HTTP and store in fs9”](#upload-via-http-and-store-in-fs9) SQL ```sql SELECT extensions.fs9_write( '/imports/feed.json', http_get('https://api.example.com/feed')->>'content' ); ``` ### Clean up old files [Section titled “Clean up old files”](#clean-up-old-files) SQL ```sql -- List files older than 7 days SELECT path, mtime FROM extensions.fs9('/logs/') WHERE type = 'file' AND mtime < to_char(now() - interval '7 days', 'YYYY-MM-DD"T"HH24:MI:SS"Z"'); -- Remove them SELECT extensions.fs9_remove('/logs/2026-02-*.jsonl'); ``` ▶ Run ## Permissions [Section titled “Permissions”](#permissions) All fs9 scalar functions and the table function require **superuser** privileges. The default `admin` role is a superuser. Regular database users created via `db9 db users create` cannot use fs9. ### Connect key scopes [Section titled “Connect key scopes”](#connect-key-scopes) When accessing fs9 via the WebSocket API (used by the TypeScript SDK), connect keys control access: | Scope | Permissions | | -------- | --------------------------------------------------- | | `fs9:ro` | Read-only: stat, read, readdir, size, mtime, exists | | `fs9:rw` | Read and write: all operations | Connect keys are created via the REST API: Terminal ```bash curl -X POST https://api.db9.ai/customer/databases//connect-keys \ -H "Authorization: Bearer $DB9_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "agent-fs", "scopes": ["fs9:rw"]}' ``` ## WebSocket API [Section titled “WebSocket API”](#websocket-api-1) The TypeScript SDK uses a WebSocket connection for fs9 file operations. This provides streaming support for large files and avoids SQL overhead for file I/O. The WebSocket server listens on port 5480 by default. The protocol uses JSON frames with an `op` field and a request `id` for correlation. Available operations: `auth`, `stat`, `readdir`, `mkdir`, `read`, `write`, `pwrite`, `append`, `truncate`, `unlink`, `rm`, `rename`, `symlink`, `readlink`. Files larger than 1 MB are automatically streamed in 64 KB chunks. Connection limits: * Auth timeout: 10 seconds * Idle timeout: 5 minutes * Max connections per tenant: 50 * Max JSON frame: 2 MB See the [TypeScript SDK](/docs/sdk/) for the client-side API. ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ----------------------------------------- | --------- | | Max file size | 100 MB | | Max total bytes per glob query | 100 MB | | Max files per glob expansion | 10,000 | | Concurrent read budget (scalar functions) | 128 MB | | Page size (internal storage) | 16 KB | | Write stream flush threshold | 256 KB | | WebSocket max connections per tenant | 50 | | WebSocket idle timeout | 5 minutes | | WebSocket max JSON frame | 2 MB | | WebSocket streaming chunk size | 64 KB | ## Error Messages [Section titled “Error Messages”](#error-messages) | Error | SQLSTATE | Cause | | ----------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `fs9: permission denied (superuser required)` | `42501` | Non-superuser role attempted an fs9 operation | | `fs9 resource was not found` | `58P01` | The path does not exist — including the parent directory of a write | | `fs9 resource has the wrong object type` | `42809` | The path exists but is not the kind of object the call needs (reading a directory, for example) | | `fs9 resource already exists` | `58P02` | Creating something that is already there — e.g. `fs9_mkdir` on an existing directory | | `fs9 directory is not empty` | `42809` | `fs9_remove` on a directory that still has entries; pass `true` as the second argument to recurse | | `permission denied for fs9 resource` | `42501` | The role may use fs9 but not this particular resource | | `fs9 resource exceeds the supported size limit` | | File exceeds 100 MB | | `internal error` | `XX000` | Argument validation failures (a negative `fs9_read_at` offset or `fs9_truncate` size) and I/O faults both surface this way, with no detail | Argument-validation messages are **not** passed through to the client. `fs9_truncate` still builds the text `fs9_truncate: size must be non-negative` internally, but the wire only ever carries `internal error`, so do not match on it. ## Serverless Functions [Section titled “Serverless Functions”](#serverless-functions) fs9 is also accessible from Serverless Functions via `ctx.fs9` — you can read and write files from your deployed JavaScript/TypeScript code without going through SQL. See the [Serverless Functions runtime](/docs/functions/runtime/) for details. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Analyze Agent Logs with fs9](/docs/guides/analyze-agent-logs-with-fs9/) — Tutorial: write, query, and aggregate agent logs * [Scheduled Jobs with pg\_cron](/docs/guides/scheduled-jobs-with-pg-cron/) — Automate fs9 reports on a schedule * [HTTP from SQL](/docs/guides/http-from-sql/) — Fetch external data and store in fs9 * [Extensions Overview](/docs/extensions/) — All 9 built-in extensions * [Limits and Quotas](/docs/platform/limits-and-quotas/) — All operational limits * [TypeScript SDK](/docs/sdk/) — Programmatic fs9 access via WebSocket # Full-Text Search > Full-text search with language-specific tokenizers including jieba for Chinese and simple for English. DB9 supports full-text search with language-specific tokenizers. ## Available Tokenizers [Section titled “Available Tokenizers”](#available-tokenizers) | Tokenizer | Aliases | Description | | --------------- | --------------------- | ------------------------------------------------------------------------------------------- | | `jieba` | `chinese`, `zhparser` | Chinese word segmentation (jieba-rs) | | `chinese_ngram` | `zhparser_ngram` | Chinese + bigram overlay for multi-char words | | `simple` | - | Whitespace tokenizer for English/Latin. No stemming, no stopword removal | | `english` | - | Whitespace tokenizer plus a minimal stopword list (`a`, `an`, `is`, `the`). **No stemming** | | `english_stem` | - | Snowball stemming plus the full PostgreSQL English stopword list (\~174 words) | The default configuration is `simple` (`SHOW default_text_search_config`). Any other configuration name — including `french`, `german`, and `spanish` — raises `unknown text search configuration`. `english` does not stem — use `english_stem` for PostgreSQL-compatible behavior In PostgreSQL, `to_tsvector('english', ...)` applies Snowball stemming. In DB9 the `english` configuration only strips a minimal stopword list; it does **not** stem. A query for `running` therefore will not match a document containing `runs`. Use `english_stem` when you want PostgreSQL-compatible English search: SQL ```sql -- english: no stemming — 'running' does not match 'runs' SELECT to_tsvector('english', 'he runs fast') @@ plainto_tsquery('english', 'running') AS english_match; -- english_stem: stems both sides — 'running' matches 'runs' SELECT to_tsvector('english_stem', 'he runs fast') @@ plainto_tsquery('english_stem', 'running') AS english_stem_match; ``` ▶ Run Stemming is algorithmic (Snowball), not dictionary-based, so irregular forms are not folded together — `runs` and `running` both stem to `run`, but `ran` stems to `ran` and will not match them. Compare how each configuration tokenizes the same input: SQL ```sql SELECT to_tsvector('simple', 'the a is database running') AS simple, to_tsvector('english', 'the a is database running') AS english, to_tsvector('english_stem', 'the a is database running') AS english_stem; ``` ▶ Run ## GIN Indexes for Full-Text Search [Section titled “GIN Indexes for Full-Text Search”](#gin-indexes-for-full-text-search) GIN (Generalized Inverted Index) indexes are the standard way to accelerate full-text search queries. Create a GIN index on a `tsvector` column or expression to avoid sequential scans: SQL ```sql -- Index on a tsvector column CREATE INDEX idx_tsv ON documents USING GIN (tsv); -- Index on an expression CREATE INDEX idx_content_fts ON documents USING GIN (to_tsvector('simple', content)); ``` The optimizer uses GIN indexes automatically when the `@@` operator matches an indexed expression. ## Chinese Text Search (jieba) [Section titled “Chinese Text Search (jieba)”](#chinese-text-search-jieba) SQL ```sql -- Create index CREATE INDEX idx_content_fts ON documents USING gin(to_tsvector('jieba', content)); -- Search SELECT * FROM documents WHERE to_tsvector('jieba', content) @@ plainto_tsquery('jieba', '关键词'); ``` ## English Text Search [Section titled “English Text Search”](#english-text-search) Use `simple` for exact-token matching, or `english_stem` when you want stemming and stopword removal (the closest match to PostgreSQL’s `english` configuration): SQL ```sql -- Exact tokens (no stemming) CREATE INDEX idx_content_fts ON documents USING gin(to_tsvector('simple', content)); SELECT * FROM documents WHERE to_tsvector('simple', content) @@ to_tsquery('simple', 'keyword'); -- Stemmed search: a query for 'running' also matches 'runs' CREATE INDEX idx_content_stem ON documents USING gin(to_tsvector('english_stem', content)); SELECT * FROM documents WHERE to_tsvector('english_stem', content) @@ plainto_tsquery('english_stem', 'running'); ``` Use the **same configuration** on both sides of `@@` and in the index expression. The configurations tokenize differently, so mixing them (for example indexing with `simple` but querying with `english_stem`) changes which rows match. ## Ranking Results [Section titled “Ranking Results”](#ranking-results) SQL ```sql SELECT content, ts_rank(to_tsvector('jieba', content), plainto_tsquery('jieba', '搜索词')) as rank FROM documents WHERE to_tsvector('jieba', content) @@ plainto_tsquery('jieba', '搜索词') ORDER BY rank DESC LIMIT 10; ``` ▶ Run ## Query Types [Section titled “Query Types”](#query-types) | Function | Description | Example | | ---------------------- | ----------------- | ---------------------------------------------------- | | `plainto_tsquery` | Simple phrase | `plainto_tsquery('jieba', '人工智能')` | | `to_tsquery` | Boolean operators | `to_tsquery('simple', 'cat & dog')` | | `phraseto_tsquery` | Exact phrase | `phraseto_tsquery('simple', 'hello world')` | | `websearch_to_tsquery` | Google-style | `websearch_to_tsquery('simple', '"exact" -exclude')` | ## Boolean Search [Section titled “Boolean Search”](#boolean-search) SQL ```sql -- AND: both terms must match SELECT * FROM documents WHERE tsv @@ to_tsquery('simple', 'database & performance'); -- OR: either term matches SELECT * FROM documents WHERE tsv @@ to_tsquery('simple', 'postgres | mysql'); -- NOT: exclude term SELECT * FROM documents WHERE tsv @@ to_tsquery('simple', 'database & !oracle'); -- Prefix matching SELECT * FROM documents WHERE tsv @@ to_tsquery('simple', 'data:*'); ``` Prefix matching (`:*`) is not supported The `:*` prefix flag is silently dropped when parsing the query — `to_tsquery('simple', 'data:*')` renders as `'data'` (an exact-match lexeme), not a prefix match. No error is raised, so a query like `WHERE tsv @@ to_tsquery('simple', 'data:*')` will not match documents containing `database` unless they also contain the exact lexeme `data`. Do not rely on prefix matching until this is supported. ## Highlight Search Results [Section titled “Highlight Search Results”](#highlight-search-results) SQL ```sql SELECT ts_headline('jieba', content, plainto_tsquery('jieba', '数据库'), 'StartSel=, StopSel=, MaxWords=50' ) as highlighted FROM documents WHERE to_tsvector('jieba', content) @@ plainto_tsquery('jieba', '数据库'); ``` ▶ Run ts\_headline highlighting does not work with the jieba tokenizer `ts_headline('jieba', ...)` currently returns the input text unchanged — no `` (or other `StartSel`/`StopSel`) tags are inserted, even when `to_tsvector('jieba', ...)` correctly tokenizes the text and `@@` matches. The `simple` tokenizer is not affected — `ts_headline('simple', ...)` highlights correctly. ## Search with Weights [Section titled “Search with Weights”](#search-with-weights) SQL ```sql -- Prioritize title matches over body ALTER TABLE articles ADD COLUMN tsv tsvector; UPDATE articles SET tsv = setweight(to_tsvector('jieba', title), 'A') || setweight(to_tsvector('jieba', body), 'B'); -- Search with weighted ranking SELECT title, ts_rank(tsv, q) as rank FROM articles, plainto_tsquery('jieba', '搜索词') q WHERE tsv @@ q ORDER BY rank DESC; ``` ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | Max `tsvector` value size | 1 MB | | Max lexemes per `tsvector` | \~264,000 | | Supported text search configurations | 5 (`jieba`, `chinese_ngram`, `simple`, `english`, `english_stem`), plus the aliases `chinese`, `zhparser`, `zhparser_ngram` | | GIN index build strategy | Sequential (full table scan) | See [Limits and Quotas](/docs/platform/limits-and-quotas/) for the complete list. ## Next Steps [Section titled “Next Steps”](#next-steps) * [RAG with Built-in Embeddings](/docs/guides/rag-with-built-in-embeddings/) — Combine FTS with vector search for hybrid retrieval * [Vector Search](/docs/extensions/vector/) — Embedding and HNSW indexes for semantic search * [Extensions Overview](/docs/extensions/) — All 9 built-in extensions * [Limits and Quotas](/docs/platform/limits-and-quotas/) — All operational limits # hstore — Key-Value > PostgreSQL-compatible key-value store type for client compatibility — use JSONB for new code. `hstore` in DB9 is a **metadata-only shim** — `CREATE EXTENSION hstore` succeeds and registers extension metadata, but the `hstore` type itself does not exist. Any attempt to use `hstore` as a column type or cast to `hstore` will fail with `ERROR: type "hstore" does not exist`. **For new code, use `JSONB` instead.** JSONB provides a superset of hstore functionality with full operator support, better performance, and broad ecosystem support. ## Installation [Section titled “Installation”](#installation) SQL ```sql CREATE EXTENSION IF NOT EXISTS hstore; ``` ▶ Run ## What Is Supported [Section titled “What Is Supported”](#what-is-supported) | Feature | Supported | | ------------------------- | ------------------------------------------------- | | `CREATE EXTENSION hstore` | Yes — registers extension metadata only | | `hstore` column type | No — `type "hstore" does not exist` | | Cast from `text` | No — `'key=>value'::hstore` fails with type error | | Basic key access (`->`) | No — type does not exist | | Containment (`@>`) | No — type does not exist | ## What Is Not Supported [Section titled “What Is Not Supported”](#what-is-not-supported) | Feature | Status | JSONB Alternative | | ----------------------------------------- | ------------- | ------------------------------------- | | `%`, `?`, `?&`, \`? | \` operators | Not supported | | \`hstore | | hstore\` concatenation | | `hs - key` deletion | Not supported | `jsonb - key` | | `akeys()`, `avals()`, `hstore_to_array()` | Not supported | `jsonb_object_keys()`, `jsonb_each()` | | `hstore_to_json()`, `hstore_to_jsonb()` | Not supported | Cast directly with `::jsonb` | | `populate_record()`, `hstore_to_record()` | Not supported | `jsonb_populate_record()` | | `slice()`, `skeys()`, `svals()` | Not supported | `jsonb` operators | ## Migrate to JSONB [Section titled “Migrate to JSONB”](#migrate-to-jsonb) If you have existing code using hstore, JSONB provides equivalent functionality with full operator support: SQL ```sql -- hstore: CREATE TABLE with hstore column -- JSONB equivalent: CREATE TABLE settings ( id SERIAL PRIMARY KEY, data JSONB ); -- hstore: INSERT 'key=>value' syntax -- JSONB equivalent: INSERT INTO settings (data) VALUES ('{"theme": "dark", "lang": "en"}'); -- hstore: data->'key' (returns TEXT) -- JSONB equivalent: SELECT data->>'theme' FROM settings; -- Returns: 'dark' -- hstore: data @> 'key=>value' -- JSONB equivalent: SELECT * FROM settings WHERE data @> '{"theme": "dark"}'; -- hstore: delete a key data - 'key' -- JSONB equivalent: UPDATE settings SET data = data - 'theme'; -- hstore: get all keys akeys(data) -- JSONB equivalent: SELECT jsonb_object_keys(data) FROM settings; ``` ## Why JSONB [Section titled “Why JSONB”](#why-jsonb) | Feature | hstore (DB9) | JSONB | | ----------------- | ---------------------------- | ----------------------- | | Key-value storage | Not available (type missing) | Full support | | Nested values | No | Yes | | Operator support | None | Full | | GIN indexing | No | Yes | | Schema validation | No | Yes (check constraints) | | Ecosystem support | Limited | Universal | ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ----------------------- | ----------------------------------- | | hstore type | Does not exist — metadata shim only | | Operators available | None — type does not exist | | Recommended alternative | `JSONB` — full operator support | ## Next Steps [Section titled “Next Steps”](#next-steps) * [SQL Reference](/docs/sql/) — SQL engine compatibility and data types * [Extensions Overview](/docs/extensions/) — All 9 built-in extensions * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — Full supported/unsupported feature surface # http — HTTP Client > Make HTTP requests directly from SQL — GET, POST, PUT, DELETE, HEAD, and PATCH. Make HTTP requests directly from SQL. Functions are available in two calling conventions: **scalar** (returns JSONB) and **table** (returns rows). ## Installation [Section titled “Installation”](#installation) The `http` extension is pre-enabled — no `CREATE EXTENSION` required. ## Scalar Functions (JSONB) [Section titled “Scalar Functions (JSONB)”](#scalar-functions-jsonb) The scalar form returns a single JSONB value containing the full response. This is the recommended calling convention for new code. SQL ```sql -- GET request — returns JSONB SELECT http_get('https://api.example.com/data'); -- Extract fields from the JSONB response SELECT (http_get('https://httpbin.org/get'))->>'status' AS status; -- POST with JSON body SELECT http_post( 'https://api.example.com/endpoint', '{"key": "value"}', 'application/json' ); -- Parse the response content as JSON SELECT (http_get('https://api.ipify.org?format=json')->>'content')::json->>'ip' AS my_ip; ``` ### Scalar Response Shape [Section titled “Scalar Response Shape”](#scalar-response-shape) The JSONB object contains: | Key | Type | Description | | -------------- | -------- | -------------------------------------------------------------- | | `status` | `number` | HTTP status code (200, 404, etc.). | | `content` | `string` | Response body text. | | `content_type` | `string` | Response Content-Type header. | | `headers` | `array` | Response headers as `[{"field": "...", "value": "..."}, ...]`. | ### Available Scalar Functions [Section titled “Available Scalar Functions”](#available-scalar-functions) | Function | Description | | ------------------------------------------------ | ------------------------------------------------------ | | `http_get(url, headers?)` | HTTP GET, returns JSONB. Optional `headers` JSONB. | | `http_post(url, body, content_type, headers?)` | HTTP POST, returns JSONB. Optional `headers` JSONB. | | `http_put(url, body, content_type, headers?)` | HTTP PUT, returns JSONB. Optional `headers` JSONB. | | `http_delete(url, headers?)` | HTTP DELETE, returns JSONB. Optional `headers` JSONB. | | `http_head(url, headers?)` | HTTP HEAD, returns JSONB. Optional `headers` JSONB. | | `http_patch(url, body, content_type, headers?)` | HTTP PATCH, returns JSONB. Optional `headers` JSONB. | | `http(method, url, headers, content_type, body)` | Generic HTTP request with full control, returns JSONB. | ## Table Functions (Row) [Section titled “Table Functions (Row)”](#table-functions-row) The table-valued form requires the schema qualifier Which calling convention you get depends on how you qualify the name: | Call | Result | | ---------------------------------------------------- | ----------------------------------------------------------------------------- | | `SELECT http_get(url)` | Scalar — returns JSONB (used by every example on this page) | | `SELECT * FROM extensions.http_get(url)` | Table-valued — returns `status`, `content_type`, `headers`, `content` columns | | `SELECT * FROM http_get(url)` (unqualified) | `ERROR: function http_get(unknown) does not exist` (`42883`) | | `SELECT extensions.http_get(url)` (qualified scalar) | `ERROR: function extensions.http_get(text) does not exist` (`42883`) | So use the bare name for the scalar JSONB form, and the `extensions.` prefix for the row form: SQL ```sql SELECT status, content_type FROM extensions.http_get('https://example.com'); ``` The same schema-qualifier rule applies to `extensions.http_post(...)` and the other verbs. ## Real-World Examples [Section titled “Real-World Examples”](#real-world-examples) ### POST to Webhook [Section titled “POST to Webhook”](#post-to-webhook) SQL ```sql -- Send alert to webhook SELECT http_post( 'https://hooks.slack.com/services/xxx/yyy/zzz', '{"text": "Database backup completed!"}', 'application/json' ); ``` ### Check URL Status [Section titled “Check URL Status”](#check-url-status) SQL ```sql -- Health check multiple endpoints SELECT url, (http_head(url)->>'status')::int AS status FROM (VALUES ('https://api.example.com/health'), ('https://db9.ai'), ('https://google.com') ) t(url); ``` SSRF protection is built-in DB9 blocks outbound HTTP requests to private IP ranges (RFC 1918: `10.x`, `172.16.x`, `192.168.x`), loopback (`127.x`), and link-local addresses. Only public HTTPS endpoints are reachable. HTTP (non-TLS) is also blocked. ## Custom Headers [Section titled “Custom Headers”](#custom-headers) Pass request headers as a JSONB value. Two formats are supported: SQL ```sql -- Object format (recommended) '{"Authorization": "Bearer sk-..."}'::jsonb -- Array format (pgsql-http compatible) '[{"field": "Authorization", "value": "Bearer sk-..."}]'::jsonb ``` ### Bearer Token Authentication [Section titled “Bearer Token Authentication”](#bearer-token-authentication) SQL ```sql -- GET with Bearer token SELECT (http_get( 'https://api.example.com/protected', '{"Authorization": "Bearer sk-your-token-here"}'::jsonb ))->>'content' AS body; -- POST with Bearer token SELECT (http_post( 'https://api.example.com/data', '{"event": "user_signup"}', 'application/json', '{"Authorization": "Bearer sk-your-token-here"}'::jsonb ))->>'status' AS status; ``` ### API Key Header [Section titled “API Key Header”](#api-key-header) SQL ```sql -- X-API-Key header SELECT (http_post( 'https://api.example.com/webhook', '{"event": "test"}', 'application/json', '{"X-API-Key": "my-api-key"}'::jsonb ))->>'status' AS status; -- Vendor-specific key header (e.g. OpenAI, Anthropic) SELECT (http_post( 'https://api.openai.com/v1/chat/completions', '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Hello"}]}', 'application/json', '{"Authorization": "Bearer sk-proj-...", "OpenAI-Organization": "org-..."}'::jsonb ))->>'content' AS response; ``` ### Multiple Headers [Section titled “Multiple Headers”](#multiple-headers) SQL ```sql -- Pass several headers at once (object format) SELECT http_get( 'https://api.example.com/data', '{"Authorization": "Bearer token", "X-Request-ID": "req-001", "Accept": "application/json"}'::jsonb ); -- Using the generic http() function for full control SELECT http( 'POST', 'https://api.example.com/endpoint', '{"Authorization": "Bearer token", "X-Idempotency-Key": "key-123"}'::jsonb, 'application/json', '{"payload": "data"}' ); ``` ### Store a token in a table for reuse [Section titled “Store a token in a table for reuse”](#store-a-token-in-a-table-for-reuse) To avoid repeating credentials inline, store them in a table and join at query time: SQL ```sql CREATE TABLE api_credentials ( name TEXT PRIMARY KEY, headers JSONB NOT NULL ); INSERT INTO api_credentials VALUES ( 'my-api', '{"Authorization": "Bearer sk-your-token"}'::jsonb ); -- Use stored headers in a request SELECT http_get('https://api.example.com/data', headers) FROM api_credentials WHERE name = 'my-api'; ``` > **Security note:** Credentials stored in tables are visible to any role with `SELECT` access. Use row-level security or store secrets outside the database for production workloads. ## Error Handling [Section titled “Error Handling”](#error-handling) ### Check HTTP status codes [Section titled “Check HTTP status codes”](#check-http-status-codes) The `status` field in the JSONB response contains the HTTP status code. Always check it before using the response body: SQL ```sql -- Check status before using the response SELECT CASE WHEN (response->>'status')::int = 200 THEN response->>'content' WHEN (response->>'status')::int = 401 THEN 'Unauthorized — check your API key' WHEN (response->>'status')::int = 429 THEN 'Rate limited — retry later' ELSE 'Error: HTTP ' || (response->>'status')::int END AS result FROM (SELECT http_get('https://api.example.com/data') AS response) r; ``` ### Filter to successful responses only [Section titled “Filter to successful responses only”](#filter-to-successful-responses-only) SQL ```sql -- Only process rows where the API returned 200 SELECT url, (response->>'content')::jsonb AS data FROM my_urls CROSS JOIN LATERAL (SELECT http_get(my_urls.url) AS response) r WHERE (response->>'status')::int = 200; ``` ### Raise an exception on failure [Section titled “Raise an exception on failure”](#raise-an-exception-on-failure) Use `RAISE EXCEPTION` inside a function or `DO` block to abort on non-200: SQL ```sql DO $$ DECLARE response JSONB; status_code INT; BEGIN response := http_post( 'https://api.example.com/notify', '{"event": "test"}', 'application/json', '{"Authorization": "Bearer sk-..."}'::jsonb ); status_code := (response->>'status')::int; IF status_code < 200 OR status_code >= 300 THEN RAISE EXCEPTION 'HTTP request failed with status %: %', status_code, response->>'content'; END IF; END $$; ``` ### Handle timeouts [Section titled “Handle timeouts”](#handle-timeouts) Requests that exceed 5 seconds raise a PostgreSQL error. Catch it with `EXCEPTION`: SQL ```sql DO $$ BEGIN PERFORM http_get('https://slow-api.example.com/data'); EXCEPTION WHEN OTHERS THEN RAISE WARNING 'HTTP request failed: %', SQLERRM; END $$; ``` ### Common error messages [Section titled “Common error messages”](#common-error-messages) | Error | Cause | | ------------------------------------------- | --------------------------------------- | | `http: insecure http requests are disabled` | Plain HTTP URL — use `https://` | | `http: host is not allowed` | SSRF protection blocked the hostname | | `http: ip is not allowed` | SSRF protection blocked the resolved IP | | `http: timeout` | Request exceeded the 5-second timeout | | `http: response too large` | Response body exceeded 1 MB limit | ## Safety Boundaries [Section titled “Safety Boundaries”](#safety-boundaries) | Limit | Value | | -------------------------- | ---------------------------------- | | Protocol | HTTPS only (HTTP blocked) | | Max requests per statement | 100 | | Max concurrent requests | 20 | | Max response size | 1 MB | | Request timeout | 5 seconds | | SSRF protection | Private/internal IP ranges blocked | ## Next Steps [Section titled “Next Steps”](#next-steps) * [HTTP from SQL Guide](/docs/guides/http-from-sql/) — Tutorial: call APIs, send webhooks, and enrich data from SQL * [Extensions Overview](/docs/extensions/) — All 9 built-in extensions * [Scheduled Jobs with pg\_cron](/docs/extensions/pg-cron/) — Combine HTTP calls with scheduled jobs # Parquet Import > Import Parquet files into DB9 tables via COPY FROM, from HTTP URLs or fs9 paths. Import Parquet files into DB9 tables using `COPY ... WITH (FORMAT parquet)`, either from an HTTP URL or from a file stored in [fs9](/docs/extensions/fs9/) using the `fs9://` scheme. No external tools or ETL pipelines are required. ## Installation [Section titled “Installation”](#installation) SQL ```sql CREATE EXTENSION IF NOT EXISTS parquet; ``` ▶ Run read\_parquet() requires the schema qualifier The unqualified `read_parquet()` is **not** registered as a table-valued function and returns: ```plaintext ERROR: function read_parquet(unknown) does not exist -- sqlstate 42883 ``` Call it schema-qualified instead — `extensions.read_parquet(...)` works: SQL ```sql SELECT count(*) FROM extensions.read_parquet('https://example.com/data.parquet'); ``` You can also use `COPY FROM ... WITH (FORMAT parquet)` to import Parquet data into an existing table — see below. ## COPY FROM Parquet [Section titled “COPY FROM Parquet”](#copy-from-parquet) Use `COPY ... WITH (FORMAT parquet)` to import Parquet data into an existing table, from either source: SQL ```sql -- Import from an HTTP URL COPY my_table FROM 'https://example.com/export.parquet' WITH (FORMAT parquet); -- Import from a Parquet file stored in fs9 COPY my_table FROM 'fs9:///data/export.parquet' WITH (FORMAT parquet); ``` Columns are matched by name (case-insensitive). Extra columns in the Parquet file are ignored, and target-table columns with no matching Parquet column are filled with `NULL`. Type mismatches are not always rejected Many incompatible column pairs are caught at import (`cannot cast type X to Y`, or `invalid input syntax for type X`), but **numeric source columns are not checked against non-numeric targets**. Importing a numeric Parquet column (`INT32`, `INT64`, `FLOAT`, `DOUBLE`) into a `uuid`, `date`, `time`, `timestamp`, `timestamptz`, `interval`, `inet` or `bytea` column currently succeeds and stores the raw value, with no error — a `uuid` column ends up holding integers. Text source columns are checked against these same targets and rejected where the cast is genuinely invalid (`TEXT` into `bytea` is a valid cast and is accepted). Treat that list as indicative rather than exhaustive: verify the Parquet schema matches your target table rather than relying on the import to reject a mismatch. Tracked in db9-server#4059. Use the `fs9://` scheme for fs9 files Sources must be a URL. `http://`, `https://` and `fs9://` are accepted; a **bare** path is not: ```plaintext ERROR: unsupported Parquet URL scheme ``` Write `fs9:///data/export.parquet` (three slashes — `fs9://` plus the absolute fs9 path) rather than `/data/export.parquet`. The same rule applies to `extensions.read_parquet()`. ## Type Mapping [Section titled “Type Mapping”](#type-mapping) Parquet physical types are mapped to PostgreSQL types during COPY FROM: | Parquet Type | PostgreSQL Type | | ------------------------ | ------------------ | | `BOOLEAN` | `BOOLEAN` | | `INT32` | `INTEGER` | | `INT64` | `BIGINT` | | `FLOAT` | `REAL` | | `DOUBLE` | `DOUBLE PRECISION` | | `BYTE_ARRAY` (UTF8) | `TEXT` | | `BYTE_ARRAY` (binary) | `BYTEA` | | `INT96` (timestamp) | `TIMESTAMP` | | `DATE` logical type | `DATE` | | `TIME` logical type | `TIME` | | `TIMESTAMP` logical type | `TIMESTAMP` | | `DECIMAL` logical type | `NUMERIC` | ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ---------------------------------------- | ------------------------------------ | | Max Parquet file size (`fs9://` sources) | 100 MB | | Supported Parquet versions | v1, v2 | | Max columns per file | Unlimited (practical limit: storage) | | Remote URL connect timeout | 10 seconds | | Remote URL request timeout | 60 seconds | | Supported compression codecs | Snappy, Gzip, Zstd, uncompressed | See [Limits and Quotas](/docs/platform/limits-and-quotas/) for the complete list. ## Error Messages [Section titled “Error Messages”](#error-messages) Most errors fall into two groups. Those raised while *fetching* the source do not echo the URL or path, so the message alone does not identify the file — check the statement’s source argument. Those raised while *importing* rows do name the table, row group and row. The first row below is neither: it is a function-resolution error raised before either stage. | Error | Cause | | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ERROR: function read_parquet(unknown) does not exist` (`42883`) | Unqualified `read_parquet()` is not on the default `search_path`. Call `extensions.read_parquet(...)`, or use `COPY FROM`. See the note below — the unqualified call *does* work in the session that ran `CREATE EXTENSION`. | | `ERROR: Parquet file was not found` (`58P01`) | The remote URL returned a 404. Verify the URL is accessible. | | `ERROR: could not resolve Parquet file host` (`58030`) | The host could not be resolved. Check the hostname. | | `ERROR: could not connect to Parquet file host` (`58030`) | Connection failed or timed out. | | `ERROR: Parquet HTTP response has no valid Content-Length` (`58030`) | The URL answered `200` but sent no usable `Content-Length` — commonly an HTML page rather than a file. | | `ERROR: internal error` (`XX000`) | The body was fetched but is not a valid Parquet file, or is truncated. | | `ERROR: Parquet URL is not allowed by network policy` (`42501`) | The URL points at a private, loopback or otherwise blocked address. Use a publicly reachable host, or `fs9://`. | | `ERROR: Parquet file host returned HTTP status ` (`58030`) | The host answered with an unexpected status (for example `500`). | | `ERROR: permission denied for Parquet file` (`42501`) | The remote URL returned 401 or 403. | | `ERROR: COPY , row group N, row M: cannot cast type X to Y` | A Parquet column’s type cannot be cast to the target column’s type at all (for example `INTEGER` into `BOOLEAN`). | | `ERROR: COPY
, row group N, row M: invalid input syntax for type X: "..."` | The target type is parseable in principle but this value is not — for example `TEXT` into `INTEGER`, or `DOUBLE` into `INTEGER` (`"1.5"`). Note the reported row is the first *offending* row, not necessarily row 1. | | `ERROR: unsupported Parquet URL scheme` (`22023`) | The source was a bare path, or used a scheme other than `http`, `https` or `fs9`. Use `fs9:///path` for fs9 files, or an `http(s)://` URL. | | `ERROR: fs9 resource was not found` (`58P01`) | The `fs9://` path does not exist. | | `ERROR: fs9 resource has the wrong object type` (`42809`) | The `fs9://` path exists but is not a file — most often a directory. | `CREATE EXTENSION parquet` changes your `search_path` `CREATE EXTENSION ... parquet` appends `extensions` to the current session’s `search_path` (`"$user", public` becomes `"$user", public, extensions`). For the rest of *that* session an unqualified `read_parquet(...)` resolves and works. Every later session starts from the default `search_path` again, where the same call fails with `42883`. Always write `extensions.read_parquet(...)` in anything you save — a script that worked in the session where you enabled the extension will fail the next time it runs. ## Next Steps [Section titled “Next Steps”](#next-steps) * [HTTP from SQL](/docs/extensions/http/) — Fetch Parquet files from external HTTP endpoints * [Extensions Overview](/docs/extensions/) — All 9 built-in extensions * [Limits and Quotas](/docs/platform/limits-and-quotas/) — All operational limits # pg_cron — Scheduled Tasks > Schedule SQL statements to run on a cron schedule — with upsert, job management, execution history, and CLI support. pg\_cron runs SQL statements on a recurring schedule. Jobs are persisted in TiKV, executed by a background worker, and tracked with full execution history. Job execution is unavailable in the current release `cron.schedule(...)` returns an error in this release — job scheduling and execution require the worker subsystem, which is disabled for the current release (`PreActivationSeal`): ```plaintext ERROR: feature "cron" is unavailable (PreActivationSeal) ``` `CREATE EXTENSION pg_cron` still succeeds, and the catalog views (`cron.job`, `cron.job_run_details`, `cron.running_jobs`) and management functions (`cron.unschedule`, `cron.cancel`) resolve normally — they just have no jobs to show while scheduling is disabled. Use this page as reference for when the worker engine is enabled; do not build on scheduled execution yet. pg\_cron is a **default extension** — it is pre-installed when a database is created. You can verify with: SQL ```sql SELECT * FROM pg_extension WHERE extname = 'pg_cron'; ``` ▶ Run If needed, install manually: SQL ```sql CREATE EXTENSION IF NOT EXISTS pg_cron; ``` ▶ Run ## Scheduling Jobs [Section titled “Scheduling Jobs”](#scheduling-jobs) ### 3-argument form (recommended) [Section titled “3-argument form (recommended)”](#3-argument-form-recommended) Named jobs are idempotent The 3-argument form assigns a name to the job. If a job with the same name already exists for the current user, it updates the existing job instead of creating a duplicate (upsert). This is safe to call repeatedly in migration scripts. SQL ```sql SELECT cron.schedule('cleanup', '0 3 * * *', $$DELETE FROM logs WHERE created_at < now() - interval '7 days'$$); -- Returns: job_id (e.g., 1) -- Call again with the same name to update schedule or command SELECT cron.schedule('cleanup', '0 4 * * *', $$DELETE FROM logs WHERE created_at < now() - interval '30 days'$$); -- Returns: same job_id (1) ``` ### 2-argument form [Section titled “2-argument form”](#2-argument-form) Creates an anonymous job (no name, no upsert). Returns the assigned job ID. SQL ```sql SELECT cron.schedule('*/5 * * * *', 'SELECT check_alerts()'); ``` ## Cron Expression Format [Section titled “Cron Expression Format”](#cron-expression-format) Minimum granularity is 1 minute pg\_cron uses standard 5-field cron expressions. Six-field expressions (with seconds) and special strings like `@daily` or `@hourly` are not supported. Jobs cannot run more frequently than once per minute. pg\_cron uses standard 5-field cron expressions. Six-field expressions (with seconds) and special strings (`@daily`, `@hourly`) are not supported. Output ```text ┌───────────── minute (0–59) │ ┌───────────── hour (0–23) │ │ ┌───────────── day of month (1–31) │ │ │ ┌───────────── month (1–12) │ │ │ │ ┌───────────── day of week (1–7, Sunday = 1, Saturday = 7) │ │ │ │ │ * * * * * ``` Supported operators: `*` (any), `,` (list), `-` (range), `/` (step). Day-of-week numbering differs from standard cron DB9 numbers days **1–7 starting at Sunday**, so every numeric day is shifted by one compared to standard cron and upstream pg\_cron: | Day | Sun | Mon | Tue | Wed | Thu | Fri | Sat | | ------------- | --- | --- | --- | --- | --- | --- | --- | | **DB9** | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | Standard cron | 0 | 1 | 2 | 3 | 4 | 5 | 6 | `0` is rejected outright, before the job is created: ```plaintext ERROR: invalid cron expression '0 3 * * 0' Days of Week must be greater than or equal to 1. ('0' specified.) ``` A value copied from a standard crontab will silently run **one day early** — `1-5` means Sunday–Thursday here, not Monday–Friday. Prefer the day names (`SUN`–`SAT`), which are accepted and unambiguous. | Expression | Runs | | ----------------- | ---------------------------------- | | `* * * * *` | Every minute | | `*/5 * * * *` | Every 5 minutes | | `0 * * * *` | Every hour | | `0 0 * * *` | Daily at midnight | | `0 3 * * 2-6` | Weekdays at 3 AM (Mon–Fri) | | `0,30 9-17 * * *` | Every 30 min during business hours | | `0 0 1 * *` | First day of every month | | `0 0 * * 1` | Every Sunday at midnight | ## Managing Jobs [Section titled “Managing Jobs”](#managing-jobs) ### View scheduled jobs [Section titled “View scheduled jobs”](#view-scheduled-jobs) SQL ```sql SELECT jobid, jobname, schedule, command, active FROM cron.job ORDER BY jobid; ``` ### Modify a job [Section titled “Modify a job”](#modify-a-job) `cron.alter_job` updates individual fields. Pass `NULL` for fields you do not want to change. SQL ```sql -- Change schedule only SELECT cron.alter_job(1, '0 4 * * *', NULL, NULL, NULL, NULL, NULL); -- Disable a job SELECT cron.alter_job(1, NULL, NULL, NULL, NULL, false, NULL); -- Set max runtime to 30 minutes SELECT cron.alter_job(1, NULL, NULL, NULL, NULL, NULL, '30min'); ``` **Signature:** `cron.alter_job(job_id, schedule, command, database, username, active, max_runtime)` All arguments after `job_id` accept `NULL` to leave unchanged. The `database` argument must always be `NULL` (cross-database scheduling is not supported). Only superusers can change the `username` (job owner). **max\_runtime formats:** `'30min'`, `'2h'`, `'60s'`, `'5000ms'`, or an integer (milliseconds). Use `'0'` for no limit. ### Delete a job [Section titled “Delete a job”](#delete-a-job) SQL ```sql -- By name SELECT cron.unschedule('cleanup'); -- By ID SELECT cron.unschedule(1); ``` Returns `true` if the job existed and was deleted, `false` if it did not exist. Deleting a job also removes its execution history. ### Cancel a running job [Section titled “Cancel a running job”](#cancel-a-running-job) SQL ```sql SELECT cron.cancel(5); ``` Returns `true` if a running execution was found and a cancel signal was sent. **Superuser only.** ## Execution History [Section titled “Execution History”](#execution-history) ### View past runs [Section titled “View past runs”](#view-past-runs) SQL ```sql SELECT jobid, runid, status, return_message, start_time, end_time FROM cron.job_run_details WHERE jobid = 1 ORDER BY runid DESC LIMIT 20; ``` ### Status values [Section titled “Status values”](#status-values) | Status | Meaning | | ----------- | -------------------------------------- | | `starting` | Job is about to execute | | `running` | Execution in progress | | `succeeded` | Completed successfully | | `failed` | SQL error, timeout, or orphan recovery | | `cancelled` | Cancelled via `cron.cancel()` | ### View currently running jobs [Section titled “View currently running jobs”](#view-currently-running-jobs) SQL ```sql SELECT run_id, job_id, username, command, elapsed_ms FROM cron.running_jobs; ``` **Superuser only** — non-superusers see an empty result set. ### Execution history retention [Section titled “Execution history retention”](#execution-history-retention) Run records are automatically cleaned up after 7 days (configurable via `DB9_CRON_RUN_RETENTION_DAYS`). ## Virtual Tables [Section titled “Virtual Tables”](#virtual-tables) ### cron.job [Section titled “cron.job”](#cronjob) | Column | Type | Description | | ------------- | ------- | --------------------------------------------------------- | | `jobid` | BIGINT | Unique job identifier | | `schedule` | TEXT | 5-field cron expression | | `command` | TEXT | SQL command | | `nodename` | TEXT | Always `localhost` | | `nodeport` | BIGINT | Always `5433` | | `database` | TEXT | Database name | | `username` | TEXT | Job owner | | `active` | BOOLEAN | Whether the job is enabled | | `jobname` | TEXT | Job name (NULL for anonymous jobs) | | `max_runtime` | TEXT | Formatted max execution time (e.g., `30min`) or `default` | | `next_run_at` | TEXT | Next scheduled execution (epoch format) | Non-superusers see only their own jobs. ### cron.job\_run\_details [Section titled “cron.job\_run\_details”](#cronjob_run_details) | Column | Type | Description | | ---------------- | ------ | ------------------------------------------------------------ | | `jobid` | BIGINT | Job ID | | `runid` | BIGINT | Unique run identifier | | `job_pid` | BIGINT | Process ID (NULL if not started) | | `database` | TEXT | Database name | | `username` | TEXT | Job owner | | `command` | TEXT | SQL command executed | | `status` | TEXT | `starting`, `running`, `succeeded`, `failed`, or `cancelled` | | `return_message` | TEXT | Output or error message | | `start_time` | TEXT | Start timestamp (epoch ms) | | `end_time` | TEXT | End timestamp (epoch ms) | Non-superusers see only runs from their own jobs. ### cron.running\_jobs [Section titled “cron.running\_jobs”](#cronrunning_jobs) | Column | Type | Description | | ------------ | ------ | -------------------------- | | `run_id` | BIGINT | Current run identifier | | `job_id` | BIGINT | Job ID | | `keyspace` | TEXT | TiKV keyspace | | `db_id` | BIGINT | Internal database ID | | `username` | TEXT | Job owner | | `command` | TEXT | SQL command being executed | | `started_at` | TEXT | Start timestamp (epoch ms) | | `elapsed_ms` | BIGINT | Milliseconds since start | **Superuser only.** ## CLI Commands [Section titled “CLI Commands”](#cli-commands) All commands use `db9 db cron ` — the database comes before the subcommand. ### List jobs [Section titled “List jobs”](#list-jobs) Terminal ```bash db9 db cron list db9 db cron list --json ``` ### Create a job [Section titled “Create a job”](#create-a-job) Terminal ```bash # Inline SQL (command is a positional argument) db9 db cron create '0 3 * * *' 'VACUUM' # Named job (enables upsert) db9 db cron create '*/15 * * * *' 'REFRESH MATERIALIZED VIEW mv_stats' --name refresh # SQL from file db9 db cron create '0 0 * * *' --name daily-report --file report.sql ``` ### Delete a job [Section titled “Delete a job”](#delete-a-job-1) Terminal ```bash db9 db cron delete ``` ### View execution history [Section titled “View execution history”](#view-execution-history) Terminal ```bash db9 db cron history db9 db cron history --job cleanup --limit 50 ``` ### Enable or disable a job [Section titled “Enable or disable a job”](#enable-or-disable-a-job) Terminal ```bash db9 db cron enable db9 db cron disable ``` ### Check job status [Section titled “Check job status”](#check-job-status) Terminal ```bash db9 db cron status db9 db cron status ``` ## Execution Model [Section titled “Execution Model”](#execution-model) * Each job runs in its own temporary database connection with the job owner’s credentials * Commands auto-commit (no wrapping transaction) * Output and errors are captured in `return_message` * The worker polls for due jobs every 60 seconds * A claim guard prevents the same job from running twice in the same minute ### Timeouts [Section titled “Timeouts”](#timeouts) | Timeout | Default | Description | | --------------------- | ---------- | ----------------------------------------------------- | | Job execution timeout | 30 minutes | Global default; overridable per job via `max_runtime` | | Orphan recovery | 5 minutes | Running jobs with no heartbeat are marked failed | If a job exceeds its timeout, it is terminated and marked `failed` with the message “orphan recovery: execution timed out”. ## Permissions [Section titled “Permissions”](#permissions) | Operation | Non-superuser | Superuser | | ---------------------- | ----------------- | -------------------------- | | Schedule jobs | Own jobs | Any | | View jobs (`cron.job`) | Own jobs only | All | | Alter jobs | Own jobs only | Any; can also change owner | | Unschedule jobs | Own jobs only | Any | | Cancel running jobs | No | Yes | | View running jobs | No (empty result) | Yes | | View execution history | Own runs only | All | ## Practical Patterns [Section titled “Practical Patterns”](#practical-patterns) ### Cleanup old data [Section titled “Cleanup old data”](#cleanup-old-data) SQL ```sql SELECT cron.schedule('cleanup-logs', '0 3 * * 1', $$ DELETE FROM audit_logs WHERE created_at < now() - interval '90 days' $$); ``` ### Periodic API polling with HTTP [Section titled “Periodic API polling with HTTP”](#periodic-api-polling-with-http) SQL ```sql SELECT cron.schedule('sync-rates', '0 * * * *', $$ INSERT INTO exchange_rates (fetched_at, data) SELECT now(), (http_get('https://api.example.com/rates')->>'content')::jsonb $$); ``` ### Write periodic reports to fs9 [Section titled “Write periodic reports to fs9”](#write-periodic-reports-to-fs9) SQL ```sql SELECT cron.schedule('daily-metrics', '0 6 * * *', $$ SELECT extensions.fs9_write( '/reports/metrics-' || to_char(now(), 'YYYY-MM-DD') || '.csv', (SELECT string_agg(metric || ',' || value::text, E'\n') FROM app_metrics WHERE ts > now() - interval '1 day') ) $$); ``` ### Refresh a materialized view [Section titled “Refresh a materialized view”](#refresh-a-materialized-view) SQL ```sql SELECT cron.schedule('refresh-mv', '*/30 * * * *', 'REFRESH MATERIALIZED VIEW mv_dashboard_stats'); ``` ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ---------------------------- | -------------- | | Max jobs per database | 50 | | Max concurrent jobs (global) | 32 | | Cron expression fields | 5 (no seconds) | | Default job timeout | 30 minutes | | Orphan recovery timeout | 5 minutes | | Execution history retention | 7 days | | Worker poll interval | 60 seconds | See [Limits and Quotas](/docs/platform/limits-and-quotas/) for the complete list. ## Error Messages [Section titled “Error Messages”](#error-messages) | Error | Cause | | ------------------------------------------------------------------------------- | ------------------------------------ | | `extension pg_cron is not installed` | Run `CREATE EXTENSION pg_cron` first | | `invalid cron expression: 6-field expressions (with seconds) are not supported` | Use 5-field syntax | | `invalid cron expression: special strings like '@daily' are not supported` | Use `0 0 * * *` instead | | `cron.schedule: {arg} must not be NULL` | Required argument is NULL | | `cron.unschedule: must be superuser or owner of the job` | Permission denied | | `cron.cancel: must be superuser` | Only superusers can cancel | | `cron.alter_job: cross-database scheduling not supported` | `database` arg must be NULL | ## Scheduling Serverless Functions [Section titled “Scheduling Serverless Functions”](#scheduling-serverless-functions) pg\_cron can also be used to schedule [Serverless Functions](/docs/functions/configuration/#cron-scheduling) on a recurring basis — trigger a deployed function from a cron job instead of (or in addition to) running SQL directly. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Scheduled Jobs with pg\_cron](/docs/guides/scheduled-jobs-with-pg-cron/) — Tutorial: create, monitor, and manage periodic jobs * [HTTP from SQL](/docs/guides/http-from-sql/) — Call external APIs from SQL — combine with cron for polling * [fs9 — File System](/docs/extensions/fs9/) — Write periodic reports to the file system * [Extensions Overview](/docs/extensions/) — All 9 built-in extensions * [Limits and Quotas](/docs/platform/limits-and-quotas/) — All operational limits * [CLI Reference](/docs/cli/) — db9 db cron command details # uuid-ossp > Generate universally unique identifiers (UUIDs) in SQL — v4 random and v7 time-ordered. Generate universally unique identifiers (UUIDs) in SQL. ## Built-in (No Extension Required) [Section titled “Built-in (No Extension Required)”](#built-in-no-extension-required) `uuid_generate_v4()` is available by default without any extension: SQL ```sql -- Generate a random UUID (v4) SELECT uuid_generate_v4(); -- Result: 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' -- Use in table definition CREATE TABLE uuid_demo_users ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, name TEXT ); -- Insert without specifying id INSERT INTO uuid_demo_users (name) VALUES ('Alice'); ``` ▶ Run ## Extension (PostgreSQL Compatibility) [Section titled “Extension (PostgreSQL Compatibility)”](#extension-postgresql-compatibility) For full PostgreSQL compatibility, you can also install the extension: SQL ```sql CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; ``` ▶ Run ## Available Functions [Section titled “Available Functions”](#available-functions) | Function | Description | | -------------------- | --------------------------------------------------- | | `uuid_generate_v4()` | Random UUID v4 (most common) | | `gen_random_uuid()` | Alias for uuid\_generate\_v4() | | `uuidv7()` | Time-ordered UUID v7 (recommended for primary keys) | ## UUID v7 (Time-Ordered) [Section titled “UUID v7 (Time-Ordered)”](#uuid-v7-time-ordered) UUID v7 embeds a timestamp, making it ideal for primary keys where natural ordering by creation time is desired: SQL ```sql -- Generate UUID v7 SELECT uuidv7(); -- Result: '018e1c6a-5b00-7abc-8def-1234567890ab' -- UUIDs are naturally sorted by creation time CREATE TABLE uuid_demo_events ( id UUID DEFAULT uuidv7() PRIMARY KEY, name TEXT, created_at TIMESTAMP DEFAULT NOW() ); -- Insert multiple rows INSERT INTO uuid_demo_events (name) VALUES ('first'), ('second'), ('third'); -- UUIDs are already in chronological order SELECT id, name FROM uuid_demo_events ORDER BY id; ``` ▶ Run **Why UUID v7 over v4?** * **Time-ordered**: Better for B-tree index performance * **Sortable**: Natural chronological ordering without timestamp column * **Unique**: Still globally unique like v4 ## Common Patterns [Section titled “Common Patterns”](#common-patterns) SQL ```sql -- Generate multiple UUIDs SELECT uuid_generate_v4() FROM generate_series(1, 5); -- UUID as text SELECT uuid_generate_v4()::text; -- Check if valid UUID SELECT 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid; ``` ▶ Run ## Next Steps [Section titled “Next Steps”](#next-steps) * [Extensions Overview](/docs/extensions/) — All 9 built-in extensions * [SQL Reference](/docs/sql/) — SQL engine compatibility and data types * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — Full PostgreSQL compatibility surface # Vector Search > Store vector embeddings, build HNSW indexes, and run k-NN similarity search — with optional built-in embedding generation directly in SQL. DB9 provides pgvector-compatible vector storage and HNSW indexing, plus a built-in embedding service that generates vectors directly in SQL without external API calls. Two extensions work together: * **vector** — the `VECTOR` type, distance operators, and HNSW indexes * **embedding** — the `embedding()` function for server-side text embedding ## Installation [Section titled “Installation”](#installation) SQL ```sql CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS embedding; -- optional: enables EMBEDDING() function ``` ▶ Run Both extensions require superuser privileges (the default `admin` role). ## VECTOR Type [Section titled “VECTOR Type”](#vector-type) Store fixed-dimension vectors using the `VECTOR(n)` type: SQL ```sql CREATE TABLE documents ( id SERIAL PRIMARY KEY, content TEXT NOT NULL, embedding VECTOR(1024) ); ``` Vectors can be inserted as text literals or cast from arrays: SQL ```sql -- Text literal INSERT INTO documents (content, embedding) VALUES ('hello world', '[0.1, 0.2, 0.3, ...]'); -- Array cast INSERT INTO documents (content, embedding) VALUES ('hello world', ARRAY[0.1, 0.2, 0.3]::vector); ``` Vectors are stored as 32-bit floats (`float4`) internally, matching the pgvector storage format. Each element keeps roughly 7 significant decimal digits — e.g. `[0.123456789]` reads back as `[0.12345679]`. Dimensions must match the column definition for all operations. ## Distance Operators [Section titled “Distance Operators”](#distance-operators) | Operator | Metric | Function equivalent | | -------- | ----------------------- | ------------------------------------- | | `<->` | L2 (Euclidean) distance | `l2_distance(a, b)` | | `<=>` | Cosine distance | `cosine_distance(a, b)` | | `<#>` | Negative inner product | `vector_negative_inner_product(a, b)` | All operators return `FLOAT8`. Both operands must have the same number of dimensions. SQL ```sql -- Cosine similarity search SELECT content, embedding <=> '[0.1, 0.2, ...]'::vector AS distance FROM documents ORDER BY distance LIMIT 5; ``` Cosine distance returns 0 for identical vectors and approaches 2 for opposite vectors. If either vector has zero norm, it returns `NaN` (cosine similarity is undefined for a zero vector). The `<#>` operator returns the **negative** dot product (pgvector convention), so lower values indicate higher similarity — `'[1,2,3]' <#> '[4,5,6]'` is `-32`. The `inner_product(a, b)` function returns the plain (positive) dot product — `inner_product('[1,2,3]', '[4,5,6]')` is `32`. Use the `<#>` operator (or `vector_negative_inner_product`) for distance ordering, and `inner_product` when you want the raw dot product. ## Distance and Utility Functions [Section titled “Distance and Utility Functions”](#distance-and-utility-functions) | Function | Signature | Description | | ------------------------------------- | --------------------------- | ---------------------------------------------- | | `l2_distance(a, b)` | `(VECTOR, VECTOR) → FLOAT8` | Euclidean distance | | `cosine_distance(a, b)` | `(VECTOR, VECTOR) → FLOAT8` | Cosine distance (1 - cosine similarity) | | `inner_product(a, b)` | `(VECTOR, VECTOR) → FLOAT8` | Inner product (dot product), e.g. `32` | | `vector_negative_inner_product(a, b)` | `(VECTOR, VECTOR) → FLOAT8` | Negative dot product; backs the `<#>` operator | | `vector_dims(v)` | `(VECTOR) → INT4` | Number of dimensions | | `vector_norm(v)` | `(VECTOR) → FLOAT8` | L2 norm (magnitude) | | `l2_normalize(v)` | `(VECTOR) → VECTOR` | Unit vector (v / ‖v‖) | SQL ```sql SELECT vector_dims(embedding) FROM documents LIMIT 1; -- Returns: 1024 SELECT vector_norm('[3, 4]'::vector); -- Returns: 5.0 SELECT l2_normalize('[3, 4]'::vector); -- Returns: [0.6, 0.8] ``` ## HNSW Indexes [Section titled “HNSW Indexes”](#hnsw-indexes) HNSW (Hierarchical Navigable Small World) indexes enable fast approximate nearest-neighbor search. Without an index, distance queries require a full table scan. Queries fall back to an exact sequential scan when no index is present, so all distance operators and functions (`<->`, `<=>`, `<#>`, `l2_distance`, `cosine_distance`, `inner_product`) return correct results either way — the index changes speed, not answers. HNSW index building is disabled in the current release HNSW is gated behind a server-side background-worker class that is **not enabled in production today**. The two connection paths fail differently, and neither gives you a working index: * **Over the PostgreSQL wire protocol (`psql`, ORMs, drivers)** — `CREATE INDEX ... USING hnsw` is rejected outright with `55000`: `ERROR: feature "hnsw_index" is unavailable (DisabledByConfiguration)` * **Over the HTTP SQL API** — the statement reports `CREATE INDEX` and the index shows up in `pg_indexes`, but it is never used: `EXPLAIN` still returns a `Seq Scan` plan after creating it. Treat this success as cosmetic. If the table does not meet HNSW’s structural requirements — see below; the most common miss is a table with **no single-column primary key** — you get a bare `XX000` (`internal error`) instead, on both paths, and nothing is created. That error names neither HNSW nor the primary key, so check the table shape before concluding the feature is broken in some other way. `DB9_BG_LEDGER_ACTIVE_CLASSES` is a server environment variable, not a session GUC, so it cannot be turned on from SQL. **This affects speed, not correctness.** Exact search still returns correct results at any table size — keep using the distance operators and `ORDER BY ... LIMIT` as shown below. The rest of this section documents HNSW for when the worker class is enabled. ### Creating an index [Section titled “Creating an index”](#creating-an-index) SQL ```sql -- Cosine distance (most common for text embeddings) CREATE INDEX idx_docs_embedding ON documents USING hnsw (embedding vector_cosine_ops); -- L2 distance CREATE INDEX idx_docs_embedding ON documents USING hnsw (embedding vector_l2_ops); -- Inner product CREATE INDEX idx_docs_embedding ON documents USING hnsw (embedding vector_ip_ops); ``` ### Tuning parameters [Section titled “Tuning parameters”](#tuning-parameters) SQL ```sql -- Custom build parameters CREATE INDEX idx_docs_embedding ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); ``` | Parameter | Default | Description | | ----------------- | ------- | -------------------------------------------------------------------------------------- | | `m` | 16 | Connections per layer. Higher = better recall, more memory. Range: 4–32. | | `ef_construction` | 64 | Search width during build. Higher = better index quality, slower build. Range: 40–512. | ### Search-time tuning [Section titled “Search-time tuning”](#search-time-tuning) SQL ```sql -- Increase search accuracy (higher = better recall, slower) SET hnsw.ef_search = 100; -- Query uses the index automatically SELECT content, embedding <=> query_vec AS distance FROM documents ORDER BY distance LIMIT 10; ``` The default `ef_search` is 40. Higher values improve recall at the cost of latency. ### When the index is used [Section titled “When the index is used”](#when-the-index-is-used) The optimizer uses an HNSW index when all of these are true: * The query has `ORDER BY distance_function(vector_column, constant) ASC` * A `LIMIT k` clause is present * An HNSW index exists on the vector column with the matching distance metric * The index is in `Ready` state SQL ```sql -- This uses the HNSW index (ORDER BY + LIMIT + matching metric) SELECT * FROM documents ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector LIMIT 10; -- This does NOT use the index (no LIMIT) SELECT * FROM documents ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector; ``` ### Index constraints [Section titled “Index constraints”](#index-constraints) Which of these errors you actually see today depends on the transport These constraints describe HNSW as it behaves when the worker class is enabled. While the gate is on, the wire protocol stops early and the HTTP SQL API keeps validating, so the two paths report different things: | Violation | PostgreSQL wire protocol | HTTP SQL API | | ----------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------- | | Multi-column index | `0A000` — `access method "hnsw" does not support multicolumn indexes` | same | | No PK, composite PK, or partial index (`WHERE`) | bare `XX000` (`internal error`) | same | | Negative `INTEGER`/`BIGINT` PK | `55000` — the gate fires before the check | `22023` — the named message below **is** returned | | Valid table | `55000` — `feature "hnsw_index" is unavailable (DisabledByConfiguration)` | `CREATE INDEX`, and a phantom index the planner never uses | A bare `XX000` means you tripped one of the structural requirements below — that holds on both paths. `55000` is pgwire-only and means the opposite: the table is fine and only the gate is stopping you. * Single vector column only — `access method "hnsw" does not support multicolumn indexes` * The indexed column must declare its dimensions (`VECTOR(1024)`, not bare `VECTOR`). Declare the dimension when you create the table: SQL ```sql -- Works CREATE TABLE documents (id SERIAL PRIMARY KEY, embedding VECTOR(1024)); CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops); -- Cannot be indexed: no declared dimension CREATE TABLE bad (id SERIAL PRIMARY KEY, embedding VECTOR); CREATE INDEX ON bad USING hnsw (embedding vector_cosine_ops); ``` * The table must have a **single-column primary key**. A table with no primary key or a composite primary key is rejected with `HNSW indexes require a single-column primary key`. * If the primary key is `INTEGER` or `BIGINT`, its values must be **non-negative**. A negative key present at build time fails the `CREATE INDEX`; inserting one afterwards fails the `INSERT` with `HNSW index requires non-negative INTEGER primary key`. `UUID` and `TEXT` primary keys have no such restriction. * No partial indexes — a `WHERE` clause is rejected with `HNSW indexes do not support partial index predicates (WHERE clause)` * No UNIQUE constraint on vector columns * `IVFFlat` is not available at all (`access method "ivfflat" does not exist`); HNSW is the only vector index type. ## Built-in Embedding Generation [Section titled “Built-in Embedding Generation”](#built-in-embedding-generation) The `embedding` extension provides server-side text embedding. The default model is `text-embedding-v4` with 1024 dimensions. ### embedding() [Section titled “embedding()”](#embedding) SQL ```sql -- Default model and dimensions SELECT embedding('hello world'); -- Custom model SELECT embedding('hello world', 'text-embedding-v4'); -- Custom dimensions: use the embedding.dimensions session setting instead -- of the 3-argument form (see caution below) SET embedding.dimensions = 512; SELECT embedding('hello world'); ``` ▶ Run | Argument | Type | Required | Description | | ---------- | ---- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | text | TEXT | Yes | Text to embed (must not be empty) | | model | TEXT | No | Model name (default: server-configured) | | dimensions | INT | — | Wire protocol only — passing a third argument drops the connection over the HTTP SQL API. Set `embedding.dimensions` instead for portable code (see below). | Returns `VECTOR`. Requires superuser privileges. Only 256, 512 and 1024 dimensions are accepted Any other value fails with a bare `XX000` (`internal error`) that does not mention dimensions. This applies on **both** transports via the `embedding.dimensions` session setting, and on the wire protocol via the third argument. (Over the HTTP SQL API the third argument drops the connection regardless of the width you ask for — see the caution below.) SQL ```sql SELECT embedding('hello', 'text-embedding-v4', 768); -- XX000 internal error SET embedding.dimensions = 768; -- accepted SELECT embedding('hello'); -- XX000 internal error ``` `768` is a common embedding width elsewhere, so this is easy to trip over. The default is `1024`. 3-argument form works only over the wire protocol Calling `embedding()` with all three arguments (text, model, dimensions) works over the PostgreSQL wire protocol (`psql`, ORMs, drivers), but **drops the connection over the HTTP SQL API** — which is what `db9 db sql` uses: ```plaintext SELECT embedding('hello world', 'text-embedding-v4', 512); -- pgwire: returns a 512-dimension VECTOR -- HTTP: error: connection closed ``` For code that must work on both, use the 2-argument form (`embedding(text, model)`) plus the `embedding.dimensions` session setting instead, as shown above — that path works over both transports. ### embed\_text() [Section titled “embed\_text()”](#embed_text) An alternative with explicit model selection: SQL ```sql SELECT embed_text('text-embedding-v4', 'hello world'); SELECT embed_text('text-embedding-v4', 'hello world', '{"dimensions": 512}'); ``` ▶ Run The third argument is a JSON string with options. ### Auto-embedding distance functions [Section titled “Auto-embedding distance functions”](#auto-embedding-distance-functions) These functions embed text and compute distance in one call — useful for search queries where you don’t want to manage the embedding step: SQL ```sql -- Embed query text and compute cosine distance against stored vectors SELECT content FROM documents ORDER BY vec_embed_cosine_distance(embedding, 'database for AI agents') LIMIT 5; ``` ▶ Run | Function | Equivalent to | | -------------------------------------- | --------------------------------------- | | `vec_embed_l2_distance(vec, text)` | `l2_distance(vec, embedding(text))` | | `vec_embed_cosine_distance(vec, text)` | `cosine_distance(vec, embedding(text))` | | `vec_embed_inner_product(vec, text)` | `inner_product(vec, embedding(text))` | These functions work with HNSW index scans — the embedding is computed once at plan time, not per row. ### Embedding usage tracking [Section titled “Embedding usage tracking”](#embedding-usage-tracking) SQL ```sql SELECT * FROM extensions.embedding_usage(); ``` Returns a single row with: * `tokens_used` (BIGINT) — cumulative tokens consumed today (UTC) * `resets_at` (TIMESTAMPTZ) — next UTC midnight ### Session configuration [Section titled “Session configuration”](#session-configuration) Override the server-configured embedding settings per session: SQL ```sql SET embedding.provider = 'openai'; SET embedding.model = 'text-embedding-3-small'; SET embedding.api_key = 'sk-...'; SET embedding.dimensions = 1536; ``` | Setting | Default | Description | | ----------------------- | ------------------- | ------------------------------------- | | `embedding.provider` | `openai` | API provider (`openai` or `bedrock`) | | `embedding.endpoint` | (server-configured) | API endpoint URL | | `embedding.api_key` | (server-configured) | API key | | `embedding.model` | `text-embedding-v4` | Model name | | `embedding.dimensions` | `1024` | Output dimensions | | `embedding.max_calls` | `100` | Max embedding calls per SQL statement | | `embedding.concurrency` | `5` | Max concurrent API calls per tenant | | `hnsw.ef_search` | `40` | HNSW search expansion factor | ## Document Chunking with CHUNK\_TEXT [Section titled “Document Chunking with CHUNK\_TEXT”](#document-chunking-with-chunk_text) For long documents, split text into overlapping chunks before embedding using the built-in `CHUNK_TEXT` table-valued function: SQL ```sql -- Chunk a document and embed each chunk INSERT INTO chunks (doc_id, chunk_index, chunk_text, embedding) SELECT 1, c.chunk_index, c.chunk_text, embedding(c.chunk_text) FROM CHUNK_TEXT( content => (SELECT content FROM documents WHERE id = 1), max_chars => 1500, overlap_chars => 200, title => (SELECT title FROM documents WHERE id = 1) ) AS c; ``` `CHUNK_TEXT` is markdown-aware — it prefers to break at paragraph boundaries, headings, and list items. See [CHUNK\_TEXT](/docs/extensions/chunk-text/) for full reference. ## End-to-End Example [Section titled “End-to-End Example”](#end-to-end-example) SQL ```sql -- Setup CREATE EXTENSION vector; CREATE EXTENSION embedding; -- Create table CREATE TABLE docs ( id SERIAL PRIMARY KEY, content TEXT NOT NULL, vec VECTOR(1024) ); -- Insert with server-side embedding INSERT INTO docs (content, vec) VALUES ('PostgreSQL is a relational database', embedding('PostgreSQL is a relational database')), ('DB9 provides serverless Postgres for AI agents', embedding('DB9 provides serverless Postgres for AI agents')), ('Vector search finds similar documents', embedding('Vector search finds similar documents')); -- Create HNSW index — see the caution above: this is rejected over the wire -- protocol (55000) and creates an unused index over the HTTP SQL API. -- The semantic search below works either way, via exact search. CREATE INDEX idx_docs_vec ON docs USING hnsw (vec vector_cosine_ops); -- Semantic search SELECT content, vec <=> embedding('database for AI')::vector AS distance FROM docs ORDER BY distance LIMIT 3; -- Or use the auto-embedding shortcut SELECT content FROM docs ORDER BY vec_embed_cosine_distance(vec, 'database for AI') LIMIT 3; ``` ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ----------------------------------------- | ------------------------------------- | | Embedding calls per statement | 100 (configurable) | | Concurrent embedding API calls per tenant | 5 (configurable) | | HNSW default m | 16 | | HNSW default ef\_construction | 64 | | HNSW default ef\_search | 40 | | Embedding token usage | Tracked daily, resets at UTC midnight | ## Caveats [Section titled “Caveats”](#caveats) HNSW index requirements HNSW indexes require a **single-column primary key**. Tables with no primary key or a composite primary key are rejected. `UUID` and `TEXT` primary keys are supported; if the key is `INTEGER` or `BIGINT`, every value must be **non-negative** — a negative key fails the `CREATE INDEX`, and once the index exists it also fails the `INSERT`. Plan your schema accordingly. Embedding requires superuser The `embedding()` and `embed_text()` functions require superuser privileges. Regular database users cannot call them directly. Use a `SECURITY DEFINER` wrapper function to expose embedding to non-superuser roles. * **IVFFlat is not supported.** Only HNSW indexes are available for vector similarity search. * **Embedding requires superuser.** Regular database users cannot call `embedding()` or `embed_text()`. * **HNSW requires a single-column primary key.** `UUID` and `TEXT` keys work; `INTEGER`/`BIGINT` keys must be non-negative. No-PK and composite-PK tables are rejected. * **HNSW requires a dimensioned column.** Index the column as `VECTOR(n)`; a bare `VECTOR` column drops the connection at `CREATE INDEX`. * **No partial HNSW indexes.** WHERE clauses on HNSW index creation are not supported. * **Embedding results are cached** per (model, dimensions, text) within a statement to avoid redundant API calls. * **Distance operators require matching dimensions.** Comparing vectors of different sizes raises an error. ## Next Steps [Section titled “Next Steps”](#next-steps) * [CHUNK\_TEXT](/docs/extensions/chunk-text/) — Split documents into overlapping chunks for RAG * [RAG with Built-in Embeddings](/docs/guides/rag-with-built-in-embeddings/) — Tutorial: build a complete RAG pipeline * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — Full PostgreSQL compatibility surface * [Extensions Overview](/docs/extensions/) — All 9 built-in extensions * [Limits and Quotas](/docs/platform/limits-and-quotas/) — All operational limits * [SQL Reference: Functions](/docs/sql/functions/) — Built-in function reference # Serverless Functions > Deploy and run JavaScript/TypeScript functions alongside your database — with native SQL access, filesystem scope, secrets, outbound HTTP, and cron scheduling. DB9 Functions let you deploy JavaScript or TypeScript code that runs **inside your database environment**. Unlike traditional serverless platforms where you connect to a database over the network, DB9 functions have direct, zero-latency access to your SQL engine and filesystem through the `ctx` object. index.js ```js module.exports = { handler: async (input, ctx) => { // Query your database — no connection strings, no drivers const stats = await ctx.db.query(` SELECT count(*) as tables FROM information_schema.tables WHERE table_schema = 'public' `); // Write a report to the filesystem — in the same function const report = [ `DB9 Function Report — ${new Date().toISOString()}`, `Tables in public schema: ${stats.rows[0][0]}`, `Input received: ${JSON.stringify(input)}`, `Run ID: ${ctx.self.runId}`, ].join("\n"); await ctx.fs9.write("/reports/status.txt", report); return { tables: Number(stats.rows[0][0]), reportSaved: true, runId: ctx.self.runId }; } }; ``` ▶ Run **What makes DB9 Functions different:** * **Native SQL access** — `ctx.db.query()` runs queries directly, no connection strings or drivers * **Filesystem built in** — `ctx.fs9` reads and writes files in the same function that queries your database * **No infrastructure** — deploy with one CLI command, no Dockerfiles or build pipelines * **Secrets management** — bind API keys and tokens without hardcoding them ## Quick Start [Section titled “Quick Start”](#quick-start) ### 1. Write a function [Section titled “1. Write a function”](#1-write-a-function) hello.js ```js module.exports = { handler: async (input, ctx) => { const greeting = `Hello, ${input?.name || "world"}!`; // Query the database to show native SQL access const time = await ctx.db.query("SELECT NOW()::text as server_time"); return { greeting, serverTime: time.rows[0][0], runId: ctx.self.runId }; } }; ``` ▶ Run ### 2. Deploy [Section titled “2. Deploy”](#2-deploy) Terminal ```bash db9 functions create hello --db myapp -f hello.js ``` ### 3. Invoke [Section titled “3. Invoke”](#3-invoke) Terminal ```bash db9 functions invoke hello --db myapp --payload '{"name":"Alice"}' --json ``` JSON ```json { "status": "succeeded", "result_json": "{\"greeting\":\"Hello, Alice!\",\"serverTime\":\"2026-04-01 17:41:31.458000\",\"runId\":\"5e0be3ff-...\"}" } ``` Note The entrypoint must be a named export called `handler`. The runtime uses CommonJS — use `module.exports = { handler: ... }`, not `export default`. ## How Functions Work [Section titled “How Functions Work”](#how-functions-work) ### Architecture [Section titled “Architecture”](#architecture) When you deploy a function, the CLI uploads your code to the DB9 API. Each deploy creates a new **version**. When you invoke a function, the runtime: 1. Loads the active version of your function code 2. Creates an isolated execution context with the `ctx` object 3. Calls your `handler(input, ctx)` with the parsed JSON payload 4. Captures the return value, console output, and execution metadata 5. Stores the result as a **run record** accessible via `history` and `logs` ### Runtime [Section titled “Runtime”](#runtime) Functions execute in a sandboxed JavaScript runtime. Each invocation is isolated — there is no shared memory or global state between runs. The runtime provides: * **CommonJS module system** — `module.exports`, `require` (built-ins only) * **Global `fetch()`** — outbound HTTP (requires allowlist configuration) * **Console API** — `console.log()`, `console.error()` captured as logs * **Standard globals** — `Date`, `JSON`, `Math`, `Promise`, `setTimeout`, `Buffer`, `TextEncoder`, `TextDecoder` ### Execution Role [Section titled “Execution Role”](#execution-role) Functions execute SQL as the `authenticated` role, not `admin`. This means: * Functions **cannot** access tables unless the `authenticated` role has been granted privileges * Functions **cannot** use extensions that require superuser (like fs9 from SQL) * Functions **can** use `ctx.fs9` — filesystem access is handled through the runtime API, not SQL Before deploying functions that read or write tables, grant access from the `admin` role: SQL ```sql -- Grant access to all current tables GRANT ALL ON ALL TABLES IN SCHEMA public TO authenticated; -- Required for functions that call nextval()/currval() directly (see note below) GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO authenticated; -- Grant access to future tables created by admin ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO authenticated; ``` ▶ Run Sequences behave differently from tables `ALTER DEFAULT PRIVILEGES ... ON SEQUENCES` is accepted, but it only applies to sequences you create explicitly with `CREATE SEQUENCE`. Sequences created implicitly by a `SERIAL` / `BIGSERIAL` column do not inherit default privileges. **Sequence privileges are enforced.** A function that calls `nextval()`, `currval()`, or `setval()` directly needs a grant on that sequence, or it fails with `permission denied for sequence` (`42501`). `setval()` additionally requires `UPDATE`. Inserting into a `SERIAL` / `BIGSERIAL` column is the exception — the column default resolves without a sequence grant, so functions that only insert need the table grant alone. Tip If your function creates its own tables, those tables are owned by `authenticated` and accessible by default. The grant is only needed for tables created by the `admin` role (e.g., through `db9 db sql` or migrations). ## Writing Functions [Section titled “Writing Functions”](#writing-functions) ### JavaScript [Section titled “JavaScript”](#javascript) index.js ```js module.exports = { handler: async (input, ctx) => { const result = await doWork(input); return { status: "ok", result }; } }; ``` ### TypeScript [Section titled “TypeScript”](#typescript) index.ts ```ts module.exports = { handler: async (input: Record, ctx: any) => { const value: number = 42; return { result: value * 2, type: typeof value }; } }; ``` The CLI auto-detects `.ts` files and transpiles them before deploying. Type annotations are stripped — there is no type checking at deploy time. To pipe TypeScript from stdin: Terminal ```bash cat handler.ts | db9 functions create my-func --db myapp --ts ``` ### Function Signature [Section titled “Function Signature”](#function-signature) ```plaintext handler(input: object | null, ctx: Context) => Promise | object ``` | Parameter | Type | Description | | ---------- | ---------------- | ------------------------------------------------------------------------------------------------ | | `input` | `object \| null` | Parsed JSON from `--payload` (CLI) or `input` field (API). `null` if no payload provided. | | `ctx` | `Context` | Runtime context: `ctx.db`, `ctx.fs9`, `ctx.self`. See [Runtime & ctx](/docs/functions/runtime/). | | **Return** | `object` | Any JSON-serializable value. Stored as `result_json` in the run record. | If the function throws, the run status is `failed` with `error_code: "execution_error"` and the error message captured in `error_message`. ### Logging [Section titled “Logging”](#logging) Use `console.log()` and `console.error()` inside your function. Output is captured and retrievable: Terminal ```bash db9 functions logs my-func --db myapp ``` ## Deploying [Section titled “Deploying”](#deploying) ### Create a function [Section titled “Create a function”](#create-a-function) Terminal ```bash # From a file db9 functions create my-func --db myapp -f index.js # From stdin (pipe bundled output) cat dist/index.js | db9 functions create my-func --db myapp -f - # With TypeScript db9 functions create my-func --db myapp -f index.ts # With filesystem access db9 functions create my-func --db myapp -f index.js \ --fs9-scope /data:ro --fs9-scope /output:rw # With secrets db9 functions create my-func --db myapp -f index.js \ --secret API_KEY=my_api_key # With execution limits db9 functions create my-func --db myapp -f index.js \ --timeout 60000 \ --limits-json '{"timeout_ms":60000,"memory_mb":256}' ``` ### Update a function [Section titled “Update a function”](#update-a-function) Each update creates a new version. The active version is updated immediately. Terminal ```bash db9 functions update my-func --db myapp -f index.js db9 functions update my-func --db myapp --timeout 60000 db9 functions update my-func --db myapp --secret NEW_KEY=new_secret ``` ### List functions [Section titled “List functions”](#list-functions) Terminal ```bash db9 functions list --database myapp db9 functions list --database myapp --json ``` CLI flag inconsistency `functions list` requires `--database` (long form). Other functions subcommands accept `--db` (short form). This is a known CLI inconsistency. ## Invoking [Section titled “Invoking”](#invoking) ### Via CLI [Section titled “Via CLI”](#via-cli) Terminal ```bash db9 functions invoke my-func --db myapp db9 functions invoke my-func --db myapp --payload '{"key":"value"}' db9 functions invoke my-func --db myapp --json ``` The response includes the run status, result, and version: JSON ```json { "function_id": "1245b49d-...", "run_id": "d3dc72c8-...", "status": "succeeded", "result_json": "{\"message\":\"hello\"}", "version_id": "7df52fc3-..." } ``` ### Via REST API [Section titled “Via REST API”](#via-rest-api) Terminal ```bash curl -X POST \ -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"input": {"key": "value"}}' \ https://api.db9.ai/customer/databases/{database_id}/functions/{function_id}/invoke ``` See [REST API — Functions](/docs/api/#functions) for the full endpoint reference. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Runtime & ctx](/docs/functions/runtime/) — ctx.db, ctx.fs9, ctx.self — the function runtime API * [Configuration](/docs/functions/configuration/) — Secrets, fs9 scope, network, cron, limits * [Examples](/docs/functions/examples/) — Real-world patterns: webhooks, ETL, APIs * [Troubleshooting](/docs/functions/troubleshooting/) — Errors, logs, debugging, limitations * [CLI — Functions](/docs/cli/#functions) — Full CLI command reference * [REST API — Functions](/docs/api/#functions) — Deploy, invoke, and manage via API # Configuration > Secrets, filesystem scope, network access, cron scheduling, versioning, and execution limits for DB9 serverless functions. ## Secrets [Section titled “Secrets”](#secrets) Secrets let you pass sensitive values (API keys, tokens, connection strings) to functions without embedding them in code. ### Create a secret [Section titled “Create a secret”](#create-a-secret) Terminal ```bash # From stdin (recommended for CI/CD) echo -n "sk-abc123" | db9 functions secrets set MY_API_KEY --db myapp --value-stdin # Inline (visible in shell history — not recommended) db9 functions secrets set MY_API_KEY --db myapp --value "sk-abc123" ``` ### Bind secrets to a function [Section titled “Bind secrets to a function”](#bind-secrets-to-a-function) The `--secret` flag maps an **alias** (used in your code) to a **secret name** (stored in the database): Terminal ```bash db9 functions create my-func --db myapp -f index.js \ --secret API_KEY=MY_API_KEY \ --secret DB_TOKEN=external_db_token ``` ### Access secrets in code [Section titled “Access secrets in code”](#access-secrets-in-code) JavaScript ```js module.exports = { handler: async (input, ctx) => { const apiKey = ctx.secrets.get("API_KEY"); const response = await fetch("https://api.example.com/data", { headers: { "Authorization": `Bearer ${apiKey}` }, }); return await response.json(); } }; ``` ### Manage secrets [Section titled “Manage secrets”](#manage-secrets) Terminal ```bash # List secrets (names only — values are never shown) db9 functions secrets list --db myapp # Update (set uses create-or-update semantics) db9 functions secrets set MY_API_KEY --db myapp --value "new-value" # Delete db9 functions secrets delete MY_API_KEY --db myapp ``` Account requirement Secrets require a claimed (non-anonymous) account. Run `db9 claim` to upgrade your account before using secrets. ## Filesystem Access (fs9) [Section titled “Filesystem Access (fs9)”](#filesystem-access-fs9) Functions can read and write files in the database’s filesystem. Access is controlled by `--fs9-scope` flags at deploy time: Terminal ```bash db9 functions create my-func --db myapp -f index.js \ --fs9-scope /data:ro \ --fs9-scope /output:rw ``` | Mode | Description | | ---- | ---------------------------------------------- | | `ro` | Read-only access to the path and its children | | `rw` | Read-write access to the path and its children | The `--fs9-scope` flags declare which paths your function intends to access and whether access is read-only or read-write. Currently, `ctx.fs9` calls succeed even without `--fs9-scope`, but you should always declare scopes explicitly — enforcement may be enabled in a future release. ### Reading files [Section titled “Reading files”](#reading-files) JavaScript ```js // Read as UTF-8 string const text = await ctx.fs9.read("/data/config.json"); const config = JSON.parse(text); // Read as base64 (for binary files like images, xlsx) const base64 = await ctx.fs9.readBase64("/data/image.png"); const buffer = Buffer.from(base64, "base64"); ``` ### Writing files [Section titled “Writing files”](#writing-files) JavaScript ```js await ctx.fs9.write("/output/report.csv", csvContent); await ctx.fs9.write("/output/data.json", JSON.stringify(results, null, 2)); ``` ### Listing and inspecting [Section titled “Listing and inspecting”](#listing-and-inspecting) JavaScript ```js const entries = await ctx.fs9.list("/data/"); // Returns: [{ path, type, size, mtime, ... }, ...] const stat = await ctx.fs9.stat("/data/file.txt"); // Returns: { path, type: "file", size: 1024, mtime: "2026-04-01T...", // generation: 1, mode: 420, sealed: false, storage: "inline" } ``` ### Deleting [Section titled “Deleting”](#deleting) JavaScript ```js await ctx.fs9.delete("/output/old-report.csv"); ``` ## Network Access (fetch) [Section titled “Network Access (fetch)”](#network-access-fetch) Functions have access to the global `fetch()` API for outbound HTTP requests. Network access is **blocked by default** — only URLs on the allowlist can be reached. JavaScript ```js module.exports = { handler: async (input, ctx) => { const response = await fetch("https://api.example.com/data", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: input.query }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } }; ``` If a URL is not in the allowlist, `fetch()` throws with: Output ```text EACCES: permission denied, connect 'https://...': url not in allowlist ``` Note The `network_allowlist` is currently configurable via the REST API only, not through the CLI. See [REST API — Functions](/docs/api/#functions) for the endpoint. ## Cron Scheduling [Section titled “Cron Scheduling”](#cron-scheduling) Functions can be invoked on a schedule using pg\_cron. This is useful for periodic data cleanup, report generation, and automated maintenance. ### Schedule via SQL [Section titled “Schedule via SQL”](#schedule-via-sql) Use `cron.schedule()` to invoke a function on a recurring schedule: SQL ```sql SELECT cron.schedule( 'nightly-cleanup', '0 3 * * *', $$SELECT http_post( 'https://api.db9.ai/customer/databases/{db_id}/functions/{function_id}/invoke', '{"input": {}}', 'application/json', jsonb_build_object('Authorization', 'Bearer ' || current_setting('app.api_token')) )$$ ); ``` ### Cron expression format [Section titled “Cron expression format”](#cron-expression-format) | Expression | Meaning | | ------------- | ---------------------------- | | `*/5 * * * *` | Every 5 minutes | | `0 * * * *` | Every hour | | `0 3 * * *` | Daily at 3:00 AM UTC | | `0 0 * * 0` | Weekly on Sunday at midnight | | `0 12 1 * *` | Monthly on the 1st at noon | See [Scheduled Jobs with pg\_cron](/docs/guides/scheduled-jobs-with-pg-cron/) for the complete scheduling guide. ## Versioning [Section titled “Versioning”](#versioning) Every `db9 functions update` creates a new **version**. The active version is updated immediately — there is no staged deployment. Terminal ```bash # Deploy version 1 db9 functions create my-func --db myapp -f v1.js # Deploy version 2 (replaces version 1 immediately) db9 functions update my-func --db myapp -f v2.js ``` Each run record stores the `version_id` that was used, so you can trace which version produced a given result: Terminal ```bash db9 functions history my-func --db myapp -n 5 --json ``` There is currently no built-in rollback command. To revert, re-deploy the previous version’s code: Terminal ```bash db9 functions update my-func --db myapp -f previous-version.js ``` ## Execution Limits [Section titled “Execution Limits”](#execution-limits) | Limit | Default | Max | Description | | ----------------------- | -------------- | --------------- | ---------------------------------- | | `timeout_ms` | 30,000 (30s) | 300,000 (5 min) | Maximum execution time | | `memory_mb` | System default | — | Memory limit | | `max_db_queries` | System default | — | SQL queries per invocation | | `max_db_rows` | System default | — | Total rows returned across queries | | `max_outbound_requests` | System default | — | Outbound HTTP requests | | `max_response_bytes` | System default | — | Max size per HTTP response | | `network_timeout_ms` | System default | — | Per-request network timeout | | `max_fs9_requests` | System default | — | fs9 API calls per invocation | | `max_fs9_read_bytes` | System default | — | Total bytes read from fs9 | | `max_fs9_write_bytes` | System default | — | Total bytes written to fs9 | Set limits via flags or JSON: Terminal ```bash # Convenience flag (timeout only) db9 functions create my-func --db myapp -f index.js --timeout 60000 # Full limits JSON db9 functions create my-func --db myapp -f index.js \ --limits-json '{"timeout_ms":60000,"memory_mb":256,"max_db_queries":100}' # From file db9 functions create my-func --db myapp -f index.js \ --limits-file ./limits.json ``` ## Visibility [Section titled “Visibility”](#visibility) Functions have a `visibility` field that defaults to `private`. This controls whether the function can be invoked via a publishable key (public endpoint) without authentication. Not yet in prod CLI The `--visibility` flag exists in source but is not available in CLI v2.1.1. Currently all functions are private and require a bearer token to invoke via the API. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Overview](/docs/functions/) — Quick start and deployment basics * [Runtime & ctx](/docs/functions/runtime/) — ctx.db, ctx.fs9, ctx.self API * [Examples](/docs/functions/examples/) — Real-world patterns * [Troubleshooting](/docs/functions/troubleshooting/) — Errors, logs, debugging # Examples > Real-world DB9 serverless function patterns — data transformation, webhooks, scheduled jobs, APIs, and external integrations. ## Data Transformation: XLSX to CSV [Section titled “Data Transformation: XLSX to CSV”](#data-transformation-xlsx-to-csv) Convert an uploaded spreadsheet to CSV using the [xlsx](https://www.npmjs.com/package/xlsx) package. This example requires [bundling with esbuild](/docs/functions/runtime/#bundling-with-esbuild). src/index.js ```js const XLSX = require("xlsx"); module.exports = { handler: async (input, ctx) => { const base64 = await ctx.fs9.readBase64(input.path); const workbook = XLSX.read(Buffer.from(base64, "base64"), { type: "buffer" }); const results = []; for (const sheetName of workbook.SheetNames) { const csv = XLSX.utils.sheet_to_csv(workbook.Sheets[sheetName]); const outPath = `/output/${sheetName}.csv`; await ctx.fs9.write(outPath, csv); results.push({ sheet: sheetName, path: outPath, rows: csv.split("\n").length }); } return { sheets: results }; } }; ``` Terminal ```bash npm run build cat dist/index.js | db9 functions create xlsx-to-csv --db myapp -f - \ --fs9-scope /data:ro --fs9-scope /output:rw db9 functions invoke xlsx-to-csv --db myapp \ --payload '{"path":"/data/report.xlsx"}' ``` See [c4pt0r/db9-function-xls-csv-tbl](https://github.com/c4pt0r/db9-function-xls-csv-tbl) for a complete working example. ## Webhook Handler [Section titled “Webhook Handler”](#webhook-handler) Accept external webhook payloads and store them in a table. webhook.js ```js module.exports = { handler: async (input, ctx) => { if (!input?.event || !input?.data) { throw new Error("Missing event or data field"); } await ctx.db.query( `INSERT INTO webhook_events (event_type, payload, source_ip, received_at) VALUES ($1, $2, $3, NOW())`, [input.event, JSON.stringify(input.data), input.source || "unknown"] ); const count = await ctx.db.query( "SELECT count(*) as total FROM webhook_events WHERE event_type = $1", [input.event] ); return { accepted: true, eventType: input.event, totalEvents: count.rows[0][0] }; } }; ``` Terminal ```bash db9 functions create webhook-handler --db myapp -f webhook.js ``` Invoke via the REST API from an external service: Terminal ```bash curl -X POST \ -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"input": {"event": "user.created", "data": {"userId": 42, "email": "alice@example.com"}}}' \ https://api.db9.ai/customer/databases/{db_id}/functions/{fn_id}/invoke ``` ## Scheduled Data Cleanup [Section titled “Scheduled Data Cleanup”](#scheduled-data-cleanup) Combine a function with pg\_cron to clean up stale data on a schedule. cleanup.js ```js module.exports = { handler: async (input, ctx) => { const sessions = await ctx.db.query( "DELETE FROM sessions WHERE expires_at < NOW() RETURNING id" ); const logs = await ctx.db.query( "DELETE FROM audit_logs WHERE created_at < NOW() - INTERVAL '90 days' RETURNING id" ); // Archive old events to fs9 const oldEvents = await ctx.db.query( `SELECT * FROM events WHERE created_at < NOW() - INTERVAL '30 days'` ); if (oldEvents.row_count > 0) { const csv = oldEvents.rows.map(row => row.join(",")).join("\n"); const archivePath = `/archives/events-${new Date().toISOString().slice(0, 10)}.csv`; await ctx.fs9.write(archivePath, csv); await ctx.db.query( "DELETE FROM events WHERE created_at < NOW() - INTERVAL '30 days'" ); } return { sessionsDeleted: sessions.row_count, logsDeleted: logs.row_count, eventsArchived: oldEvents.row_count }; } }; ``` Terminal ```bash db9 functions create cleanup --db myapp -f cleanup.js \ --fs9-scope /archives:rw --timeout 120000 ``` ## Analytics API Endpoint [Section titled “Analytics API Endpoint”](#analytics-api-endpoint) Build an analytics endpoint that aggregates data and returns formatted results. analytics.js ```js module.exports = { handler: async (input, ctx) => { const days = input?.days || 7; const [activity, topUsers, summary] = await Promise.all([ ctx.db.query(` SELECT date_trunc('day', created_at) as day, count(*) as events FROM events WHERE created_at >= NOW() - $1 * INTERVAL '1 day' GROUP BY 1 ORDER BY 1 `, [days]), ctx.db.query(` SELECT user_id, count(*) as actions FROM events WHERE created_at >= NOW() - $1 * INTERVAL '1 day' GROUP BY 1 ORDER BY 2 DESC LIMIT 10 `, [days]), ctx.db.query(` SELECT count(*) as total_events, count(DISTINCT user_id) as unique_users, min(created_at) as earliest, max(created_at) as latest FROM events WHERE created_at >= NOW() - $1 * INTERVAL '1 day' `, [days]) ]); return { period: `${days} days`, summary: { totalEvents: summary.rows[0]?.[0], uniqueUsers: summary.rows[0]?.[1], }, dailyActivity: activity.rows.map(([day, count]) => ({ day, count })), topUsers: topUsers.rows.map(([userId, actions]) => ({ userId, actions })), }; } }; ``` ## External API Integration with Secrets [Section titled “External API Integration with Secrets”](#external-api-integration-with-secrets) Call an external API using secrets for authentication. summarize.js ```js module.exports = { handler: async (input, ctx) => { const apiKey = ctx.secrets.get("OPENAI_KEY"); const doc = await ctx.db.query( "SELECT content FROM documents WHERE id = $1", [input.documentId] ); if (doc.row_count === 0) throw new Error("Document not found"); const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "gpt-4o-mini", messages: [ { role: "system", content: "Summarize the following document in 3 sentences." }, { role: "user", content: doc.rows[0][0] } ] }), }); const result = await response.json(); const summary = result.choices[0].message.content; await ctx.db.query( "UPDATE documents SET summary = $1, summarized_at = NOW() WHERE id = $2", [summary, input.documentId] ); return { documentId: input.documentId, summary }; } }; ``` Terminal ```bash db9 functions secrets set OPENAI_KEY --db myapp db9 functions create summarize --db myapp -f summarize.js \ --secret OPENAI_KEY=OPENAI_KEY ``` Note This example requires `https://api.openai.com` to be in the `network_allowlist`. Configure the allowlist via the REST API. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Overview](/docs/functions/) — Quick start and deployment basics * [Runtime & ctx](/docs/functions/runtime/) — ctx.db, ctx.fs9, ctx.self API * [Configuration](/docs/functions/configuration/) — Secrets, fs9 scope, network, cron, limits * [Troubleshooting](/docs/functions/troubleshooting/) — Errors, logs, debugging # Runtime & ctx > The DB9 function runtime context — ctx.db for SQL, ctx.fs9 for filesystem, ctx.self for metadata. Every function receives two arguments: `input` (the JSON payload) and `ctx` (the runtime context). The `ctx` object is the primary way functions interact with your database. JavaScript ```js module.exports = { handler: async (input, ctx) => { // ctx.db — SQL query interface // ctx.fs9 — filesystem read/write interface // ctx.self — metadata about the current function run } }; ``` ▶ Run ## ctx.self [Section titled “ctx.self”](#ctxself) Metadata about the current execution: | Property | Type | Description | | ------------- | -------- | -------------------------------------------------------- | | `functionId` | `string` | UUID of the function | | `versionId` | `string` | UUID of the active version being executed | | `runId` | `string` | UUID of the current run | | `triggerType` | `string` | How the function was invoked: `invoke`, `cron`, or `api` | JavaScript ```js module.exports = { handler: async (input, ctx) => { console.log(`Run ${ctx.self.runId} triggered via ${ctx.self.triggerType}`); return { runId: ctx.self.runId }; } }; ``` ▶ Run ## ctx.db [Section titled “ctx.db”](#ctxdb) SQL query interface. Has a single method: ### `ctx.db.query(sql, params?)` [Section titled “ctx.db.query(sql, params?)”](#ctxdbquerysql-params) Execute a SQL statement with optional parameterized values. JavaScript ```js const result = await ctx.db.query( "SELECT id, name, email FROM users WHERE active = $1 LIMIT $2", [true, 10] ); ``` ▶ Run **Parameters:** * `sql` — SQL string with `$1`, `$2`, etc. for parameter placeholders * `params` — optional array of parameter values **Returns** a `QueryResult`: | Field | Type | Description | | ----------- | --------------------- | ------------------------------------------------------------- | | `columns` | `Array<{name, type}>` | Column metadata | | `rows` | `unknown[][]` | Row tuples, ordered to match `columns` | | `row_count` | `number` | Number of rows affected or returned | | `command` | `string` | SQL command tag: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, etc. | Rows are returned as arrays of values (not objects). Map them yourself if you need named fields: JavaScript ```js module.exports = { handler: async (input, ctx) => { const result = await ctx.db.query("SELECT id, name, email FROM users"); const users = result.rows.map(([id, name, email]) => ({ id, name, email })); return { users, total: result.row_count }; } }; ``` ▶ Run ## SQL Access Patterns [Section titled “SQL Access Patterns”](#sql-access-patterns) ### Read data [Section titled “Read data”](#read-data) JavaScript ```js module.exports = { handler: async (input, ctx) => { const result = await ctx.db.query( "SELECT id, title, status FROM tasks WHERE assignee = $1 ORDER BY created_at DESC", [input.userId] ); return { tasks: result.rows.map(([id, title, status]) => ({ id, title, status })), count: result.row_count }; } }; ``` ### Write data [Section titled “Write data”](#write-data) JavaScript ```js module.exports = { handler: async (input, ctx) => { await ctx.db.query( "INSERT INTO events (type, payload, created_at) VALUES ($1, $2, NOW())", [input.eventType, JSON.stringify(input.data)] ); return { recorded: true }; } }; ``` ▶ Run ### Transactions [Section titled “Transactions”](#transactions) Transactions are not currently supported The runtime auto-commits each `ctx.db.query()` call individually. `BEGIN`, `COMMIT`, and `ROLLBACK` are accepted without error but have no effect — rolled-back statements still persist. Do not rely on transactions for atomicity. If you need multi-statement atomicity, use a single SQL statement (e.g., a CTE with `INSERT ... SELECT`) or handle compensation logic in your application code. ### Aggregations [Section titled “Aggregations”](#aggregations) JavaScript ```js module.exports = { handler: async (input, ctx) => { const result = await ctx.db.query(` SELECT date_trunc('day', created_at) as day, count(*) as total, count(DISTINCT user_id) as unique_users FROM events WHERE created_at >= NOW() - INTERVAL '7 days' GROUP BY 1 ORDER BY 1 DESC `); return { metrics: result.rows.map(([day, total, unique_users]) => ({ day, total, unique_users })) }; } }; ``` ### Permission Setup [Section titled “Permission Setup”](#permission-setup) Functions run as the `authenticated` role. Tables created by the `admin` role need explicit grants: SQL ```sql GRANT ALL ON ALL TABLES IN SCHEMA public TO authenticated; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO authenticated; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO authenticated; ``` ▶ Run Sequence privileges are enforced, so the sequence grant is required for any function that calls `nextval()`, `currval()`, or `setval()` directly — see [Execution Role](/docs/functions/#execution-role) for the details and the `SERIAL` caveat. ## ctx.fs9 [Section titled “ctx.fs9”](#ctxfs9) Filesystem interface for reading and writing files in your database’s storage. Use `--fs9-scope` at deploy time to declare which paths your function accesses. | Method | Description | | ---------------------- | ------------------------------------------------------------------------ | | `read(path)` | Read file contents as UTF-8 string | | `readBase64(path)` | Read file contents as base64-encoded string | | `write(path, content)` | Write or overwrite a file | | `list(path?)` | List directory entries with metadata | | `stat(path)` | Get file metadata (size, type, mtime, generation, mode, sealed, storage) | | `delete(path)` | Delete a file or directory | JavaScript ```js module.exports = { handler: async (input, ctx) => { // Write a file await ctx.fs9.write("/reports/summary.txt", "Report content here"); // Read it back const content = await ctx.fs9.read("/reports/summary.txt"); // List directory contents const entries = await ctx.fs9.list("/reports/"); // Get file metadata const stat = await ctx.fs9.stat("/reports/summary.txt"); // stat: { path, type: "file", size: 19, mtime: "2026-04-01T...", // generation: 2, mode: 420, sealed: false, storage: "inline" } return { content, fileCount: entries.length, stat }; } }; ``` The `list()` and `stat()` responses include full metadata for each entry: | Field | Type | Description | | ------------ | --------- | -------------------------------------- | | `path` | `string` | Full path of the entry | | `type` | `string` | `file` or `dir` | | `size` | `number` | Size in bytes | | `mtime` | `string` | Last modified time (ISO 8601) | | `generation` | `number` | Version generation counter | | `mode` | `number` | Unix file mode | | `sealed` | `boolean` | Whether the file is sealed (immutable) | | `storage` | `string` | Storage backend (e.g., `inline`) | ### Reading binary files [Section titled “Reading binary files”](#reading-binary-files) For binary files (images, xlsx, zip), use `readBase64`: JavaScript ```js const base64 = await ctx.fs9.readBase64("/data/image.png"); const buffer = Buffer.from(base64, "base64"); ``` ## Bundling and Dependencies [Section titled “Bundling and Dependencies”](#bundling-and-dependencies) The default deployment mode is a **single file**. If your function needs npm packages or multiple source files, bundle them with [esbuild](https://esbuild.github.io/) before deploying. ### Why bundle? [Section titled “Why bundle?”](#why-bundle) * The runtime does not have access to `node_modules` * `require()` of external packages fails at runtime * Multi-file `import ./utils` is not supported ### Bundling with esbuild [Section titled “Bundling with esbuild”](#bundling-with-esbuild) **1. Initialize a project:** Terminal ```bash mkdir my-function && cd my-function npm init -y npm install --save-dev esbuild ``` **2. Install dependencies you need:** Terminal ```bash npm install xlsx csv-stringify ``` **3. Write your function:** src/index.js ```js const XLSX = require("xlsx"); module.exports = { handler: async (input, ctx) => { const base64 = await ctx.fs9.readBase64(input.path); const workbook = XLSX.read(Buffer.from(base64, "base64"), { type: "buffer" }); const sheet = workbook.Sheets[workbook.SheetNames[0]]; const csv = XLSX.utils.sheet_to_csv(sheet); const outPath = input.path.replace(/\.xlsx?$/, ".csv"); await ctx.fs9.write(outPath, csv); return { output: outPath, rows: csv.split("\n").length }; } }; ``` **4. Add a build script to `package.json`:** package.json (scripts section) ```json { "scripts": { "build": "esbuild src/index.js --bundle --platform=node --target=es2020 --outfile=dist/index.js" } } ``` **5. Build and deploy:** Terminal ```bash npm run build cat dist/index.js | db9 functions create xlsx-to-csv --db myapp -f - \ --fs9-scope /data:ro --fs9-scope /output:rw ``` ### Bundle size limits [Section titled “Bundle size limits”](#bundle-size-limits) Keep bundles under **5 MB** for reliable deployments. Use esbuild’s tree-shaking and `--external` flag to minimize size. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Overview](/docs/functions/) — Quick start and deployment basics * [Configuration](/docs/functions/configuration/) — Secrets, fs9 scope, network, cron, limits * [Examples](/docs/functions/examples/) — Real-world patterns # Troubleshooting > Debug DB9 serverless functions — runs, logs, error codes, common issues, and known limitations. ## Runs and Logs [Section titled “Runs and Logs”](#runs-and-logs) ### View run history [Section titled “View run history”](#view-run-history) Terminal ```bash # Last 20 runs (default) db9 functions history my-func --db myapp # Last 50 runs db9 functions history my-func --db myapp -n 50 # Specific run details db9 functions history my-func --db myapp # JSON output db9 functions history my-func --db myapp --json ``` ### Run record fields [Section titled “Run record fields”](#run-record-fields) | Field | Description | | --------------- | --------------------------- | | `id` | Unique run ID | | `status` | `succeeded` or `failed` | | `result_json` | Return value (if succeeded) | | `error_code` | Error type (if failed) | | `error_message` | Error details (if failed) | | `trigger_type` | `invoke`, `cron`, or `api` | | `started_at` | Execution start time | | `finished_at` | Execution end time | | `attempt` | Retry attempt number | | `version_id` | Which version was executed | ### View logs [Section titled “View logs”](#view-logs) Terminal ```bash db9 functions logs my-func --db myapp ``` Logs capture all `console.log()` and `console.error()` output from the function. Tip Add `console.log()` statements during development to trace execution. Logs are captured per-run and visible via `db9 functions logs`. ## Error Codes [Section titled “Error Codes”](#error-codes) ### Runtime errors [Section titled “Runtime errors”](#runtime-errors) | Error Code | Cause | Resolution | | ---------------------- | ------------------------------------------------------ | -------------------------------------------------- | | `execution_error` | Function threw an exception or returned invalid result | Check `error_message` and logs for the stack trace | | `timeout` | Function exceeded `timeout_ms` limit | Increase timeout or optimize code | | `entrypoint_not_found` | `handler` export not found | Use `module.exports = { handler: ... }` | ### SQL errors [Section titled “SQL errors”](#sql-errors) SQL errors from `ctx.db.query()` propagate as exceptions: | Error | Cause | | ------------------------------- | ---------------------------------------------------------------- | | `permission denied for table X` | The `authenticated` role lacks privileges. Run `GRANT` as admin. | | `relation "X" does not exist` | Table not found. Check the table name and schema. | | `syntax error at or near "..."` | Invalid SQL. Check your query string. | ## Common Issues [Section titled “Common Issues”](#common-issues) ### Function created but invoke fails [Section titled “Function created but invoke fails”](#function-created-but-invoke-fails) 1. Check the run status and error: Terminal ```bash db9 functions history my-func --db myapp -n 1 --json ``` 2. Check the logs: Terminal ```bash db9 functions logs my-func --db myapp ``` 3. Common causes: * **`execution_error`** — unhandled exception. Check logs for the stack trace. * **`execution_timeout`** — increase with `--timeout`. * **`permission denied for table X`** — grant access: `GRANT ALL ON TABLE X TO authenticated;` ### ”Entrypoint handler is not a function” [Section titled “”Entrypoint handler is not a function””](#entrypoint-handler-is-not-a-function) Your code doesn’t export `handler` as a named property. The runtime expects: JavaScript ```js // Correct module.exports = { handler: async (input, ctx) => { ... } }; // Wrong — bare function declaration isn't exported async function handler(input, ctx) { ... } // Wrong — module.exports as function (not object with handler key) module.exports = async function handler(input, ctx) { ... }; ``` ### “Unexpected token ‘export’” [Section titled ““Unexpected token ‘export’””](#unexpected-token-export) The runtime uses CommonJS, not ES modules. `export default` is a syntax error: JavaScript ```js // Wrong — ES module syntax is not supported export default async function handler(input, ctx) { ... } // Correct — use CommonJS module.exports = { handler: async (input, ctx) => { ... } }; ``` ### “Unsupported runtime imports detected” [Section titled ““Unsupported runtime imports detected””](#unsupported-runtime-imports-detected) The CLI detected `require()` calls that would fail at runtime. This happens when you use: * `import somePackage from "package-name"` — external npm packages are not available * `import { something } from "./other-file"` — multi-file imports are not supported Bundle your code with esbuild to resolve all imports at build time. See [Bundling](/docs/functions/runtime/#bundling-and-dependencies). ### TypeScript transpile fails [Section titled “TypeScript transpile fails”](#typescript-transpile-fails) * Ensure your TypeScript syntax is valid * Use `import type { ... }` (not `import { ... }`) for type imports * Do not use `import` or `require` for runtime dependencies ### Network fetch blocked [Section titled “Network fetch blocked”](#network-fetch-blocked) If `fetch()` throws `EACCES: permission denied ... url not in allowlist`, the target URL must be added to the `network_allowlist` via the REST API. ### SQL permission denied [Section titled “SQL permission denied”](#sql-permission-denied) Functions run as the `authenticated` role. Tables created by `admin` need explicit grants: SQL ```sql -- Replace my_table with your own table name GRANT ALL ON TABLE my_table TO authenticated; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO authenticated; ``` ## Limitations [Section titled “Limitations”](#limitations) | Limitation | Details | | -------------------------------- | ------------------------------------------------------------------------------- | | **Single-file deployment** | Multi-file imports are not supported. Bundle with esbuild for complex projects. | | **No runtime npm packages** | `require("axios")` fails. Bundle dependencies at build time. | | **TypeScript is transpile-only** | No type checking at deploy time. Types are stripped. | | **No persistent state** | Each invocation is isolated. Use `ctx.db` or `ctx.fs9` to persist data. | | **Network requires allowlist** | `fetch()` is blocked by default. Configure `network_allowlist` via REST API. | | **No local emulator** | Test by deploying to your database. There is no local dev server. | | **No delete command** | Functions cannot be deleted via CLI. Contact support if needed. | | **Authenticated role** | Functions run as `authenticated`, not `admin`. Grant table access explicitly. | | **Bundle size** | Large bundles may hit the proxy body-size limit (413 error). Keep under 5 MB. | ## Next Steps [Section titled “Next Steps”](#next-steps) * [Overview](/docs/functions/) — Quick start and deployment basics * [Runtime & ctx](/docs/functions/runtime/) — ctx.db, ctx.fs9, ctx.self API * [Configuration](/docs/functions/configuration/) — Secrets, fs9 scope, network, cron, limits * [Examples](/docs/functions/examples/) — Real-world patterns # AI Coding Prompts > Pre-built LLM prompts for 10+ tech stack combinations — copy and paste into Claude Code, Cursor, Copilot, or any AI assistant to generate DB9 integration code. Pre-built prompts you can copy directly into an AI coding assistant to scaffold a working DB9 integration. Each prompt includes the DB9 connection pattern, framework best practices, and DB9-specific features. **Compatible with:** Claude Code, Cursor, GitHub Copilot, ChatGPT, and any LLM that generates code. Already using an AI agent? If you’re using [Claude Code](/docs/agent-workflows/claude-code/) or another agent with the DB9 skill installed, the agent already knows how to work with DB9. These prompts are for **general-purpose AI assistants** that don’t have the DB9 skill. ## How to use [Section titled “How to use”](#how-to-use) 1. Pick the prompt that matches your tech stack below. 2. Click the copy button on the code block. 3. Paste into your AI assistant. 4. Replace `YOUR_DATABASE_URL` with your actual [connection string](/docs/connect/). *** ## JavaScript / TypeScript [Section titled “JavaScript / TypeScript”](#javascript--typescript) ### Next.js + Prisma [Section titled “Next.js + Prisma”](#nextjs--prisma) Prompt ```text Build a Next.js 14+ App Router application with Prisma ORM connected to a DB9 database. DB9 connection details: - Connection string format: postgresql://.admin@pg.db9.io:5433/postgres?sslmode=require - Store the connection string in .env.local as DATABASE_URL - DB9 is PostgreSQL-compatible on port 5433 — no custom adapter needed Requirements: 1. Initialize Prisma with the PostgreSQL provider 2. Create a schema with at least two related models (e.g. User and Post) 3. Set up a singleton PrismaClient in lib/prisma.ts to avoid multiple instances in dev 4. Create Server Components that fetch data with Prisma 5. Create a Route Handler (app/api/) for mutations 6. Create a Server Action for form submissions 7. Add proper error handling and TypeScript types Prisma setup commands: npm install prisma @prisma/client npx prisma init npx prisma db push (for quick schema sync) npx prisma generate DB9 features to consider: - Use db9 branch create for safe schema experiments before pushing to main - DB9 supports JSONB columns for flexible data - Vector search is available via pgvector for AI/RAG features - Anonymous databases can be created instantly for prototyping: db9 create --name myapp Reference docs: https://db9.ai/docs/guides/nextjs/ and https://db9.ai/docs/guides/prisma/ ``` ### Next.js + Drizzle [Section titled “Next.js + Drizzle”](#nextjs--drizzle) Prompt ```text Build a Next.js 14+ App Router application with Drizzle ORM connected to a DB9 database. DB9 connection details: - Connection string format: postgresql://.admin@pg.db9.io:5433/postgres?sslmode=require - Store the connection string in .env.local as DATABASE_URL - DB9 is PostgreSQL-compatible on port 5433 — no custom adapter needed Requirements: 1. Install drizzle-orm and drizzle-kit with the postgres-js driver 2. Define a schema in src/db/schema.ts using drizzle's pgTable 3. Set up the database client in src/db/index.ts 4. Configure drizzle.config.ts pointing to DB9 5. Create Server Components that query with Drizzle 6. Create Route Handlers for mutations 7. Use Drizzle's type-safe query builder — avoid raw SQL 8. Add proper connection pooling with postgres-js Install commands: npm install drizzle-orm postgres npm install -D drizzle-kit Push schema: npx drizzle-kit push DB9 features to consider: - Use db9 branch create for safe schema migrations before pushing to main - DB9 supports JSONB columns — use Drizzle's jsonb() type - Vector search is available via pgvector for AI/RAG features - Anonymous databases require no signup: db9 create --name myapp Reference docs: https://db9.ai/docs/guides/nextjs/ and https://db9.ai/docs/guides/drizzle/ ``` ### Express + Knex.js [Section titled “Express + Knex.js”](#express--knexjs) Prompt ```text Build an Express.js REST API with Knex.js query builder connected to a DB9 database. DB9 connection details: - Connection string format: postgresql://.admin@pg.db9.io:5433/postgres?sslmode=require - Store the connection string in a .env file as DATABASE_URL - DB9 is PostgreSQL-compatible on port 5433 — use the pg driver Requirements: 1. Set up Express with proper middleware (cors, json parsing, error handling) 2. Configure Knex with the pg driver pointing to DB9 3. Create a migration for at least two related tables 4. Build CRUD endpoints for the primary resource 5. Use Knex transactions for multi-step operations 6. Add request validation 7. Structure the project: routes/, controllers/, db/ directories Install commands: npm install express knex pg dotenv npx knex init npx knex migrate:make create_tables npx knex migrate:latest Knex config for DB9: client: 'pg' connection: process.env.DATABASE_URL pool: { min: 2, max: 10 } ssl: { rejectUnauthorized: false } DB9 features to consider: - Use db9 branch create to test migrations safely before applying to main - pg_cron is available for scheduled background jobs via SQL - HTTP from SQL: call external APIs directly from DB9 with http_get()/http_post() - Anonymous databases require no signup: db9 create --name myapp Reference docs: https://db9.ai/docs/guides/knex/ and https://db9.ai/docs/connect/ ``` ### SvelteKit + Drizzle [Section titled “SvelteKit + Drizzle”](#sveltekit--drizzle) Prompt ```text Build a SvelteKit application with Drizzle ORM connected to a DB9 database. DB9 connection details: - Connection string format: postgresql://.admin@pg.db9.io:5433/postgres?sslmode=require - Store the connection string in a .env file as DATABASE_URL - DB9 is PostgreSQL-compatible on port 5433 — no custom adapter needed Requirements: 1. Create a SvelteKit project with TypeScript 2. Install drizzle-orm with the postgres-js driver 3. Define schema in src/lib/server/db/schema.ts using pgTable 4. Set up the db client in src/lib/server/db/index.ts (server-only module) 5. Use SvelteKit load functions (+page.server.ts) for data fetching 6. Create form actions for mutations 7. Use Drizzle's type-safe query builder throughout 8. Ensure database code only runs server-side Install commands: npm install drizzle-orm postgres npm install -D drizzle-kit npx drizzle-kit push DB9 features to consider: - Use db9 branch create for safe schema experiments - DB9 supports JSONB columns — use Drizzle's jsonb() type - Vector search via pgvector for AI-powered features - Anonymous databases require no signup: db9 create --name myapp Reference docs: https://db9.ai/docs/guides/drizzle/ and https://db9.ai/docs/connect/ ``` *** ## Python [Section titled “Python”](#python) ### Python + SQLAlchemy [Section titled “Python + SQLAlchemy”](#python--sqlalchemy) Prompt ```text Build a Python application with SQLAlchemy 2.0 connected to a DB9 database. DB9 connection details: - Connection string format: postgresql://.admin@pg.db9.io:5433/postgres?sslmode=require - IMPORTANT: SQLAlchemy requires the prefix postgresql+psycopg:// (not postgresql://) Convert the DB9 connection string by replacing "postgresql://" with "postgresql+psycopg://" - Store the connection string in a .env file as DATABASE_URL Requirements: 1. Use SQLAlchemy 2.0 API with mapped_column and type-annotated models 2. Use psycopg3 (psycopg[binary]) as the driver — not psycopg2 3. Create an engine with pool_pre_ping=True 4. Define at least two related models with relationships 5. Use sessionmaker and context managers for transactions 6. Implement CRUD operations using the 2.0 query API 7. Add Alembic for migrations Install commands: pip install "sqlalchemy>=2.0.0" "psycopg[binary]>=3.1.0" python-dotenv pip install alembic # for migrations DB9 features to consider: - Use db9 branch create for safe schema experiments before applying migrations - Vector search: pip install pgvector, then use the Vector type for embedding columns - DB9 supports JSONB — use sqlalchemy.dialects.postgresql.JSONB - Anonymous databases require no signup: db9 create --name myapp Reference docs: https://db9.ai/docs/guides/python-sqlalchemy/ ``` ### Django [Section titled “Django”](#django) Prompt ```text Build a Django application connected to a DB9 database. DB9 connection details: - Host: pg.db9.io - Port: 5433 - Database name: postgres - User: .admin - DB9 is PostgreSQL-compatible — use Django's built-in postgresql backend Requirements: 1. Create a Django project with at least one app 2. Configure DATABASES in settings.py for DB9 3. Define models with at least two related tables 4. Create and apply migrations 5. Set up Django REST Framework for an API (or use views) 6. Add admin site configuration 7. Use environment variables for credentials (django-environ or python-dotenv) Django DATABASES config for DB9: DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'postgres', 'USER': '.admin', 'PASSWORD': '', 'HOST': 'pg.db9.io', 'PORT': '5433', 'OPTIONS': {'sslmode': 'require'}, } } Install commands: pip install django psycopg[binary] django-environ django-admin startproject myproject python manage.py startapp myapp python manage.py migrate DB9 features to consider: - Use db9 branch create to test migrations on a branch before applying to main - Django's JSONField works with DB9's JSONB support - Vector search: install pgvector and django-pgvector for AI/RAG features - Anonymous databases require no signup: db9 create --name myapp Reference docs: https://db9.ai/docs/connect/ ``` ### Flask [Section titled “Flask”](#flask) Prompt ```text Build a Flask REST API with SQLAlchemy connected to a DB9 database. DB9 connection details: - Connection string format: postgresql://.admin@pg.db9.io:5433/postgres?sslmode=require - IMPORTANT: For SQLAlchemy, replace "postgresql://" with "postgresql+psycopg://" - Store the connection string in a .env file as DATABASE_URL Requirements: 1. Set up Flask with Flask-SQLAlchemy for ORM integration 2. Use SQLAlchemy 2.0 style models with mapped_column 3. Use psycopg3 (psycopg[binary]) as the driver 4. Define at least two related models 5. Create RESTful CRUD endpoints using Flask blueprints 6. Use Flask-Migrate (Alembic) for database migrations 7. Add proper error handling and JSON responses 8. Structure the project with an application factory pattern Install commands: pip install flask flask-sqlalchemy "psycopg[binary]>=3.1.0" flask-migrate python-dotenv Flask-SQLAlchemy config for DB9: SQLALCHEMY_DATABASE_URI must use the postgresql+psycopg:// prefix SQLALCHEMY_ENGINE_OPTIONS = {"pool_pre_ping": True} DB9 features to consider: - Use db9 branch create for safe schema experiments before migrating - Vector search: pip install pgvector for AI/RAG features - DB9 supports JSONB for flexible document storage - Anonymous databases require no signup: db9 create --name myapp Reference docs: https://db9.ai/docs/guides/python-sqlalchemy/ and https://db9.ai/docs/connect/ ``` *** ## Go [Section titled “Go”](#go) ### Go + GORM [Section titled “Go + GORM”](#go--gorm) Prompt ```text Build a Go REST API with GORM connected to a DB9 database. DB9 connection details: - Connection string (DSN): postgresql://.admin@pg.db9.io:5433/postgres?sslmode=require - GORM uses the pgx v5 driver via gorm.io/driver/postgres - Store the DSN in an environment variable DATABASE_URL Requirements: 1. Set up a Go project with GORM and the postgres driver 2. Define struct models with gorm tags for at least two related tables 3. Use AutoMigrate for schema setup 4. Build a REST API using net/http or chi/gin router 5. Implement CRUD operations with proper error handling 6. Use GORM transactions for multi-step operations 7. Add connection pooling configuration via database/sql Install commands: go mod init myapp go get gorm.io/gorm gorm.io/driver/postgres GORM connection setup: dsn := os.Getenv("DATABASE_URL") db, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}), &gorm.Config{}) sqlDB, _ := db.DB() sqlDB.SetMaxOpenConns(25) sqlDB.SetMaxIdleConns(5) DB9 features to consider: - Use db9 branch create for safe schema experiments before AutoMigrate on main - Vector search: use pgvector-go for embedding columns - DB9 supports JSONB — use datatypes.JSON from gorm.io/datatypes - Anonymous databases require no signup: db9 create --name myapp Reference docs: https://db9.ai/docs/guides/gorm/ ``` *** ## Rust [Section titled “Rust”](#rust) ### Rust + SQLx [Section titled “Rust + SQLx”](#rust--sqlx) Prompt ```text Build a Rust web service with SQLx connected to a DB9 database. DB9 connection details: - Connection string format: postgresql://.admin@pg.db9.io:5433/postgres?sslmode=require - Store the connection string in a .env file as DATABASE_URL - SQLx connects via the PostgreSQL protocol — DB9 is fully compatible Requirements: 1. Set up a Rust project with SQLx (postgres feature + runtime-tokio + tls-rustls) 2. Use sqlx::PgPool for connection pooling 3. Define models as Rust structs deriving FromRow 4. Use compile-time checked queries with sqlx::query_as! where possible 5. Build a REST API using Axum or Actix-web 6. Implement CRUD endpoints with proper error handling 7. Use SQLx migrations for schema management Cargo.toml dependencies: sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "macros"] } tokio = { version = "1", features = ["full"] } axum = "0.7" # or actix-web dotenvy = "0.15" serde = { version = "1", features = ["derive"] } SQLx commands: sqlx database create (not needed — DB9 database already exists) sqlx migrate add create_tables sqlx migrate run DB9 features to consider: - Use db9 branch create to test migrations on a branch before applying to main - Vector search: use pgvector with SQLx for embedding storage and similarity search - DB9 supports JSONB — use sqlx::types::Json - Anonymous databases require no signup: db9 create --name myapp Reference docs: https://db9.ai/docs/connect/ ``` *** ## Ruby [Section titled “Ruby”](#ruby) ### Rails [Section titled “Rails”](#rails) Prompt ```text Build a Ruby on Rails application connected to a DB9 database. DB9 connection details: - Host: pg.db9.io - Port: 5433 - Database name: postgres - User: .admin - DB9 is PostgreSQL-compatible — use the standard pg gem Requirements: 1. Create a new Rails app with the PostgreSQL adapter 2. Configure config/database.yml for DB9 3. Generate at least two related models with a migration 4. Set up Active Record associations 5. Create a RESTful controller with CRUD actions 6. Add model validations 7. Use Rails credentials or environment variables for the password database.yml config for DB9: default: &default adapter: postgresql host: pg.db9.io port: 5433 database: postgres username: .admin password: <%= ENV["DB9_PASSWORD"] %> sslmode: require Install commands: rails new myapp --database=postgresql cd myapp rails generate model User name:string email:string rails generate model Post title:string body:text user:references rails db:migrate DB9 features to consider: - Use db9 branch create to test migrations on a branch before applying to production - Rails jsonb columns work natively with DB9 - Vector search: use the neighbor gem with pgvector for AI/RAG features - Anonymous databases require no signup: db9 create --name myapp Reference docs: https://db9.ai/docs/connect/ ``` *** ## What’s included in every prompt [Section titled “What’s included in every prompt”](#whats-included-in-every-prompt) Each prompt above follows a consistent structure: | Section | Purpose | | ---------------------- | ---------------------------------------------------------------------------- | | **Connection details** | DB9 host, port (5433), connection string format, environment variable naming | | **Framework setup** | Install commands, project structure, configuration | | **Code requirements** | Models, CRUD, transactions, error handling | | **DB9 features** | Branching, anonymous databases, vector search, JSONB, and other capabilities | | **Reference links** | Pointers to the relevant DB9 docs for the AI to consult | ## Next steps [Section titled “Next steps”](#next-steps) * **Install the DB9 CLI** to create databases: [Quick Start](/docs/quickstart/) * **Set up agent integration** for deeper AI workflows: [Agent Workflows](/docs/agent-workflows/overview/) * **Give Claude Code the DB9 skill** for native database access: [Claude Code](/docs/agent-workflows/claude-code/) * **Connect your app** with detailed framework guides: [Connection Strings](/docs/connect/) # Analyze Agent Logs with fs9 > Store and query agent logs as files inside DB9 using the fs9 extension — parse JSONL, aggregate tool calls, and measure run durations with standard SQL. AI agents produce structured logs — tool invocations, model calls, lifecycle events, errors. The fs9 extension lets you write those logs to DB9’s built-in filesystem and then query them with SQL, without loading them into a separate observability tool. This guide walks through a practical flow: write agent logs, query them as structured data, and build useful aggregations. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database (see [Quick Start](/docs/quickstart/)) * The `fs9` extension enabled SQL ```sql CREATE EXTENSION IF NOT EXISTS fs9; ``` ## How fs9 Works [Section titled “How fs9 Works”](#how-fs9-works) fs9 exposes a per-database filesystem backed by TiKV. Files are isolated per tenant — each database has its own `/` root. You interact with it through: * **Scalar functions** like `fs9_write()`, `fs9_read()`, `fs9_append()` for file operations * **A table function** `extensions.fs9()` that reads files as SQL result sets with automatic format detection The table function auto-detects `.jsonl`, `.csv`, `.tsv`, and plain text formats by file extension. ## 1. Write Agent Logs to the Filesystem [Section titled “1. Write Agent Logs to the Filesystem”](#1-write-agent-logs-to-the-filesystem) Agents typically emit logs as JSONL (one JSON object per line). Use `fs9_write()` or `fs9_append()` to store them. Create the directory first — `fs9_write()` and `fs9_append()` do **not** create missing parent directories, and fail with `fs: NotFound` (`58030`) if the parent does not exist: SQL ```sql -- `true` creates parent directories as needed SELECT fs9_mkdir('/logs/agent', true); ``` SQL ```sql -- Write a batch of agent events as JSONL SELECT fs9_write('/logs/agent/2026-03-12.jsonl', '{"ts":"2026-03-12T10:00:01Z","event":"tool_call","tool":"sql","args":{"query":"SELECT count(*) FROM users"},"duration_ms":42} {"ts":"2026-03-12T10:00:02Z","event":"tool_call","tool":"http_get","args":{"url":"https://api.example.com/status"},"duration_ms":310} {"ts":"2026-03-12T10:00:03Z","event":"tool_call","tool":"fs_write","args":{"path":"/data/report.csv"},"duration_ms":15} {"ts":"2026-03-12T10:00:05Z","event":"model_call","model":"claude-sonnet-4-20250514","tokens_in":1200,"tokens_out":350,"duration_ms":890} {"ts":"2026-03-12T10:00:06Z","event":"tool_call","tool":"sql","args":{"query":"INSERT INTO reports VALUES (...)"},"duration_ms":28} {"ts":"2026-03-12T10:00:08Z","event":"lifecycle","phase":"complete","status":"success","total_duration_ms":7200} '); ``` For streaming logs, use `fs9_append()` to add lines without overwriting: SQL ```sql SELECT fs9_append('/logs/agent/2026-03-12.jsonl', '{"ts":"2026-03-12T10:01:00Z","event":"tool_call","tool":"sql","args":{"query":"SELECT * FROM metrics"},"duration_ms":55} '); ``` ## 2. Query Logs as Structured Data [Section titled “2. Query Logs as Structured Data”](#2-query-logs-as-structured-data) The `extensions.fs9()` table function reads `.jsonl` files and returns each line as a JSONB row: SQL ```sql SELECT _line_number, line FROM extensions.fs9('/logs/agent/2026-03-12.jsonl') LIMIT 5; ``` Each row has: | Column | Type | Description | | -------------- | ----- | ------------------------------------ | | `_line_number` | INT | 1-based line index | | `line` | JSONB | The parsed JSON object | | `_path` | TEXT | Source file path (useful with globs) | Since `line` is JSONB, you can use standard PostgreSQL JSON operators to extract fields: SQL ```sql SELECT line->>'event' AS event_type, line->>'tool' AS tool, (line->>'duration_ms')::int AS duration_ms FROM extensions.fs9('/logs/agent/2026-03-12.jsonl') WHERE line->>'event' = 'tool_call'; ``` ## 3. Aggregate Tool Call Patterns [Section titled “3. Aggregate Tool Call Patterns”](#3-aggregate-tool-call-patterns) Count tool calls by tool name: SQL ```sql SELECT line->>'tool' AS tool, count(*) AS calls, round(avg((line->>'duration_ms')::numeric)) AS avg_ms, max((line->>'duration_ms')::int) AS max_ms FROM extensions.fs9('/logs/agent/2026-03-12.jsonl') WHERE line->>'event' = 'tool_call' GROUP BY 1 ORDER BY calls DESC; ``` Example output: | tool | calls | avg\_ms | max\_ms | | --------- | ----- | ------- | ------- | | sql | 3 | 42 | 55 | | http\_get | 1 | 310 | 310 | | fs\_write | 1 | 15 | 15 | ## 4. Track Token Usage [Section titled “4. Track Token Usage”](#4-track-token-usage) Aggregate model calls to see token consumption: SQL ```sql SELECT line->>'model' AS model, count(*) AS calls, sum((line->>'tokens_in')::int) AS total_tokens_in, sum((line->>'tokens_out')::int) AS total_tokens_out, round(avg((line->>'duration_ms')::numeric)) AS avg_latency_ms FROM extensions.fs9('/logs/agent/2026-03-12.jsonl') WHERE line->>'event' = 'model_call' GROUP BY 1; ``` ## 5. Query Across Multiple Log Files [Section titled “5. Query Across Multiple Log Files”](#5-query-across-multiple-log-files) Use glob patterns to query all logs in a directory or across dates: SQL ```sql -- All JSONL files in the agent logs directory SELECT _path, line->>'event' AS event, line->>'tool' AS tool FROM extensions.fs9('/logs/agent/*.jsonl') LIMIT 20; -- Recursive search across all subdirectories SELECT _path, count(*) AS events FROM extensions.fs9('/logs/**/*.jsonl') GROUP BY 1 ORDER BY events DESC; ``` Glob limits: a single glob can match up to 10,000 files and 100 MB total. For larger log volumes, query specific date ranges instead of using `**`. ## 6. Query CSV Logs [Section titled “6. Query CSV Logs”](#6-query-csv-logs) If your agent writes CSV logs, fs9 auto-detects the format and maps headers to column names: SQL ```sql -- Write a CSV log SELECT fs9_write('/logs/requests.csv', 'timestamp,method,path,status,duration_ms 2026-03-12T10:00:01Z,GET,/api/users,200,42 2026-03-12T10:00:02Z,POST,/api/tasks,201,85 2026-03-12T10:00:03Z,GET,/api/users/1,404,12 '); -- Query it — column names come from the header row SELECT method, path, status, duration_ms FROM extensions.fs9('/logs/requests.csv') WHERE status != '200'; ``` ## 7. File Management [Section titled “7. File Management”](#7-file-management) Check what logs exist and how large they are: SQL ```sql -- List log files SELECT path, size, mtime FROM extensions.fs9('/logs/agent/') ORDER BY mtime DESC; -- Check a specific file's size SELECT fs9_size('/logs/agent/2026-03-12.jsonl'); -- Read a partial range (first 1024 bytes) SELECT fs9_read_at('/logs/agent/2026-03-12.jsonl', 0, 1024); -- Delete old logs SELECT fs9_remove('/logs/agent/2026-03-01.jsonl'); -- Delete a directory recursively SELECT fs9_remove('/logs/old/', true); ``` ## Limits and Caveats [Section titled “Limits and Caveats”](#limits-and-caveats) * **Superuser required** — all fs9 operations require the database admin role. * **Max file size** — 100 MB per file. For larger log volumes, split by date or category. * **Max glob result** — a single glob query can scan up to 10,000 files and 100 MB total. * **Concurrent read budget** — 128 MB across all concurrent `fs9_read()` calls globally. * **UTF-8 only** — binary content read as text uses lossy UTF-8 conversion. Use `fs9_write()` with BYTEA for binary data. * **No streaming** — the table function loads the entire file into memory. For very large files, use `fs9_read_at()` to read in ranges. ## Next Pages [Section titled “Next Pages”](#next-pages) * [fs9 Extension Reference](/docs/extensions/fs9/) — full function reference, format detection rules, and named parameters * [RAG with Built-in Embeddings](/docs/guides/rag-with-built-in-embeddings/) — vector search and retrieval pipelines * [HTTP from SQL](/docs/guides/http-from-sql/) — call external APIs from SQL * [Extensions Overview](/docs/extensions/) — all 9 built-in extensions * [CLI Reference](/docs/cli/) — `db9 db sql` for running queries from the terminal # Astro > Use Astro with DB9 — connect from server endpoints and SSR pages using Prisma, Drizzle, or node-postgres. Astro connects to DB9 through any standard PostgreSQL driver when running in SSR mode. DB9 is PostgreSQL-compatible, so no special adapter or driver is needed. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Node.js 18+ * Astro 4+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name astro-app ``` Get the connection string: Terminal ```bash db9 db status astro-app ``` Set the connection string as an environment variable: .env ```env DATABASE_URL="postgresql://astro-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require" ``` ## Enable SSR [Section titled “Enable SSR”](#enable-ssr) Astro defaults to static output, which cannot connect to a database at request time. Switch to server-rendered mode and add the Node adapter: Terminal ```bash npx astro add node ``` Then set `output: 'server'` in your config: astro.config.mjs ```javascript import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone', }), }); ``` ## Setup [Section titled “Setup”](#setup) * Prisma ### Install [Section titled “Install”](#install) Terminal ```bash npm install prisma @prisma/client npx prisma init ``` ### Schema [Section titled “Schema”](#schema) prisma/schema.prisma ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique name String createdAt DateTime @default(now()) } ``` Push the schema and generate the client: Terminal ```bash npx prisma db push npx prisma generate ``` ### Singleton client [Section titled “Singleton client”](#singleton-client) src/lib/prisma.ts ```typescript import { PrismaClient } from '@prisma/client'; const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; export const prisma = globalForPrisma.prisma ?? new PrismaClient(); if (import.meta.env.DEV) { globalForPrisma.prisma = prisma; } ``` * Drizzle ### Install [Section titled “Install”](#install-1) Terminal ```bash npm install drizzle-orm pg npm install -D drizzle-kit @types/pg ``` ### Schema [Section titled “Schema”](#schema-1) src/lib/schema.ts ```typescript import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).unique().notNull(), name: varchar('name', { length: 100 }).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), }); ``` ### Client [Section titled “Client”](#client) src/lib/db.ts ```typescript import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; const pool = new Pool({ connectionString: import.meta.env.DATABASE_URL, }); export const db = drizzle(pool); ``` ## Server Endpoint [Section titled “Server Endpoint”](#server-endpoint) Create an API route that handles GET and POST requests: * Prisma src/pages/api/users.ts ```typescript import type { APIRoute } from 'astro'; import { prisma } from '../../lib/prisma'; export const GET: APIRoute = async () => { const users = await prisma.user.findMany({ orderBy: { createdAt: 'desc' }, }); return new Response(JSON.stringify(users), { headers: { 'Content-Type': 'application/json' }, }); }; export const POST: APIRoute = async ({ request }) => { const body = await request.json(); const user = await prisma.user.create({ data: { email: body.email, name: body.name }, }); return new Response(JSON.stringify(user), { status: 201, headers: { 'Content-Type': 'application/json' }, }); }; ``` * Drizzle src/pages/api/users.ts ```typescript import type { APIRoute } from 'astro'; import { db } from '../../lib/db'; import { users } from '../../lib/schema'; export const GET: APIRoute = async () => { const allUsers = await db.select().from(users).orderBy(users.createdAt); return new Response(JSON.stringify(allUsers), { headers: { 'Content-Type': 'application/json' }, }); }; export const POST: APIRoute = async ({ request }) => { const body = await request.json(); const [user] = await db.insert(users).values({ email: body.email, name: body.name, }).returning(); return new Response(JSON.stringify(user), { status: 201, headers: { 'Content-Type': 'application/json' }, }); }; ``` ## SSR Page [Section titled “SSR Page”](#ssr-page) Fetch data server-side in the frontmatter of an `.astro` page: * Prisma src/pages/users.astro ```astro --- import { prisma } from '../lib/prisma'; const users = await prisma.user.findMany({ orderBy: { createdAt: 'desc' }, }); ---

Users

    {users.map((user) => (
  • {user.name} ({user.email})
  • ))}
``` * Drizzle src/pages/users.astro ```astro --- import { db } from '../lib/db'; import { users } from '../lib/schema'; const allUsers = await db.select().from(users).orderBy(users.createdAt); ---

Users

    {allUsers.map((user) => (
  • {user.name} ({user.email})
  • ))}
``` ## Production Notes [Section titled “Production Notes”](#production-notes) * **SSR required**: DB9 connections happen at request time. You must use `output: 'server'` (or `output: 'hybrid'` for mixed pages). Static builds cannot query a database. * **Node adapter**: Use `@astrojs/node` in standalone or middleware mode. Edge adapters (Cloudflare, Vercel Edge) do not support raw TCP connections required by PostgreSQL. * **Port 5433**: DB9 uses port 5433, not the default PostgreSQL port 5432. * **TLS required**: Always include `sslmode=require` in your connection string. * **Connection pooling**: Start with a small pool size (5–10). DB9 handles per-tenant connection management server-side. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Pages return empty data or build errors [Section titled “Pages return empty data or build errors”](#pages-return-empty-data-or-build-errors) Astro’s default `output: 'static'` mode pre-renders pages at build time. Database queries in `.astro` frontmatter or API routes require `output: 'server'`. Update `astro.config.mjs` and add the Node adapter. ### `ECONNREFUSED` on port 5432 [Section titled “ECONNREFUSED on port 5432”](#econnrefused-on-port-5432) DB9 uses port **5433**. Check that your `DATABASE_URL` contains `:5433/` and not `:5432/`. ### Adapter errors on deploy [Section titled “Adapter errors on deploy”](#adapter-errors-on-deploy) DB9 requires a TCP connection (pgwire protocol). If you deploy to Cloudflare Pages, Deno Deploy, or another edge platform, the PostgreSQL driver will fail. Use `@astrojs/node` with a Node.js-compatible host (Docker, Railway, Fly.io, traditional VPS). ### `prisma db push` fails [Section titled “prisma db push fails”](#prisma-db-push-fails) DB9 has limited `information_schema` support. See the [Prisma guide](/docs/guides/prisma/) for workarounds. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Prisma](/docs/guides/prisma/) — full Prisma integration guide * [Drizzle](/docs/guides/drizzle/) — full Drizzle integration guide * [Connect](/docs/connect/) — connection strings and authentication * [Production Checklist](/docs/production-checklist/) — deployment readiness # Branching Workflows > Use DB9 database branches for preview environments, safe schema changes, test isolation, and rollback — with isolated branches that don't affect production. DB9 can create a branch of any database — an independent copy with its own credentials, connection string, and data. Branches are useful for preview environments, isolated testing, safe schema migrations, and data recovery. This guide shows how to create, use, and manage branches through the CLI and SDK. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 account with the CLI installed (see [Quick Start](/docs/quickstart/)) * An existing database to branch from Terminal ```bash db9 create --name production ``` ▶ Run ## How Branching Works [Section titled “How Branching Works”](#how-branching-works) When you create a branch, DB9 clones schema and data from the source database into a new, independent database. The branch gets its own: * TiKV keyspace (isolated storage) * Admin credentials * Connection string * Lifecycle (create, delete independently of the parent) Changes to the branch do not affect the parent, and changes to the parent do not propagate to the branch. Branch creation is **asynchronous** — the command returns immediately with status `CLONING`, and you poll until the branch reaches `ACTIVE`. 1. **Create a Branch** Terminal ```bash db9 branch create production --name feature-auth ``` ▶ Run The command returns immediately with the branch details: Output ```text Name: feature-auth State: CLONING Parent: production ``` **Wait for the branch to be ready** Poll the branch status until it reaches `ACTIVE`: Terminal ```bash db9 db status feature-auth ``` ▶ Run Or in a script: Terminal ```bash while true; do state=$(db9 db status feature-auth --json | jq -r .state) if [ "$state" = "ACTIVE" ]; then echo "Branch is ready" break elif [ "$state" = "CREATE_FAILED" ]; then echo "Branch creation failed" db9 db status feature-auth --json | jq .state_reason exit 1 fi sleep 2 done ``` **Show credentials on creation** Terminal ```bash db9 branch create production --name staging --show-secrets # Or individually: db9 branch create production --name staging --show-password --show-connection-string ``` ▶ Run `--show-secrets` is shorthand for `--show-password --show-connection-string`. This prints the admin password and connection string in the output. Avoid this in CI logs — use `db9 db connect` instead for short-lived credentials. 2. **Connect to a Branch** A branch is a regular DB9 database. Connect with any PostgreSQL tool: Terminal ```bash db9 db connect feature-auth ``` ▶ Run Or get the connection string: Terminal ```bash db9 db connect feature-auth --output quiet ``` Then use it with `psql`, your ORM, or any PostgreSQL driver. 3. **List Branches** Terminal ```bash db9 branch list production ``` ▶ Run Shows all branches of a database with their state and creation time. Supports `--output json` and `--output csv` for programmatic use. 4. **Delete a Branch** Terminal ```bash db9 branch delete feature-auth ``` ▶ Run Deletion is permanent — there is no undo or trash. Deleting a branch does not affect the parent database. 5. **SDK Usage** Use the TypeScript SDK for programmatic branch management: TypeScript ```typescript import { createDb9Client } from 'get-db9'; const client = createDb9Client(); // Create a branch (first argument is the parent database ID) const branch = await client.databases.branch('production-db-id', { name: 'feature-auth' }); console.log(branch.id, branch.state); // "CLONING" // Poll until ready let status; do { status = await client.databases.get(branch.id); if (status.state === 'CREATE_FAILED') throw new Error(status.state_reason); await new Promise(r => setTimeout(r, 2000)); } while (status.state !== 'ACTIVE'); // Use the branch — it has its own connection string console.log(status.connection_string); // Delete when done await client.databases.delete(branch.id); ``` ▶ Run ## Practical Workflows [Section titled “Practical Workflows”](#practical-workflows) ### Preview environments in CI [Section titled “Preview environments in CI”](#preview-environments-in-ci) Create a branch per pull request for integration testing: Terminal ```bash BRANCH_NAME="pr-${PR_NUMBER}" # Create branch from production db9 branch create production --name "$BRANCH_NAME" # Wait for it while [ "$(db9 db status "$BRANCH_NAME" --json | jq -r .state)" != "ACTIVE" ]; do sleep 2 done # Run migrations and tests against the branch db9 db sql "$BRANCH_NAME" -f migrations/latest.sql npm test -- --database-url="$(db9 db connect "$BRANCH_NAME" --output quiet)" # Clean up db9 branch delete "$BRANCH_NAME" ``` ### Safe schema migration testing [Section titled “Safe schema migration testing”](#safe-schema-migration-testing) Test a migration in an isolated branch before applying it: Terminal ```bash # Branch production db9 branch create production --name migration-test # Wait for ready # ... # Apply migration db9 db sql migration-test -f migrations/0042_add_index.sql # Validate db9 db sql migration-test -q "SELECT count(*) FROM users" db9 db sql migration-test -q "EXPLAIN SELECT * FROM users WHERE email = 'test@example.com'" # If satisfied, apply to production db9 db sql production -f migrations/0042_add_index.sql # Clean up test branch db9 branch delete migration-test ``` ### Task isolation for agents [Section titled “Task isolation for agents”](#task-isolation-for-agents) Give each agent task its own database branch so parallel agents don’t interfere with each other: Terminal ```bash TASK_ID="task-$(uuidgen | head -c 8)" db9 branch create shared-db --name "$TASK_ID" # ... wait for ACTIVE ... # Agent works against isolated branch db9 db sql "$TASK_ID" -q "CREATE TABLE scratch (id SERIAL, data JSONB)" db9 db sql "$TASK_ID" -q "INSERT INTO scratch (data) VALUES ('{\"result\": \"done\"}')" # Clean up when task completes db9 branch delete "$TASK_ID" ``` ## Limits and Caveats [Section titled “Limits and Caveats”](#limits-and-caveats) * **Asynchronous creation** — branches are not immediately ready. Always poll for `ACTIVE` state before connecting. * **Full data copy** — branch creation clones both schema and data from the parent. * **Max 2 concurrent branch creations** — attempting a third returns a 429 error. Wait for in-progress branches to finish. * **Branches count toward database quota** — anonymous accounts are limited to 5 databases total (including branches). Run `db9 claim` to remove the limit. * **No automatic merge** — there is no built-in way to merge branch changes back to the parent. Export and re-apply manually. * **Deletion is permanent** — deleted branches cannot be recovered. * **Branch naming** — names must be unique within your account and cannot be empty. ## Next Pages [Section titled “Next Pages”](#next-pages) * [CI Ephemeral Databases](/docs/guides/ci-ephemeral-databases/) — disposable databases and branches in CI pipelines * [CLI Reference](/docs/cli/) — `db9 branch` command details and global flags * [TypeScript SDK](/docs/sdk/) — `databases.branch()` and programmatic management * [Scheduled Jobs with pg\_cron](/docs/guides/scheduled-jobs-with-pg-cron/) — automate branch cleanup or scheduled workflows * [Agent Workflows](/docs/agent-workflows/overview/) — using branches in agent pipelines * [Platform: Provisioning](/docs/platform/provisioning/) — database creation and lifecycle * [Recovery and Branch Lifecycle](/docs/platform/recovery-and-branch-lifecycle/) — branch states, automatic recovery, and backup expectations # CI Ephemeral Databases > Use disposable DB9 databases in CI pipelines — create per-run databases or branches, run tests against real PostgreSQL, and clean up automatically. Every CI run can get its own disposable DB9 database with real PostgreSQL compatibility — no Docker, no shared state, no flaky mocks. Create a database at the start of the job, run your tests, and delete it when done. This guide shows two strategies and how to wire them into GitHub Actions and other CI systems. ## When to Use Each Strategy [Section titled “When to Use Each Strategy”](#when-to-use-each-strategy) | Strategy | Best for | Speed | Data | | ---------------------- | -------------------------------------------------- | ------------------ | ------------------- | | **Ephemeral database** | Clean-slate tests, schema-only validation | Instant (under 1s) | Empty | | **Branch** | Tests that need production data, migration testing | Seconds to minutes | Full copy of parent | Use **ephemeral databases** when your tests create their own fixtures. Use **branches** when you need a copy of real data to test against. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 account with the CLI installed ([Quick Start](/docs/quickstart/)) * A CI environment that can run shell commands (GitHub Actions, GitLab CI, etc.) Store DB9\_TOKEN as a CI secret Never hardcode the `DB9_TOKEN` in your workflow files. Store it as an encrypted repository secret and reference it as `${{ secrets.DB9_TOKEN }}` in GitHub Actions (or the equivalent in your CI system). * A `DB9_TOKEN` for authentication (create one with `db9 token create`) ## Strategy 1: Ephemeral Database per CI Run [Section titled “Strategy 1: Ephemeral Database per CI Run”](#strategy-1-ephemeral-database-per-ci-run) Create a fresh, empty database for each CI run. Tests apply their own migrations and seed data. ### CLI workflow [Section titled “CLI workflow”](#cli-workflow) Terminal ```bash # Generate a unique name for this run DB_NAME="ci-${GITHUB_RUN_ID:-$(date +%s)}" # Create the database (synchronous, <1 second) db9 create --name "$DB_NAME" --show-connection-string # Get the connection string for your test runner export DATABASE_URL=$(db9 db connect "$DB_NAME" --output quiet) # Run migrations and tests npx prisma db push npm test # Clean up db9 delete "$DB_NAME" --yes ``` Database creation is instant Database creation is synchronous — the database is ready to accept connections as soon as `db9 create` returns, with no polling required. ### GitHub Actions example [Section titled “GitHub Actions example”](#github-actions-example) .github/workflows/test.yml ```yaml name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest env: DB9_TOKEN: ${{ secrets.DB9_TOKEN }} steps: - uses: actions/checkout@v4 - name: Install DB9 CLI run: curl -fsSL https://db9.ai/install | sh - name: Create ephemeral database run: | DB_NAME="ci-${{ github.run_id }}-${{ github.run_attempt }}" db9 create --name "$DB_NAME" echo "DB_NAME=$DB_NAME" >> "$GITHUB_ENV" echo "DATABASE_URL=$(db9 db connect "$DB_NAME" --output quiet)" >> "$GITHUB_ENV" - name: Run tests run: | npx prisma db push npm test - name: Cleanup if: always() run: db9 delete "$DB_NAME" --yes ``` Key points: * `DB9_TOKEN` authenticates the CLI — store it as a repository secret. * `if: always()` on the cleanup step ensures the database is deleted even if tests fail. * Include `run_attempt` in the name to avoid conflicts on re-runs. ### SDK workflow (TypeScript) [Section titled “SDK workflow (TypeScript)”](#sdk-workflow-typescript) Use `instantDatabase` for test harnesses that manage their own lifecycle: test/setup.ts ```typescript import { instantDatabase, createDb9Client } from 'get-db9'; let databaseId: string; export async function setupTestDb() { const db = await instantDatabase({ name: `ci-${process.env.GITHUB_RUN_ID || Date.now()}`, }); databaseId = db.databaseId; return { connectionString: db.connectionString, databaseId: db.databaseId, }; } export async function teardownTestDb() { if (databaseId) { const client = createDb9Client(); await client.databases.delete(databaseId); } } ``` `instantDatabase` is idempotent — if the database already exists, it returns the existing one without error. The `seed` parameter only runs on first creation. ## Strategy 2: Branch per CI Run [Section titled “Strategy 2: Branch per CI Run”](#strategy-2-branch-per-ci-run) Create a branch from a shared parent database. The branch starts with a full copy of the parent’s schema and data — useful for testing migrations against real data or running integration tests that expect pre-existing records. ### CLI workflow [Section titled “CLI workflow”](#cli-workflow-1) Terminal ```bash # Branch from the staging database BRANCH_NAME="ci-${GITHUB_RUN_ID:-$(date +%s)}" db9 branch create staging --name "$BRANCH_NAME" # Wait for the branch to become ACTIVE while true; do STATE=$(db9 db status "$BRANCH_NAME" --json | jq -r .state) if [ "$STATE" = "ACTIVE" ]; then break elif [ "$STATE" = "CREATE_FAILED" ]; then echo "Branch creation failed" db9 db status "$BRANCH_NAME" --json | jq .state_reason exit 1 fi sleep 2 done # Get the connection string export DATABASE_URL=$(db9 db connect "$BRANCH_NAME" --output quiet) # Run tests against the branch (has all of staging's data) npm test # Clean up db9 delete "$BRANCH_NAME" --yes ``` Branch creation is **asynchronous** — you must poll until the state is `ACTIVE` before connecting. Branches of small databases typically complete in seconds; larger databases take longer. ### GitHub Actions example [Section titled “GitHub Actions example”](#github-actions-example-1) .github/workflows/integration.yml ```yaml name: Integration Tests on: [push] jobs: test: runs-on: ubuntu-latest env: DB9_TOKEN: ${{ secrets.DB9_TOKEN }} steps: - uses: actions/checkout@v4 - name: Install DB9 CLI run: curl -fsSL https://db9.ai/install | sh - name: Create branch from staging run: | BRANCH_NAME="ci-${{ github.run_id }}-${{ github.run_attempt }}" db9 branch create staging --name "$BRANCH_NAME" # Poll until ready for i in $(seq 1 60); do STATE=$(db9 db status "$BRANCH_NAME" --json | jq -r .state) [ "$STATE" = "ACTIVE" ] && break [ "$STATE" = "CREATE_FAILED" ] && exit 1 sleep 2 done echo "DB_NAME=$BRANCH_NAME" >> "$GITHUB_ENV" echo "DATABASE_URL=$(db9 db connect "$BRANCH_NAME" --output quiet)" >> "$GITHUB_ENV" - name: Apply pending migration run: db9 db sql "$DB_NAME" -f migrations/latest.sql - name: Run integration tests run: npm test - name: Cleanup if: always() run: db9 delete "$DB_NAME" --yes ``` ### SDK workflow (TypeScript) [Section titled “SDK workflow (TypeScript)”](#sdk-workflow-typescript-1) test/branch-setup.ts ```typescript import { createDb9Client } from 'get-db9'; const client = createDb9Client(); export async function createTestBranch(parentId: string) { const branch = await client.databases.branch(parentId, { name: `ci-${process.env.GITHUB_RUN_ID || Date.now()}`, }); // Poll until ACTIVE let status; do { status = await client.databases.get(branch.id); if (status.state === 'CREATE_FAILED') { throw new Error(`Branch failed: ${status.state_reason}`); } await new Promise((r) => setTimeout(r, 2000)); } while (status.state !== 'ACTIVE'); return { databaseId: branch.id, connectionString: status.connection_string, }; } export async function deleteTestBranch(databaseId: string) { await client.databases.delete(databaseId); } ``` ## Parallel Test Workers [Section titled “Parallel Test Workers”](#parallel-test-workers) When running tests in parallel (e.g., Jest workers, pytest-xdist), give each worker its own database to avoid conflicts. ### One database per worker [Section titled “One database per worker”](#one-database-per-worker) test/worker-setup.ts ```typescript import { instantDatabase, createDb9Client } from 'get-db9'; const runId = process.env.GITHUB_RUN_ID || Date.now(); const workerId = process.env.JEST_WORKER_ID || '1'; export async function getWorkerDb() { return instantDatabase({ name: `ci-${runId}-w${workerId}`, seed: ` CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT UNIQUE, name TEXT); CREATE TABLE posts (id SERIAL PRIMARY KEY, title TEXT, author_id INT REFERENCES users(id)); `, }); } ``` The `seed` SQL runs only when the database is first created — subsequent calls with the same name return the existing database. ### Cleanup all worker databases [Section titled “Cleanup all worker databases”](#cleanup-all-worker-databases) Terminal ```bash # Delete all databases matching the CI run prefix for db in $(db9 list --json | jq -r '.[].name' | grep "^ci-${GITHUB_RUN_ID}"); do db9 delete "$db" --yes done ``` ## Short-Lived Credentials [Section titled “Short-Lived Credentials”](#short-lived-credentials) For CI environments where you want to limit credential exposure, use connect tokens instead of the admin password: Terminal ```bash # Get a short-lived token (10-minute TTL) TOKEN_JSON=$(db9 db connect "$DB_NAME" --json) # Extract the connection string (embeds host, port, user, and token) CONNECTION_STRING=$(echo "$TOKEN_JSON" | jq -r .connection_string) # Use with psql or any driver. # The connection string carries no sslmode parameter, so append one to require TLS. psql "${CONNECTION_STRING}?sslmode=require" ``` Connect tokens are RS256 JWTs that expire automatically. They cannot be used after expiry, so leaked CI logs pose less risk than static passwords. ## Cleanup Strategies [Section titled “Cleanup Strategies”](#cleanup-strategies) ### Immediate cleanup (recommended) [Section titled “Immediate cleanup (recommended)”](#immediate-cleanup-recommended) Delete the database in the same CI job using `if: always()`: YAML ```yaml - name: Cleanup if: always() run: db9 delete "$DB_NAME" --yes ``` ### Scheduled cleanup (fallback) [Section titled “Scheduled cleanup (fallback)”](#scheduled-cleanup-fallback) If CI jobs sometimes fail without running cleanup, add a scheduled workflow to catch orphans: .github/workflows/cleanup.yml ```yaml name: DB9 Cleanup on: schedule: - cron: '0 */6 * * *' # Every 6 hours jobs: cleanup: runs-on: ubuntu-latest env: DB9_TOKEN: ${{ secrets.DB9_TOKEN }} steps: - name: Install DB9 CLI run: curl -fsSL https://db9.ai/install | sh - name: Delete stale CI databases run: | db9 list --json | jq -r '.[].name' | grep '^ci-' | while read name; do echo "Deleting $name" db9 delete "$name" --yes done ``` ### Naming conventions [Section titled “Naming conventions”](#naming-conventions) Use deterministic names so CI runs don’t accumulate orphans: | Pattern | Example | Use case | | ----------------------- | ---------------- | ------------------------------------------- | | `ci-{run_id}` | `ci-12345678` | One database per run | | `ci-{run_id}-w{worker}` | `ci-12345678-w3` | Parallel workers | | `ci-{run_id}-{attempt}` | `ci-12345678-2` | Re-runs without conflicts | | `pr-{pr_number}` | `pr-42` | Preview environments (reused across pushes) | ## Limits to Know [Section titled “Limits to Know”](#limits-to-know) * **Anonymous accounts**: Limited to 5 databases total (including branches). Run `db9 claim` to remove the limit. * **Concurrent branch creations**: Maximum 2 at a time. A third returns HTTP 429 — retry after the in-progress branches finish. * **Branch creation is async**: Always poll for `ACTIVE` state. Database creation (`db9 create`) is synchronous and instant. * **Branches are full copies**: Each branch consumes its own storage. Large parent databases take longer to branch and use more quota. * **Deletion is permanent**: There is no undo. Ensure cleanup runs only delete databases with your CI prefix. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `409 Conflict` on database creation [Section titled “409 Conflict on database creation”](#409-conflict-on-database-creation) The database name already exists. Include the run attempt number in the name to handle CI re-runs, or use `instantDatabase` in the SDK which is idempotent. ### `429 Too Many Requests` on branch creation [Section titled “429 Too Many Requests on branch creation”](#429-too-many-requests-on-branch-creation) Only 2 branches can be created concurrently. If your CI creates branches in parallel, serialize the creation steps or use ephemeral databases instead (no concurrency limit on `db9 create`). ### Database not cleaned up after failed CI run [Section titled “Database not cleaned up after failed CI run”](#database-not-cleaned-up-after-failed-ci-run) Add `if: always()` to your cleanup step. For persistent orphans, use the scheduled cleanup workflow above. All CI databases should use a consistent naming prefix (e.g., `ci-`) for safe bulk deletion. ### `ECONNREFUSED` in CI [Section titled “ECONNREFUSED in CI”](#econnrefused-in-ci) Verify the connection string uses port **5433** and host `pg.db9.io`. Ensure `sslmode=require` is set — DB9 requires TLS for all connections. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Branching Workflows](/docs/guides/branching-workflows/) — branch concepts, lifecycle, and non-CI use cases * [Connect](/docs/connect/) — connection strings and authentication * [TypeScript SDK](/docs/sdk/) — `instantDatabase()` and programmatic database management * [Limits and Quotas](/docs/platform/limits-and-quotas/) — account and branching limits * [Production Checklist](/docs/production-checklist/) — deployment readiness # Django > Use Django with DB9 — connect with Django ORM over standard PostgreSQL, define models, run migrations, and build views. Django connects to DB9 through the standard PostgreSQL backend using `psycopg2`. No special adapter or driver is needed — Django’s ORM, migrations, and management commands work as expected. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Python 3.10+ * Django 4.2+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name django-app ``` Get the connection string: Terminal ```bash db9 db status django-app ``` Set the connection string as an environment variable: .env.local ```env DATABASE_URL="postgresql://django-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require" ``` ## Configure Database Connection [Section titled “Configure Database Connection”](#configure-database-connection) * dj-database-url Install dependencies: Terminal ```bash pip install django psycopg2-binary dj-database-url python-dotenv ``` settings.py ```python import dj_database_url from dotenv import load_dotenv load_dotenv(".env.local") DATABASES = { "default": dj_database_url.config( default="postgresql://django-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require", conn_max_age=600, ) } ``` * Direct config Install dependencies: Terminal ```bash pip install django psycopg2-binary ``` settings.py ```python DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "postgres", "USER": "django-app.admin", "PASSWORD": "YOUR_PASSWORD", "HOST": "pg.db9.io", "PORT": "5433", "OPTIONS": { "sslmode": "require", }, } } ``` ## Define Models [Section titled “Define Models”](#define-models) blog/models.py ```python from django.db import models class User(models.Model): email = models.EmailField(unique=True) name = models.CharField(max_length=100) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "users" def __str__(self): return self.name class Post(models.Model): title = models.CharField(max_length=500) content = models.TextField(blank=True) published = models.BooleanField(default=False) author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts") created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "posts" def __str__(self): return self.title ``` ## Run Migrations [Section titled “Run Migrations”](#run-migrations) Generate and apply migrations: Terminal ```bash python manage.py makemigrations blog python manage.py migrate ``` Verify the tables exist: Terminal ```bash python manage.py dbshell \dt ``` ## Views and URLs [Section titled “Views and URLs”](#views-and-urls) ### List view [Section titled “List view”](#list-view) blog/views.py ```python from django.http import JsonResponse from .models import User, Post def user_list(request): users = User.objects.all().values("id", "name", "email", "created_at") return JsonResponse(list(users), safe=False) def create_user(request): if request.method == "POST": import json data = json.loads(request.body) user = User.objects.create( email=data["email"], name=data["name"], ) return JsonResponse({"id": user.id, "name": user.name}, status=201) return JsonResponse({"error": "POST required"}, status=405) def post_list(request): posts = Post.objects.select_related("author").values( "id", "title", "content", "author__name", "created_at" ) return JsonResponse(list(posts), safe=False) ``` ### URL configuration [Section titled “URL configuration”](#url-configuration) blog/urls.py ```python from django.urls import path from . import views urlpatterns = [ path("users/", views.user_list, name="user-list"), path("users/create/", views.create_user, name="create-user"), path("posts/", views.post_list, name="post-list"), ] ``` myproject/urls.py ```python from django.urls import path, include urlpatterns = [ path("api/", include("blog.urls")), ] ``` ## Django REST Framework [Section titled “Django REST Framework”](#django-rest-framework) For a full API layer, add Django REST Framework: Terminal ```bash pip install djangorestframework ``` blog/serializers.py ```python from rest_framework import serializers from .models import User, Post class PostSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ["id", "title", "content", "published", "author", "created_at"] class UserSerializer(serializers.ModelSerializer): posts = PostSerializer(many=True, read_only=True) class Meta: model = User fields = ["id", "email", "name", "posts", "created_at"] ``` blog/views\_api.py ```python from rest_framework import viewsets from .models import User, Post from .serializers import UserSerializer, PostSerializer class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.prefetch_related("posts").all() serializer_class = UserSerializer class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.select_related("author").all() serializer_class = PostSerializer ``` blog/urls.py ```python from django.urls import path, include from rest_framework.routers import DefaultRouter from .views_api import UserViewSet, PostViewSet router = DefaultRouter() router.register("users", UserViewSet) router.register("posts", PostViewSet) urlpatterns = [ path("api/", include(router.urls)), ] ``` ## Production Notes [Section titled “Production Notes”](#production-notes) Never expose the connection string to the client DB9 connections must happen server-side only. The connection string contains your admin credentials — never expose it in client-facing code or templates. Use environment variables and keep `.env.local` out of version control. * **Server-side only**: Django runs on the server by default, so all DB queries go through the backend. Never log or render the connection string. * **Connection pooling**: Set `CONN_MAX_AGE` to reuse connections across requests. For high-traffic apps, consider `django-db-connection-pool` or PgBouncer. * **Port 5433**: DB9 uses port 5433, not the default PostgreSQL port 5432. Double-check your `DATABASES` config. * **TLS required**: Always include `sslmode=require` in the connection string or set it in `OPTIONS` for DB9’s hosted service. * **WSGI server**: Use Gunicorn or uWSGI in production. Django’s development server (`runserver`) is not designed for production traffic. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Connection refused on port 5432 [Section titled “Connection refused on port 5432”](#connection-refused-on-port-5432) DB9 uses port **5433**, not 5432. Update your `DATABASES` config or `DATABASE_URL` to use the correct port. ### `OperationalError: FATAL: authentication failed` [Section titled “OperationalError: FATAL: authentication failed”](#operationalerror-fatal-authentication-failed) Verify the username format. DB9 expects `{database-name}.admin` as the username (e.g., `django-app.admin`). ### Migrations fail with `relation already exists` [Section titled “Migrations fail with relation already exists”](#migrations-fail-with-relation-already-exists) If you created tables manually before running migrations, use `python manage.py migrate --fake` to mark existing migrations as applied. Then run future migrations normally. ### SSL connection errors [Section titled “SSL connection errors”](#ssl-connection-errors) Ensure `sslmode=require` is set. If using the direct config, add it under `OPTIONS`: Python ```python "OPTIONS": { "sslmode": "require", } ``` ### `psycopg2` not found [Section titled “psycopg2 not found”](#psycopg2-not-found) Install the binary package: `pip install psycopg2-binary`. For production, compile from source with `pip install psycopg2` (requires `libpq-dev`). ## Next Pages [Section titled “Next Pages”](#next-pages) * [SQLAlchemy](/docs/guides/python-sqlalchemy/) — Python ORM alternative with SQLAlchemy 2.0 * [Connect](/docs/connect/) — connection strings and authentication * [Production Checklist](/docs/production-checklist/) — deployment readiness # Drizzle > Use Drizzle ORM with DB9 — type-safe schema definitions, queries, transactions, raw SQL for advanced features, and vector search. Drizzle connects to DB9 using the `pg` (node-postgres) driver over pgwire. No special adapter is needed — Drizzle’s type-safe query builder, prepared statements, and transaction support work out of the box. Full Drizzle compatibility DB9 passes 100% of Drizzle compatibility tests (75/75) covering CRUD, transactions, advanced SQL, and vector operations. No custom adapter needed. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Node.js 18+ * Drizzle ORM 0.29+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name drizzle-app ``` Get the connection string: Terminal ```bash db9 db status drizzle-app ``` ## Project Setup [Section titled “Project Setup”](#project-setup) Terminal ```bash mkdir drizzle-db9 && cd drizzle-db9 npm init -y npm install drizzle-orm pg npm install -D drizzle-kit @types/pg typescript ts-node ``` ## Schema Definition [Section titled “Schema Definition”](#schema-definition) Drizzle defines schemas in TypeScript using `pgTable`: schema.ts ```typescript import { pgTable, serial, varchar, text, boolean, integer, jsonb, timestamp } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).unique().notNull(), name: varchar('name', { length: 100 }), metadata: jsonb('metadata'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), }); export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: varchar('title', { length: 500 }).notNull(), content: text('content'), published: boolean('published').default(false), authorId: integer('author_id').notNull().references(() => users.id, { onDelete: 'cascade' }), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), }); ``` ## Connect and Create Tables [Section titled “Connect and Create Tables”](#connect-and-create-tables) Use raw SQL to create tables rather than `drizzle-kit push`, which relies on schema introspection that may not fully work with DB9: setup.ts ```typescript import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; import { sql } from 'drizzle-orm'; const pool = new Pool({ connectionString: 'postgresql://drizzle-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres', }); const db = drizzle(pool); async function setup() { await db.execute(sql` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(100), metadata JSONB, created_at TIMESTAMPTZ DEFAULT now() ) `); await db.execute(sql` CREATE TABLE IF NOT EXISTS posts ( id SERIAL PRIMARY KEY, title VARCHAR(500) NOT NULL, content TEXT, published BOOLEAN DEFAULT false, author_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TIMESTAMPTZ DEFAULT now() ) `); console.log('Tables created'); await pool.end(); } setup(); ``` Terminal ```bash npx ts-node setup.ts ``` ## CRUD Operations [Section titled “CRUD Operations”](#crud-operations) TypeScript ```typescript import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; import { eq, and, sql } from 'drizzle-orm'; import { users, posts } from './schema'; const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); const db = drizzle(pool); // Create const [user] = await db.insert(users).values({ email: 'alice@example.com', name: 'Alice', metadata: { role: 'admin', level: 3 }, }).returning(); // Create with relation const [post] = await db.insert(posts).values({ title: 'Getting Started with DB9', content: 'DB9 is a serverless PostgreSQL-compatible database.', authorId: user.id, }).returning(); // Read with filter const admins = await db.select() .from(users) .where(eq(users.name, 'Alice')); // Update await db.update(users) .set({ name: 'Alice Updated' }) .where(eq(users.id, user.id)); // Upsert (insert ... on conflict) await db.insert(users) .values({ email: 'alice@example.com', name: 'Alice V2' }) .onConflictDoUpdate({ target: users.email, set: { name: 'Alice V2' }, }); // Delete (cascades to posts) await db.delete(users).where(eq(users.id, user.id)); ``` ## Transactions [Section titled “Transactions”](#transactions) Drizzle supports interactive transactions with PostgreSQL isolation levels: TypeScript ```typescript // Interactive transaction const result = await db.transaction(async (tx) => { const [user] = await tx.insert(users).values({ email: 'bob@example.com', name: 'Bob', }).returning(); const [post] = await tx.insert(posts).values({ title: 'First Post', authorId: user.id, }).returning(); return { user, post }; }); // With isolation level await db.transaction(async (tx) => { const allUsers = await tx.select().from(users); return allUsers; }, { isolationLevel: 'repeatable read', }); ``` Supported isolation levels: `read committed`, `repeatable read`. `serializable` is not supported and returns an error — use `repeatable read` instead. ## Raw SQL for Advanced Features [Section titled “Raw SQL for Advanced Features”](#raw-sql-for-advanced-features) Some PostgreSQL features require raw SQL via `db.execute`: ### Window functions [Section titled “Window functions”](#window-functions) TypeScript ```typescript const ranked = await db.execute(sql` SELECT name, email, ROW_NUMBER() OVER (ORDER BY created_at) AS row_num, RANK() OVER (ORDER BY name) AS name_rank FROM users `); ``` ### CTEs and recursive queries [Section titled “CTEs and recursive queries”](#ctes-and-recursive-queries) TypeScript ```typescript const hierarchy = await db.execute(sql` WITH RECURSIVE tree AS ( SELECT id, name, 0 AS depth FROM categories WHERE parent_id IS NULL UNION ALL SELECT c.id, c.name, t.depth + 1 FROM categories c JOIN tree t ON c.parent_id = t.id WHERE t.depth < 10 ) SELECT * FROM tree ORDER BY depth, name `); ``` ### DISTINCT ON [Section titled “DISTINCT ON”](#distinct-on) TypeScript ```typescript const latest = await db.execute(sql` SELECT DISTINCT ON (author_id) * FROM posts ORDER BY author_id, created_at DESC `); ``` ### Vector similarity search [Section titled “Vector similarity search”](#vector-similarity-search) TypeScript ```typescript // Create vector table and index await db.execute(sql` CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS documents ( id SERIAL PRIMARY KEY, content TEXT NOT NULL, embedding VECTOR(1024) ); -- HNSW index building is disabled in the current release: this is rejected -- over the wire protocol (55000) and creates an unused index over the HTTP -- SQL API. The search below works either way, via an exact scan. CREATE INDEX IF NOT EXISTS idx_docs ON documents USING hnsw (embedding vector_cosine_ops); `); // Search const queryVector = '[0.1, 0.2, ...]'; const results = await db.execute(sql` SELECT content, embedding <=> ${queryVector}::vector AS distance FROM documents ORDER BY distance LIMIT 5 `); ``` ## Schema Changes [Section titled “Schema Changes”](#schema-changes) Since `drizzle-kit push` may not fully work with DB9’s schema introspection, manage schema changes with raw SQL: TypeScript ```typescript // Add a column await db.execute(sql` ALTER TABLE users ADD COLUMN IF NOT EXISTS bio TEXT `); // Add an index await db.execute(sql` CREATE INDEX IF NOT EXISTS idx_users_email ON users (email) `); ``` For teams, track migrations as numbered SQL files and apply them in order. ## Production Notes [Section titled “Production Notes”](#production-notes) * **Connection pooling**: Drizzle uses `pg.Pool` for connection management. DB9 supports multiple concurrent connections per tenant. * **Driver**: Drizzle uses the `pg` (node-postgres) driver, not `postgres.js`. Use `drizzle-orm/node-postgres`, not `drizzle-orm/postgres-js`. * **Prepared statements**: Drizzle uses prepared statements by default. These work correctly with DB9’s extended query protocol. * **Type parser conflict**: Drizzle modifies the global `pg` type parsers at import time. If you use multiple ORMs in the same Node.js process, Drizzle’s parser changes can affect other libraries. Run Drizzle in its own process if you see unexpected type coercion. * **Connection string**: Use the format `postgresql://{db}.admin:{password}@pg.db9.io:5433/postgres`. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `drizzle-kit push` fails [Section titled “drizzle-kit push fails”](#drizzle-kit-push-fails) DB9 has limited `information_schema` support. Use raw SQL for DDL operations instead of `drizzle-kit push`. You can still use your Drizzle schema definitions for type-safe queries — the schema file is used by the query builder at compile time, not at runtime. ### Connection timeout [Section titled “Connection timeout”](#connection-timeout) Ensure the connection string includes the correct tenant ID in the username (e.g., `drizzle-app.admin`). DB9 routes connections by parsing the tenant from the username. ### Type coercion issues [Section titled “Type coercion issues”](#type-coercion-issues) Drizzle overrides `pg`’s default type parsers for dates, numerics, and other types. If you see unexpected values (e.g., dates as strings instead of Date objects), check that your Drizzle version matches your `pg` driver version. You can reset type parsers after import if needed. ### JSONB query differences [Section titled “JSONB query differences”](#jsonb-query-differences) Drizzle’s JSONB operators work with DB9. For advanced JSONB queries not covered by the type-safe API, use `db.execute(sql`…`)` with PostgreSQL’s native `->`, `->>`, and `@>` operators. ## Verified Compatibility [Section titled “Verified Compatibility”](#verified-compatibility) Tested with Drizzle v0.29+ against DB9. All 75 tests pass covering: | Category | Status | | ----------------------------------------------- | ------------------- | | Connection and pooling | Pass | | CRUD operations | Pass | | Transactions and isolation levels | Pass | | Query filters and JSONB | Pass | | Window functions, CTEs, subqueries | Pass | | Vector operations; HNSW index building disabled | Pass (exact search) | | DDL operations | Pass | ## Next Pages [Section titled “Next Pages”](#next-pages) * [Prisma](/docs/guides/prisma/) — Prisma ORM with DB9 * [Connect](/docs/connect/) — connection strings and authentication * [RAG with Built-in Embeddings](/docs/guides/rag-with-built-in-embeddings/) — vector search with server-side embedding * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full PostgreSQL compatibility surface * [TypeScript SDK](/docs/sdk/) — programmatic database management # Express / Hono > Use Express or Hono with DB9 — build a Node.js API connected to DB9 using Prisma, Drizzle, or node-postgres. Express and Hono connect to DB9 through any standard PostgreSQL driver over pgwire. DB9 is PostgreSQL-compatible, so no special adapter is needed. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Node.js 18+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name express-app ``` Get the connection string: Terminal ```bash db9 db status express-app ``` Set the connection string as an environment variable: .env ```env DATABASE_URL="postgresql://express-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require" ``` ## Setup [Section titled “Setup”](#setup) * Express + Prisma Terminal ```bash mkdir express-db9 && cd express-db9 npm init -y npm install express @prisma/client dotenv npm install -D prisma typescript @types/express @types/node ts-node npx prisma init npx tsc --init ``` Set `DATABASE_URL` in `.env` (see above), then define your schema: prisma/schema.prisma ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique name String createdAt DateTime @default(now()) @map("created_at") @@map("users") } ``` Push the schema and generate the client: Terminal ```bash npx prisma db push npx prisma generate ``` * Express + pg Terminal ```bash mkdir express-db9 && cd express-db9 npm init -y npm install express pg dotenv npm install -D typescript @types/express @types/pg @types/node ts-node npx tsc --init ``` Create the connection pool: src/db.ts ```typescript import { Pool } from 'pg'; import 'dotenv/config'; export const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); ``` Create the table: src/setup.ts ```typescript import { pool } from './db'; async function setup() { await pool.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ) `); console.log('Table created'); await pool.end(); } setup(); ``` Terminal ```bash npx ts-node src/setup.ts ``` * Hono + Drizzle Terminal ```bash mkdir hono-db9 && cd hono-db9 npm init -y npm install hono @hono/node-server drizzle-orm pg dotenv npm install -D drizzle-kit @types/pg typescript @types/node ts-node npx tsc --init ``` Define the schema: src/schema.ts ```typescript import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).unique().notNull(), name: varchar('name', { length: 100 }).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), }); ``` Create the DB client: src/db.ts ```typescript import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; import 'dotenv/config'; const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); export const db = drizzle(pool); ``` Create the table with raw SQL (recommended over `drizzle-kit push` for DB9): src/setup.ts ```typescript import { Pool } from 'pg'; import 'dotenv/config'; const pool = new Pool({ connectionString: process.env.DATABASE_URL }); async function setup() { await pool.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ) `); console.log('Table created'); await pool.end(); } setup(); ``` Terminal ```bash npx ts-node src/setup.ts ``` ## CRUD Routes [Section titled “CRUD Routes”](#crud-routes) * Express + Prisma src/index.ts ```typescript import express from 'express'; import { PrismaClient } from '@prisma/client'; import 'dotenv/config'; const app = express(); const prisma = new PrismaClient(); app.use(express.json()); app.get('/users', async (_req, res) => { const users = await prisma.user.findMany({ orderBy: { createdAt: 'desc' }, }); res.json(users); }); app.post('/users', async (req, res) => { const user = await prisma.user.create({ data: { email: req.body.email, name: req.body.name }, }); res.status(201).json(user); }); app.get('/users/:id', async (req, res) => { const user = await prisma.user.findUnique({ where: { id: Number(req.params.id) }, }); if (!user) return res.status(404).json({ error: 'Not found' }); res.json(user); }); app.delete('/users/:id', async (req, res) => { await prisma.user.delete({ where: { id: Number(req.params.id) } }); res.status(204).end(); }); app.listen(3000, () => console.log('Listening on http://localhost:3000')); ``` * Express + pg src/index.ts ```typescript import express from 'express'; import { pool } from './db'; import 'dotenv/config'; const app = express(); app.use(express.json()); app.get('/users', async (_req, res) => { const { rows } = await pool.query( 'SELECT * FROM users ORDER BY created_at DESC' ); res.json(rows); }); app.post('/users', async (req, res) => { const { email, name } = req.body; const { rows } = await pool.query( 'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *', [email, name] ); res.status(201).json(rows[0]); }); app.get('/users/:id', async (req, res) => { const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [ req.params.id, ]); if (!rows.length) return res.status(404).json({ error: 'Not found' }); res.json(rows[0]); }); app.delete('/users/:id', async (req, res) => { await pool.query('DELETE FROM users WHERE id = $1', [req.params.id]); res.status(204).end(); }); app.listen(3000, () => console.log('Listening on http://localhost:3000')); ``` * Hono + Drizzle src/index.ts ```typescript import { Hono } from 'hono'; import { serve } from '@hono/node-server'; import { db } from './db'; import { users } from './schema'; import { eq } from 'drizzle-orm'; const app = new Hono(); app.get('/users', async (c) => { const allUsers = await db.select().from(users).orderBy(users.createdAt); return c.json(allUsers); }); app.post('/users', async (c) => { const body = await c.req.json(); const [user] = await db .insert(users) .values({ email: body.email, name: body.name }) .returning(); return c.json(user, 201); }); app.get('/users/:id', async (c) => { const id = Number(c.req.param('id')); const [user] = await db.select().from(users).where(eq(users.id, id)); if (!user) return c.json({ error: 'Not found' }, 404); return c.json(user); }); app.delete('/users/:id', async (c) => { const id = Number(c.req.param('id')); await db.delete(users).where(eq(users.id, id)); return c.body(null, 204); }); serve({ fetch: app.fetch, port: 3000 }, () => { console.log('Listening on http://localhost:3000'); }); ``` ## Error Handling [Section titled “Error Handling”](#error-handling) * Express + Prisma src/index.ts ```typescript import { Prisma } from '@prisma/client'; import { Request, Response, NextFunction } from 'express'; app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { if (err instanceof Prisma.PrismaClientKnownRequestError) { if (err.code === 'P2002') { return res.status(409).json({ error: 'Duplicate entry' }); } } console.error(err); res.status(500).json({ error: 'Internal server error' }); }); ``` * Express + pg src/index.ts ```typescript import { Request, Response, NextFunction } from 'express'; app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { if ((err as any).code === '23505') { return res.status(409).json({ error: 'Duplicate entry' }); } console.error(err); res.status(500).json({ error: 'Internal server error' }); }); ``` * Hono + Drizzle src/index.ts ```typescript app.onError((err, c) => { if ((err as any).code === '23505') { return c.json({ error: 'Duplicate entry' }, 409); } console.error(err); return c.json({ error: 'Internal server error' }, 500); }); ``` ## Production Notes [Section titled “Production Notes”](#production-notes) * **Port 5433**: DB9 uses port 5433, not the default PostgreSQL port 5432. Double-check your `DATABASE_URL`. * **TLS required**: Always include `sslmode=require` in the connection string for DB9’s hosted service. * **Connection pooling**: Start with 5-10 connections (`max` option in `pg.Pool`, or Prisma’s `connection_limit` URL parameter). DB9 handles per-tenant pooling server-side. * **Graceful shutdown**: Close the database pool when the process exits to avoid leaked connections: - Express + Prisma src/index.ts ```typescript process.on('SIGTERM', async () => { await prisma.$disconnect(); process.exit(0); }); ``` - Express + pg src/index.ts ```typescript process.on('SIGTERM', async () => { await pool.end(); process.exit(0); }); ``` - Hono + Drizzle src/index.ts ```typescript import { Pool } from 'pg'; // keep a reference to the pool (export it from db.ts) process.on('SIGTERM', async () => { await pool.end(); process.exit(0); }); ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `ECONNREFUSED` on port 5432 [Section titled “ECONNREFUSED on port 5432”](#econnrefused-on-port-5432) DB9 uses port **5433**, not 5432. Verify your `DATABASE_URL` includes `:5433`. ### Connection pool exhaustion [Section titled “Connection pool exhaustion”](#connection-pool-exhaustion) If you see “too many connections” errors, reduce the pool size and confirm you are not creating a new pool on every request. Reuse a single pool instance across the application. ### TLS / SSL errors [Section titled “TLS / SSL errors”](#tls--ssl-errors) DB9 requires TLS. Make sure your connection string includes `?sslmode=require`. If you configure `pg.Pool` options directly, set `ssl: true` or `ssl: { rejectUnauthorized: false }` for development. ### `prisma db push` or `drizzle-kit push` fails [Section titled “prisma db push or drizzle-kit push fails”](#prisma-db-push-or-drizzle-kit-push-fails) DB9 has limited `information_schema` support. Use `prisma db push` for Prisma (works in most cases) or manage tables with raw SQL for Drizzle. See the [Prisma guide](/docs/guides/prisma/) and [Drizzle guide](/docs/guides/drizzle/) for details. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Prisma](/docs/guides/prisma/) — full Prisma integration guide * [Drizzle](/docs/guides/drizzle/) — full Drizzle integration guide * [Connect](/docs/connect/) — connection strings and authentication * [Production Checklist](/docs/production-checklist/) — deployment readiness # Flask > Use Flask with DB9 — connect with SQLAlchemy over standard PostgreSQL, define models, and build API routes. Flask connects to DB9 through SQLAlchemy and any standard PostgreSQL driver (psycopg2 or psycopg3). DB9 is PostgreSQL-compatible, so no special adapter is needed. This guide shows how to set up a Flask app with DB9 using Flask-SQLAlchemy for model management and Flask-Migrate for schema migrations. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Python 3.10+ * Flask 3.0+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name flask-app ``` Get the connection string: Terminal ```bash db9 db status flask-app ``` Set the connection string as an environment variable: .env ```env DATABASE_URL="postgresql://flask-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require" ``` ## Project Setup [Section titled “Project Setup”](#project-setup) Terminal ```bash mkdir flask-db9 && cd flask-db9 python -m venv venv source venv/bin/activate pip install flask flask-sqlalchemy flask-migrate psycopg2-binary python-dotenv ``` ## Configure the App [Section titled “Configure the App”](#configure-the-app) app/\_\_init\_\_.py ```python import os from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from dotenv import load_dotenv load_dotenv() db = SQLAlchemy() migrate = Migrate() def create_app(): app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["DATABASE_URL"] app.config["SQLALCHEMY_ENGINE_OPTIONS"] = { "pool_size": 5, "pool_recycle": 300, } db.init_app(app) migrate.init_app(app, db) from app import routes app.register_blueprint(routes.bp) return app ``` ## Define Models [Section titled “Define Models”](#define-models) app/models.py ```python from datetime import datetime, timezone from app import db class User(db.Model): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(255), unique=True, nullable=False) name = db.Column(db.String(100), nullable=False) created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) posts = db.relationship("Post", backref="author", lazy=True) class Post(db.Model): __tablename__ = "posts" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(500), nullable=False) content = db.Column(db.Text) published = db.Column(db.Boolean, default=False) author_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) ``` ## Run Migrations [Section titled “Run Migrations”](#run-migrations) Terminal ```bash flask db init flask db migrate -m "create users and posts" flask db upgrade ``` ## Routes [Section titled “Routes”](#routes) app/routes.py ```python from flask import Blueprint, jsonify, request from app import db from app.models import User bp = Blueprint("api", __name__, url_prefix="/api") @bp.route("/users", methods=["GET"]) def list_users(): users = User.query.order_by(User.created_at.desc()).all() return jsonify([ {"id": u.id, "name": u.name, "email": u.email} for u in users ]) @bp.route("/users", methods=["POST"]) def create_user(): data = request.get_json() user = User(name=data["name"], email=data["email"]) db.session.add(user) db.session.commit() return jsonify({"id": user.id, "name": user.name, "email": user.email}), 201 ``` ## Run the App [Section titled “Run the App”](#run-the-app) wsgi.py ```python from app import create_app app = create_app() ``` Terminal ```bash flask --app wsgi run --debug ``` Test with curl: Terminal ```bash curl -X POST http://localhost:5000/api/users \ -H "Content-Type: application/json" \ -d '{"name": "Alice", "email": "alice@example.com"}' curl http://localhost:5000/api/users ``` ## Production Notes [Section titled “Production Notes”](#production-notes) * **Server-side only**: DB9 connections happen on the server. Never expose the connection string to client-side code. * **Connection pooling**: Start with `pool_size=5`. DB9 handles per-tenant connection pooling on the server side. * **TLS required**: Use `sslmode=require` in the connection string. * **Port 5433**: DB9 uses port 5433, not the default PostgreSQL port 5432. * **WSGI server**: Use Gunicorn or uWSGI in production instead of the Flask dev server. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `ECONNREFUSED` on port 5432 [Section titled “ECONNREFUSED on port 5432”](#econnrefused-on-port-5432) DB9 uses port **5433**, not 5432. Verify your `DATABASE_URL` includes the correct port. ### `psycopg2` not found [Section titled “psycopg2 not found”](#psycopg2-not-found) Install psycopg2-binary for development or psycopg2 (with libpq) for production: Terminal ```bash pip install psycopg2-binary ``` ### Migration errors [Section titled “Migration errors”](#migration-errors) If `flask db upgrade` fails with schema errors, check that your `DATABASE_URL` points to the correct DB9 database. DB9 supports standard PostgreSQL DDL, but some `information_schema` queries may differ. See the [SQLAlchemy guide](/docs/guides/python-sqlalchemy/) for details. ### SSL connection errors [Section titled “SSL connection errors”](#ssl-connection-errors) Ensure `sslmode=require` is in your connection string. If using psycopg2, the SSL parameters are passed through the connection string automatically. ## Next Pages [Section titled “Next Pages”](#next-pages) * [SQLAlchemy](/docs/guides/python-sqlalchemy/) — full SQLAlchemy integration guide * [Connect](/docs/connect/) — connection strings and authentication * [Production Checklist](/docs/production-checklist/) — deployment readiness # GORM > Use GORM with DB9 — Go struct models, AutoMigrate, CRUD, transactions, raw SQL, and vector search over pgwire. GORM connects to DB9 using the `pgx` v5 driver via `gorm.io/driver/postgres`. Struct-based models, AutoMigrate, the query builder, and transactions work out of the box. DB9’s E2E smoke tests validate CRUD, transactions, savepoints, joins, subqueries, window functions, DDL, and vector operations through GORM. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Go 1.22+ * GORM 1.25+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name gorm-app ``` Get the connection string: Terminal ```bash db9 db status gorm-app ``` ## Project Setup [Section titled “Project Setup”](#project-setup) Terminal ```bash mkdir gorm-db9 && cd gorm-db9 go mod init gorm-db9 go get gorm.io/gorm gorm.io/driver/postgres ``` ## Connection [Section titled “Connection”](#connection) main.go ```go package main import ( "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" "log" ) func main() { dsn := "postgresql://gorm-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require" db, err := gorm.Open( postgres.New(postgres.Config{DSN: dsn}), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }, ) if err != nil { log.Fatalf("failed to connect: %v", err) } // Verify connection sqlDB, _ := db.DB() if err := sqlDB.Ping(); err != nil { log.Fatalf("ping failed: %v", err) } log.Println("Connected to DB9") } ``` ## Define Models [Section titled “Define Models”](#define-models) Use Go structs with GORM tags: models.go ```go package main import "time" type User struct { ID uint `gorm:"primaryKey"` Email string `gorm:"not null;uniqueIndex;size:255"` Name string `gorm:"not null;size:100"` Age int `gorm:"default:0"` IsActive bool `gorm:"default:true"` Bio *string `gorm:"type:text"` Metadata map[string]any `gorm:"serializer:json"` CreatedAt time.Time UpdatedAt time.Time Posts []Post `gorm:"foreignKey:AuthorID"` } type Post struct { ID uint `gorm:"primaryKey"` Title string `gorm:"not null;size:500"` Content string `gorm:"type:text;not null"` Published bool `gorm:"default:false"` AuthorID uint `gorm:"not null"` Author User `gorm:"foreignKey:AuthorID"` CreatedAt time.Time } ``` GORM features used: * `primaryKey` — auto-increment primary key * `uniqueIndex` — creates a unique index * `serializer:json` — automatically encodes/decodes `map[string]any` as JSONB * `CreatedAt` / `UpdatedAt` — auto-managed by GORM ## Create Tables [Section titled “Create Tables”](#create-tables) Use `AutoMigrate` to create tables from your model definitions: Go ```go if err := db.AutoMigrate(&User{}, &Post{}); err != nil { log.Fatalf("AutoMigrate: %v", err) } ``` For production, use raw SQL for more control over schema changes: Go ```go db.Exec(`ALTER TABLE users ADD COLUMN IF NOT EXISTS phone TEXT`) db.Exec(`CREATE INDEX IF NOT EXISTS idx_users_email ON users (email)`) ``` ## CRUD Operations [Section titled “CRUD Operations”](#crud-operations) Go ```go // Create user := User{ Email: "alice@example.com", Name: "Alice", Age: 30, Metadata: map[string]any{"role": "admin"}, } if err := db.Create(&user).Error; err != nil { log.Fatalf("create: %v", err) } // user.ID is now populated // Read by primary key var found User db.First(&found, user.ID) // Read with condition var alice User db.Where("email = ?", "alice@example.com").First(&alice) // Read with multiple conditions var active []User db.Where("age >= ? AND is_active = ?", 25, true). Order("name ASC"). Limit(10). Find(&active) // Update db.Model(&user).Update("name", "Alice Updated") // Update multiple fields db.Model(&user).Updates(map[string]any{ "name": "Alice V2", "age": 31, }) // Delete db.Delete(&user) ``` ## Transactions [Section titled “Transactions”](#transactions) ### Transaction block (auto-commit/rollback) [Section titled “Transaction block (auto-commit/rollback)”](#transaction-block-auto-commitrollback) Go ```go err := db.Transaction(func(tx *gorm.DB) error { user := User{Email: "txn@example.com", Name: "Txn User", Age: 25} if err := tx.Create(&user).Error; err != nil { return err } post := Post{Title: "Atomic Post", Content: "Created in a transaction.", AuthorID: user.ID} if err := tx.Create(&post).Error; err != nil { return err } return nil // auto-commits // return errors.New("...") would auto-rollback }) ``` ### Manual control [Section titled “Manual control”](#manual-control) Go ```go tx := db.Begin() if err := tx.Create(&User{Email: "manual@example.com", Name: "Manual"}).Error; err != nil { tx.Rollback() log.Fatal(err) } tx.Commit() ``` ### Nested transactions (savepoints) [Section titled “Nested transactions (savepoints)”](#nested-transactions-savepoints) GORM automatically uses savepoints for nested `Transaction` calls: Go ```go db.Transaction(func(tx *gorm.DB) error { tx.Create(&User{Email: "outer@example.com", Name: "Outer"}) // Creates a savepoint internally tx.Transaction(func(nested *gorm.DB) error { nested.Exec("UPDATE users SET age = age + 1 WHERE email = 'outer@example.com'") return nil }) return nil }) ``` ### Isolation levels [Section titled “Isolation levels”](#isolation-levels) Go ```go tx := db.Begin() tx.Exec("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ") // ... queries run at the requested isolation level tx.Commit() ``` Supported: `READ COMMITTED`, `REPEATABLE READ`. `SERIALIZABLE` is not implemented — it does not error, it is silently downgraded to `REPEATABLE READ` with a server `WARNING`. Set `REPEATABLE READ` explicitly so the intent is visible in your code. ## Raw SQL [Section titled “Raw SQL”](#raw-sql) Use `db.Raw` for SELECT queries and `db.Exec` for statements: Go ```go // Raw SELECT var count int64 db.Raw("SELECT COUNT(*) FROM users WHERE age > ?", 25).Scan(&count) // Scan into struct var results []struct { Name string `gorm:"column:name"` Total int64 `gorm:"column:total"` } db.Raw(` SELECT name, COUNT(*) AS total FROM users GROUP BY name HAVING COUNT(*) >= 1 `).Scan(&results) // Window functions var ranked []struct { ID int `gorm:"column:id"` RN int64 `gorm:"column:rn"` } db.Raw(` SELECT id, ROW_NUMBER() OVER (PARTITION BY author_id ORDER BY id) AS rn FROM posts `).Scan(&ranked) // Joins var ids []int db.Raw(` SELECT u.id FROM users u INNER JOIN posts p ON p.author_id = u.id `).Scan(&ids) // Subquery db.Raw(` SELECT id FROM users WHERE id IN (SELECT author_id FROM posts WHERE published = true) `).Scan(&ids) ``` ## Vector Search [Section titled “Vector Search”](#vector-search) GORM does not have a native vector column type. Use raw SQL for vector operations: Go ```go // Create vector table and index db.Exec(` CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS embeddings ( id SERIAL PRIMARY KEY, name VARCHAR(255), embedding vector(3) NOT NULL ); CREATE INDEX IF NOT EXISTS idx_embeddings ON embeddings USING hnsw (embedding vector_l2_ops); `) // Insert vectors db.Exec(`INSERT INTO embeddings (name, embedding) VALUES (?, ?)`, "doc-1", "[1.0, 2.0, 3.0]") // k-NN search by L2 distance var ids []int db.Raw(` SELECT id FROM embeddings WHERE embedding <-> '[1.0, 0.0, 0.0]' < 1.0 ORDER BY embedding <-> '[1.0, 0.0, 0.0]' `).Scan(&ids) // Cosine distance search var results []struct { Name string `gorm:"column:name"` Distance float64 `gorm:"column:distance"` } db.Raw(` SELECT name, cosine_distance(embedding, '[1.0, 1.0, 1.0]') AS distance FROM embeddings ORDER BY distance ASC LIMIT 5 `).Scan(&results) ``` DB9 recognizes HNSW indexes with `vector_l2_ops`, `vector_cosine_ops`, and `vector_ip_ops`; IVFFlat is not available at all. But HNSW **index building is disabled in the current release** — a `CREATE INDEX ... USING hnsw` is rejected over the wire protocol with `55000` and creates an unused index over the HTTP SQL API. Vector search still returns correct results via an exact scan. ## Schema Changes [Section titled “Schema Changes”](#schema-changes) For production, manage migrations with raw SQL: Go ```go // Add a column db.Exec("ALTER TABLE users ADD COLUMN IF NOT EXISTS phone TEXT") // Create an index db.Exec("CREATE INDEX IF NOT EXISTS idx_users_age ON users (age)") // Drop a column db.Exec("ALTER TABLE users DROP COLUMN IF EXISTS phone") ``` `AutoMigrate` can be used for development but relies on `information_schema` introspection that may not fully work with DB9 for complex alterations. ## Production Notes [Section titled “Production Notes”](#production-notes) * **Driver**: GORM uses `gorm.io/driver/postgres` which wraps `pgx` v5. No need to install `lib/pq`. * **Connection pooling**: Access the underlying `*sql.DB` via `db.DB()` to configure pool settings (`SetMaxOpenConns`, `SetMaxIdleConns`, `SetConnMaxLifetime`). * **JSON columns**: Use `gorm:"serializer:json"` for `map[string]any` fields. GORM automatically handles JSON encoding and decoding. * **Timestamp precision**: DB9 stores timestamps with microsecond precision, matching PostgreSQL. * **Connection string**: Use the format `postgresql://gorm-app.admin:{password}@pg.db9.io:5433/postgres?sslmode=require`. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `AutoMigrate` fails or creates unexpected schema [Section titled “AutoMigrate fails or creates unexpected schema”](#automigrate-fails-or-creates-unexpected-schema) DB9’s `information_schema` support is limited. For complex schema changes, use `db.Exec()` with raw DDL. `AutoMigrate` works for initial table creation but may not reliably detect existing schema differences. ### Connection refused [Section titled “Connection refused”](#connection-refused) Verify the host (`pg.db9.io`), port (`5433`), and username format (`{database-name}.admin`). DB9 routes connections by parsing the tenant from the username. ### SSL errors [Section titled “SSL errors”](#ssl-errors) DB9 requires TLS in production. Ensure your connection string includes `sslmode=require`. For local development, use `sslmode=disable`. ### Timestamp drift [Section titled “Timestamp drift”](#timestamp-drift) DB9 stores timestamps with microsecond precision, matching PostgreSQL. Go’s `time.Time` carries nanoseconds, so a value still loses its sub-microsecond digits on a round-trip — compare with a tolerance rather than for exact equality: Go ```go delta := createdAt.Sub(fromDB.CreatedAt).Abs() if delta > time.Microsecond { // unexpected drift } ``` ## Verified Compatibility [Section titled “Verified Compatibility”](#verified-compatibility) Tested with GORM 1.25+ and pgx v5 against DB9. E2E smoke tests cover: | Category | Status | | ----------------------------------------------- | ------------------- | | Connection and pooling | Pass | | CRUD (Create, First, Where, Update, Delete) | Pass | | Transactions and savepoints | Pass | | AutoMigrate | Pass | | JSON serialization (map → JSONB) | Pass | | Joins (INNER, LEFT) | Pass | | GROUP BY / HAVING | Pass | | Subqueries and window functions | Pass | | Vector operations; HNSW index building disabled | Pass (exact search) | | DDL (ALTER TABLE, CREATE INDEX) | Pass | ## Next Pages [Section titled “Next Pages”](#next-pages) * [SQLAlchemy](/docs/guides/python-sqlalchemy/) — Python ORM alternative * [Connect](/docs/connect/) — connection strings and authentication * [Vector Extension](/docs/extensions/vector/) — HNSW indexes and distance operators * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full PostgreSQL compatibility surface * [Prisma](/docs/guides/prisma/) — Node.js ORM alternative # HTTP from SQL > Call external APIs, webhooks, and services directly from SQL using DB9's built-in http extension — with security boundaries, timeouts, and rate limits. DB9’s http extension lets you make HTTP requests from SQL — fetch data from APIs, post to webhooks, or check service health without leaving the database. This is useful when agents need to call external services as part of a SQL workflow, or when you want to enrich query results with live data. This guide covers the common patterns, shows working examples, and explains the security and operational limits. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database (see [Quick Start](/docs/quickstart/)) * The `http` extension is enabled by default — no `CREATE EXTENSION` needed The http extension is one of two extensions enabled by default (along with `pg_cron`). Running `CREATE EXTENSION http` is harmless but unnecessary. Call the scalar form by its bare name Every example on this page uses the **scalar** form, which returns JSONB — call it as `http_get(url)`, without a schema qualifier. Writing `extensions.http_get(url)` in a `SELECT` list fails: ```plaintext ERROR: function extensions.http_get(text) does not exist SQLSTATE: 42883 ``` The `extensions.` prefix selects a different, table-valued form that is only valid in `FROM` position (`SELECT * FROM extensions.http_get(url)`). See [the http extension reference](/docs/extensions/http/) for the full comparison. ## 1. Make a GET Request [Section titled “1. Make a GET Request”](#1-make-a-get-request) Fetch data from an external API: SQL ```sql SELECT http_get('https://httpbin.org/get?name=db9'); ``` The function returns a JSONB object with four keys: | Key | Type | Description | | -------------- | ------ | -------------------------------------------------------- | | `status` | number | HTTP status code (200, 404, 500, etc.) | | `content_type` | string | Response Content-Type header, or NULL | | `headers` | array | Response headers as `[{"field": "...", "value": "..."}]` | | `content` | string | Response body as text | Extract individual fields using the JSONB `->>'key'` operator: SQL ```sql SELECT (http_get('https://httpbin.org/get?name=db9')->>'status')::int AS status, http_get('https://httpbin.org/get?name=db9')->>'content' AS content; ``` ### Parse JSON responses [Section titled “Parse JSON responses”](#parse-json-responses) Most APIs return JSON. Cast `content` to JSONB to extract fields: SQL ```sql SELECT (r->>'status')::int AS status, (r->>'content')::jsonb->>'origin' AS origin_ip FROM (SELECT http_get('https://httpbin.org/get') AS r) _; ``` ## 2. POST JSON to an API [Section titled “2. POST JSON to an API”](#2-post-json-to-an-api) Send data with `http_post()`: SQL ```sql SELECT (r->>'status')::int AS status, (r->>'content')::jsonb->>'json' AS echoed_body FROM (SELECT http_post( 'https://httpbin.org/post', '{"event": "user_signup", "user_id": 42}', 'application/json' ) AS r) _; ``` The three required arguments are: 1. **url** — the endpoint 2. **body** — request body as text 3. **content\_type** — the Content-Type header value ### Send form data [Section titled “Send form data”](#send-form-data) SQL ```sql SELECT (http_post( 'https://httpbin.org/post', 'username=alice&action=login', 'application/x-www-form-urlencoded' )->>'status')::int AS status; ``` ## 3. Add Custom Headers [Section titled “3. Add Custom Headers”](#3-add-custom-headers) Pass headers as JSONB — either object or array format: SQL ```sql -- Object format (simpler) SELECT (r->>'status')::int AS status, r->>'content' AS content FROM (SELECT http_get( 'https://httpbin.org/headers', '{"Authorization": "Bearer sk-test-123", "X-Request-ID": "req-abc"}'::jsonb ) AS r) _; -- Array format (pgsql-http compatible) SELECT (r->>'status')::int AS status, r->>'content' AS content FROM (SELECT http_get( 'https://httpbin.org/headers', '[{"field": "Authorization", "value": "Bearer sk-test-123"}]'::jsonb ) AS r) _; ``` ## 4. Other HTTP Methods [Section titled “4. Other HTTP Methods”](#4-other-http-methods) All standard methods are available: SQL ```sql -- PUT SELECT (http_put( 'https://httpbin.org/put', '{"name": "updated"}', 'application/json' )->>'status')::int AS status; -- PATCH SELECT (http_patch( 'https://httpbin.org/patch', '{"status": "active"}', 'application/json' )->>'status')::int AS status; -- DELETE SELECT (http_delete('https://httpbin.org/delete')->>'status')::int AS status; -- HEAD (returns headers only, no body) SELECT (http_head('https://httpbin.org/get')->>'status')::int AS status, http_head('https://httpbin.org/get')->>'headers' AS headers; ``` ### Universal function [Section titled “Universal function”](#universal-function) The `http()` function accepts the method as a string — useful when the method comes from a column or variable: SQL ```sql SELECT (r->>'status')::int AS status, r->>'content' AS content FROM (SELECT http( 'POST', 'https://httpbin.org/post', '{"Authorization": "Bearer token"}'::jsonb, 'application/json', '{"payload": "data"}' ) AS r) _; ``` Arguments: `method, url, [headers], [content_type], [body]`. ## 5. Practical Patterns [Section titled “5. Practical Patterns”](#5-practical-patterns) ### Send a webhook notification [Section titled “Send a webhook notification”](#send-a-webhook-notification) SQL ```sql SELECT (http_post( 'https://hooks.slack.com/services/T00/B00/xxx', '{"text": "New signup: user_id=42"}', 'application/json' )->>'status')::int AS status; ``` ### Enrich rows with API data [Section titled “Enrich rows with API data”](#enrich-rows-with-api-data) SQL ```sql SELECT u.id, u.email, (h.r->>'content')::jsonb->>'company' AS company FROM users u CROSS JOIN LATERAL (SELECT http_get( 'https://api.example.com/enrich?email=' || u.email ) AS r) h WHERE u.needs_enrichment = true LIMIT 10; ``` ### Store API responses in a table [Section titled “Store API responses in a table”](#store-api-responses-in-a-table) SQL ```sql CREATE TABLE api_snapshots ( id SERIAL PRIMARY KEY, fetched_at TIMESTAMPTZ DEFAULT now(), status INT, body JSONB ); INSERT INTO api_snapshots (status, body) SELECT (r->>'status')::int, (r->>'content')::jsonb FROM (SELECT http_get('https://api.example.com/metrics') AS r) _; ``` ### Health check multiple endpoints [Section titled “Health check multiple endpoints”](#health-check-multiple-endpoints) SQL ```sql SELECT url, (http_head(url)->>'status')::int AS status_code FROM (VALUES ('https://api.example.com/health'), ('https://db9.ai'), ('https://httpbin.org/status/200') ) t(url); ``` ## Security Boundaries [Section titled “Security Boundaries”](#security-boundaries) The http extension has built-in SSRF (Server-Side Request Forgery) protection: * **HTTPS only** — plain HTTP requests are blocked by default * **Private IPs blocked** — requests to `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, and link-local ranges are rejected * **DNS rebinding protection** — domain names are resolved and the resulting IP is validated against the same rules * **No credentials in URLs** — `user:pass@host` syntax is rejected * **Port restricted** — only standard ports (443 for HTTPS, 80 for HTTP) are allowed If a request is blocked, you get a clear error message: ```plaintext http: insecure http requests are disabled http: host is not allowed http: ip is not allowed ``` ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ------------------------------ | ------------------------------------------ | | Request timeout | 5 seconds (1 second connect timeout) | | Max response body | 1 MB | | Max request body | 256 KB | | Max redirects | 3 | | Max requests per SQL statement | 100 | | Concurrent requests per tenant | 20 (5 reserved for interactive, 15 shared) | These limits are fixed and cannot be changed with session parameters. ### What this means in practice [Section titled “What this means in practice”](#what-this-means-in-practice) * **Large responses** — if an API returns more than 1 MB, the request fails. Paginate or filter on the API side. * **Slow APIs** — anything over 5 seconds total (including DNS, connect, and transfer) times out. * **Bulk calls** — a single `SELECT` that calls `http_get()` for 100+ rows hits the per-statement limit. Break into batches. * **Concurrent load** — 20 concurrent requests per tenant prevents one database from monopolizing network resources. ## Caveats [Section titled “Caveats”](#caveats) * **Superuser only** — all http functions require the database admin role. * **UTF-8 responses only** — non-UTF-8 response bodies cause an error. Binary APIs (images, protobuf) are not supported. * **HEAD requests don’t follow redirects** — `http_head()` returns the redirect status (301, 302) rather than following it. Use `http_get()` if you need to follow redirects. * **Redirect method changes** — on 301-303 redirects, POST/PUT/PATCH become GET. Only 307-308 preserve the original method. * **No proxy support** — environment proxy variables are ignored. ## Next Pages [Section titled “Next Pages”](#next-pages) * [http Extension Reference](/docs/extensions/http/) — function signatures and header format details * [Scheduled Jobs with pg\_cron](/docs/guides/scheduled-jobs-with-pg-cron/) — combine HTTP calls with cron for periodic API polling * [Analyze Agent Logs with fs9](/docs/guides/analyze-agent-logs-with-fs9/) — store and query structured data in the filesystem * [Extensions Overview](/docs/extensions/) — all 9 built-in extensions * [CLI Reference](/docs/cli/) — `db9 db sql` for running queries from the terminal # Knex.js > Use Knex.js with DB9 — query builder, schema builder, transactions, raw SQL, and vector search. Knex connects to DB9 using the `pg` (node-postgres) driver over pgwire. The fluent query builder, schema builder, transaction support, and raw SQL all work without modification. DB9 passes 100% of Knex compatibility tests (97/97) covering CRUD, transactions, advanced SQL, and vector operations. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Node.js 18+ * Knex 3.1+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name knex-app ``` Get the connection string: Terminal ```bash db9 db status knex-app ``` ## Project Setup [Section titled “Project Setup”](#project-setup) Terminal ```bash mkdir knex-db9 && cd knex-db9 npm init -y npm install knex pg npm install -D typescript @types/pg @types/node ``` ## Connection [Section titled “Connection”](#connection) db.ts ```typescript import Knex from 'knex'; const db = Knex({ client: 'pg', connection: { host: 'pg.db9.io', port: 5433, user: 'knex-app.admin', password: 'YOUR_PASSWORD', database: 'postgres', ssl: { rejectUnauthorized: false }, }, pool: { min: 0, max: 10, }, }); export default db; ``` Test the connection: TypeScript ```typescript const result = await db.raw('SELECT 1 AS value'); console.log(result.rows); // [{ value: 1 }] ``` ## Create Tables [Section titled “Create Tables”](#create-tables) Use the Knex schema builder or raw SQL: setup.ts ```typescript import db from './db'; async function setup() { await db.schema.createTable('users', (table) => { table.increments('id').primary(); table.string('email', 255).notNullable().unique(); table.string('name', 100).notNullable(); table.integer('age').defaultTo(0); table.boolean('is_active').defaultTo(true); table.text('bio'); table.jsonb('metadata'); table.uuid('external_id'); table.timestamp('created_at', { useTz: true }).defaultTo(db.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(db.fn.now()); table.index('email'); }); await db.schema.createTable('posts', (table) => { table.increments('id').primary(); table.string('title', 500).notNullable(); table.text('content').notNullable(); table.boolean('published').defaultTo(false); table.integer('author_id').notNullable().references('id').inTable('users').onDelete('CASCADE'); table.timestamp('created_at', { useTz: true }).defaultTo(db.fn.now()); }); await db.schema.createTable('tags', (table) => { table.increments('id').primary(); table.string('name', 100).notNullable().unique(); }); await db.schema.createTable('post_tags', (table) => { table.integer('post_id').notNullable().references('id').inTable('posts').onDelete('CASCADE'); table.integer('tag_id').notNullable().references('id').inTable('tags').onDelete('CASCADE'); table.primary(['post_id', 'tag_id']); }); console.log('Tables created'); await db.destroy(); } setup(); ``` ## CRUD Operations [Section titled “CRUD Operations”](#crud-operations) TypeScript ```typescript import db from './db'; // Insert with RETURNING const [user] = await db('users') .insert({ email: 'alice@example.com', name: 'Alice', age: 30, metadata: { role: 'admin' } }) .returning('*'); // Bulk insert const users = await db('users') .insert([ { email: 'bob@example.com', name: 'Bob', age: 25 }, { email: 'carol@example.com', name: 'Carol', age: 35 }, ]) .returning('*'); // Select with filters const results = await db('users') .select('id', 'name', 'email') .where('age', '>=', 25) .orderBy('name', 'asc') .limit(10) .offset(0); // Get single row const alice = await db('users') .where({ email: 'alice@example.com' }) .first(); // Aggregates const count = await db('users').count('* as count').first(); const totalAge = await db('users').sum('age as total').first(); // Update with RETURNING const [updated] = await db('users') .where({ id: user.id }) .update({ name: 'Alice Updated' }) .returning(['id', 'name', 'updated_at']); // Upsert (INSERT ... ON CONFLICT) await db('users') .insert({ email: 'alice@example.com', name: 'Alice V2', age: 31 }) .onConflict('email') .merge(['name', 'age']); // Delete await db('users').where({ id: user.id }).del(); ``` ### WHERE operators [Section titled “WHERE operators”](#where-operators) TypeScript ```typescript // Comparison await db('users').where('age', '>', 25); await db('users').where('age', '<=', 30); await db('users').whereIn('name', ['Alice', 'Bob']); await db('users').whereNotNull('bio'); await db('users').whereNull('bio'); // Pattern matching await db('users').where('name', 'like', '%li%'); await db('users').where('name', 'ilike', '%alice%'); // Compound conditions await db('users') .where('age', '>=', 30) .whereNotNull('bio') .orWhere(function () { this.where('age', '<', 25).whereNull('bio'); }) .orderBy('age', 'desc'); ``` ## Joins [Section titled “Joins”](#joins) TypeScript ```typescript // INNER JOIN const postsWithAuthors = await db('users') .innerJoin('posts', 'users.id', 'posts.author_id') .select('users.name', 'posts.title'); // LEFT JOIN const allUsersWithPosts = await db('users') .leftJoin('posts', 'users.id', 'posts.author_id') .select('users.name', 'posts.title') .orderBy('users.name'); // GROUP BY with HAVING const activeAuthors = await db('users') .innerJoin('posts', 'users.id', 'posts.author_id') .select('users.name') .count('posts.id as post_count') .groupBy('users.name') .having(db.raw('COUNT(posts.id) > 1')) .orderBy('post_count', 'desc'); ``` ## Transactions [Section titled “Transactions”](#transactions) ### Callback-based (auto-commit/rollback) [Section titled “Callback-based (auto-commit/rollback)”](#callback-based-auto-commitrollback) TypeScript ```typescript await db.transaction(async (trx) => { const [user] = await trx('users') .insert({ email: 'txn@example.com', name: 'Txn User', age: 30 }) .returning('*'); await trx('posts') .insert({ title: 'Atomic Post', content: 'Created in a transaction.', author_id: user.id }); // Auto-commits on success, auto-rolls back on error }); ``` ### Manual control [Section titled “Manual control”](#manual-control) TypeScript ```typescript const trx = await db.transaction(); try { await trx('users') .insert({ email: 'manual@example.com', name: 'Manual', age: 28 }); await trx.commit(); } catch (err) { await trx.rollback(); throw err; } ``` ### Isolation levels [Section titled “Isolation levels”](#isolation-levels) TypeScript ```typescript const trx = await db.transaction(); try { await trx.raw('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ'); const users = await trx('users').select('*'); await trx.commit(); } catch (err) { await trx.rollback(); } ``` Supported: `READ COMMITTED`, `REPEATABLE READ`. `SERIALIZABLE` is not implemented — it does not error, it is silently downgraded to `REPEATABLE READ` with a server `WARNING`. Set `REPEATABLE READ` explicitly so the intent is visible in your code. ## CTEs and Subqueries [Section titled “CTEs and Subqueries”](#ctes-and-subqueries) TypeScript ```typescript // CTE with .with() const olderUsers = await db .with('older', db('users').where('age', '>=', 30)) .select('name', 'age') .from('older') .orderBy('age'); // Subquery in WHERE const aboveAvg = await db('users') .where('age', '>', db('users').avg('age')); // Subquery in SELECT const ranked = await db('users') .select('name') .select( db('users') .count('*') .where('age', '<=', db.ref('users.age')) .as('rank') ) .orderBy('age'); ``` ## Raw SQL for Advanced Features [Section titled “Raw SQL for Advanced Features”](#raw-sql-for-advanced-features) TypeScript ```typescript // Window functions const ranked = await db.raw(` SELECT name, age, ROW_NUMBER() OVER (ORDER BY age DESC) AS rank, DENSE_RANK() OVER (ORDER BY age DESC) AS dense_rank FROM users ORDER BY rank `); // Recursive CTE const hierarchy = await db.raw(` WITH RECURSIVE tree AS ( SELECT id, name, manager_id, 1 AS level FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.id, e.name, e.manager_id, t.level + 1 FROM employees e JOIN tree t ON e.manager_id = t.id ) SELECT * FROM tree ORDER BY level, name `); // DISTINCT ON const firstPerDept = await db.raw(` SELECT DISTINCT ON (department_id) name, department_id, salary FROM employees ORDER BY department_id, salary DESC `); // Parameterized queries const filtered = await db.raw( 'SELECT * FROM users WHERE age > ? AND age < ?', [20, 35] ); ``` ## Vector Search [Section titled “Vector Search”](#vector-search) Knex does not have a native vector type. Use raw SQL for vector operations: TypeScript ```typescript // Create vector table and index await db.raw(` CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS embeddings ( id SERIAL PRIMARY KEY, name VARCHAR(255), embedding vector(3) NOT NULL ); CREATE INDEX IF NOT EXISTS idx_embeddings ON embeddings USING hnsw (embedding vector_l2_ops); `); // Insert vectors const [doc] = await db('embeddings') .insert({ name: 'doc-1', embedding: '[1.0, 2.0, 3.0]' }) .returning('*'); // Cosine distance search const results = await db('embeddings') .select('name') .select(db.raw("cosine_distance(embedding, '[1.0, 1.0, 1.0]') AS distance")) .orderBy('distance', 'asc') .limit(5); // L2 distance with operator const nearest = await db.raw(` SELECT name FROM embeddings WHERE embedding <-> '[1.0, 0.0, 0.0]' < 1.0 ORDER BY embedding <-> '[1.0, 0.0, 0.0]' `); ``` DB9 recognizes HNSW indexes with `vector_l2_ops`, `vector_cosine_ops`, and `vector_ip_ops`; IVFFlat is not available at all. But HNSW **index building is disabled in the current release** — a `CREATE INDEX ... USING hnsw` is rejected over the wire protocol with `55000` and creates an unused index over the HTTP SQL API. Vector search still returns correct results via an exact scan. ## Schema Changes [Section titled “Schema Changes”](#schema-changes) Use the Knex schema builder or raw SQL for migrations: TypeScript ```typescript // Add a column await db.schema.alterTable('users', (table) => { table.text('phone'); }); // Create an index await db.raw('CREATE INDEX IF NOT EXISTS idx_users_age ON users (age)'); // Drop a column await db.schema.alterTable('users', (table) => { table.dropColumn('phone'); }); ``` For teams, use Knex’s built-in migration system with raw SQL inside migration files for full control over DDL. ## Production Notes [Section titled “Production Notes”](#production-notes) * **Driver**: Knex uses `pg` (node-postgres). Set `client: 'pg'` in the configuration. * **Connection pooling**: Configure `pool.min` and `pool.max`. Knex manages the pool internally; DB9 supports multiple concurrent connections per tenant. * **RETURNING**: Knex’s `.returning()` works with DB9 for INSERT, UPDATE, and DELETE. * **JSONB**: Knex passes JSONB values through to PostgreSQL. Use `db.raw()` for JSONB operators (`->`, `->>`, `@>`). * **Connection string**: Use `knex-app.admin` as the username, with host `pg.db9.io` and port `5433`. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Connection timeout [Section titled “Connection timeout”](#connection-timeout) Verify the host (`pg.db9.io`), port (`5433`), and username format (`{database-name}.admin`). DB9 routes connections by parsing the tenant from the username. ### Schema builder operations fail [Section titled “Schema builder operations fail”](#schema-builder-operations-fail) DB9’s `information_schema` support is limited. If `db.schema.alterTable` doesn’t work as expected, use `db.raw()` with direct DDL statements. ### JSONB queries [Section titled “JSONB queries”](#jsonb-queries) For JSONB operations beyond simple value storage, use `db.raw()` with PostgreSQL’s native JSONB operators: TypeScript ```typescript const admins = await db('users') .whereRaw("metadata @> ?::jsonb", [JSON.stringify({ role: 'admin' })]); ``` ## Verified Compatibility [Section titled “Verified Compatibility”](#verified-compatibility) Tested with Knex 3.1+ against DB9. All 97 tests pass covering: | Category | Status | | ----------------------------------------------- | ------------------- | | Connection and pooling | Pass | | CRUD with RETURNING | Pass | | WHERE operators and pattern matching | Pass | | Joins (INNER, LEFT) | Pass | | Transactions and isolation levels | Pass | | CTEs and subqueries | Pass | | Window functions | Pass | | Aggregates with GROUP BY / HAVING | Pass | | Vector operations; HNSW index building disabled | Pass (exact search) | | DDL operations | Pass | ## Next Pages [Section titled “Next Pages”](#next-pages) * [Sequelize](/docs/guides/sequelize/) — Sequelize ORM with DB9 * [Prisma](/docs/guides/prisma/) — Prisma ORM with DB9 * [Connect](/docs/connect/) — connection strings and authentication * [Vector Extension](/docs/extensions/vector/) — HNSW indexes and distance operators * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full PostgreSQL compatibility surface # Laravel > Use Laravel with DB9 — connect with Eloquent over standard PostgreSQL, define models, run migrations, and build controllers. Laravel connects to DB9 through PHP’s PDO PostgreSQL driver and Eloquent ORM. No special adapter or driver is needed — DB9 speaks the PostgreSQL wire protocol, so Laravel’s `pgsql` connection works out of the box. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * PHP 8.1+ * Composer * Laravel 10+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name laravel-app ``` Get the connection string: Terminal ```bash db9 db status laravel-app ``` Set the connection details as environment variables: Terminal ```bash export DB9_HOST="pg.db9.io" export DB9_PASSWORD="YOUR_PASSWORD" ``` ## Configure Database Connection [Section titled “Configure Database Connection”](#configure-database-connection) Update your `.env` file with the DB9 connection details: .env ```env DB_CONNECTION=pgsql DB_HOST=pg.db9.io DB_PORT=5433 DB_DATABASE=postgres DB_USERNAME=laravel-app.admin DB_PASSWORD=YOUR_PASSWORD ``` Then update the `pgsql` section of `config/database.php` to include `sslmode`: config/database.php ```php 'pgsql' => [ 'driver' => 'pgsql', 'host' => env('DB_HOST', 'pg.db9.io'), 'port' => env('DB_PORT', '5433'), 'database' => env('DB_DATABASE', 'postgres'), 'username' => env('DB_USERNAME', 'laravel-app.admin'), 'password' => env('DB_PASSWORD', ''), 'charset' => 'utf8', 'prefix' => '', 'prefix_indexes' => true, 'search_path' => 'public', 'sslmode' => 'require', ], ``` Use port 5433 and database name “postgres” DB9 uses port `5433` (not `5432`) and the database name is always `postgres`. Laravel defaults to port `5432` if you omit `DB_PORT`. ## Define Models [Section titled “Define Models”](#define-models) Generate a `User` model with a migration: Terminal ```bash php artisan make:model User -m ``` Generate a `Post` model with a migration: Terminal ```bash php artisan make:model Post -m ``` Edit the migration for users: database/migrations/xxxx\_xx\_xx\_create\_users\_table.php ```php public function up(): void { Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('email')->unique(); $table->timestamps(); }); } ``` Edit the migration for posts: database/migrations/xxxx\_xx\_xx\_create\_posts\_table.php ```php public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('body')->nullable(); $table->boolean('published')->default(false); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->timestamps(); }); } ``` Add relationships to the models: app/Models/User.php ```php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; class User extends Model { protected $fillable = ['name', 'email']; public function posts(): HasMany { return $this->hasMany(Post::class); } } ``` app/Models/Post.php ```php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Post extends Model { protected $fillable = ['title', 'body', 'published', 'user_id']; public function user(): BelongsTo { return $this->belongsTo(User::class); } } ``` ## Run Migrations [Section titled “Run Migrations”](#run-migrations) Terminal ```bash php artisan migrate ``` Tip Do not run `php artisan db:create` or attempt to create a new database — the `postgres` database already exists on DB9. Run `migrate` directly. Verify the tables exist: Terminal ```bash php artisan tinker >>> \DB::select('SELECT tablename FROM pg_tables WHERE schemaname = \'public\''); ``` ## Controllers and Routes [Section titled “Controllers and Routes”](#controllers-and-routes) Generate a resource controller: Terminal ```bash php artisan make:controller UserController --resource ``` Add CRUD logic: app/Http/Controllers/UserController.php ```php namespace App\Http\Controllers; use App\Models\User; use Illuminate\Http\Request; class UserController extends Controller { public function index() { return User::all(); } public function store(Request $request) { $validated = $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users', ]); $user = User::create($validated); return response()->json($user, 201); } public function show(User $user) { return $user->load('posts'); } public function update(Request $request, User $user) { $validated = $request->validate([ 'name' => 'sometimes|string|max:255', 'email' => 'sometimes|email|unique:users,email,' . $user->id, ]); $user->update($validated); return response()->json($user); } public function destroy(User $user) { $user->delete(); return response()->noContent(); } } ``` Register the routes: routes/api.php ```php use App\Http\Controllers\UserController; Route::apiResource('users', UserController::class); ``` Start the server and test: Terminal ```bash php artisan serve curl http://localhost:8000/api/users ``` ## Production Notes [Section titled “Production Notes”](#production-notes) * **Port 5433**: DB9 listens on port `5433`, not the PostgreSQL default `5432`. * **TLS required**: Always use `sslmode=require` in your database config for DB9’s hosted service. * **Connection pooling**: Set `DB_CONNECTION` pool size through Laravel’s `database.php` config or use an external pooler like PgBouncer for high-traffic apps. * **Database name**: The database is always `postgres`. Do not attempt to create additional databases. * **Queue workers**: If running Laravel queues with a database driver, each worker opens its own connection. Size your pool accordingly. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Connection refused on port 5432 [Section titled “Connection refused on port 5432”](#connection-refused-on-port-5432) DB9 uses port **5433**. Laravel defaults to `5432` if `DB_PORT` is not set. Check your `.env` file: env ```env DB_PORT=5433 ``` ### PDO pgsql extension not found [Section titled “PDO pgsql extension not found”](#pdo-pgsql-extension-not-found) Install the PHP PostgreSQL extension. On Ubuntu/Debian: Terminal ```bash sudo apt install php-pgsql ``` On macOS with Homebrew: Terminal ```bash brew install php ``` The `pdo_pgsql` extension is included by default. Verify with `php -m | grep pgsql`. ### Migration errors: relation already exists [Section titled “Migration errors: relation already exists”](#migration-errors-relation-already-exists) If tables were created manually before running migrations, use the `--pretend` flag to check what SQL would run, then mark migrations as complete: Terminal ```bash php artisan migrate --pretend php artisan migrate --path=database/migrations/xxxx_xx_xx_specific_migration.php ``` ### SSL connection errors [Section titled “SSL connection errors”](#ssl-connection-errors) Confirm `sslmode` is set to `require` in your `config/database.php` under the `pgsql` connection. If you are using a `DATABASE_URL` environment variable, append `?sslmode=require`: env ```env DATABASE_URL="postgresql://laravel-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require" ``` ### Authentication failed [Section titled “Authentication failed”](#authentication-failed) Verify the username format. DB9 expects `.admin` as the username (e.g., `laravel-app.admin`). ## Next Pages [Section titled “Next Pages”](#next-pages) * [Connect](/docs/connect/) — connection strings and authentication * [Production Checklist](/docs/production-checklist/) — deployment readiness # Next.js > Use Next.js with DB9 — connect from Server Components, Route Handlers, and Server Actions using Prisma, Drizzle, or node-postgres. Next.js connects to DB9 through any standard PostgreSQL driver over pgwire. DB9 is PostgreSQL-compatible, so Prisma, Drizzle, node-postgres (`pg`), and other drivers work without a custom adapter. This guide shows how to set up a Next.js app with DB9 using the most common patterns: Prisma for schema-first workflows and Drizzle for TypeScript-first query building. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Node.js 18+ * Next.js 14+ (App Router recommended) ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name nextjs-app ``` Get the connection string: Terminal ```bash db9 db status nextjs-app ``` Set the connection string as an environment variable: .env.local ```env DATABASE_URL="postgresql://nextjs-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require" ``` * Prisma Prisma passes 100% of DB9 compatibility tests (89/89). See the full [Prisma guide](/docs/guides/prisma/) for detailed coverage. ### Setup [Section titled “Setup”](#setup) Terminal ```bash npx create-next-app@latest nextjs-db9 --typescript --app cd nextjs-db9 npm install prisma @prisma/client npx prisma init ``` ### Schema [Section titled “Schema”](#schema) prisma/schema.prisma ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique name String posts Post[] createdAt DateTime @default(now()) } model Post { id Int @id @default(autoincrement()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId Int createdAt DateTime @default(now()) } ``` Create the tables with raw SQL (recommended over `prisma migrate` for DB9): Terminal ```bash npx prisma db push ``` Generate the client: Terminal ```bash npx prisma generate ``` ### Singleton client [Section titled “Singleton client”](#singleton-client) Create a shared Prisma instance to avoid connection exhaustion during development: lib/prisma.ts ```typescript import { PrismaClient } from '@prisma/client'; const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; export const prisma = globalForPrisma.prisma ?? new PrismaClient(); if (process.env.NODE_ENV !== 'production') { globalForPrisma.prisma = prisma; } ``` ### Server Component [Section titled “Server Component”](#server-component) app/users/page.tsx ```typescript import { prisma } from '@/lib/prisma'; export default async function UsersPage() { const users = await prisma.user.findMany({ include: { posts: true }, orderBy: { createdAt: 'desc' }, }); return (
    {users.map((user) => (
  • {user.name} — {user.posts.length} posts
  • ))}
); } ``` ### Server Action [Section titled “Server Action”](#server-action) app/users/actions.ts ```typescript 'use server'; import { prisma } from '@/lib/prisma'; import { revalidatePath } from 'next/cache'; export async function createUser(formData: FormData) { await prisma.user.create({ data: { email: formData.get('email') as string, name: formData.get('name') as string, }, }); revalidatePath('/users'); } ``` ### Route Handler [Section titled “Route Handler”](#route-handler) app/api/users/route.ts ```typescript import { prisma } from '@/lib/prisma'; import { NextResponse } from 'next/server'; export async function GET() { const users = await prisma.user.findMany(); return NextResponse.json(users); } export async function POST(request: Request) { const body = await request.json(); const user = await prisma.user.create({ data: body }); return NextResponse.json(user, { status: 201 }); } ``` * Drizzle Drizzle passes 100% of DB9 compatibility tests (75/75). See the full [Drizzle guide](/docs/guides/drizzle/) for detailed coverage. ### Setup [Section titled “Setup”](#setup-1) Terminal ```bash npx create-next-app@latest nextjs-db9 --typescript --app cd nextjs-db9 npm install drizzle-orm pg npm install -D drizzle-kit @types/pg ``` ### Schema [Section titled “Schema”](#schema-1) lib/schema.ts ```typescript import { pgTable, serial, varchar, text, boolean, integer, timestamp } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).unique().notNull(), name: varchar('name', { length: 100 }).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), }); export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: varchar('title', { length: 500 }).notNull(), content: text('content'), published: boolean('published').default(false), authorId: integer('author_id').notNull().references(() => users.id), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), }); ``` ### Singleton client [Section titled “Singleton client”](#singleton-client-1) lib/db.ts ```typescript import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; const globalForDb = globalThis as unknown as { pool: Pool }; const pool = globalForDb.pool ?? new Pool({ connectionString: process.env.DATABASE_URL, }); if (process.env.NODE_ENV !== 'production') { globalForDb.pool = pool; } export const db = drizzle(pool); ``` ### Server Component [Section titled “Server Component”](#server-component-1) app/users/page.tsx ```typescript import { db } from '@/lib/db'; import { users, posts } from '@/lib/schema'; import { eq } from 'drizzle-orm'; export default async function UsersPage() { const allUsers = await db.select().from(users).orderBy(users.createdAt); return (
    {allUsers.map((user) => (
  • {user.name} ({user.email})
  • ))}
); } ``` ### Server Action [Section titled “Server Action”](#server-action-1) app/users/actions.ts ```typescript 'use server'; import { db } from '@/lib/db'; import { users } from '@/lib/schema'; import { revalidatePath } from 'next/cache'; export async function createUser(formData: FormData) { await db.insert(users).values({ email: formData.get('email') as string, name: formData.get('name') as string, }); revalidatePath('/users'); } ``` * node-postgres For minimal setups without an ORM: lib/db.ts ```typescript import { Pool } from 'pg'; const globalForDb = globalThis as unknown as { pool: Pool }; const pool = globalForDb.pool ?? new Pool({ connectionString: process.env.DATABASE_URL, }); if (process.env.NODE_ENV !== 'production') { globalForDb.pool = pool; } export default pool; ``` app/api/users/route.ts ```typescript import pool from '@/lib/db'; import { NextResponse } from 'next/server'; export async function GET() { const { rows } = await pool.query('SELECT id, name, email FROM users ORDER BY id'); return NextResponse.json(rows); } ``` ## Connection Singleton Pattern [Section titled “Connection Singleton Pattern”](#connection-singleton-pattern) Next.js in development mode reloads modules on every request due to Hot Module Replacement. Without a singleton, each reload creates a new connection pool, quickly exhausting DB9’s per-tenant connection limit. The `globalForPrisma` / `globalForDb` pattern stores the client on `globalThis` so it survives reloads: TypeScript ```typescript const globalForPool = globalThis as unknown as { pool: Pool }; const pool = globalForPool.pool ?? new Pool({ connectionString: process.env.DATABASE_URL }); if (process.env.NODE_ENV !== 'production') { globalForPool.pool = pool; } ``` In production, this is not needed — Next.js loads modules once — but the pattern is safe in both environments. ## Production Notes [Section titled “Production Notes”](#production-notes) Never expose the connection string to the client DB9 connections must happen server-side only (Server Components, Server Actions, Route Handlers). The connection string contains your admin credentials — never expose it in client components or browser code. Use the [Browser SDK](/docs/sdk-browser/) with a scoped publishable key for client-side data access. * **Server-side only**: DB9 connections must happen on the server (Server Components, Server Actions, Route Handlers, `getServerSideProps`). Never expose the connection string to the client. * **Connection pooling**: Start with 5–10 connections (`pool.max`). DB9 handles per-tenant connection pooling on the server side. * **TLS required**: Use `sslmode=require` in the connection string for DB9’s hosted service. * **Port 5433**: DB9 uses port 5433, not the default PostgreSQL port 5432. * **Edge Runtime**: DB9 requires a TCP pgwire connection. The Node.js runtime is required — Edge Runtime does not support raw TCP sockets. Use `export const runtime = 'nodejs'` in routes that access DB9. Edge Runtime is not supported DB9 uses raw TCP (pgwire protocol) which is not available in Next.js Edge Runtime (Cloudflare Workers-based). Add `export const runtime = 'nodejs'` to any Route Handler or Middleware that connects to DB9. * **Vercel deployment**: Set `DATABASE_URL` in Vercel’s environment variables. The connection works from Vercel Serverless Functions (Node.js runtime). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Connection pool exhaustion in development [Section titled “Connection pool exhaustion in development”](#connection-pool-exhaustion-in-development) If you see “too many connections” errors during development, ensure you’re using the singleton pattern described above. Restart the dev server to release stale connections. ### `ECONNREFUSED` on port 5432 [Section titled “ECONNREFUSED on port 5432”](#econnrefused-on-port-5432) DB9 uses port **5433**, not 5432. Verify your `DATABASE_URL` includes the correct port. ### Edge Runtime errors [Section titled “Edge Runtime errors”](#edge-runtime-errors) DB9 requires pgwire (TCP), which is not available in Edge Runtime. Add `export const runtime = 'nodejs'` to any route or page that queries DB9. ### `prisma migrate` fails [Section titled “prisma migrate fails”](#prisma-migrate-fails) DB9 has limited `information_schema` support. Use `prisma db push` for schema sync or manage tables with raw SQL. See the [Prisma guide](/docs/guides/prisma/) for details. ### `drizzle-kit push` fails [Section titled “drizzle-kit push fails”](#drizzle-kit-push-fails) Similar to Prisma, use raw SQL for DDL operations. See the [Drizzle guide](/docs/guides/drizzle/) for details. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Prisma](/docs/guides/prisma/) — full Prisma integration guide (89/89 tests) * [Drizzle](/docs/guides/drizzle/) — full Drizzle integration guide (75/75 tests) * [Connect](/docs/connect/) — connection strings and authentication * [Production Checklist](/docs/production-checklist/) — deployment readiness * [TypeScript SDK](/docs/sdk/) — programmatic database management # Nuxt > Use Nuxt with DB9 — connect from server routes and API handlers using Prisma, Drizzle, or node-postgres. Nuxt connects to DB9 through any standard PostgreSQL driver over pgwire. DB9 is PostgreSQL-compatible, so no special adapter is needed. This guide shows how to set up a Nuxt 3 app with DB9 using Prisma and Drizzle. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Node.js 18+ * Nuxt 3+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name nuxt-app ``` Get the connection string: Terminal ```bash db9 db status nuxt-app ``` Set the connection string as an environment variable: .env ```env DATABASE_URL="postgresql://nuxt-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require" ``` ## Setup [Section titled “Setup”](#setup) * Prisma Terminal ```bash npx nuxi@latest init nuxt-db9 cd nuxt-db9 npm install prisma @prisma/client npx prisma init ``` Define your schema: prisma/schema.prisma ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique name String createdAt DateTime @default(now()) } ``` Push the schema and generate the client: Terminal ```bash npx prisma db push npx prisma generate ``` Create a server utility to share the Prisma instance: server/utils/prisma.ts ```typescript import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); export default prisma; ``` * Drizzle Terminal ```bash npx nuxi@latest init nuxt-db9 cd nuxt-db9 npm install drizzle-orm pg npm install -D drizzle-kit @types/pg ``` Define your schema: server/db/schema.ts ```typescript import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).unique().notNull(), name: varchar('name', { length: 100 }).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), }); ``` Create a server utility for the database connection: server/utils/db.ts ```typescript import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); export const db = drizzle(pool); ``` ## Server API Route [Section titled “Server API Route”](#server-api-route) * Prisma server/api/users.get.ts ```typescript import prisma from '~/server/utils/prisma'; export default defineEventHandler(async () => { return await prisma.user.findMany({ orderBy: { createdAt: 'desc' }, }); }); ``` server/api/users.post.ts ```typescript import prisma from '~/server/utils/prisma'; export default defineEventHandler(async (event) => { const body = await readBody(event); return await prisma.user.create({ data: { email: body.email, name: body.name, }, }); }); ``` * Drizzle server/api/users.get.ts ```typescript import { db } from '~/server/utils/db'; import { users } from '~/server/db/schema'; export default defineEventHandler(async () => { return await db.select().from(users).orderBy(users.createdAt); }); ``` server/api/users.post.ts ```typescript import { db } from '~/server/utils/db'; import { users } from '~/server/db/schema'; export default defineEventHandler(async (event) => { const body = await readBody(event); const result = await db.insert(users).values({ email: body.email, name: body.name, }).returning(); return result[0]; }); ``` ## Composable Usage [Section titled “Composable Usage”](#composable-usage) Call the API routes from a page using `useFetch`: pages/users.vue ```vue ``` ## Production Notes [Section titled “Production Notes”](#production-notes) * **Server-side only**: All database access happens in the `server/` directory. Nuxt server routes and utilities never ship to the client, so your connection string stays private. * **Port 5433**: DB9 uses port 5433, not the default PostgreSQL port 5432. Verify your `DATABASE_URL` includes the correct port. * **TLS required**: Always include `sslmode=require` in your connection string for DB9’s hosted service. * **Nitro server preset**: Use the Node.js preset for deployment. Edge presets (Cloudflare Workers, Deno Deploy) do not support raw TCP connections required by pgwire. Set `nitro.preset` in `nuxt.config.ts`: nuxt.config.ts ```typescript export default defineNuxtConfig({ nitro: { preset: 'node-server', }, }); ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `ECONNREFUSED` on port 5432 [Section titled “ECONNREFUSED on port 5432”](#econnrefused-on-port-5432) DB9 uses port **5433**, not 5432. Check that your `DATABASE_URL` includes `:5433`. ### Edge preset not supported [Section titled “Edge preset not supported”](#edge-preset-not-supported) DB9 requires pgwire (TCP), which is not available in edge runtimes. Switch to the `node-server` or `node-cluster` Nitro preset. ### Environment variables not available in Nitro [Section titled “Environment variables not available in Nitro”](#environment-variables-not-available-in-nitro) Nitro reads `.env` files automatically in development. For production, set `DATABASE_URL` through your hosting platform’s environment variable configuration. If needed, add `runtimeConfig` in `nuxt.config.ts`: nuxt.config.ts ```typescript export default defineNuxtConfig({ runtimeConfig: { databaseUrl: process.env.DATABASE_URL, }, }); ``` Then access it in server routes with `useRuntimeConfig().databaseUrl`. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Prisma](/docs/guides/prisma/) — full Prisma integration guide * [Drizzle](/docs/guides/drizzle/) — full Drizzle integration guide * [Connect](/docs/connect/) — connection strings and authentication * [Production Checklist](/docs/production-checklist/) — deployment readiness # Prisma > Use Prisma ORM with DB9 — connection setup, schema definition, CRUD operations, transactions, raw SQL for advanced features, and vector search. Prisma connects to DB9 using the standard PostgreSQL provider over pgwire. No special configuration is needed — Prisma’s binary protocol, connection pooling, and query builder work out of the box. Full Prisma compatibility DB9 passes 100% of Prisma compatibility tests (89/89) covering CRUD, transactions, relations, advanced SQL, and vector operations. No custom adapter or workarounds needed. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Node.js 18+ * Prisma 5.7+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name prisma-app ``` Get the connection string: Terminal ```bash db9 db status prisma-app ``` ## Project Setup [Section titled “Project Setup”](#project-setup) Terminal ```bash mkdir prisma-db9 && cd prisma-db9 npm init -y npm install @prisma/client npm install -D prisma typescript @types/node ts-node npx prisma init ``` Set the connection string in `.env`: Use port 5433 and database name “postgres” DB9 uses port `5433` (not `5432`) and the database name is always `postgres`. If Prisma generates a connection string with different values, update it manually. Terminal ```bash DATABASE_URL="postgresql://prisma-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres" ``` ## Schema Definition [Section titled “Schema Definition”](#schema-definition) Edit `prisma/schema.prisma`: prisma/schema.prisma ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique name String? metadata Json? createdAt DateTime @default(now()) @map("created_at") posts Post[] @@map("users") } model Post { id Int @id @default(autoincrement()) title String content String? published Boolean @default(false) authorId Int @map("author_id") author User @relation(fields: [authorId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @map("created_at") @@map("posts") } ``` ## Create Tables [Section titled “Create Tables”](#create-tables) Use raw SQL to create tables rather than `prisma migrate`, which relies on schema introspection that may not fully work with DB9: setup.ts ```typescript import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); async function setup() { await prisma.$executeRawUnsafe(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(100), metadata JSONB, created_at TIMESTAMPTZ DEFAULT now() ) `); await prisma.$executeRawUnsafe(` CREATE TABLE IF NOT EXISTS posts ( id SERIAL PRIMARY KEY, title VARCHAR(500) NOT NULL, content TEXT, published BOOLEAN DEFAULT false, author_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TIMESTAMPTZ DEFAULT now() ) `); console.log('Tables created'); await prisma.$disconnect(); } setup(); ``` Then generate the Prisma client: Terminal ```bash npx prisma generate npx ts-node setup.ts ``` ## CRUD Operations [Section titled “CRUD Operations”](#crud-operations) TypeScript ```typescript import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); // Create const user = await prisma.user.create({ data: { email: 'alice@example.com', name: 'Alice', metadata: { role: 'admin', level: 3 }, }, }); // Create with relation const post = await prisma.post.create({ data: { title: 'Getting Started with DB9', content: 'DB9 is a serverless PostgreSQL-compatible database.', authorId: user.id, }, }); // Read with filter const admins = await prisma.user.findMany({ where: { metadata: { path: ['role'], equals: 'admin' } }, include: { posts: true }, }); // Update await prisma.user.update({ where: { id: user.id }, data: { name: 'Alice Updated' }, }); // Upsert await prisma.user.upsert({ where: { email: 'alice@example.com' }, update: { name: 'Alice V2' }, create: { email: 'alice@example.com', name: 'Alice V2' }, }); // Delete (cascades to posts) await prisma.user.delete({ where: { id: user.id } }); ``` ## Transactions [Section titled “Transactions”](#transactions) Prisma supports interactive transactions with all PostgreSQL isolation levels: TypeScript ```typescript // Interactive transaction const result = await prisma.$transaction(async (tx) => { const user = await tx.user.create({ data: { email: 'bob@example.com', name: 'Bob' }, }); const post = await tx.post.create({ data: { title: 'First Post', authorId: user.id }, }); return { user, post }; }); // With isolation level await prisma.$transaction( async (tx) => { // Operations here run at REPEATABLE READ const count = await tx.user.count(); return count; }, { isolationLevel: 'RepeatableRead' } ); ``` Supported isolation levels: `ReadCommitted`, `RepeatableRead`, `Serializable` (runs as REPEATABLE READ on DB9). ## Raw SQL for Advanced Features [Section titled “Raw SQL for Advanced Features”](#raw-sql-for-advanced-features) Some PostgreSQL features require raw SQL via `$queryRaw`: ### Window functions [Section titled “Window functions”](#window-functions) TypeScript ```typescript const ranked = await prisma.$queryRaw` SELECT name, email, ROW_NUMBER() OVER (ORDER BY created_at) AS row_num, RANK() OVER (ORDER BY name) AS name_rank FROM users `; ``` ### CTEs and recursive queries [Section titled “CTEs and recursive queries”](#ctes-and-recursive-queries) TypeScript ```typescript const hierarchy = await prisma.$queryRaw` WITH RECURSIVE tree AS ( SELECT id, name, 0 AS depth FROM categories WHERE parent_id IS NULL UNION ALL SELECT c.id, c.name, t.depth + 1 FROM categories c JOIN tree t ON c.parent_id = t.id WHERE t.depth < 10 ) SELECT * FROM tree ORDER BY depth, name `; ``` ### DISTINCT ON [Section titled “DISTINCT ON”](#distinct-on) TypeScript ```typescript const latest = await prisma.$queryRaw` SELECT DISTINCT ON (author_id) * FROM posts ORDER BY author_id, created_at DESC `; ``` ### Vector similarity search [Section titled “Vector similarity search”](#vector-similarity-search) TypeScript ```typescript // Create vector table and index await prisma.$executeRawUnsafe(` CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS documents ( id SERIAL PRIMARY KEY, content TEXT NOT NULL, embedding VECTOR(1024) ); -- HNSW index building is disabled in the current release: this is rejected -- over the wire protocol (55000) and creates an unused index over the HTTP -- SQL API. The search below works either way, via an exact scan. CREATE INDEX IF NOT EXISTS idx_docs ON documents USING hnsw (embedding vector_cosine_ops); `); // Search const results = await prisma.$queryRaw` SELECT content, embedding <=> ${queryVector}::vector AS distance FROM documents ORDER BY distance LIMIT 5 `; ``` ## Schema Changes [Section titled “Schema Changes”](#schema-changes) Since `prisma migrate` may not fully work with DB9’s schema introspection, manage schema changes with raw SQL: TypeScript ```typescript // Add a column await prisma.$executeRawUnsafe(` ALTER TABLE users ADD COLUMN IF NOT EXISTS bio TEXT `); // Add an index await prisma.$executeRawUnsafe(` CREATE INDEX IF NOT EXISTS idx_users_email ON users (email) `); // Re-generate the Prisma client after schema changes // npx prisma generate ``` For teams, track migrations as numbered SQL files and apply them in order. ## Production Notes [Section titled “Production Notes”](#production-notes) * **Connection pooling**: Prisma manages its own connection pool. DB9 supports multiple concurrent connections per tenant. * **Binary protocol**: Prisma uses the PostgreSQL binary wire protocol via `pg` driver. This is fully supported. * **Prepared statements**: Prisma uses prepared statements by default. These work correctly with DB9’s extended query protocol. * **Connection string**: Use the format `postgresql://{db}.admin:{password}@pg.db9.io:5433/postgres`. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `prisma migrate` fails [Section titled “prisma migrate fails”](#prisma-migrate-fails) DB9 has limited `information_schema` support. Use raw SQL for DDL operations instead of `prisma migrate dev`. You can still use `prisma generate` to regenerate the client from your schema file. ### Connection timeout [Section titled “Connection timeout”](#connection-timeout) Ensure the connection string includes the correct tenant ID in the username (e.g., `prisma-app.admin`). DB9 routes connections by parsing the tenant from the username. ### JSONB query differences [Section titled “JSONB query differences”](#jsonb-query-differences) Prisma’s JSONB path filtering works with DB9. If you see unexpected results with the `->` operator, use `->>` for text extraction or `@>` for containment checks in raw queries. ### Type mismatches in raw queries [Section titled “Type mismatches in raw queries”](#type-mismatches-in-raw-queries) When using `$queryRaw` with vector types or custom types, cast explicitly: TypeScript ```typescript const results = await prisma.$queryRaw` SELECT * FROM documents ORDER BY embedding <=> ${vectorString}::vector LIMIT 10 `; ``` ## Verified Compatibility [Section titled “Verified Compatibility”](#verified-compatibility) Tested with Prisma v5.7+ against DB9. All 89 tests pass covering: | Category | Tests | Status | | ----------------------------------------------- | ----- | ------------------- | | Connection and pooling | 5 | Pass | | CRUD operations | 19 | Pass | | Transactions and isolation levels | 11 | Pass | | Query filters and JSONB | 23 | Pass | | Window functions, CTEs, subqueries | 30+ | Pass | | Vector operations; HNSW index building disabled | 22 | Pass (exact search) | | DDL operations | 1 | Pass | ## Next Pages [Section titled “Next Pages”](#next-pages) * [Drizzle](/docs/guides/drizzle/) — type-safe SQL with Drizzle ORM * [Connect](/docs/connect/) — connection strings and authentication * [RAG with Built-in Embeddings](/docs/guides/rag-with-built-in-embeddings/) — vector search with server-side embedding * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full PostgreSQL compatibility surface * [TypeScript SDK](/docs/sdk/) — programmatic database management # SQLAlchemy > Use SQLAlchemy 2.0 with DB9 — ORM models, CRUD, transactions, vector search, full-text search, and hybrid retrieval over pgwire. SQLAlchemy connects to DB9 using the `psycopg` (psycopg3) driver over pgwire. The modern SQLAlchemy 2.0 API with `mapped_column`, type-annotated models, and session-based transactions works without modification. DB9’s E2E test suite validates CRUD, JSONB, joins, subqueries, CTEs, window functions, DDL, transactions, savepoints, and vector operations through SQLAlchemy. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Python 3.10+ * SQLAlchemy 2.0+ * psycopg 3.1+ (with binary support) ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name sqlalchemy-app ``` Get the connection string: Terminal ```bash db9 db status sqlalchemy-app ``` ## Project Setup [Section titled “Project Setup”](#project-setup) Terminal ```bash mkdir sqlalchemy-db9 && cd sqlalchemy-db9 python -m venv .venv && source .venv/bin/activate pip install "sqlalchemy>=2.0.0" "psycopg[binary]>=3.1.0" ``` For vector search, also install: Terminal ```bash pip install "pgvector>=0.2.5" ``` ## Connection [Section titled “Connection”](#connection) SQLAlchemy requires the `postgresql+psycopg://` prefix. Normalize the connection string from DB9: db.py ```python from sqlalchemy import create_engine from sqlalchemy.engine import Engine from sqlalchemy.orm import Session, sessionmaker def build_engine(database_url: str) -> Engine: url = database_url.strip() if url.startswith("postgresql://"): url = "postgresql+psycopg://" + url[len("postgresql://"):] elif url.startswith("postgres://"): url = "postgresql+psycopg://" + url[len("postgres://"):] return create_engine(url, pool_pre_ping=True) engine = build_engine( "postgresql://sqlalchemy-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres" ) SessionFactory = sessionmaker(bind=engine, expire_on_commit=False) ``` Key engine options: * `pool_pre_ping=True` verifies the connection before each use. * `expire_on_commit=False` keeps loaded attributes accessible after commit. ## Define Models [Section titled “Define Models”](#define-models) Use SQLAlchemy 2.0 declarative style with `mapped_column`: models.py ```python from datetime import datetime from sqlalchemy import Integer, String, Text, DateTime, ForeignKey, func from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship class Base(DeclarativeBase): pass class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True) email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) name: Mapped[str] = mapped_column(String(100), nullable=False) metadata_: Mapped[dict] = mapped_column("metadata", JSONB, default=dict) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) posts: Mapped[list["Post"]] = relationship(back_populates="author", cascade="all, delete-orphan") class Post(Base): __tablename__ = "posts" id: Mapped[int] = mapped_column(Integer, primary_key=True) title: Mapped[str] = mapped_column(String(500), nullable=False) content: Mapped[str | None] = mapped_column(Text) author_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE")) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) author: Mapped["User"] = relationship(back_populates="posts") ``` ## Create Tables [Section titled “Create Tables”](#create-tables) Use `Base.metadata.create_all` to create tables from your model definitions: setup.py ```python from db import engine from models import Base Base.metadata.create_all(engine) print("Tables created") ``` For production, manage schema changes with raw SQL rather than relying on Alembic’s introspection, which may not fully work with DB9’s `information_schema` support: Python ```python from sqlalchemy import text with engine.begin() as conn: conn.execute(text("ALTER TABLE users ADD COLUMN IF NOT EXISTS bio TEXT")) conn.execute(text("CREATE INDEX IF NOT EXISTS idx_users_email ON users (email)")) ``` ## CRUD Operations [Section titled “CRUD Operations”](#crud-operations) Python ```python from sqlalchemy import select, text from db import SessionFactory from models import User, Post # Create with SessionFactory() as session: user = User(email="alice@example.com", name="Alice", metadata_={"role": "admin"}) session.add(user) session.commit() post = Post(title="Getting Started", content="Hello DB9", author_id=user.id) session.add(post) session.commit() # Read with SessionFactory() as session: user = session.get(User, 1) print(user.name) # "Alice" users = session.execute( select(User).where(User.name == "Alice") ).scalars().all() # Update with SessionFactory() as session: user = session.get(User, 1) user.name = "Alice Updated" session.commit() # Upsert (INSERT ... ON CONFLICT) from sqlalchemy.dialects.postgresql import insert with SessionFactory() as session: stmt = insert(User).values(email="alice@example.com", name="Alice V2") stmt = stmt.on_conflict_do_update( index_elements=["email"], set_={"name": stmt.excluded.name}, ) session.execute(stmt) session.commit() # Delete with SessionFactory() as session: user = session.get(User, 1) session.delete(user) # cascades to posts session.commit() ``` ## JSONB Queries [Section titled “JSONB Queries”](#jsonb-queries) SQLAlchemy’s PostgreSQL JSONB support works with DB9: Python ```python from sqlalchemy import select from models import User with SessionFactory() as session: # Containment query admins = session.execute( select(User).where(User.metadata_.contains({"role": "admin"})) ).scalars().all() # Raw SQL for advanced JSONB operations from sqlalchemy import text with engine.begin() as conn: conn.execute(text( "UPDATE users SET metadata = jsonb_set(metadata, '{level}', '5'::jsonb) WHERE id = 1" )) ``` ## Transactions [Section titled “Transactions”](#transactions) Python ```python # Explicit commit with SessionFactory() as session: session.add(User(email="bob@example.com", name="Bob")) session.flush() # sends to DB but doesn't commit session.commit() # commits the transaction # Rollback with SessionFactory() as session: session.add(User(email="temp@example.com", name="Temp")) session.flush() session.rollback() # user was not persisted # Savepoints with engine.begin() as conn: conn.execute(text("SAVEPOINT sp1")) conn.execute(text("INSERT INTO users (email, name) VALUES ('x@x.com', 'X')")) conn.execute(text("ROLLBACK TO SAVEPOINT sp1")) conn.execute(text("RELEASE SAVEPOINT sp1")) ``` Supported isolation levels: `READ COMMITTED`, `REPEATABLE READ`. `SERIALIZABLE` is not implemented — it does not error, it is silently downgraded to `REPEATABLE READ` with a server `WARNING`. Set `REPEATABLE READ` explicitly so the intent is visible in your code. Python ```python with engine.begin() as conn: conn.execute(text("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")) # ... queries run at the requested isolation level ``` ## Joins and Aggregations [Section titled “Joins and Aggregations”](#joins-and-aggregations) Python ```python from sqlalchemy import select, func from models import User, Post with SessionFactory() as session: # Join with aggregation results = session.execute( select(User.name, func.count(Post.id).label("post_count")) .join(Post, User.id == Post.author_id) .group_by(User.name) .having(func.count(Post.id) >= 1) .order_by(User.name) ).all() # Subquery subq = ( select(Post.author_id, func.count().label("cnt")) .group_by(Post.author_id) .subquery() ) top_authors = session.execute( select(User.name) .join(subq, User.id == subq.c.author_id) .where(subq.c.cnt >= 2) ).scalars().all() ``` ## Window Functions and CTEs [Section titled “Window Functions and CTEs”](#window-functions-and-ctes) Use raw SQL for window functions and CTEs: Python ```python from sqlalchemy import text with engine.begin() as conn: # Window function rows = conn.execute(text(""" SELECT name, email, ROW_NUMBER() OVER (ORDER BY created_at) AS row_num FROM users """)).fetchall() # CTE rows = conn.execute(text(""" WITH recent AS ( SELECT * FROM posts WHERE created_at > now() - interval '7 days' ) SELECT u.name, r.title FROM users u JOIN recent r ON u.id = r.author_id """)).fetchall() ``` ## Vector Search [Section titled “Vector Search”](#vector-search) Install `pgvector` for SQLAlchemy vector type support: vector\_models.py ```python from pgvector.sqlalchemy import Vector from sqlalchemy import Integer, Text from sqlalchemy.orm import Mapped, mapped_column from models import Base EMBEDDING_DIM = 1024 class Document(Base): __tablename__ = "documents" id: Mapped[int] = mapped_column(Integer, primary_key=True) content: Mapped[str] = mapped_column(Text, nullable=False) embedding: Mapped[list[float]] = mapped_column(Vector(EMBEDDING_DIM), nullable=False) ``` Create the table and HNSW index: Python ```python from sqlalchemy import text from db import engine from vector_models import Base with engine.begin() as conn: conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector")) Base.metadata.create_all(conn) conn.execute(text(""" CREATE INDEX IF NOT EXISTS idx_documents_embedding ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64) """)) ``` Search by cosine distance: Python ```python from sqlalchemy import select from vector_models import Document def search(session, query_embedding: list[float], top_k: int = 5): distance = Document.embedding.cosine_distance(query_embedding) stmt = ( select(Document, distance.label("distance")) .order_by(distance) .limit(top_k) ) return session.execute(stmt).all() with SessionFactory() as session: results = search(session, query_embedding=[0.1, 0.2, ...]) for doc, dist in results: print(f"{doc.content} (distance: {dist:.4f})") ``` IVFFlat indexes are not supported DB9 recognizes HNSW indexes (`vector_cosine_ops`, `vector_l2_ops`, `vector_ip_ops`) but **not** IVFFlat. If your code uses `CREATE INDEX ... USING ivfflat`, change it to `USING hnsw` — but note that HNSW **index building is disabled in the current release**: the statement is rejected over the wire protocol with `55000` and creates an unused index over the HTTP SQL API. Vector search still returns correct results via an exact scan. The supported operator classes are `vector_cosine_ops`, `vector_l2_ops` and `vector_ip_ops`; IVFFlat is not available at all. ## Full-Text Search [Section titled “Full-Text Search”](#full-text-search) DB9 supports PostgreSQL full-text search with `to_tsvector`, `websearch_to_tsquery`, and `ts_rank`: Python ```python from sqlalchemy import text with SessionFactory() as session: results = session.execute(text(""" SELECT content, ts_rank(to_tsvector('english', content), websearch_to_tsquery('english', :query)) AS rank FROM documents WHERE to_tsvector('english', content) @@ websearch_to_tsquery('english', :query) ORDER BY rank DESC LIMIT :top_k """), {"query": "database serverless", "top_k": 10}).mappings().all() ``` GIN indexes on text columns are used at runtime — a full-text predicate plans as an `Index Scan` and `EXPLAIN ANALYZE` reports `KV Table Scan Pairs: 0`, versus a `Seq Scan` over every row without the index. ## Production Notes [Section titled “Production Notes”](#production-notes) * **Driver**: Use `psycopg` (psycopg3), not `psycopg2`. The connection string prefix must be `postgresql+psycopg://`. * **Connection pooling**: SQLAlchemy’s built-in pool works with DB9. Use `pool_pre_ping=True` to handle idle connection drops. * **Alembic migrations**: Alembic’s autogenerate relies on `information_schema` introspection that may not fully work with DB9. Use explicit raw SQL for schema changes instead. * **hstore**: Pass `use_native_hstore=False` to `create_engine` if you use hstore columns. This keeps hstore values as strings rather than relying on psycopg’s automatic conversion. * **Connection string**: Use the format `postgresql+psycopg://sqlalchemy-app.admin:{password}@pg.db9.io:5433/postgres`. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `OperationalError: FATAL: authentication failed` [Section titled “OperationalError: FATAL: authentication failed”](#operationalerror-fatal-authentication-failed) Verify the tenant ID in the username. DB9 expects `{database-name}.admin` as the username (e.g., `sqlalchemy-app.admin`). ### `psycopg2` import errors [Section titled “psycopg2 import errors”](#psycopg2-import-errors) DB9’s SQLAlchemy integration uses `psycopg` (v3), not `psycopg2`. Install `psycopg[binary]` and use the `postgresql+psycopg://` prefix, not `postgresql+psycopg2://`. ### `CREATE EXTENSION vector` fails [Section titled “CREATE EXTENSION vector fails”](#create-extension-vector-fails) DB9 includes pgvector natively. The extension may already be available. Wrap the call in a try/except or use `CREATE EXTENSION IF NOT EXISTS vector`. ### Alembic `autogenerate` produces empty migrations [Section titled “Alembic autogenerate produces empty migrations”](#alembic-autogenerate-produces-empty-migrations) DB9’s `information_schema` coverage is limited. Write migration SQL manually and use Alembic’s `op.execute()` to run it, or manage migrations as numbered SQL files. ## Verified Compatibility [Section titled “Verified Compatibility”](#verified-compatibility) Tested with SQLAlchemy 2.0+ and psycopg 3.1+ against DB9. E2E smoke tests cover: | Category | Status | | ----------------------------------------------- | ------------------- | | Connection and pooling | Pass | | CRUD operations | Pass | | JSONB containment queries | Pass | | Transactions and savepoints | Pass | | Joins and aggregations | Pass | | Subqueries and CTEs | Pass | | Window functions | Pass | | DDL (ALTER TABLE, CREATE INDEX) | Pass | | Vector operations; HNSW index building disabled | Pass (exact search) | | Upsert (ON CONFLICT) | Pass | ## Next Pages [Section titled “Next Pages”](#next-pages) * [RAG with Built-in Embeddings](/docs/guides/rag-with-built-in-embeddings/) — vector search with DB9-native embedding * [Connect](/docs/connect/) — connection strings and authentication * [Vector Extension](/docs/extensions/vector/) — HNSW indexes and distance operators * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full PostgreSQL compatibility surface * [Prisma](/docs/guides/prisma/) — Node.js ORM alternative * [Drizzle](/docs/guides/drizzle/) — TypeScript ORM alternative # RAG with Built-in Embeddings > Build a retrieval-augmented generation pipeline using DB9's native embedding() function and vector search — no external embedding service needed. DB9 includes a built-in `embedding()` function and HNSW vector indexes. You can build a complete RAG retrieval pipeline inside the database without managing embedding API keys in your application — the server handles the embedding API calls. This guide walks through the full flow: create a table, embed documents, build an index, and query by semantic similarity. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database (see [Quick Start](/docs/quickstart/)) * The `embedding` and `vector` extensions enabled SQL ```sql CREATE EXTENSION IF NOT EXISTS embedding; CREATE EXTENSION IF NOT EXISTS vector; ``` ▶ Run 1. **Create a Documents Table** SQL ```sql CREATE TABLE documents ( id SERIAL PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, embedding vector(1024) ); ``` The `embedding()` function returns 1024-dimensional vectors by default, so the column uses `vector(1024)`. 2. **Insert Documents with Embeddings** Use `embedding()` inline to generate vectors at insert time: SQL ```sql INSERT INTO documents (title, content, embedding) VALUES ('PostgreSQL basics', 'PostgreSQL is an open-source relational database with strong SQL compliance.', embedding('PostgreSQL is an open-source relational database with strong SQL compliance.')), ('Vector search', 'Vector search finds semantically similar items using distance metrics like cosine similarity.', embedding('Vector search finds semantically similar items using distance metrics like cosine similarity.')), ('DB9 overview', 'DB9 is a serverless PostgreSQL-compatible database with built-in embeddings, file storage, and HTTP from SQL.', embedding('DB9 is a serverless PostgreSQL-compatible database with built-in embeddings, file storage, and HTTP from SQL.')); ``` ▶ Run Each `embedding()` call sends the text to the configured embedding model and returns a `vector(1024)`. You can also embed a different string than the stored content — for example, a concatenation of title and content: SQL ```sql INSERT INTO documents (title, content, embedding) VALUES ('My document', 'Full document body here...', embedding('My document: Full document body here...')); ``` ▶ Run 3. **Build an HNSW Index** For fast approximate nearest-neighbor search, create an HNSW index: SQL ```sql CREATE INDEX idx_documents_embedding ON documents USING hnsw (embedding vector_cosine_ops); ``` ▶ Run HNSW is disabled in the current release This step is a no-op today. Over the wire protocol the statement is rejected with `55000` (`feature "hnsw_index" is unavailable (DisabledByConfiguration)`); over the HTTP SQL API it reports `CREATE INDEX` but the planner never uses the index. Every query in this guide still returns correct results via exact search — see [HNSW Indexes](/docs/extensions/vector/#hnsw-indexes). The operator class determines the distance metric used by the index: | Operator class | Distance metric | Best for | | ------------------- | ------------------------------ | ----------------------------------------- | | `vector_cosine_ops` | Cosine distance (`<=>`) | Semantic similarity (recommended default) | | `vector_l2_ops` | Euclidean distance (`<->`) | Absolute distance | | `vector_ip_ops` | Negative inner product (`<#>`) | Pre-normalized vectors | You can tune index build parameters for your dataset size: SQL ```sql CREATE INDEX idx_documents_embedding_tuned ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 32, ef_construction = 128); ``` ▶ Run 4. **Query by Semantic Similarity** Find the most relevant documents for a natural-language query: SQL ```sql SELECT id, title, embedding <=> embedding('How does vector search work?') AS distance FROM documents ORDER BY embedding <=> embedding('How does vector search work?') LIMIT 5; ``` ▶ Run The `<=>` operator computes cosine distance. Lower values mean higher similarity. **Tune search accuracy** The `hnsw.ef_search` parameter controls the accuracy-speed tradeoff at query time: SQL ```sql -- Higher = more accurate, slower (default: 40) SET hnsw.ef_search = 100; SELECT id, title FROM documents ORDER BY embedding <=> embedding('serverless database') LIMIT 5; ``` ▶ Run **Auto-embed queries with VEC\_EMBED functions** Instead of calling `embedding()` explicitly, you can use helper functions that embed the query text automatically: SQL ```sql SELECT id, title FROM documents ORDER BY VEC_EMBED_COSINE_DISTANCE(embedding, 'How does vector search work?') LIMIT 5; ``` ▶ Run Available auto-embed distance functions: | Function | Metric | | --------------------------------------------- | ------------- | | `VEC_EMBED_COSINE_DISTANCE(vector_col, text)` | Cosine | | `VEC_EMBED_L2_DISTANCE(vector_col, text)` | Euclidean | | `VEC_EMBED_INNER_PRODUCT(vector_col, text)` | Inner product | 5. **Use the Results in a RAG Pipeline** A typical RAG flow: 1. **User asks a question** — your application receives a natural-language query 2. **Retrieve context** — run the similarity query above to get the top-k documents 3. **Build prompt** — concatenate the retrieved document content into an LLM prompt 4. **Generate answer** — send the prompt to your LLM (Claude, GPT, etc.) The retrieval step is a single SQL query. Here is a complete retrieval query that returns the context your LLM needs: SQL ```sql SELECT title, content FROM documents ORDER BY embedding <=> embedding('What is DB9?') LIMIT 3; ``` ▶ Run **Hybrid retrieval: vector + full-text search** Combine vector similarity with PostgreSQL full-text search for better recall: SQL ```sql -- Add a tsvector column for FTS ALTER TABLE documents ADD COLUMN tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || content)) STORED; CREATE INDEX idx_documents_fts ON documents USING gin(tsv); -- Hybrid query: vector similarity + keyword match SELECT id, title, embedding <=> embedding('serverless database') AS vec_distance, ts_rank(tsv, plainto_tsquery('english', 'serverless database')) AS fts_rank FROM documents WHERE tsv @@ plainto_tsquery('english', 'serverless database') ORDER BY embedding <=> embedding('serverless database') LIMIT 5; ``` ▶ Run ## embedding() Function Reference [Section titled “embedding() Function Reference”](#embedding-function-reference) SQL ```sql -- Default: text-embedding-v4 model, 1024 dimensions embedding('your text') -- Explicit model and dimensions embedding('your text', 'text-embedding-v4', 1024) ``` | Parameter | Type | Default | Description | | ------------ | ---- | ------------------- | ------------------------------------- | | `text` | TEXT | (required) | Input text to embed. Cannot be empty. | | `model` | TEXT | `text-embedding-v4` | Embedding model name. | | `dimensions` | INT | 1024 | Output vector dimensions. | The function returns `NULL` for `NULL` input. Empty strings are rejected with an error. ### Check token usage [Section titled “Check token usage”](#check-token-usage) Monitor your embedding API consumption: SQL ```sql SELECT * FROM extensions.embedding_usage(); ``` ▶ Run Returns `tokens_used` (daily count) and `resets_at` (next midnight UTC). ## Distance Operators and Functions [Section titled “Distance Operators and Functions”](#distance-operators-and-functions) | Operator | Function | Metric | | -------- | ----------------------- | ------------------------------- | | `<=>` | `cosine_distance(a, b)` | Cosine distance (0 = identical) | | `<->` | `l2_distance(a, b)` | Euclidean distance | | `<#>` | `inner_product(a, b)` | Negative dot product | Utility functions: SQL ```sql SELECT vector_dims(embedding) FROM documents LIMIT 1; -- 1024 SELECT vector_norm(embedding) FROM documents LIMIT 1; -- L2 magnitude SELECT l2_normalize(embedding) FROM documents LIMIT 1; -- unit vector ``` ▶ Run ## Current Limitations [Section titled “Current Limitations”](#current-limitations) * **Model selection** — the default provider supports `text-embedding-v4`. Custom models require server-side configuration. * **Superuser only** — `embedding()` requires the database admin role. Non-admin users receive a permission error. * **Daily token budget** — embedding calls consume tokens from a daily quota. Check usage with `extensions.embedding_usage()`. * **Dimensions must match** — the vector column width must match the dimensions returned by the model. The default is 1024. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Vector Search Extension](/docs/extensions/vector/) — HNSW configuration, operator reference, and vector arithmetic * [Full-Text Search](/docs/extensions/fts/) — GIN indexes, ranking, and language support * [Extensions Overview](/docs/extensions/) — all 9 built-in extensions * [Agent Workflows](/docs/agent-workflows/overview/) — using RAG in agent pipelines * [CLI Reference](/docs/cli/) — `db9 db sql` for running queries from the terminal # Rails > Use Ruby on Rails with DB9 — connect with ActiveRecord over standard PostgreSQL, define models, run migrations, and build controllers. Rails connects to DB9 through the standard `pg` gem and ActiveRecord. No special adapter or driver is needed — DB9 speaks the PostgreSQL wire protocol, so ActiveRecord’s PostgreSQL adapter works out of the box. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Ruby 3.1+ * Rails 7+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name rails-app ``` Get the connection string: Terminal ```bash db9 db status rails-app ``` Set the connection string as an environment variable: Terminal ```bash export DATABASE_URL="postgresql://rails-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require" ``` ## Create a New Rails App [Section titled “Create a New Rails App”](#create-a-new-rails-app) Terminal ```bash rails new rails-db9 --database=postgresql cd rails-db9 ``` The `--database=postgresql` flag adds the `pg` gem to your `Gemfile` and generates the correct `config/database.yml` template. ## Configure Database Connection [Section titled “Configure Database Connection”](#configure-database-connection) The simplest approach is to use the `DATABASE_URL` environment variable. Rails will pick it up automatically. To configure directly, edit `config/database.yml`: config/database.yml ```yaml default: &default adapter: postgresql host: pg.db9.io port: 5433 database: postgres username: rails-app.admin password: <%= ENV["DB9_PASSWORD"] %> sslmode: require pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: <<: *default test: <<: *default production: <<: *default pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 10 } %> ``` Use port 5433 and database name “postgres” DB9 uses port `5433` (not `5432`) and the database name is always `postgres`. Update these if Rails generates defaults with different values. ## Define Models [Section titled “Define Models”](#define-models) Generate a `User` model: Terminal ```bash rails generate model User name:string email:string ``` Generate a `Post` model with a foreign key to `User`: Terminal ```bash rails generate model Post title:string body:text published:boolean user:references ``` Edit the generated models to add validations and associations: app/models/user.rb ```ruby class User < ApplicationRecord has_many :posts, dependent: :destroy validates :name, presence: true validates :email, presence: true, uniqueness: true end ``` app/models/post.rb ```ruby class Post < ApplicationRecord belongs_to :user validates :title, presence: true end ``` ## Run Migrations [Section titled “Run Migrations”](#run-migrations) Terminal ```bash rails db:migrate ``` Tip Do not run `rails db:create` — the `postgres` database already exists on DB9. Go straight to `db:migrate`. ## Controllers and Routes [Section titled “Controllers and Routes”](#controllers-and-routes) Generate a Users controller: Terminal ```bash rails generate controller Users ``` Add a simple CRUD controller: app/controllers/users\_controller.rb ```ruby class UsersController < ApplicationController def index @users = User.all render json: @users end def show @user = User.find(params[:id]) render json: @user, include: :posts end def create @user = User.new(user_params) if @user.save render json: @user, status: :created else render json: { errors: @user.errors }, status: :unprocessable_entity end end def update @user = User.find(params[:id]) if @user.update(user_params) render json: @user else render json: { errors: @user.errors }, status: :unprocessable_entity end end def destroy User.find(params[:id]).destroy head :no_content end private def user_params params.require(:user).permit(:name, :email) end end ``` Wire up the routes: config/routes.rb ```ruby Rails.application.routes.draw do resources :users end ``` Start the server and test: Terminal ```bash rails server curl http://localhost:3000/users ``` ## Production Notes [Section titled “Production Notes”](#production-notes) * **Port 5433**: DB9 listens on port `5433`, not the PostgreSQL default `5432`. * **TLS required**: Always use `sslmode=require` in your connection string or `database.yml`. * **Connection pool size**: Set `pool` in `database.yml` to match `RAILS_MAX_THREADS`. Start with 5–10 connections. * **Database name**: The database is always `postgres`. Do not run `rails db:create` — it will fail because the database already exists and you cannot create new ones. * **Puma threads**: If using Puma, ensure the pool size in `database.yml` is at least equal to `RAILS_MAX_THREADS` to avoid connection checkout timeouts. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Connection refused on port 5432 [Section titled “Connection refused on port 5432”](#connection-refused-on-port-5432) DB9 uses port **5433**. Check your `DATABASE_URL` or `database.yml` — Rails defaults to `5432` if no port is specified. ### `rails db:create` fails [Section titled “rails db:create fails”](#rails-dbcreate-fails) The `postgres` database already exists on DB9. Skip `db:create` and run `db:migrate` directly. ### ActiveRecord::ConnectionNotEstablished [Section titled “ActiveRecord::ConnectionNotEstablished”](#activerecordconnectionnotestablished) Verify that your username follows the format `.admin` (e.g., `rails-app.admin`). DB9 routes connections by parsing the tenant from the username. Also confirm `sslmode=require` is set. ### Migrations fail with permission errors [Section titled “Migrations fail with permission errors”](#migrations-fail-with-permission-errors) DB9 connects you to the `postgres` database with admin privileges for your tenant. If you see schema-related errors, confirm you are not trying to modify system tables or create additional databases. ### Slow first request [Section titled “Slow first request”](#slow-first-request) The first connection to DB9 may take slightly longer due to TLS handshake and tenant routing. Subsequent requests reuse pooled connections. Preload the connection pool by adding an initializer: config/initializers/db9\_warmup.rb ```ruby ActiveRecord::Base.connection_pool.checkout ``` ## Next Pages [Section titled “Next Pages”](#next-pages) * [Connect](/docs/connect/) — connection strings and authentication * [Production Checklist](/docs/production-checklist/) — deployment readiness # Remix > Use Remix with DB9 — connect from loaders and actions using Prisma, Drizzle, or node-postgres. Remix connects to DB9 through any standard PostgreSQL driver over pgwire. Loaders and actions run server-side, so your database credentials stay off the client. No special adapter or driver is needed. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Node.js 18+ * Remix 2+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name remix-app ``` Get the connection string: Terminal ```bash db9 db status remix-app ``` Set the connection string as an environment variable: .env ```env DATABASE_URL="postgresql://remix-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require" ``` ## Setup [Section titled “Setup”](#setup) * Prisma Terminal ```bash npx create-remix@latest remix-db9 cd remix-db9 npm install prisma @prisma/client npx prisma init ``` Define your schema: prisma/schema.prisma ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique name String posts Post[] createdAt DateTime @default(now()) } model Post { id Int @id @default(autoincrement()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId Int createdAt DateTime @default(now()) } ``` Push the schema to DB9: Terminal ```bash npx prisma db push npx prisma generate ``` Create a singleton client to avoid connection exhaustion during development: app/db.server.ts ```typescript import { PrismaClient } from '@prisma/client'; let prisma: PrismaClient; declare global { var __prisma: PrismaClient | undefined; } if (process.env.NODE_ENV === 'production') { prisma = new PrismaClient(); } else { if (!global.__prisma) { global.__prisma = new PrismaClient(); } prisma = global.__prisma; } export { prisma }; ``` * Drizzle Terminal ```bash npx create-remix@latest remix-db9 cd remix-db9 npm install drizzle-orm pg npm install -D drizzle-kit @types/pg ``` Define your schema: app/schema.server.ts ```typescript import { pgTable, serial, varchar, text, boolean, integer, timestamp } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).unique().notNull(), name: varchar('name', { length: 100 }).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), }); export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: varchar('title', { length: 500 }).notNull(), content: text('content'), published: boolean('published').default(false), authorId: integer('author_id').notNull().references(() => users.id), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), }); ``` Create a singleton client: app/db.server.ts ```typescript import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; let pool: Pool; declare global { var __pool: Pool | undefined; } if (process.env.NODE_ENV === 'production') { pool = new Pool({ connectionString: process.env.DATABASE_URL }); } else { if (!global.__pool) { global.__pool = new Pool({ connectionString: process.env.DATABASE_URL }); } pool = global.__pool; } export const db = drizzle(pool); ``` Note The `.server.ts` suffix tells Remix to exclude the file from client bundles. Always use it for database modules. ## Loader (Read Data) [Section titled “Loader (Read Data)”](#loader-read-data) Loaders run on the server before a route renders. Use them to fetch data from DB9. * Prisma app/routes/users.tsx ```typescript import type { LoaderFunctionArgs } from '@remix-run/node'; import { json } from '@remix-run/node'; import { useLoaderData } from '@remix-run/react'; import { prisma } from '~/db.server'; export async function loader({ request }: LoaderFunctionArgs) { const users = await prisma.user.findMany({ include: { posts: true }, orderBy: { createdAt: 'desc' }, }); return json({ users }); } export default function UsersPage() { const { users } = useLoaderData(); return (
    {users.map((user) => (
  • {user.name} — {user.posts.length} posts
  • ))}
); } ``` * Drizzle app/routes/users.tsx ```typescript import type { LoaderFunctionArgs } from '@remix-run/node'; import { json } from '@remix-run/node'; import { useLoaderData } from '@remix-run/react'; import { db } from '~/db.server'; import { users } from '~/schema.server'; export async function loader({ request }: LoaderFunctionArgs) { const allUsers = await db.select().from(users).orderBy(users.createdAt); return json({ users: allUsers }); } export default function UsersPage() { const { users: allUsers } = useLoaderData(); return (
    {allUsers.map((user) => (
  • {user.name} ({user.email})
  • ))}
); } ``` ## Action (Write Data) [Section titled “Action (Write Data)”](#action-write-data) Actions handle form submissions and other mutations, also server-side. * Prisma app/routes/users.new\.tsx ```typescript import type { ActionFunctionArgs } from '@remix-run/node'; import { json, redirect } from '@remix-run/node'; import { Form } from '@remix-run/react'; import { prisma } from '~/db.server'; export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const email = formData.get('email') as string; const name = formData.get('name') as string; await prisma.user.create({ data: { email, name } }); return redirect('/users'); } export default function NewUserPage() { return (
); } ``` * Drizzle app/routes/users.new\.tsx ```typescript import type { ActionFunctionArgs } from '@remix-run/node'; import { json, redirect } from '@remix-run/node'; import { Form } from '@remix-run/react'; import { db } from '~/db.server'; import { users } from '~/schema.server'; export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const email = formData.get('email') as string; const name = formData.get('name') as string; await db.insert(users).values({ email, name }); return redirect('/users'); } export default function NewUserPage() { return (
); } ``` ## Production Notes [Section titled “Production Notes”](#production-notes) Never expose the connection string to the client DB9 connections must happen server-side only (loaders, actions, resource routes). The connection string contains your admin credentials — never import database modules from client code. Use the `.server.ts` suffix to guarantee server-only bundling. * **Server-side only**: Loaders and actions run on the server. Never import your `db.server.ts` module from a client-side file. * **Port 5433**: DB9 uses port 5433, not the default PostgreSQL port 5432. * **TLS required**: Always include `sslmode=require` in the connection string for DB9’s hosted service. * **Node.js runtime required**: DB9 uses the pgwire protocol over TCP. Deploy Remix on a Node.js server (Express, Hono, etc.), not on an edge runtime without TCP socket support. * **Connection pooling**: Start with 5–10 connections. The singleton pattern in `db.server.ts` prevents pool exhaustion during development HMR cycles. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `ECONNREFUSED` on port 5432 [Section titled “ECONNREFUSED on port 5432”](#econnrefused-on-port-5432) DB9 uses port **5433**, not 5432. Check that your `DATABASE_URL` includes `:5433` in the connection string. ### Edge runtime not supported [Section titled “Edge runtime not supported”](#edge-runtime-not-supported) DB9 requires a TCP connection (pgwire protocol). If you deploy Remix to an edge runtime (e.g., Cloudflare Workers), database queries will fail. Use a Node.js-based server adapter instead. ### Connection pool exhaustion in development [Section titled “Connection pool exhaustion in development”](#connection-pool-exhaustion-in-development) Remix’s dev server reloads modules on file changes. Without the singleton pattern in `db.server.ts`, each reload opens a new connection pool. If you see “too many connections” errors, verify you are using the `global` caching pattern shown above and restart the dev server. ### `prisma db push` fails [Section titled “prisma db push fails”](#prisma-db-push-fails) DB9 has limited `information_schema` support. If schema push fails, create tables with raw SQL instead. See the [Prisma guide](/docs/guides/prisma/) for details. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Prisma](/docs/guides/prisma/) — full Prisma integration guide * [Drizzle](/docs/guides/drizzle/) — full Drizzle integration guide * [Connect](/docs/connect/) — connection strings and authentication * [Production Checklist](/docs/production-checklist/) — deployment readiness # Scheduled Jobs with pg_cron > Set up periodic SQL jobs in DB9 using pg_cron — schedule cleanup tasks, API polling, data syncs, and monitor execution history. DB9 includes pg\_cron for running SQL on a schedule. You can automate cleanup tasks, periodic data syncs, API polling, health checks, and any other SQL workflow that needs to run at regular intervals. This guide walks through creating jobs, monitoring their execution, handling failures, and combining scheduled jobs with other DB9 extensions. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database (see [Quick Start](/docs/quickstart/)) * The `pg_cron` extension is enabled by default — no `CREATE EXTENSION` needed Running `CREATE EXTENSION pg_cron` is harmless but unnecessary. All functions live in the `cron` schema. ## 1. Schedule a Job [Section titled “1. Schedule a Job”](#1-schedule-a-job) Use `cron.schedule()` with a name, cron expression, and SQL command: SQL ```sql SELECT cron.schedule('cleanup-old-logs', '0 3 * * *', 'DELETE FROM logs WHERE created_at < now() - INTERVAL ''30 days'''); ``` This creates a job named `cleanup-old-logs` that runs daily at 3:00 AM UTC. The function returns the job ID. ### Cron expression format [Section titled “Cron expression format”](#cron-expression-format) Standard 5-field syntax — minute, hour, day of month, month, day of week: Output ```text ┌───────────── minute (0-59) │ ┌───────────── hour (0-23) │ │ ┌───────────── day of month (1-31) │ │ │ ┌───────────── month (1-12) │ │ │ │ ┌───────────── day of week (1-7, Sunday=1, Saturday=7) │ │ │ │ │ * * * * * ``` Common patterns: | Expression | Meaning | | -------------- | ----------------------------- | | `* * * * *` | Every minute | | `*/5 * * * *` | Every 5 minutes | | `0 * * * *` | Every hour | | `0 3 * * *` | Daily at 3:00 AM | | `0 0 * * 1` | Weekly on Sunday at midnight | | `0 12 1 * *` | Monthly on the 1st at noon | | `30 9 * * 2-6` | Weekdays at 9:30 AM (Mon–Fri) | Wildcards (`*`), ranges (`1-5`), steps (`*/5`), and lists (`1,3,5`) are all supported. Shorthand like `@daily` or `@hourly` is not supported — use the explicit 5-field form. ### Upsert semantics [Section titled “Upsert semantics”](#upsert-semantics) If you call `cron.schedule()` with the same job name, it updates the existing job instead of creating a duplicate: SQL ```sql -- First call creates the job SELECT cron.schedule('sync', '*/15 * * * *', 'SELECT sync_data()'); -- Second call with same name updates schedule and command SELECT cron.schedule('sync', '*/5 * * * *', 'SELECT sync_data_v2()'); -- Returns the same job ID ``` This makes it safe to put `cron.schedule()` calls in migration scripts or seed files — they are idempotent. ## 2. List and Inspect Jobs [Section titled “2. List and Inspect Jobs”](#2-list-and-inspect-jobs) View all scheduled jobs: SQL ```sql SELECT jobid, jobname, schedule, active, next_run_at FROM cron.job; ``` ▶ Run | Column | Description | | ------------- | ---------------------------------------------- | | `jobid` | Auto-assigned job ID | | `jobname` | Name you provided (or NULL for anonymous jobs) | | `schedule` | Cron expression | | `command` | SQL to execute | | `active` | Whether the job is enabled | | `next_run_at` | Computed next execution time (ISO 8601, UTC) | You can also use the CLI: Terminal ```bash db9 db cron list ``` ## 3. Monitor Execution History [Section titled “3. Monitor Execution History”](#3-monitor-execution-history) Every job run is recorded in `cron.job_run_details`: SQL ```sql SELECT jobid, status, return_message, start_time, end_time FROM cron.job_run_details ORDER BY runid DESC LIMIT 10; ``` ▶ Run Status values: | Status | Meaning | | ----------- | -------------------------- | | `starting` | Queued, not yet executing | | `running` | Currently executing | | `succeeded` | Completed without error | | `failed` | Execution raised an error | | `cancelled` | Stopped by `cron.cancel()` | ### Find failed runs [Section titled “Find failed runs”](#find-failed-runs) SQL ```sql SELECT jobid, command, return_message, start_time FROM cron.job_run_details WHERE status = 'failed' ORDER BY runid DESC LIMIT 20; ``` ▶ Run The `return_message` column contains the error text, which helps diagnose failures. ### Check run duration [Section titled “Check run duration”](#check-run-duration) SQL ```sql SELECT jobid, status, start_time, end_time FROM cron.job_run_details WHERE jobid = 1 ORDER BY runid DESC LIMIT 5; ``` ▶ Run Run history is retained for 7 days and then automatically cleaned up. ### CLI shortcut [Section titled “CLI shortcut”](#cli-shortcut) Terminal ```bash db9 db cron history db9 db cron history --limit 50 db9 db cron history --job cleanup-old-logs ``` ## 4. Modify and Manage Jobs [Section titled “4. Modify and Manage Jobs”](#4-modify-and-manage-jobs) ### Disable a job (pause without deleting) [Section titled “Disable a job (pause without deleting)”](#disable-a-job-pause-without-deleting) SQL ```sql SELECT cron.alter_job(1, NULL, NULL, NULL, NULL, false); ``` The job remains in `cron.job` but stops executing until re-enabled. ### Re-enable a job [Section titled “Re-enable a job”](#re-enable-a-job) SQL ```sql SELECT cron.alter_job(1, NULL, NULL, NULL, NULL, true); ``` ### Change the schedule [Section titled “Change the schedule”](#change-the-schedule) SQL ```sql SELECT cron.alter_job(1, '0 4 * * *', NULL, NULL, NULL, NULL); ``` ### Set a per-job timeout [Section titled “Set a per-job timeout”](#set-a-per-job-timeout) SQL ```sql -- Allow this job up to 30 minutes before it is marked failed SELECT cron.alter_job(1, NULL, NULL, NULL, NULL, NULL, '30min'); ``` Supported formats: `30min`, `2h`, `60s`, `5000ms`, or a plain integer (milliseconds). The default timeout is 5 minutes. ### Delete a job [Section titled “Delete a job”](#delete-a-job) SQL ```sql SELECT cron.unschedule('cleanup-old-logs'); -- by name SELECT cron.unschedule(1); -- by ID ``` Deleting a job also removes its run history. CLI equivalents: Terminal ```bash db9 db cron enable db9 db cron disable db9 db cron delete ``` ## 5. Practical Patterns [Section titled “5. Practical Patterns”](#5-practical-patterns) ### Periodic cleanup [Section titled “Periodic cleanup”](#periodic-cleanup) SQL ```sql SELECT cron.schedule('expire-sessions', '*/15 * * * *', 'DELETE FROM sessions WHERE expires_at < now()'); ``` ### Aggregate metrics on a schedule [Section titled “Aggregate metrics on a schedule”](#aggregate-metrics-on-a-schedule) SQL ```sql SELECT cron.schedule('hourly-metrics', '0 * * * *', $$INSERT INTO hourly_stats (hour, total_events, unique_users) SELECT date_trunc('hour', now() - INTERVAL '1 hour'), count(*), count(DISTINCT user_id) FROM events WHERE created_at >= now() - INTERVAL '1 hour'$$); ``` Use dollar quoting (`$$...$$`) for multi-line or quote-heavy commands. ### Poll an external API with http [Section titled “Poll an external API with http”](#poll-an-external-api-with-http) Combine pg\_cron with the [http extension](/docs/extensions/http/) to fetch data on a schedule: SQL ```sql SELECT cron.schedule('poll-status', '*/5 * * * *', $$INSERT INTO api_snapshots (fetched_at, status, body) SELECT now(), status, content::jsonb FROM extensions.http_get('https://api.example.com/status')$$); ``` ### Trigger a Serverless Function on a schedule [Section titled “Trigger a Serverless Function on a schedule”](#trigger-a-serverless-function-on-a-schedule) Use pg\_cron to invoke a deployed [Serverless Function](/docs/functions/configuration/#cron-scheduling) at regular intervals — useful when the scheduled work is better expressed as application code than SQL: SQL ```sql SELECT cron.schedule('daily-etl', '0 2 * * *', $$SELECT http_post('https:///etl', '{}', 'application/json')$$); ``` ### Write periodic reports to the filesystem [Section titled “Write periodic reports to the filesystem”](#write-periodic-reports-to-the-filesystem) Combine with [fs9](/docs/extensions/fs9/) to write scheduled outputs: SQL ```sql SELECT cron.schedule('daily-report', '0 6 * * *', $$SELECT fs9_write('/reports/' || to_char(now(), 'YYYY-MM-DD') || '.csv', (SELECT string_agg(id || ',' || name || ',' || total, E'\n') FROM daily_summary))$$); ``` ## 6. Cancel a Running Job [Section titled “6. Cancel a Running Job”](#6-cancel-a-running-job) If a job is running too long, a superuser can cancel it: SQL ```sql SELECT cron.cancel(1); -- by job ID ``` To see what is currently running: SQL ```sql SELECT run_id, job_id, command, elapsed_ms FROM cron.running_jobs; ``` ▶ Run Both `cron.cancel()` and `cron.running_jobs` require superuser privileges. ## Limits and Caveats [Section titled “Limits and Caveats”](#limits-and-caveats) * **Max 50 jobs per database** — additional `cron.schedule()` calls fail once this limit is reached. * **Max 32 concurrent executions** — across all databases on the same server. * **Poll interval** — the scheduler checks for due jobs every 60 seconds. Jobs cannot execute more frequently than once per minute. * **No sub-minute scheduling** — the minimum granularity is one minute. * **No shorthand expressions** — `@daily`, `@hourly`, `@reboot` are not supported. Use the 5-field form. * **No cross-database scheduling** — jobs always execute in the database where they were created. * **Run history retention** — 7 days by default. Older records are automatically cleaned up. * **Superuser required** — `cron.cancel()` and `cron.running_jobs` require the admin role. `cron.schedule()`, `cron.unschedule()`, and `cron.alter_job()` work for any user but are scoped to their own jobs. ## Next Pages [Section titled “Next Pages”](#next-pages) * [pg\_cron Extension Reference](/docs/extensions/pg-cron/) — function signatures and cron expression details * [HTTP from SQL](/docs/guides/http-from-sql/) — call external APIs that you can combine with scheduled jobs * [Analyze Agent Logs with fs9](/docs/guides/analyze-agent-logs-with-fs9/) — write and query files from SQL * [Extensions Overview](/docs/extensions/) — all 9 built-in extensions * [CLI Reference](/docs/cli/) — `db9 db cron` commands for job management # Sequelize > Use Sequelize with DB9 — model definitions, associations, CRUD with operators, transactions, raw SQL, and vector search. Sequelize connects to DB9 using the `pg` (node-postgres) driver over pgwire. Model definitions with `Model.init()`, associations (hasMany, belongsTo, belongsToMany), the `Op` query operators, and managed transactions work out of the box. DB9 passes 100% of Sequelize compatibility tests (87/87) covering CRUD, associations, transactions, advanced SQL, and vector operations. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Node.js 18+ * Sequelize 6.35+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name sequelize-app ``` Get the connection string: Terminal ```bash db9 db status sequelize-app ``` ## Project Setup [Section titled “Project Setup”](#project-setup) Terminal ```bash mkdir sequelize-db9 && cd sequelize-db9 npm init -y npm install sequelize pg npm install -D typescript @types/node ``` ## Connection [Section titled “Connection”](#connection) connection.ts ```typescript import { Sequelize } from 'sequelize'; const sequelize = new Sequelize({ dialect: 'postgres', host: 'pg.db9.io', port: 5433, username: 'sequelize-app.admin', password: 'YOUR_PASSWORD', database: 'postgres', logging: false, pool: { max: 5, min: 0, acquire: 30000, idle: 10000, }, }); export default sequelize; ``` Test the connection: TypeScript ```typescript await sequelize.authenticate(); console.log('Connected to DB9'); ``` ## Model Definitions [Section titled “Model Definitions”](#model-definitions) Sequelize uses `Model.init()` with column type definitions: models/User.ts ```typescript import { Model, DataTypes, Optional } from 'sequelize'; import sequelize from '../connection'; interface UserAttributes { id: number; email: string; name: string; age: number; isActive: boolean; bio: string | null; metadata: Record | null; } interface UserCreationAttributes extends Optional {} export class User extends Model implements UserAttributes { declare id: number; declare email: string; declare name: string; declare age: number; declare isActive: boolean; declare bio: string | null; declare metadata: Record | null; declare createdAt: Date; declare updatedAt: Date; declare posts?: Post[]; } User.init( { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, email: { type: DataTypes.STRING(255), allowNull: false, unique: true }, name: { type: DataTypes.STRING(100), allowNull: false }, age: { type: DataTypes.INTEGER, defaultValue: 0 }, isActive: { type: DataTypes.BOOLEAN, defaultValue: true }, bio: { type: DataTypes.TEXT, allowNull: true }, metadata: { type: DataTypes.JSONB, allowNull: true }, }, { sequelize, tableName: 'users', timestamps: true } ); ``` models/Post.ts ```typescript import { Model, DataTypes, Optional } from 'sequelize'; import sequelize from '../connection'; interface PostAttributes { id: number; title: string; content: string; published: boolean; authorId: number; } interface PostCreationAttributes extends Optional {} export class Post extends Model implements PostAttributes { declare id: number; declare title: string; declare content: string; declare published: boolean; declare authorId: number; declare createdAt: Date; } Post.init( { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, title: { type: DataTypes.STRING(500), allowNull: false }, content: { type: DataTypes.TEXT, allowNull: false }, published: { type: DataTypes.BOOLEAN, defaultValue: false }, authorId: { type: DataTypes.INTEGER, allowNull: false }, }, { sequelize, tableName: 'posts', timestamps: true } ); ``` models/Tag.ts ```typescript import { Model, DataTypes, Optional } from 'sequelize'; import sequelize from '../connection'; export class Tag extends Model { declare id: number; declare name: string; } Tag.init( { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, name: { type: DataTypes.STRING(100), allowNull: false, unique: true }, }, { sequelize, tableName: 'tags', timestamps: false } ); ``` ## Associations [Section titled “Associations”](#associations) Define associations after all models are initialized: models/index.ts ```typescript import { User } from './User'; import { Post } from './Post'; import { Tag } from './Tag'; // One-to-Many User.hasMany(Post, { foreignKey: 'authorId', as: 'posts', onDelete: 'CASCADE' }); Post.belongsTo(User, { foreignKey: 'authorId', as: 'author' }); // Many-to-Many Post.belongsToMany(Tag, { through: 'post_tags', as: 'tags' }); Tag.belongsToMany(Post, { through: 'post_tags', as: 'posts' }); export { User, Post, Tag }; ``` ## Create Tables [Section titled “Create Tables”](#create-tables) Use raw SQL rather than `sequelize.sync()` for production, as Sequelize’s schema sync relies on `information_schema` introspection that may not fully work with DB9: setup.ts ```typescript import sequelize from './connection'; async function setup() { await sequelize.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, age INT DEFAULT 0, "isActive" BOOLEAN DEFAULT true, bio TEXT, metadata JSONB, "createdAt" TIMESTAMPTZ DEFAULT now(), "updatedAt" TIMESTAMPTZ DEFAULT now() ) `); await sequelize.query(` CREATE TABLE IF NOT EXISTS posts ( id SERIAL PRIMARY KEY, title VARCHAR(500) NOT NULL, content TEXT NOT NULL, published BOOLEAN DEFAULT false, "authorId" INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, "createdAt" TIMESTAMPTZ DEFAULT now(), "updatedAt" TIMESTAMPTZ DEFAULT now() ) `); await sequelize.query(` CREATE TABLE IF NOT EXISTS tags ( id SERIAL PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL ) `); await sequelize.query(` CREATE TABLE IF NOT EXISTS post_tags ( "postId" INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, "tagId" INT NOT NULL REFERENCES tags(id) ON DELETE CASCADE, "createdAt" TIMESTAMPTZ DEFAULT now(), "updatedAt" TIMESTAMPTZ DEFAULT now(), PRIMARY KEY ("postId", "tagId") ) `); console.log('Tables created'); } setup(); ``` ## CRUD Operations [Section titled “CRUD Operations”](#crud-operations) TypeScript ```typescript import { Op } from 'sequelize'; import { User, Post, Tag } from './models'; // Create const user = await User.create({ email: 'alice@example.com', name: 'Alice', age: 30, metadata: { role: 'admin' }, }); // Bulk create const users = await User.bulkCreate([ { email: 'bob@example.com', name: 'Bob', age: 25 }, { email: 'carol@example.com', name: 'Carol', age: 35 }, ]); // Read const found = await User.findOne({ where: { email: 'alice@example.com' } }); const byId = await User.findByPk(user.id); const all = await User.findAll({ where: { age: { [Op.gte]: 25 } }, order: [['age', 'DESC']], limit: 10, offset: 0, }); // Select specific columns const names = await User.findAll({ attributes: ['id', 'name'], where: { isActive: true }, }); // Aggregates const count = await User.count(); const totalAge = await User.sum('age'); // Update await User.update({ name: 'Alice Updated' }, { where: { id: user.id } }); // Instance update const alice = await User.findByPk(user.id); alice!.age = 31; await alice!.save(); // Upsert const [upserted, created] = await User.upsert({ email: 'alice@example.com', name: 'Alice V2', age: 32, }); // Delete await User.destroy({ where: { id: user.id } }); ``` ### Operator queries [Section titled “Operator queries”](#operator-queries) TypeScript ```typescript import { Op } from 'sequelize'; // Comparison await User.findAll({ where: { age: { [Op.gt]: 25 } } }); await User.findAll({ where: { age: { [Op.lte]: 30 } } }); await User.findAll({ where: { name: { [Op.in]: ['Alice', 'Bob'] } } }); // Pattern matching await User.findAll({ where: { name: { [Op.like]: '%li%' } } }); await User.findAll({ where: { name: { [Op.iLike]: '%alice%' } } }); // NULL checks await User.findAll({ where: { bio: { [Op.is]: null } } }); // Logical operators await User.findAll({ where: { [Op.or]: [ { [Op.and]: [{ age: { [Op.gte]: 30 } }, { bio: { [Op.not]: null } }] }, { name: 'Alice' }, ], }, }); ``` ## Loading Associations [Section titled “Loading Associations”](#loading-associations) TypeScript ```typescript // Eager load (LEFT JOIN) const userWithPosts = await User.findByPk(1, { include: [{ model: Post, as: 'posts' }], }); console.log(userWithPosts!.posts); // Nested includes const userFull = await User.findByPk(1, { include: [{ model: Post, as: 'posts', include: [{ model: Tag, as: 'tags' }], }], }); // Filtered include const userPublished = await User.findByPk(1, { include: [{ model: Post, as: 'posts', where: { published: true }, }], }); // INNER JOIN (only users with posts) const authors = await User.findAll({ include: [{ model: Post, as: 'posts', required: true }], }); // Many-to-Many: add and remove tags const post = await Post.findByPk(1); const tag = await Tag.create({ name: 'db9' }); await post!.addTag(tag); await post!.removeTag(tag); ``` ## Transactions [Section titled “Transactions”](#transactions) ### Managed transactions (auto-commit/rollback) [Section titled “Managed transactions (auto-commit/rollback)”](#managed-transactions-auto-commitrollback) TypeScript ```typescript import sequelize from './connection'; await sequelize.transaction(async (t) => { const user = await User.create( { email: 'txn@example.com', name: 'Txn User', age: 30 }, { transaction: t } ); await Post.create( { title: 'Transactional Post', content: 'Atomic.', authorId: user.id }, { transaction: t } ); // Auto-commits on success, auto-rolls back on error }); ``` ### Unmanaged transactions (manual control) [Section titled “Unmanaged transactions (manual control)”](#unmanaged-transactions-manual-control) TypeScript ```typescript const t = await sequelize.transaction(); try { await User.create( { email: 'manual@example.com', name: 'Manual', age: 28 }, { transaction: t } ); await t.commit(); } catch (err) { await t.rollback(); throw err; } ``` ### Isolation levels [Section titled “Isolation levels”](#isolation-levels) TypeScript ```typescript import { Transaction } from 'sequelize'; await sequelize.transaction( { isolationLevel: Transaction.ISOLATION_LEVELS.REPEATABLE_READ }, async (t) => { const users = await User.findAll({ transaction: t }); return users; } ); ``` Supported: `READ_COMMITTED`, `REPEATABLE_READ`. `SERIALIZABLE` is not implemented — Sequelize will not error, but the transaction is silently downgraded to `REPEATABLE_READ` with a server `WARNING`. Set `REPEATABLE_READ` explicitly so the intent is visible in your code. ## Raw SQL for Advanced Features [Section titled “Raw SQL for Advanced Features”](#raw-sql-for-advanced-features) TypeScript ```typescript import { QueryTypes } from 'sequelize'; // Window functions const ranked = await sequelize.query( `SELECT name, age, ROW_NUMBER() OVER (ORDER BY age DESC) AS rank FROM users ORDER BY rank`, { type: QueryTypes.SELECT } ); // CTEs const result = await sequelize.query( `WITH active AS ( SELECT * FROM users WHERE "isActive" = true ) SELECT name, age FROM active ORDER BY age`, { type: QueryTypes.SELECT } ); // DISTINCT ON const firstPerAge = await sequelize.query( `SELECT DISTINCT ON (age) name, age FROM users ORDER BY age, name`, { type: QueryTypes.SELECT } ); // Parameterized queries const filtered = await sequelize.query( 'SELECT * FROM users WHERE age > $1 AND age < $2', { type: QueryTypes.SELECT, bind: [20, 35] } ); ``` ## Vector Search [Section titled “Vector Search”](#vector-search) Sequelize does not have a native vector column type. Use raw SQL for vector operations: TypeScript ```typescript // Create vector table and index await sequelize.query(` CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS embeddings ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, embedding vector(3) NOT NULL ); CREATE INDEX IF NOT EXISTS idx_embeddings ON embeddings USING hnsw (embedding vector_l2_ops); `); // Insert vectors await sequelize.query( `INSERT INTO embeddings (name, embedding) VALUES ($1, $2)`, { bind: ['doc-1', '[1.0, 2.0, 3.0]'] } ); // Cosine distance search const results = await sequelize.query( `SELECT name, cosine_distance(embedding, '[1.0, 1.0, 1.0]') AS distance FROM embeddings ORDER BY distance ASC LIMIT 5`, { type: QueryTypes.SELECT } ); // L2 distance with operator const nearest = await sequelize.query( `SELECT name FROM embeddings WHERE embedding <-> '[1.0, 0.0, 0.0]' < 1.0 ORDER BY embedding <-> '[1.0, 0.0, 0.0]'`, { type: QueryTypes.SELECT } ); ``` DB9 recognizes HNSW indexes with `vector_l2_ops`, `vector_cosine_ops`, and `vector_ip_ops`; IVFFlat is not available at all. But HNSW **index building is disabled in the current release** — a `CREATE INDEX ... USING hnsw` is rejected over the wire protocol with `55000` and creates an unused index over the HTTP SQL API. Vector search still returns correct results via an exact scan. ## Schema Changes [Section titled “Schema Changes”](#schema-changes) Manage migrations with raw SQL rather than Sequelize’s built-in migration tools: TypeScript ```typescript // Add a column await sequelize.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS phone TEXT'); // Create an index await sequelize.query('CREATE INDEX IF NOT EXISTS idx_users_age ON users (age)'); ``` ## Production Notes [Section titled “Production Notes”](#production-notes) * **Driver**: Sequelize uses `pg` (node-postgres). Set `dialect: 'postgres'` in the configuration. * **Connection pooling**: Configure `pool.max` based on your workload. DB9 supports multiple concurrent connections per tenant. * **Timestamps**: Use `timestamps: true` in model options for automatic `createdAt`/`updatedAt` management. * **Upsert behavior**: Sequelize’s `upsert()` works with DB9, but the `created` return value may be `null` instead of a boolean. Check both `true` and `null` if you rely on the return value. * **Connection string**: Use `sequelize-app.admin` as the username, with host `pg.db9.io` and port `5433`. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `sequelize.sync()` creates unexpected schema [Section titled “sequelize.sync() creates unexpected schema”](#sequelizesync-creates-unexpected-schema) DB9’s `information_schema` support is limited. Sequelize’s schema sync may not detect existing tables correctly. Use raw DDL and avoid `sync({ force: true })` in production. ### Upsert `created` flag is `null` [Section titled “Upsert created flag is null”](#upsert-created-flag-is-null) DB9 may return `null` for the `created` flag on upsert instead of `true`/`false`. If your logic depends on knowing whether a row was inserted or updated, query the row after upsert. ### Connection timeout [Section titled “Connection timeout”](#connection-timeout) Verify the host (`pg.db9.io`), port (`5433`), and username format (`{database-name}.admin`). DB9 routes connections by parsing the tenant from the username. ### JSONB queries [Section titled “JSONB queries”](#jsonb-queries) Sequelize’s JSONB operators work with DB9. For advanced operations not covered by the ORM API, use `sequelize.query()` with PostgreSQL’s native JSONB operators (`->`, `->>`, `@>`). ## Verified Compatibility [Section titled “Verified Compatibility”](#verified-compatibility) Tested with Sequelize 6.35+ against DB9. All 87 tests pass covering: | Category | Status | | ------------------------------------------------ | ------------------- | | Connection and pooling | Pass | | CRUD with operator queries | Pass | | Associations (hasMany, belongsTo, belongsToMany) | Pass | | Transactions and isolation levels | Pass | | Advanced SQL (window, CTE, DISTINCT ON) | Pass | | Vector operations; HNSW index building disabled | Pass (exact search) | | DDL operations | Pass | ## Next Pages [Section titled “Next Pages”](#next-pages) * [TypeORM](/docs/guides/typeorm/) — TypeORM with DB9 * [Prisma](/docs/guides/prisma/) — Prisma ORM with DB9 * [Connect](/docs/connect/) — connection strings and authentication * [Vector Extension](/docs/extensions/vector/) — HNSW indexes and distance operators * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full PostgreSQL compatibility surface # SvelteKit > Use SvelteKit with DB9 — connect from server load functions and form actions using Prisma, Drizzle, or node-postgres. SvelteKit connects to DB9 through any standard PostgreSQL driver over pgwire. DB9 is PostgreSQL-compatible, so no special adapter is needed. This guide shows how to wire up a SvelteKit app with DB9 using Prisma for schema-first workflows and Drizzle for TypeScript-first query building. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Node.js 18+ * SvelteKit 2+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name sveltekit-app ``` Get the connection string: Terminal ```bash db9 db status sveltekit-app ``` Set the connection string as an environment variable: .env ```env DATABASE_URL="postgresql://sveltekit-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require" ``` ## Setup [Section titled “Setup”](#setup) * Prisma ### Install [Section titled “Install”](#install) Terminal ```bash npx sv create sveltekit-db9 cd sveltekit-db9 npm install prisma @prisma/client npx prisma init ``` ### Schema [Section titled “Schema”](#schema) prisma/schema.prisma ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model Todo { id Int @id @default(autoincrement()) title String done Boolean @default(false) createdAt DateTime @default(now()) } ``` Push the schema to DB9: Terminal ```bash npx prisma db push ``` Generate the client: Terminal ```bash npx prisma generate ``` ### Singleton client [Section titled “Singleton client”](#singleton-client) Create a shared Prisma instance to avoid connection exhaustion during development: src/lib/server/prisma.ts ```typescript import { PrismaClient } from '@prisma/client'; const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; export const prisma = globalForPrisma.prisma ?? new PrismaClient(); if (process.env.NODE_ENV !== 'production') { globalForPrisma.prisma = prisma; } ``` * Drizzle ### Install [Section titled “Install”](#install-1) Terminal ```bash npx sv create sveltekit-db9 cd sveltekit-db9 npm install drizzle-orm pg npm install -D drizzle-kit @types/pg ``` ### Schema [Section titled “Schema”](#schema-1) src/lib/server/schema.ts ```typescript import { pgTable, serial, varchar, boolean, timestamp } from 'drizzle-orm/pg-core'; export const todos = pgTable('todos', { id: serial('id').primaryKey(), title: varchar('title', { length: 500 }).notNull(), done: boolean('done').default(false), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), }); ``` ### Singleton client [Section titled “Singleton client”](#singleton-client-1) src/lib/server/db.ts ```typescript import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; const globalForDb = globalThis as unknown as { pool: Pool }; const pool = globalForDb.pool ?? new Pool({ connectionString: process.env.DATABASE_URL, }); if (process.env.NODE_ENV !== 'production') { globalForDb.pool = pool; } export const db = drizzle(pool); ``` Push the schema to DB9: Terminal ```bash npx drizzle-kit push ``` ## Server Load Function [Section titled “Server Load Function”](#server-load-function) SvelteKit `load` functions run on the server, making them the right place to query DB9. * Prisma src/routes/todos/+page.server.ts ```typescript import { prisma } from '$lib/server/prisma'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async () => { const todos = await prisma.todo.findMany({ orderBy: { createdAt: 'desc' }, }); return { todos }; }; ``` * Drizzle src/routes/todos/+page.server.ts ```typescript import { db } from '$lib/server/db'; import { todos } from '$lib/server/schema'; import { desc } from 'drizzle-orm'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async () => { const allTodos = await db.select().from(todos).orderBy(desc(todos.createdAt)); return { todos: allTodos }; }; ``` ## Form Actions [Section titled “Form Actions”](#form-actions) SvelteKit form actions handle mutations server-side with progressive enhancement. * Prisma src/routes/todos/+page.server.ts ```typescript import { prisma } from '$lib/server/prisma'; import type { Actions, PageServerLoad } from './$types'; export const load: PageServerLoad = async () => { const todos = await prisma.todo.findMany({ orderBy: { createdAt: 'desc' }, }); return { todos }; }; export const actions: Actions = { create: async ({ request }) => { const data = await request.formData(); await prisma.todo.create({ data: { title: data.get('title') as string }, }); }, toggle: async ({ request }) => { const data = await request.formData(); const id = Number(data.get('id')); const todo = await prisma.todo.findUniqueOrThrow({ where: { id } }); await prisma.todo.update({ where: { id }, data: { done: !todo.done }, }); }, }; ``` * Drizzle src/routes/todos/+page.server.ts ```typescript import { db } from '$lib/server/db'; import { todos } from '$lib/server/schema'; import { eq, desc, not } from 'drizzle-orm'; import type { Actions, PageServerLoad } from './$types'; export const load: PageServerLoad = async () => { const allTodos = await db.select().from(todos).orderBy(desc(todos.createdAt)); return { todos: allTodos }; }; export const actions: Actions = { create: async ({ request }) => { const data = await request.formData(); await db.insert(todos).values({ title: data.get('title') as string, }); }, toggle: async ({ request }) => { const data = await request.formData(); const id = Number(data.get('id')); await db .update(todos) .set({ done: not(todos.done) }) .where(eq(todos.id, id)); }, }; ``` ## Production Notes [Section titled “Production Notes”](#production-notes) Never expose the connection string to the client DB9 connections must happen server-side only (load functions, form actions, API routes). The connection string contains your admin credentials. Place database modules in `src/lib/server/` so SvelteKit prevents them from being imported in client code. * **Server-side only**: All DB9 queries must run in load functions, form actions, or `+server.ts` API routes. Files in `src/lib/server/` are automatically excluded from client bundles. * **Port 5433**: DB9 uses port 5433, not the default PostgreSQL port 5432. * **TLS required**: Use `sslmode=require` in the connection string for DB9’s hosted service. * **Node adapter required**: DB9 requires a TCP pgwire connection. Use `@sveltejs/adapter-node` for deployment — edge adapters (Cloudflare, Vercel Edge) do not support raw TCP sockets. Install the Node adapter: Terminal ```bash npm install @sveltejs/adapter-node ``` svelte.config.js ```javascript import adapter from '@sveltejs/adapter-node'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; export default { preprocess: vitePreprocess(), kit: { adapter: adapter(), }, }; ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `ECONNREFUSED` on port 5432 [Section titled “ECONNREFUSED on port 5432”](#econnrefused-on-port-5432) DB9 uses port **5433**, not 5432. Verify your `DATABASE_URL` includes the correct port. ### Edge adapter errors [Section titled “Edge adapter errors”](#edge-adapter-errors) DB9 requires pgwire (TCP), which is not available in edge runtimes. Switch to `@sveltejs/adapter-node`. Cloudflare and Vercel Edge adapters will not work. ### Connection pool exhaustion in development [Section titled “Connection pool exhaustion in development”](#connection-pool-exhaustion-in-development) SvelteKit’s dev server reloads modules on change. Without the singleton pattern shown above, each reload opens a new connection pool. Use the `globalThis` pattern in `src/lib/server/` and restart the dev server to release stale connections. ### `prisma db push` fails [Section titled “prisma db push fails”](#prisma-db-push-fails) DB9 has limited `information_schema` support. If `prisma db push` fails, manage tables with raw SQL. See the [Prisma guide](/docs/guides/prisma/) for details. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Prisma](/docs/guides/prisma/) — full Prisma integration guide * [Drizzle](/docs/guides/drizzle/) — full Drizzle integration guide * [Connect](/docs/connect/) — connection strings and authentication * [Production Checklist](/docs/production-checklist/) — deployment readiness # TypeORM > Use TypeORM with DB9 — decorator-based entities, repository pattern, QueryBuilder, relations, transactions, and vector search. TypeORM connects to DB9 using the `pg` (node-postgres) driver over pgwire. Decorator-based entity definitions, the repository pattern, QueryBuilder, and relation loading work out of the box. DB9 passes 98% of TypeORM compatibility tests (147/150, 3 skipped) covering CRUD, relations, transactions, advanced SQL, and vector operations. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A DB9 database ([create one](/docs/quickstart/)) * Node.js 18+ * TypeORM 0.3.17+ ## Create a DB9 Database [Section titled “Create a DB9 Database”](#create-a-db9-database) Terminal ```bash db9 create --name typeorm-app ``` Get the connection string: Terminal ```bash db9 db status typeorm-app ``` ## Project Setup [Section titled “Project Setup”](#project-setup) Terminal ```bash mkdir typeorm-db9 && cd typeorm-db9 npm init -y npm install typeorm pg reflect-metadata npm install -D typescript @types/pg @types/node ``` Enable decorators in `tsconfig.json`: tsconfig.json ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "outDir": "./dist" } } ``` ## DataSource Configuration [Section titled “DataSource Configuration”](#datasource-configuration) datasource.ts ```typescript import 'reflect-metadata'; import { DataSource } from 'typeorm'; import { User } from './entities/User'; import { Post } from './entities/Post'; import { Tag } from './entities/Tag'; export const AppDataSource = new DataSource({ type: 'postgres', host: 'pg.db9.io', port: 5433, username: 'typeorm-app.admin', password: 'YOUR_PASSWORD', database: 'postgres', ssl: { rejectUnauthorized: false }, entities: [User, Post, Tag], synchronize: false, logging: process.env.DEBUG === 'true' ? ['query', 'error'] : false, }); ``` Initialize the connection: TypeScript ```typescript await AppDataSource.initialize(); console.log('Connected to DB9'); ``` ## Entity Definitions [Section titled “Entity Definitions”](#entity-definitions) ### User entity with common column types [Section titled “User entity with common column types”](#user-entity-with-common-column-types) entities/User.ts ```typescript import { Entity, PrimaryGeneratedColumn, Column, Index, CreateDateColumn, UpdateDateColumn, OneToMany, } from 'typeorm'; import { Post } from './Post'; @Entity('users') export class User { @PrimaryGeneratedColumn() id!: number; @Column({ type: 'varchar', length: 255, unique: true }) @Index() email!: string; @Column({ type: 'varchar', length: 100 }) name!: string; @Column({ type: 'int', default: 0 }) age!: number; @Column({ type: 'boolean', default: true }) isActive!: boolean; @Column({ type: 'text', nullable: true }) bio!: string | null; @Column({ type: 'jsonb', nullable: true }) metadata!: Record | null; @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; @OneToMany(() => Post, (post) => post.author) posts!: Post[]; } ``` ### Post entity with relations [Section titled “Post entity with relations”](#post-entity-with-relations) entities/Post.ts ```typescript import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, ManyToMany, JoinColumn, JoinTable, } from 'typeorm'; import { User } from './User'; import { Tag } from './Tag'; @Entity('posts') export class Post { @PrimaryGeneratedColumn() id!: number; @Column({ type: 'varchar', length: 500 }) title!: string; @Column({ type: 'text' }) content!: string; @Column({ type: 'boolean', default: false }) published!: boolean; @Column({ type: 'jsonb', nullable: true }) settings!: Record | null; @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; @Column({ type: 'int' }) authorId!: number; @ManyToOne(() => User, (user) => user.posts, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'authorId' }) author!: User; @ManyToMany(() => Tag, (tag) => tag.posts) @JoinTable({ name: 'post_tags', joinColumn: { name: 'postId', referencedColumnName: 'id' }, inverseJoinColumn: { name: 'tagId', referencedColumnName: 'id' }, }) tags!: Tag[]; } ``` ### Tag entity for ManyToMany [Section titled “Tag entity for ManyToMany”](#tag-entity-for-manytomany) entities/Tag.ts ```typescript import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToMany } from 'typeorm'; import { Post } from './Post'; @Entity('tags') export class Tag { @PrimaryGeneratedColumn() id!: number; @Column({ type: 'varchar', length: 100, unique: true }) @Index() name!: string; @ManyToMany(() => Post, (post) => post.tags) posts!: Post[]; } ``` ## Create Tables [Section titled “Create Tables”](#create-tables) Use raw SQL to create tables rather than `synchronize: true`, which relies on schema introspection that may not fully work with DB9: setup.ts ```typescript import { AppDataSource } from './datasource'; async function setup() { await AppDataSource.initialize(); await AppDataSource.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, age INT DEFAULT 0, "isActive" BOOLEAN DEFAULT true, bio TEXT, metadata JSONB, "createdAt" TIMESTAMPTZ DEFAULT now(), "updatedAt" TIMESTAMPTZ DEFAULT now() ) `); await AppDataSource.query(` CREATE TABLE IF NOT EXISTS posts ( id SERIAL PRIMARY KEY, title VARCHAR(500) NOT NULL, content TEXT NOT NULL, published BOOLEAN DEFAULT false, settings JSONB, "createdAt" TIMESTAMPTZ DEFAULT now(), "authorId" INT NOT NULL REFERENCES users(id) ON DELETE CASCADE ) `); await AppDataSource.query(` CREATE TABLE IF NOT EXISTS tags ( id SERIAL PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL ) `); await AppDataSource.query(` CREATE TABLE IF NOT EXISTS post_tags ( "postId" INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, "tagId" INT NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY ("postId", "tagId") ) `); console.log('Tables created'); await AppDataSource.destroy(); } setup(); ``` ## CRUD with Repository Pattern [Section titled “CRUD with Repository Pattern”](#crud-with-repository-pattern) TypeScript ```typescript import { AppDataSource } from './datasource'; import { User } from './entities/User'; import { Post } from './entities/Post'; await AppDataSource.initialize(); const userRepo = AppDataSource.getRepository(User); const postRepo = AppDataSource.getRepository(Post); // Create const user = userRepo.create({ email: 'alice@example.com', name: 'Alice', age: 30, metadata: { role: 'admin' }, }); const saved = await userRepo.save(user); // Create with relation const post = postRepo.create({ title: 'Getting Started with DB9', content: 'DB9 is a serverless PostgreSQL-compatible database.', authorId: saved.id, }); await postRepo.save(post); // Read const found = await userRepo.findOneBy({ email: 'alice@example.com' }); // Read with relations const withPosts = await userRepo.findOne({ where: { id: saved.id }, relations: ['posts'], }); // Read with ordering and pagination const page = await userRepo.find({ order: { age: 'DESC', name: 'ASC' }, skip: 0, take: 10, }); // Update await userRepo.update(saved.id, { name: 'Alice Updated' }); // Upsert (INSERT ... ON CONFLICT) await AppDataSource.createQueryBuilder() .insert() .into(User) .values({ email: 'alice@example.com', name: 'Alice V2', age: 31 }) .orUpdate(['name', 'age'], ['email']) .execute(); // Delete (cascades to posts) await userRepo.delete(saved.id); ``` ## QueryBuilder [Section titled “QueryBuilder”](#querybuilder) TypeORM’s QueryBuilder provides type-safe query construction: TypeScript ```typescript // SELECT with conditions const users = await AppDataSource.getRepository(User) .createQueryBuilder('user') .where('user.age >= :minAge', { minAge: 25 }) .andWhere('user."isActive" = :active', { active: true }) .orderBy('user.name', 'ASC') .getMany(); // Aggregation with GROUP BY const ageGroups = await AppDataSource.getRepository(User) .createQueryBuilder('user') .select('user.age', 'age') .addSelect('COUNT(*)', 'count') .groupBy('user.age') .having('COUNT(*) > :min', { min: 1 }) .orderBy('user.age', 'ASC') .getRawMany(); // JOIN with eager loading const usersWithPosts = await AppDataSource.getRepository(User) .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('user.id = :id', { id: 1 }) .getOne(); // Subquery in WHERE const aboveAvg = await AppDataSource.getRepository(User) .createQueryBuilder('user') .where((qb) => { const sub = qb.subQuery().select('AVG(u.age)').from(User, 'u').getQuery(); return `user.age > (${sub})`; }) .getMany(); // INSERT with RETURNING const result = await AppDataSource.createQueryBuilder() .insert() .into(User) .values({ email: 'new@example.com', name: 'New', age: 25 }) .returning(['id', 'email', 'createdAt']) .execute(); ``` ## Relations [Section titled “Relations”](#relations) TypeORM supports OneToMany, ManyToOne, and ManyToMany relations with DB9: TypeScript ```typescript // Load user with nested relations const user = await userRepo.findOne({ where: { id: 1 }, relations: ['posts', 'posts.tags'], }); // ManyToMany: add tags to a post const tag = await AppDataSource.getRepository(Tag).save({ name: 'db9' }); const post = await postRepo.findOne({ where: { id: 1 }, relations: ['tags'], }); post!.tags = [...(post!.tags || []), tag]; await postRepo.save(post!); // INNER JOIN (only users with posts) const activeAuthors = await AppDataSource.getRepository(User) .createQueryBuilder('user') .innerJoinAndSelect('user.posts', 'post') .getMany(); // LEFT JOIN with filter on relation const usersPublished = await AppDataSource.getRepository(User) .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post', 'post.published = :pub', { pub: true }) .getOne(); ``` ## Transactions [Section titled “Transactions”](#transactions) ### QueryRunner (explicit control) [Section titled “QueryRunner (explicit control)”](#queryrunner-explicit-control) TypeScript ```typescript const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const user = await queryRunner.manager.save(User, { email: 'txn@example.com', name: 'Txn User', age: 25, }); await queryRunner.manager.save(Post, { title: 'Transactional Post', content: 'Created atomically.', authorId: user.id, }); await queryRunner.commitTransaction(); } catch (err) { await queryRunner.rollbackTransaction(); throw err; } finally { await queryRunner.release(); } ``` ### Transaction manager (automatic rollback) [Section titled “Transaction manager (automatic rollback)”](#transaction-manager-automatic-rollback) TypeScript ```typescript await AppDataSource.transaction(async (manager) => { const user = await manager.save(User, { email: 'auto@example.com', name: 'Auto User', age: 28, }); await manager.save(Post, { title: 'Auto Post', content: 'Rolls back on error.', authorId: user.id, }); }); ``` ### Isolation levels [Section titled “Isolation levels”](#isolation-levels) TypeScript ```typescript // Explicit isolation level await queryRunner.startTransaction('REPEATABLE READ'); ``` Supported: `READ COMMITTED`, `REPEATABLE READ`. `SERIALIZABLE` is not implemented — it does not error, it is silently downgraded to `REPEATABLE READ` with a server `WARNING`. Set `REPEATABLE READ` explicitly so the intent is visible in your code. ## Raw SQL for Advanced Features [Section titled “Raw SQL for Advanced Features”](#raw-sql-for-advanced-features) TypeScript ```typescript // Window functions const ranked = await AppDataSource.query(` SELECT name, age, ROW_NUMBER() OVER (ORDER BY age DESC) AS rank FROM users `); // CTEs const result = await AppDataSource.query(` WITH active AS ( SELECT * FROM users WHERE "isActive" = true ) SELECT name, age FROM active ORDER BY age `); // DISTINCT ON const firstPerAge = await AppDataSource.query(` SELECT DISTINCT ON (age) name, age FROM users ORDER BY age, name `); // Parameterized queries const filtered = await AppDataSource.query( 'SELECT * FROM users WHERE age > $1 AND age < $2', [20, 35] ); ``` ## Vector Search [Section titled “Vector Search”](#vector-search) TypeORM does not have a native vector column type. Use `text` for the entity column and raw SQL for vector operations: TypeScript ```typescript // Create vector table and index await AppDataSource.query(` CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS embeddings ( id SERIAL PRIMARY KEY, name VARCHAR(100), embedding vector(3) ); CREATE INDEX IF NOT EXISTS idx_embeddings ON embeddings USING hnsw (embedding vector_l2_ops); `); // Insert vectors await AppDataSource.query( `INSERT INTO embeddings (name, embedding) VALUES ($1, $2)`, ['doc-1', '[1.0, 2.0, 3.0]'] ); // Cosine distance search const results = await AppDataSource.query(` SELECT name, cosine_distance(embedding, '[1.0, 1.0, 1.0]') AS distance FROM embeddings ORDER BY distance ASC LIMIT 5 `); // L2 distance with operator const nearest = await AppDataSource.query(` SELECT name FROM embeddings WHERE embedding <-> '[1.0, 0.0, 0.0]' < 1.0 ORDER BY embedding <-> '[1.0, 0.0, 0.0]' `); ``` DB9 recognizes HNSW indexes with `vector_l2_ops`, `vector_cosine_ops`, and `vector_ip_ops`; IVFFlat is not available at all. But HNSW **index building is disabled in the current release** — a `CREATE INDEX ... USING hnsw` is rejected over the wire protocol with `55000` and creates an unused index over the HTTP SQL API. Vector search still returns correct results via an exact scan. ## Schema Changes [Section titled “Schema Changes”](#schema-changes) Since TypeORM’s `synchronize` and migration runner rely on schema introspection that may not fully work with DB9, manage migrations with raw SQL: TypeScript ```typescript // Add a column await AppDataSource.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS phone TEXT'); // Create an index await AppDataSource.query('CREATE INDEX IF NOT EXISTS idx_users_age ON users (age)'); // Alter column type await AppDataSource.query('ALTER TABLE users ALTER COLUMN phone TYPE VARCHAR(20)'); ``` ## Production Notes [Section titled “Production Notes”](#production-notes) * **Driver**: TypeORM uses the `pg` (node-postgres) driver. Set `type: 'postgres'` in the DataSource configuration. * **reflect-metadata**: Must be imported before any TypeORM code runs (`import 'reflect-metadata'`). * **Connection pooling**: TypeORM manages a pool internally. DB9 supports multiple concurrent connections per tenant. * **Type parser conflict**: If you run TypeORM alongside Drizzle in the same process, Drizzle overrides global `pg` type parsers. Restore date parsers explicitly if needed. * **synchronize**: Set to `false` in production. Use raw SQL for schema changes. * **Connection string**: Use `typeorm-app.admin` as the username, with host `pg.db9.io` and port `5433`. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `synchronize` creates unexpected schema [Section titled “synchronize creates unexpected schema”](#synchronize-creates-unexpected-schema) DB9’s `information_schema` support is limited. TypeORM’s schema synchronization may not detect existing tables correctly. Use raw DDL and set `synchronize: false`. ### Date columns return strings [Section titled “Date columns return strings”](#date-columns-return-strings) If date columns return strings instead of `Date` objects, another library (such as Drizzle) may have modified the global `pg` type parsers. Restore them: TypeScript ```typescript import { types } from 'pg'; types.setTypeParser(types.builtins.TIMESTAMPTZ, (val: string) => new Date(val)); types.setTypeParser(types.builtins.TIMESTAMP, (val: string) => new Date(val)); types.setTypeParser(types.builtins.DATE, (val: string) => new Date(val)); ``` ### Connection refused [Section titled “Connection refused”](#connection-refused) Verify the host (`pg.db9.io`), port (`5433`), and username format (`{database-name}.admin`). DB9 routes connections by parsing the tenant from the username. ### `QueryFailedError: relation does not exist` [Section titled “QueryFailedError: relation does not exist”](#queryfailederror-relation-does-not-exist) Tables must be created before use. With `synchronize: false`, run your DDL setup script first. ## Verified Compatibility [Section titled “Verified Compatibility”](#verified-compatibility) Tested with TypeORM 0.3.17+ against DB9. 147 of 150 tests pass (3 skipped): | Category | Status | | ------------------------------------------------------------ | ------------------- | | Connection and pooling | Pass | | CRUD (insert, select, update, delete) | Pass | | Relations (OneToMany, ManyToOne, ManyToMany) | Pass | | QueryBuilder (select, join, subquery, aggregate) | Pass | | Transactions and isolation levels | Pass | | Schema and DDL operations | Pass | | Data types (int, varchar, boolean, jsonb, uuid, timestamptz) | Pass | | Error handling (constraint violations, syntax errors) | Pass | | Vector operations; HNSW index building disabled | Pass (exact search) | | Advanced SQL (window, CTE, DISTINCT ON) | Pass | ## Next Pages [Section titled “Next Pages”](#next-pages) * [Prisma](/docs/guides/prisma/) — Prisma ORM with DB9 * [Drizzle](/docs/guides/drizzle/) — Drizzle ORM with DB9 * [Connect](/docs/connect/) — connection strings and authentication * [Vector Extension](/docs/extensions/vector/) — HNSW indexes and distance operators * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full PostgreSQL compatibility surface # Migrate from Amazon RDS > Move your database from Amazon RDS for PostgreSQL to DB9 — export with pg_dump, import with the DB9 CLI, and update your application's connection string. This guide covers migrating from Amazon RDS for PostgreSQL (or Aurora PostgreSQL) to DB9. The process uses standard `pg_dump` for export and the DB9 CLI or `psql` for import. For the general PostgreSQL migration guide, see [Migrate from PostgreSQL](/docs/migrations/from-postgres/). ## What Changes and What Stays the Same [Section titled “What Changes and What Stays the Same”](#what-changes-and-what-stays-the-same) ### Stays the same [Section titled “Stays the same”](#stays-the-same) * **SQL compatibility** — DB9 supports the same DML, DDL, joins, CTEs, window functions, and subqueries you use in RDS PostgreSQL. Most queries work without changes. * **PostgreSQL drivers** — Any driver that connects via pgwire (node-postgres, psycopg, pgx, JDBC) works with DB9. * **ORM compatibility** — Prisma, Drizzle, SQLAlchemy, TypeORM, Sequelize, Knex, and GORM are tested and supported. * **Data types** — Common types (TEXT, INTEGER, BIGINT, BOOLEAN, TIMESTAMPTZ, UUID, JSONB, arrays, vectors) work identically. ### Changes [Section titled “Changes”](#changes) | Area | Amazon RDS PostgreSQL | DB9 | | ---------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | | **Connection string** | `postgresql://master:pass@instance.region.rds.amazonaws.com:5432/dbname` | `postgresql://tenant.role@pg.db9.io:5433/postgres` | | **Port** | 5432 (default) | 5433 | | **Database name** | Custom | Always `postgres` | | **Connection pooling** | External (PgBouncer on EC2, RDS Proxy) | No built-in pooler — use application-side pooling | | **Extensions** | 80+ supported | 9 built-in (http, vector, fs9, pg\_cron, embedding, hstore, uuid-ossp, parquet, zhparser) | | **IAM authentication** | Supported | Not supported — use connection string credentials | | **Read replicas** | Supported | Not supported | | **Replication** | Logical and streaming replication | Not supported | | **Table partitioning** | Supported | Not supported | | **LISTEN/NOTIFY** | Supported | Supported over pgwire (delivered on commit); the stateless HTTP SQL API can `NOTIFY` but cannot `LISTEN` | | **Automated backups** | Point-in-time recovery, snapshots | CLI-based backup (`db9 db dump`) | | **Multi-AZ** | Automatic failover | Managed by DB9 | Review the [Compatibility Matrix](/docs/platform/compatibility-matrix/) for the full list of supported and unsupported features. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Access to your RDS instance (master user credentials or IAM auth) * Network access to the RDS instance (your machine must be able to reach it — check security groups and VPC settings) * `pg_dump` installed locally (should match or be close to your RDS PostgreSQL version) * DB9 CLI installed: `curl -fsSL https://db9.ai/install | sh` * A DB9 account: `db9 create --name my-app` to create your target database 1. **Ensure Network Access to RDS** RDS instances are typically inside a VPC. To run `pg_dump` from your local machine, you need one of: * **Public accessibility** enabled on the RDS instance, with your IP allowed in the security group * **SSH tunnel** through a bastion host in the same VPC * **AWS SSM Session Manager** port forwarding Terminal ```bash # SSH tunnel example ssh -L 5432:my-instance.region.rds.amazonaws.com:5432 ec2-user@bastion-host # Then use localhost:5432 in your pg_dump command ``` If using IAM authentication, generate a temporary password: Terminal ```bash export PGPASSWORD=$(aws rds generate-db-auth-token \ --hostname my-instance.region.rds.amazonaws.com \ --port 5432 \ --username master \ --region us-east-1) ``` 2. **Export from RDS** Use `pg_dump` with your RDS connection details. **Schema and data (plain SQL format)** Terminal ```bash pg_dump --no-owner --no-privileges --no-comments \ "postgresql://master:password@my-instance.region.rds.amazonaws.com:5432/mydb?sslmode=require" \ > export.sql ``` **Schema only** Terminal ```bash pg_dump --schema-only --no-owner --no-privileges \ "postgresql://master:password@my-instance.region.rds.amazonaws.com:5432/mydb?sslmode=require" \ > schema.sql ``` Flags explained: * `--no-owner` — omits `ALTER ... OWNER TO` statements that reference RDS-specific roles (`rds_superuser`, `rds_replication`, etc.) * `--no-privileges` — omits `GRANT`/`REVOKE` statements for RDS role hierarchy * `--no-comments` — omits `COMMENT ON` statements Use **plain SQL format** (default). DB9 does not support `pg_restore` with the custom (`-Fc`) or directory (`-Fd`) formats — import via SQL text only. **Exclude RDS-internal schemas** If your dump includes RDS-specific schemas, exclude them: Terminal ```bash pg_dump --no-owner --no-privileges --no-comments \ -N rdsadmin -N rds_tools \ "postgresql://master:password@my-instance.region.rds.amazonaws.com:5432/mydb?sslmode=require" \ > export.sql ``` 3. **Clean the Export** RDS exports may contain features DB9 does not support. Remove or comment out: * **`CREATE EXTENSION`** for extensions DB9 does not have — RDS supports 80+ extensions; DB9 supports 9 built-in. Remove any `CREATE EXTENSION` for extensions not in: `http`, `uuid-ossp`, `hstore`, `fs9`, `pg_cron`, `parquet`, `zhparser`, `vector`, `embedding`. * **`CREATE PUBLICATION` / `CREATE SUBSCRIPTION`** — DB9 does not support logical replication. * **Row-level security policies** — `CREATE POLICY`, `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`. * **Table partitioning** — `PARTITION BY`, `CREATE TABLE ... PARTITION OF`. * **Foreign data wrappers** — `CREATE SERVER`, `CREATE FOREIGN TABLE` (common with `postgres_fdw` or `mysql_fdw` on RDS). A quick way to identify issues: Terminal ```bash # Check for unsupported extensions grep "CREATE EXTENSION" export.sql # Check for partitioning grep -i "PARTITION" export.sql # Check for RLS grep -i "ROW LEVEL SECURITY\|CREATE POLICY" export.sql # Check for replication grep -i "PUBLICATION\|SUBSCRIPTION" export.sql # Check for FDW grep -i "CREATE SERVER\|FOREIGN TABLE" export.sql ``` 4. **Create the DB9 Database** Terminal ```bash db9 create --name my-app --show-connection-string ``` ▶ Run This returns immediately with the connection string and credentials. 5. **Import into DB9** **Option A: CLI import (recommended for most databases)** Terminal ```bash db9 db sql my-app -f export.sql ``` Suitable for databases up to the API import limits (50,000 rows or 16 MB per table). **Option B: Direct psql import (for larger databases)** Terminal ```bash psql "$(db9 db connect my-app --output quiet)" -f export.sql ``` Streams SQL through pgwire without API size limits. **Option C: COPY for bulk data** Terminal ```bash # Import schema first psql "$(db9 db connect my-app --output quiet)" -f schema.sql # Then stream data directly from RDS into DB9 pg_dump --data-only --no-owner \ "postgresql://master:password@my-instance.region.rds.amazonaws.com:5432/mydb?sslmode=require" \ | psql "$(db9 db connect my-app --output quiet)" ``` DB9 supports `COPY` in CSV and TEXT formats over pgwire. 6. **Update Your Application** **Connection string** Replace the RDS connection string with DB9’s: Diff ```diff DATABASE_URL=postgresql://master:password@my-instance.region.rds.amazonaws.com:5432/mydb?sslmode=require DATABASE_URL=postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=require ``` Key differences: * **Username**: DB9 uses `{tenant_id}.{role}` format (e.g., `a1b2c3d4e5f6.admin`) * **Port**: 5433, not 5432 * **Database**: Always `postgres` * **Host**: `pg.db9.io` (not region-specific endpoints) **IAM authentication** If you use RDS IAM authentication to generate temporary passwords, remove that logic. DB9 uses static credentials in the connection string. **RDS Proxy** If you use RDS Proxy for connection pooling, remove it and configure pooling at the application level: TypeScript ```typescript const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10, idleTimeoutMillis: 30000, }); ``` **AWS SDK integrations** If your application uses AWS SDK to interact with RDS (automated snapshots, instance management), those APIs will not apply to DB9. Use the DB9 CLI for database management instead. For ORMs, see the integration guides: [Prisma](/docs/guides/prisma/), [Drizzle](/docs/guides/drizzle/), [SQLAlchemy](/docs/guides/python-sqlalchemy/). 7. **Validate** **Check schema** Terminal ```bash db9 db dump my-app --ddl-only ``` ▶ Run Compare the output with your original schema to confirm all tables, indexes, and constraints were created. **Check row counts** Terminal ```bash db9 db sql my-app -q "SELECT count(*) FROM users" db9 db sql my-app -q "SELECT count(*) FROM orders" ``` ▶ Run Compare row counts against the source RDS database. **Run your test suite** Terminal ```bash DATABASE_URL="$(db9 db connect my-app --output quiet)" npm test ``` **Check for unsupported features** If your tests fail, check these common differences: * **SERIALIZABLE isolation** — DB9 does not implement SERIALIZABLE. Requesting it on the wire protocol raises a `WARNING` and silently downgrades the transaction to REPEATABLE READ, so it will not fail loudly. Audit transactions that relied on serializability and add explicit `SELECT ... FOR UPDATE` locks or unique constraints * **LISTEN/NOTIFY** — supported over a direct pgwire connection; the stateless HTTP SQL API can `NOTIFY` but cannot `LISTEN` * **Advisory locks** — available, but coordination is node-local. For strict row-level coordination, use `SELECT ... FOR UPDATE` * **Index access methods** — only `btree` and `gin` can be created; `GiST`, `Hash`, `SP-GiST`, and `BRIN` are rejected at `CREATE INDEX` time with `access method "..." is not supported` (`0A000`). `hnsw` is a recognized method but index building is disabled in the current release. GIN itself is fully functional ## Rollback Plan [Section titled “Rollback Plan”](#rollback-plan) If you need to revert: 1. Your RDS instance is unchanged — switch `DATABASE_URL` back to the RDS connection string. 2. If you need to export data created in DB9 back to RDS: Terminal ```bash # Export from DB9 db9 db dump my-app -o db9-export.sql # Import to RDS psql "postgresql://master:password@my-instance.region.rds.amazonaws.com:5432/mydb?sslmode=require" \ -f db9-export.sql ``` The `db9 db dump` command outputs plain SQL (up to 50,000 rows or 16 MB per table). For larger databases, use `psql` to stream individual tables with `COPY`. ## Caveats [Section titled “Caveats”](#caveats) * **No zero-downtime migration** — DB9 does not support logical replication, so you cannot use RDS logical replication to stream changes. Plan a maintenance window or accept a brief cutover period. * **Extension gaps** — RDS supports 80+ extensions; DB9 has 9 built-in. Check your `CREATE EXTENSION` statements carefully. Common RDS extensions not in DB9 include `PostGIS`, `pg_trgm`, `pgcrypto`, `pg_stat_statements`, `ltree`. * **Dump size limits** — The `db9 db sql -f` API import has limits (50,000 rows, 16 MB per table). For larger databases, use direct `psql` connection for import. * **No read replicas** — RDS supports read replicas for scaling reads. DB9 does not have read replicas. If your application relies on read/write splitting, consolidate to a single connection string. * **No automated backups** — RDS provides automated point-in-time recovery and snapshots. DB9 uses CLI-based backup (`db9 db dump`). Set up your own backup schedule. * **AWS ecosystem loss** — CloudWatch metrics, Performance Insights, and other AWS-integrated monitoring will not apply. Use DB9’s built-in monitoring. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full list of supported and unsupported PostgreSQL features * [Connect](/docs/connect/) — connection string format and authentication options * [Migrate from PostgreSQL](/docs/migrations/from-postgres/) — general PostgreSQL migration path * [Production Checklist](/docs/production-checklist/) — deployment readiness # Migrate from Firebase > Move your database from Firebase (Firestore or Realtime Database) to DB9 — export your data, design a relational schema, and import with the DB9 CLI. Firebase offers two NoSQL databases — **Cloud Firestore** (document model) and **Realtime Database** (JSON tree). DB9 is a relational (SQL) database. Migrating from Firebase means transforming your data from a document/JSON model into relational tables. This guide covers exporting your Firebase data, designing a relational schema, transforming the data, and importing it into DB9. It focuses on the **database layer only** — Firebase Auth, Cloud Functions, Storage, and Hosting need separate replacements. ## What DB9 Replaces and What It Does Not [Section titled “What DB9 Replaces and What It Does Not”](#what-db9-replaces-and-what-it-does-not) ### DB9 replaces [Section titled “DB9 replaces”](#db9-replaces) * **Cloud Firestore / Realtime Database** — your application’s data storage layer * **Firestore queries** — replaced by SQL queries with joins, CTEs, aggregations, and window functions ### DB9 does not replace [Section titled “DB9 does not replace”](#db9-does-not-replace) | Firebase feature | What to use instead | | ----------------------------- | ------------------------------------------------------------- | | **Firebase Auth** | Third-party auth (Auth0, Clerk, Supabase Auth) or custom JWT | | **Cloud Functions** | Cloudflare Workers, Vercel Functions, AWS Lambda | | **Cloud Storage** | AWS S3, GCS, or DB9’s fs9 extension for file-as-SQL workflows | | **Hosting** | Vercel, Netlify, Cloudflare Pages | | **Realtime listeners** | Polling, application WebSockets, or an external message queue | | **Security Rules** | Enforce access control in your application layer | | **Remote Config / Analytics** | Third-party equivalents (PostHog, Amplitude, LaunchDarkly) | ## Key Differences [Section titled “Key Differences”](#key-differences) | Area | Firebase (Firestore) | DB9 | | --------------------- | ------------------------------------------- | -------------------------------------------------------------------------- | | **Data model** | Document/collection (NoSQL) | Relational tables (SQL) | | **Query language** | Firestore SDK / chained filters | Standard SQL (PostgreSQL-compatible) | | **Joins** | Not supported (denormalized data) | Full JOIN support — normalize your data | | **Transactions** | Single-document or cross-document (limited) | Full ACID transactions (READ COMMITTED default, REPEATABLE READ available) | | **Real-time updates** | Built-in `onSnapshot` listeners | Not built-in — use polling or application WebSockets | | **Indexes** | Automatic single-field, manual composite | Manual (B-tree, HNSW for vectors) | | **Scaling** | Auto-scales reads/writes | Fixed per-database, always on | | **Offline support** | Built-in client cache | Not built-in — implement at application level | | **Connection** | Firebase SDK (HTTPS) | pgwire protocol (TCP) — standard PostgreSQL drivers | | **Pricing** | Per-read/write/document | Per-database (fixed compute) | ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Access to your Firebase project (Firebase Console or `firebase` CLI) * Node.js installed (for the export/transform scripts) * DB9 CLI installed: `curl -fsSL https://db9.ai/install | sh` * A DB9 account: `db9 create --name my-app` to create your target database 1. **Export from Firebase** **Option A: Firestore — Firebase CLI** Terminal ```bash # Install Firebase CLI if needed npm install -g firebase-tools firebase login # Export all collections gcloud firestore export gs://your-bucket/firestore-export # Then download from GCS gsutil -m cp -r gs://your-bucket/firestore-export ./firestore-export ``` **Option B: Firestore — custom script (recommended for transformation)** Write a Node.js script to export each collection as JSON: JavaScript ```javascript const admin = require('firebase-admin'); const fs = require('fs'); admin.initializeApp({ credential: admin.credential.applicationDefault() }); const db = admin.firestore(); async function exportCollection(name) { const snapshot = await db.collection(name).get(); const docs = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); fs.writeFileSync(`${name}.json`, JSON.stringify(docs, null, 2)); console.log(`Exported ${docs.length} docs from ${name}`); } // Export each collection async function main() { await exportCollection('users'); await exportCollection('posts'); await exportCollection('comments'); // Add more collections as needed } main(); ``` **Option C: Realtime Database** Terminal ```bash # Export via REST API curl "https://your-project.firebaseio.com/.json?auth=YOUR_SECRET" > rtdb-export.json # Or use Firebase CLI firebase database:get / --project your-project > rtdb-export.json ``` 2. **Design Your Relational Schema** Firebase encourages denormalized data. For DB9, normalize into relational tables with foreign keys. **Example: Firestore document model → relational schema** Firestore structure: ```plaintext users/{userId} ├── name: "Alice" ├── email: "alice@example.com" └── posts (subcollection) └── {postId} ├── title: "Hello" ├── content: "World" ├── tags: ["dev", "db"] └── comments (subcollection) └── {commentId} ├── text: "Nice post" └── authorId: "user123" ``` Relational schema: SQL ```sql CREATE TABLE users ( id TEXT PRIMARY KEY, -- Firestore document ID name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE posts ( id TEXT PRIMARY KEY, -- Firestore document ID user_id TEXT NOT NULL REFERENCES users(id), title TEXT NOT NULL, content TEXT, tags TEXT[], -- PostgreSQL array for tags created_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE comments ( id TEXT PRIMARY KEY, -- Firestore document ID post_id TEXT NOT NULL REFERENCES posts(id), author_id TEXT NOT NULL REFERENCES users(id), text TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_posts_user_id ON posts(user_id); CREATE INDEX idx_comments_post_id ON comments(post_id); CREATE INDEX idx_comments_author_id ON comments(author_id); ``` Key decisions: * **Document IDs** → use as primary keys (TEXT) or generate new UUIDs * **Subcollections** → become separate tables with foreign keys * **Nested objects** → either flatten into columns or store as JSONB * **Arrays** → use PostgreSQL array types or normalize into junction tables * **Timestamps** → Firestore Timestamps → TIMESTAMPTZ 3. **Transform Data to SQL** Write a script to convert exported JSON into SQL INSERT statements: JavaScript ```javascript const fs = require('fs'); function escapeSQL(val) { if (val === null || val === undefined) return 'NULL'; if (typeof val === 'number') return String(val); if (typeof val === 'boolean') return val ? 'TRUE' : 'FALSE'; if (Array.isArray(val)) return `ARRAY[${val.map(v => `'${String(v).replace(/'/g, "''")}'`).join(',')}]::TEXT[]`; return `'${String(val).replace(/'/g, "''")}'`; } // Transform users const users = JSON.parse(fs.readFileSync('users.json', 'utf-8')); let sql = ''; sql += `-- Schema\n`; sql += `CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL);\n\n`; sql += `-- Data\n`; for (const user of users) { sql += `INSERT INTO users (id, name, email) VALUES (${escapeSQL(user.id)}, ${escapeSQL(user.name)}, ${escapeSQL(user.email)});\n`; } // Repeat for other collections... fs.writeFileSync('import.sql', sql); console.log('Generated import.sql'); ``` For large datasets, use `COPY` format instead of individual INSERTs: JavaScript ```javascript // Generate CSV for COPY const csv = users.map(u => `${u.id}\t${u.name}\t${u.email}`).join('\n'); fs.writeFileSync('users.tsv', csv); ``` 4. **Create the DB9 Database** Terminal ```bash db9 create --name my-app --show-connection-string ``` ▶ Run 5. **Import into DB9** **Import schema first, then data:** Terminal ```bash # Import schema and data from the generated SQL file db9 db sql my-app -f import.sql ``` **For larger datasets, use COPY:** Terminal ```bash # Import schema db9 db sql my-app -f schema.sql # Import data via COPY psql "$(db9 db connect my-app --output quiet)" \ -c "\COPY users FROM 'users.tsv' WITH (FORMAT text)" ``` 6. **Update Your Application** **Replace the Firebase SDK with a PostgreSQL driver** Diff ```diff import { getFirestore, collection, getDocs, query, where } from 'firebase/firestore'; const db = getFirestore(app); const q = query(collection(db, 'posts'), where('published', '==', true)); const snapshot = await getDocs(q); const posts = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); import pg from 'pg'; const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); const { rows: posts } = await pool.query('SELECT * FROM posts WHERE published = true'); ``` Or use an ORM: [Prisma](/docs/guides/prisma/), [Drizzle](/docs/guides/drizzle/), [SQLAlchemy](/docs/guides/python-sqlalchemy/). **Replace Firestore queries with SQL** | Firestore | SQL | | --------------------------------------- | ------------------------------------------- | | `where('age', '>', 18)` | `WHERE age > 18` | | `orderBy('created', 'desc').limit(10)` | `ORDER BY created DESC LIMIT 10` | | `doc('users/abc')` | `SELECT * FROM users WHERE id = 'abc'` | | Subcollection query | `JOIN` or subquery | | `arrayContains('tags', 'dev')` | `WHERE 'dev' = ANY(tags)` | | Compound queries (limited in Firestore) | Full SQL with multiple JOINs and conditions | **Replace real-time listeners** Firestore’s `onSnapshot` has no direct DB9 equivalent. Alternatives: * **Polling** — query the database on an interval * **Application WebSockets** — push changes from your API when writes happen * **Server-Sent Events** — stream updates from your API layer **Replace Security Rules** Firestore Security Rules run at the database level. With DB9, enforce access control in your API: TypeScript ```typescript // Before (Firestore Security Rules): // match /posts/{postId} { allow read: if request.auth != null; } // After (application-level): app.get('/api/posts', authMiddleware, async (req, res) => { const { rows } = await pool.query( 'SELECT * FROM posts WHERE user_id = $1', [req.user.id] ); res.json(rows); }); ``` 7. **Validate** **Check row counts** Compare the number of documents in each Firestore collection with the row count in DB9: Terminal ```bash db9 db sql my-app -q "SELECT count(*) FROM users" db9 db sql my-app -q "SELECT count(*) FROM posts" db9 db sql my-app -q "SELECT count(*) FROM comments" ``` ▶ Run **Run your test suite** Terminal ```bash DATABASE_URL="$(db9 db connect my-app --output quiet)" npm test ``` **Verify query results** Test key queries that your application relies on and compare results with the original Firestore queries. ## Rollback Plan [Section titled “Rollback Plan”](#rollback-plan) Your Firebase project is unchanged by the migration. To revert: 1. Switch your application back to the Firebase SDK and restore the original Firestore configuration. 2. If you need to export data created in DB9 back to Firestore, write a reverse transformation script that reads from DB9 and writes documents back to Firestore. ## Caveats [Section titled “Caveats”](#caveats) * **Data model redesign required** — This is not a lift-and-shift migration. You must design a relational schema, which may require significant application changes. * **No real-time listeners** — DB9 does not have built-in real-time subscriptions like Firestore’s `onSnapshot`. Implement polling or WebSockets in your application. * **No offline support** — Firestore’s built-in offline cache and sync are not available. If your app needs offline support, implement it at the application level. * **No automatic indexes** — Firestore auto-indexes every field. In DB9, create indexes manually for your query patterns. * **Security rules must move to application code** — Firestore Security Rules are enforced at the database level. With DB9, enforce access control in your API layer. * **Subcollection patterns change** — Firestore subcollections become separate tables with foreign keys. Update all queries that traverse subcollections. * **Pricing model change** — Firebase charges per-read/write. DB9 charges per-database (fixed compute). This may be cheaper or more expensive depending on your access patterns. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full list of supported and unsupported PostgreSQL features * [Connect](/docs/connect/) — connection string format and authentication options * [Prisma Guide](/docs/guides/prisma/) — type-safe ORM for DB9 * [Drizzle Guide](/docs/guides/drizzle/) — lightweight TypeScript ORM * [Production Checklist](/docs/production-checklist/) — deployment readiness # Migrate from Heroku Postgres > Move your database from Heroku Postgres to DB9 — export with pg_dump, import with the DB9 CLI, and update your application's connection string. Heroku Postgres is a managed PostgreSQL service tightly integrated with the Heroku platform. Since it runs standard PostgreSQL, migration to DB9 uses `pg_dump` for export and the DB9 CLI or `psql` for import. For the general PostgreSQL migration guide, see [Migrate from PostgreSQL](/docs/migrations/from-postgres/). ## What Changes and What Stays the Same [Section titled “What Changes and What Stays the Same”](#what-changes-and-what-stays-the-same) ### Stays the same [Section titled “Stays the same”](#stays-the-same) * **SQL compatibility** — DB9 supports the same DML, DDL, joins, CTEs, window functions, and subqueries you use in Heroku Postgres. Most queries work without changes. * **PostgreSQL drivers** — Any driver that connects via pgwire (node-postgres, psycopg, pgx, JDBC) works with DB9. * **ORM compatibility** — Prisma, Drizzle, SQLAlchemy, TypeORM, Sequelize, Knex, and GORM are tested and supported. * **Data types** — Common types (TEXT, INTEGER, BIGINT, BOOLEAN, TIMESTAMPTZ, UUID, JSONB, arrays, vectors) work identically. ### Changes [Section titled “Changes”](#changes) | Area | Heroku Postgres | DB9 | | ---------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | **Connection string** | `postgresql://user:pass@host.compute-1.amazonaws.com:5432/dbname` | `postgresql://tenant.role@pg.db9.io:5433/postgres` | | **Port** | 5432 | 5433 | | **Database name** | Auto-generated (e.g., `d1a2b3c4e5f6g7`) | Always `postgres` | | **Connection pooling** | Built-in connection pooling add-on | No built-in pooler — use application-side pooling | | **Credentials** | Rotate periodically (Heroku manages) | Static credentials in connection string | | **Extensions** | Most community extensions available | 9 built-in (http, vector, fs9, pg\_cron, embedding, hstore, uuid-ossp, parquet, zhparser) | | **Replication** | Followers (streaming replication) | Not supported | | **LISTEN/NOTIFY** | Supported | Supported over pgwire (delivered on commit); the stateless HTTP SQL API can `NOTIFY` but cannot `LISTEN` | | **Dataclips** | Supported (shareable SQL queries) | Not available — use DB9 CLI or psql | | **pg:backups** | Heroku CLI automated backups | CLI-based backup (`db9 db dump`) | Review the [Compatibility Matrix](/docs/platform/compatibility-matrix/) for the full list of supported and unsupported features. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Access to your Heroku app and database * Heroku CLI installed (`npm install -g heroku` or `brew tap heroku/brew && brew install heroku`) * `pg_dump` installed locally (comes with PostgreSQL client tools) * DB9 CLI installed: `curl -fsSL https://db9.ai/install | sh` * A DB9 account: `db9 create --name my-app` to create your target database 1. **Get Your Heroku Connection String** Terminal ```bash heroku config:get DATABASE_URL -a your-app-name ``` The connection string looks like: ```plaintext postgresql://username:password@ec2-xx-xx-xx-xx.compute-1.amazonaws.com:5432/d1a2b3c4e5f6g7 ``` Alternatively, use `heroku pg:credentials:url -a your-app-name` for detailed connection information. 2. **Export from Heroku Postgres** Use `pg_dump` with the Heroku connection string. Heroku requires SSL: **Schema and data (plain SQL format)** Terminal ```bash pg_dump --no-owner --no-privileges --no-comments \ "$(heroku config:get DATABASE_URL -a your-app-name)?sslmode=require" \ > export.sql ``` **Schema only** Terminal ```bash pg_dump --schema-only --no-owner --no-privileges \ "$(heroku config:get DATABASE_URL -a your-app-name)?sslmode=require" \ > schema.sql ``` Flags explained: * `--no-owner` — omits `ALTER ... OWNER TO` statements that reference Heroku-specific roles * `--no-privileges` — omits `GRANT`/`REVOKE` statements * `--no-comments` — omits `COMMENT ON` statements Use **plain SQL format** (default). DB9 does not support `pg_restore` with the custom (`-Fc`) or directory (`-Fd`) formats — import via SQL text only. Alternative: Heroku pg:backups You can also create a backup and download it, but the `pg_dump` approach gives you a plain SQL file that is easier to clean and import. 3. **Clean the Export** The `pg_dump` output may contain statements that DB9 does not support. Remove or comment out: * **`CREATE EXTENSION`** for extensions DB9 does not have — Remove any `CREATE EXTENSION` for extensions not in: `http`, `uuid-ossp`, `hstore`, `fs9`, `pg_cron`, `parquet`, `zhparser`, `vector`, `embedding`. * **`CREATE PUBLICATION` / `CREATE SUBSCRIPTION`** — DB9 does not support logical replication. * **Row-level security policies** — `CREATE POLICY`, `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`. * **Table partitioning** — `PARTITION BY`, `CREATE TABLE ... PARTITION OF`. A quick way to identify issues: Terminal ```bash # Check for unsupported extensions grep "CREATE EXTENSION" export.sql # Check for partitioning grep -i "PARTITION" export.sql # Check for RLS grep -i "ROW LEVEL SECURITY\|CREATE POLICY" export.sql # Check for replication grep -i "PUBLICATION\|SUBSCRIPTION" export.sql ``` 4. **Create the DB9 Database** Terminal ```bash db9 create --name my-app --show-connection-string ``` ▶ Run This returns immediately with the connection string and credentials. 5. **Import into DB9** **Option A: CLI import (recommended for most databases)** Terminal ```bash db9 db sql my-app -f export.sql ``` Suitable for databases up to the API import limits (50,000 rows or 16 MB per table). **Option B: Direct psql import (for larger databases)** Terminal ```bash psql "$(db9 db connect my-app --output quiet)" -f export.sql ``` Streams SQL through pgwire without API size limits. **Option C: COPY for bulk data** Terminal ```bash # Import schema first psql "$(db9 db connect my-app --output quiet)" -f schema.sql # Then stream data directly from Heroku into DB9 pg_dump --data-only --no-owner \ "$(heroku config:get DATABASE_URL -a your-app-name)?sslmode=require" \ | psql "$(db9 db connect my-app --output quiet)" ``` DB9 supports `COPY` in CSV and TEXT formats over pgwire. 6. **Update Your Application** **Connection string** Replace the Heroku `DATABASE_URL` with DB9’s: Diff ```diff DATABASE_URL=postgresql://user:pass@ec2-xx-xx-xx-xx.compute-1.amazonaws.com:5432/d1a2b3c4e5f6g7 DATABASE_URL=postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=require ``` If your app runs on Heroku, set the new config var: Terminal ```bash heroku config:set DATABASE_URL="postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=require" -a your-app-name ``` Key differences: * **Username**: DB9 uses `{tenant_id}.{role}` format (e.g., `a1b2c3d4e5f6.admin`) * **Port**: 5433, not 5432 * **Database**: Always `postgres` * **Host**: `pg.db9.io` * **Credentials**: Static (Heroku rotates credentials periodically — DB9 does not) **Connection pooling** If you use Heroku’s connection pooling add-on, remove the pooled connection string and configure application-side pooling: TypeScript ```typescript const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10, idleTimeoutMillis: 30000, ssl: { rejectUnauthorized: false }, }); ``` **Heroku Dataclips** Heroku Dataclips (shareable SQL queries) have no DB9 equivalent. Use the DB9 CLI to run queries: Terminal ```bash db9 db sql my-app -q "SELECT * FROM users LIMIT 10" ``` For ORMs, see the integration guides: [Prisma](/docs/guides/prisma/), [Drizzle](/docs/guides/drizzle/), [SQLAlchemy](/docs/guides/python-sqlalchemy/). 7. **Validate** **Check schema** Terminal ```bash db9 db dump my-app --ddl-only ``` ▶ Run Compare the output with your original schema to confirm all tables, indexes, and constraints were created. **Check row counts** Terminal ```bash db9 db sql my-app -q "SELECT count(*) FROM users" db9 db sql my-app -q "SELECT count(*) FROM orders" ``` ▶ Run Compare row counts against the source Heroku database. **Run your test suite** Terminal ```bash DATABASE_URL="$(db9 db connect my-app --output quiet)" npm test ``` **Check for unsupported features** If your tests fail, check these common differences: * **SERIALIZABLE isolation** — DB9 does not implement SERIALIZABLE. Requesting it on the wire protocol raises a `WARNING` and silently downgrades the transaction to REPEATABLE READ, so it will not fail loudly. Audit transactions that relied on serializability and add explicit `SELECT ... FOR UPDATE` locks or unique constraints * **LISTEN/NOTIFY** — supported over a direct pgwire connection; the stateless HTTP SQL API can `NOTIFY` but cannot `LISTEN` * **Advisory locks** — available, but coordination is node-local. For strict row-level coordination, use `SELECT ... FOR UPDATE` ## Rollback Plan [Section titled “Rollback Plan”](#rollback-plan) If you need to revert: 1. Your Heroku database is unchanged — switch `DATABASE_URL` back: Terminal ```bash # Re-attach the original Heroku Postgres add-on URL heroku config:set DATABASE_URL="$(heroku pg:credentials:url -a your-app-name | grep postgresql://)" -a your-app-name ``` 2. If you need to export data created in DB9 back to Heroku: Terminal ```bash # Export from DB9 db9 db dump my-app -o db9-export.sql # Import to Heroku psql "$(heroku config:get DATABASE_URL -a your-app-name)?sslmode=require" \ -f db9-export.sql ``` The `db9 db dump` command outputs plain SQL (up to 50,000 rows or 16 MB per table). For larger databases, use `psql` to stream individual tables with `COPY`. ## Caveats [Section titled “Caveats”](#caveats) * **No zero-downtime migration** — DB9 does not support logical replication. Plan a maintenance window for the cutover. * **Extension gaps** — If your Heroku database uses extensions not in DB9’s built-in set, those features will not be available. Check your `CREATE EXTENSION` statements. * **Dump size limits** — The `db9 db sql -f` API import has limits (50,000 rows, 16 MB per table). For larger databases, use direct `psql` connection for import. * **Credential rotation** — Heroku periodically rotates database credentials. DB9 credentials are static. If your application handles credential rotation, simplify that logic. * **Heroku add-on ecosystem** — Heroku add-ons that depend on `DATABASE_URL` (logging, monitoring, analytics) will not automatically connect to DB9. Reconfigure or replace them. * **No followers** — Heroku Postgres supports follower databases (read replicas). DB9 does not. If you use followers for read scaling, consolidate to a single connection. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full list of supported and unsupported PostgreSQL features * [Connect](/docs/connect/) — connection string format and authentication options * [Migrate from PostgreSQL](/docs/migrations/from-postgres/) — general PostgreSQL migration path * [Production Checklist](/docs/production-checklist/) — deployment readiness # Migrate from Neon > Move your database from Neon to DB9 — export with pg_dump, import with the DB9 CLI, and update your application's connection string. This guide walks through migrating a PostgreSQL database from Neon to DB9. The process uses standard PostgreSQL tooling (`pg_dump` for export) and the DB9 CLI for import. ## What Changes and What Stays the Same [Section titled “What Changes and What Stays the Same”](#what-changes-and-what-stays-the-same) ### Stays the same [Section titled “Stays the same”](#stays-the-same) * **SQL compatibility** — DB9 supports the same DML, DDL, joins, CTEs, window functions, and subqueries you use in Neon. Most queries work without changes. * **PostgreSQL drivers** — Any driver that connects via pgwire (node-postgres, psycopg, pgx, JDBC) works with DB9. * **ORM compatibility** — Prisma, Drizzle, SQLAlchemy, TypeORM, Sequelize, Knex, and GORM are tested and supported. * **Data types** — Common types (TEXT, INTEGER, BIGINT, BOOLEAN, TIMESTAMPTZ, UUID, JSONB, arrays, vectors) work identically. ### Changes [Section titled “Changes”](#changes) | Area | Neon | DB9 | | ---------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | **Connection string** | `postgresql://user:pass@ep-*.neon.tech/dbname` | `postgresql://tenant.role@pg.db9.io:5433/postgres` | | **Connection pooling** | Built-in PgBouncer (transaction mode) | No built-in pooler — use application-side pooling | | **Branching** | Copy-on-write, instant for any size | Full data copy, async (seconds to minutes) | | **Compute** | Autoscaling, scale-to-zero | Fixed per-database, always on | | **Serverless driver** | `@neondatabase/serverless` (HTTP/WebSocket) | Standard pgwire + browser HTTP scoped support (phase-1) | | **Extensions** | 40+ community extensions | 9 built-in (http, vector, fs9, pg\_cron, embedding, hstore, uuid-ossp, parquet, zhparser) | | **Replication** | Logical replication supported | Not supported | | **Row-level security** | Supported | Browser HTTP scoped support (phase-1) | | **Table partitioning** | Supported | Not supported | | **LISTEN/NOTIFY** | Supported | Supported over pgwire (delivered on commit); the stateless HTTP SQL API can `NOTIFY` but cannot `LISTEN` | | **Port** | 5432 | 5433 | | **Database name** | Custom (e.g., `neondb`) | Always `postgres` | Review the [Compatibility Matrix](/docs/platform/compatibility-matrix/) for the full list of supported and unsupported features. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Access to your Neon database (direct/unpooled connection string) * `pg_dump` installed locally (comes with PostgreSQL client tools) * DB9 CLI installed: `curl -fsSL https://db9.ai/install | sh` * A DB9 account: `db9 create --name my-app` to create your target database 1. **Export from Neon** Use `pg_dump` with Neon’s **direct (unpooled)** connection string. Do not use the pooled connection — `pg_dump` requires a direct connection. **Schema and data (plain SQL format)** Terminal ```bash pg_dump --no-owner --no-privileges --no-comments \ "postgresql://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require" \ > export.sql ``` **Schema only** Terminal ```bash pg_dump --schema-only --no-owner --no-privileges \ "postgresql://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require" \ > schema.sql ``` Flags explained: * `--no-owner` — omits `ALTER ... OWNER TO` statements that reference Neon-specific roles * `--no-privileges` — omits `GRANT`/`REVOKE` statements * `--no-comments` — omits `COMMENT ON` statements that may reference Neon internals Use **plain SQL format** (default). DB9 does not support `pg_restore` with the custom (`-Fc`) or directory (`-Fd`) formats — import via SQL text only. 2. **Clean the Export** The `pg_dump` output may contain statements that DB9 does not support. Remove or comment out: * **`CREATE EXTENSION`** for extensions DB9 does not have — DB9 supports 9 built-in extensions. Remove any `CREATE EXTENSION` for extensions not in: `http`, `uuid-ossp`, `hstore`, `fs9`, `pg_cron`, `parquet`, `zhparser`, `vector`, `embedding`. * **`CREATE PUBLICATION` / `CREATE SUBSCRIPTION`** — DB9 does not support logical replication. * **Row-level security policies** — `CREATE POLICY`, `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`. * **Table partitioning** — `PARTITION BY`, `CREATE TABLE ... PARTITION OF`. * **Advisory lock calls** — `pg_advisory_lock()`, `pg_try_advisory_lock()`. * **Custom types with WHILE loops in PL/pgSQL** — DB9 supports basic PL/pgSQL but not `WHILE` loops, `CONTINUE`, or cursors. `EXECUTE` and exception handling work only inside a `DO` block, not in a `CREATE FUNCTION` body — see the [compatibility matrix](/docs/platform/compatibility-matrix/#plpgsql). * **Locale settings** — DB9 accepts and ignores locale parameters from `pg_dump`, so these are safe to leave in. A quick way to identify issues: Terminal ```bash # Check for unsupported extensions grep "CREATE EXTENSION" export.sql # Check for partitioning grep -i "PARTITION" export.sql # Check for RLS grep -i "ROW LEVEL SECURITY\|CREATE POLICY" export.sql # Check for replication grep -i "PUBLICATION\|SUBSCRIPTION" export.sql ``` 3. **Create the DB9 Database** Terminal ```bash # Create a new database db9 create --name my-app --show-connection-string ``` ▶ Run This returns immediately with the connection string and credentials. Save them for your application config. 4. **Import into DB9** **Option A: CLI import (recommended for most databases)** Terminal ```bash db9 db sql my-app -f export.sql ``` This executes the SQL file against your DB9 database via the API. Suitable for databases up to the dump limits (50,000 rows or 16 MB per table). **Option B: Direct psql import (for larger databases)** For larger exports, use `psql` with DB9’s connection string directly: Terminal ```bash psql "$(db9 db connect my-app --output quiet)" -f export.sql ``` This streams the SQL through the pgwire protocol and handles larger files without the API dump limits. **Option C: COPY for bulk data** If your export is large and you split schema from data, you can use `COPY` for bulk loading: Terminal ```bash # Import schema first psql "$(db9 db connect my-app --output quiet)" -f schema.sql # Then import data via COPY (pg_dump with --data-only --inserts=off uses COPY by default) pg_dump --data-only --no-owner \ "postgresql://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require" \ | psql "$(db9 db connect my-app --output quiet)" ``` DB9 supports `COPY` in CSV and TEXT formats over pgwire. 5. **Update Your Application** **Connection string** Replace the Neon connection string with DB9’s: Diff ```diff DATABASE_URL=postgresql://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require DATABASE_URL=postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=require ``` Key differences: * **Username**: DB9 uses `{tenant_id}.{role}` format (e.g., `a1b2c3d4e5f6.admin`) * **Port**: 5433, not 5432 * **Database**: Always `postgres` * **Host**: `pg.db9.io` (not region-specific endpoints) **Neon serverless driver** If you use `@neondatabase/serverless`, replace it with a standard PostgreSQL driver: Diff ```diff import { neon } from '@neondatabase/serverless'; const sql = neon(process.env.DATABASE_URL); const result = await sql`SELECT * FROM users`; import pg from 'pg'; const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); const result = await pool.query('SELECT * FROM users'); ``` DB9 uses standard pgwire (TCP), so `pg` (node-postgres), `psycopg`, `pgx`, and other standard drivers work without modification. **Connection pooling** Neon provides built-in PgBouncer. DB9 does not include a connection pooler. If your application opens many connections, configure pooling at the application level: TypeScript ```typescript // node-postgres pool const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10, // DB9 handles concurrent connections well idleTimeoutMillis: 30000, }); ``` For ORMs, see the integration guides: [Prisma](/docs/guides/prisma/), [Drizzle](/docs/guides/drizzle/), [SQLAlchemy](/docs/guides/python-sqlalchemy/). **Edge Runtime** If you run code in edge/serverless environments (Cloudflare Workers, Vercel Edge Functions) that relied on Neon’s HTTP driver, move general database query workloads to a Node.js runtime over pgwire. DB9 has browser HTTP scoped support (phase-1), but does not provide full HTTP/WebSocket SQL parity with Neon’s serverless driver. See the [Next.js guide](/docs/guides/nextjs/) for patterns that work with both Server Components and API routes. 6. **Validate** **Check schema** Terminal ```bash db9 db dump my-app --ddl-only ``` ▶ Run Compare the output with your original schema to confirm all tables, indexes, and constraints were created. **Check row counts** Run a count on your key tables to verify data was imported: Terminal ```bash db9 db sql my-app -q "SELECT count(*) FROM users" db9 db sql my-app -q "SELECT count(*) FROM orders" ``` ▶ Run Compare row counts against the source Neon database. **Run your test suite** The most reliable validation is running your application’s existing test suite against the DB9 database. Update `DATABASE_URL` in your test environment and run: Terminal ```bash DATABASE_URL="$(db9 db connect my-app --output quiet)" npm test ``` **Check for unsupported features** If your tests fail, check these common differences: * **SERIALIZABLE isolation** — DB9 does not implement SERIALIZABLE. Requesting it on the wire protocol raises a `WARNING` and silently downgrades the transaction to REPEATABLE READ, so it will not fail loudly. Audit transactions that relied on serializability and add explicit `SELECT ... FOR UPDATE` locks or unique constraints * **LISTEN/NOTIFY** — supported over a direct pgwire connection; the stateless HTTP SQL API can `NOTIFY` but cannot `LISTEN` * **Advisory locks** — available, but coordination is node-local (not cross-process/global). For strict row-level coordination semantics, use `SELECT ... FOR UPDATE`. * **Row-level security** — browser HTTP scoped support exists (phase-1), but full PostgreSQL-wide parity is not available yet ## Rollback Plan [Section titled “Rollback Plan”](#rollback-plan) If you need to revert: 1. Your Neon database is unchanged — switch `DATABASE_URL` back to the Neon connection string. 2. If you need to export data created in DB9 back to Neon: Terminal ```bash # Export from DB9 db9 db dump my-app -o db9-export.sql # Import to Neon (use direct/unpooled connection) psql "postgresql://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require" \ -f db9-export.sql ``` The `db9 db dump` command outputs plain SQL (up to 50,000 rows or 16 MB per table). For larger databases, use `psql` to stream individual tables with `COPY`. ## Caveats [Section titled “Caveats”](#caveats) * **No zero-downtime migration** — DB9 does not support logical replication, so you cannot stream changes from Neon in real time. Plan a maintenance window or accept a brief cutover period. * **Extension gaps** — If your Neon database uses extensions not in DB9’s built-in set (e.g., `PostGIS`, `pg_trgm`, `pgcrypto`), those features will not be available. Check your `CREATE EXTENSION` statements. * **Dump size limits** — The `db9 db sql -f` API import has limits (50,000 rows, 16 MB per table). For larger databases, use direct `psql` connection for import. * **Branching model** — Neon branches are copy-on-write and instant. DB9 branches are full copies and take longer for large databases. Adjust CI workflows that depend on instant branching. * **Autoscaling** — Neon can scale compute to zero when idle. DB9 databases are always on. This affects cost for rarely-used databases. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full list of supported and unsupported PostgreSQL features * [Connect](/docs/connect/) — connection string format and authentication options * [Migrate from PostgreSQL](/docs/migrations/from-postgres/) — general PostgreSQL migration path * [Migrate from Supabase](/docs/migrations/from-supabase/) — Supabase-specific migration guide * [Production Checklist](/docs/production-checklist/) — deployment readiness # Migrate from PlanetScale > Move your database from PlanetScale (MySQL/Vitess) to DB9 — export your schema and data, convert from MySQL to PostgreSQL, and import with the DB9 CLI. PlanetScale is a MySQL-compatible database built on Vitess. DB9 is PostgreSQL-compatible. Migrating from PlanetScale requires converting your schema and queries from MySQL to PostgreSQL syntax. This guide covers exporting from PlanetScale, converting the MySQL dump to PostgreSQL-compatible SQL, importing into DB9, and updating your application. ## Key Differences [Section titled “Key Differences”](#key-differences) | Area | PlanetScale (MySQL/Vitess) | DB9 (PostgreSQL) | | ---------------------- | ----------------------------------------------------------- | ------------------------------------------------------------- | | **SQL dialect** | MySQL | PostgreSQL | | **Connection string** | `mysql://user:pass@host/db?ssl={"rejectUnauthorized":true}` | `postgresql://tenant.role@pg.db9.io:5433/postgres` | | **Protocol** | MySQL wire protocol | pgwire (PostgreSQL wire protocol) | | **Auto-increment** | `AUTO_INCREMENT` | `GENERATED ALWAYS AS IDENTITY` or `SERIAL` | | **String quoting** | Backticks `` ` `` for identifiers | Double quotes `"` for identifiers | | **Boolean** | `TINYINT(1)` | Native `BOOLEAN` | | **JSON** | `JSON` (stored as text internally) | `JSONB` (binary, indexable) | | **Date/time** | `DATETIME`, `TIMESTAMP` | `TIMESTAMPTZ` (timezone-aware) | | **Branching** | Git-like schema branching with deploy requests | Full data copy branches | | **Foreign keys** | Not supported (Vitess limitation) | Fully supported | | **Joins** | Supported (with Vitess limitations on cross-shard) | Full JOIN support without restrictions | | **Transactions** | Supported (single-shard only in some configs) | Full ACID (READ COMMITTED default, REPEATABLE READ available) | | **Connection pooling** | Built-in | No built-in pooler — use application-side pooling | | **Replication** | Managed by Vitess | Not supported | ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Access to your PlanetScale database (connection string from the PlanetScale dashboard) * `mysqldump` or PlanetScale CLI (`pscale`) installed * `pgloader` installed (for automated MySQL → PostgreSQL conversion), or manual conversion * DB9 CLI installed: `curl -fsSL https://db9.ai/install | sh` * A DB9 account: `db9 create --name my-app` to create your target database 1. **Export from PlanetScale** **Option A: mysqldump** Get your connection string from PlanetScale dashboard → Connect → General. Terminal ```bash mysqldump --no-tablespaces --column-statistics=0 \ -h host.connect.psdb.cloud \ -u username -p \ --ssl-mode=REQUIRED \ your_database > export.sql ``` **Option B: PlanetScale CLI** Terminal ```bash pscale db dump your-database main --output ./dump ``` This creates one SQL file per table in the `./dump` directory. 2. **Convert MySQL to PostgreSQL** MySQL and PostgreSQL have different SQL dialects. You need to convert the dump. **Option A: Manual conversion** Common MySQL → PostgreSQL conversions: ```plaintext MySQL → PostgreSQL ───────────────────────────────────────────────────────── `column_name` → "column_name" (or just remove backticks) AUTO_INCREMENT → GENERATED ALWAYS AS IDENTITY INT UNSIGNED → BIGINT TINYINT(1) → BOOLEAN DATETIME → TIMESTAMPTZ MEDIUMTEXT / LONGTEXT → TEXT ENUM('a','b','c') → TEXT CHECK (col IN ('a','b','c')) ON UPDATE CURRENT_TIMESTAMP → (use a trigger instead) ENGINE=InnoDB → (remove — not applicable) CHARACTER SET / COLLATE → (remove — DB9 is UTF-8 only) IF NOT EXISTS (in some contexts) → IF NOT EXISTS (same in PostgreSQL) ``` Example conversion of a table: SQL ```sql -- MySQL (PlanetScale) CREATE TABLE `users` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, `active` tinyint(1) DEFAULT '1', `metadata` json DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `email` (`email`) ) ENGINE=InnoDB; -- PostgreSQL (DB9) CREATE TABLE users ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, active BOOLEAN DEFAULT true, metadata JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ``` **Option B: Use a conversion script** For larger schemas, use `sed` for common patterns: Terminal ```bash cat export.sql \ | sed 's/`//g' \ | sed 's/ENGINE=InnoDB[^;]*//' \ | sed 's/AUTO_INCREMENT/GENERATED ALWAYS AS IDENTITY/g' \ | sed 's/tinyint(1)/BOOLEAN/g' \ | sed 's/int([0-9]*)/INTEGER/g' \ | sed 's/bigint([0-9]*)/BIGINT/g' \ | sed 's/varchar([0-9]*)/TEXT/g' \ | sed 's/MEDIUMTEXT/TEXT/g' \ | sed 's/LONGTEXT/TEXT/g' \ | sed 's/datetime/TIMESTAMPTZ/g' \ | sed 's/ json / JSONB /g' \ | sed '/^\/\*/d' \ | sed '/^--/d' \ | sed '/^SET /d' \ | sed '/^LOCK TABLES/d' \ | sed '/^UNLOCK TABLES/d' \ > import.sql ``` Review the output manually — automated conversion will miss edge cases. **Option C: pgloader (automated)** `pgloader` can read directly from MySQL and write to PostgreSQL, handling type conversion automatically. However, since DB9 uses pgwire, you would run pgloader against a local PostgreSQL first, then export and import into DB9: Terminal ```bash # pgloader from MySQL dump to local PostgreSQL pgloader mysql://user:pass@host/db postgresql://localhost/temp_db # Then pg_dump from local PostgreSQL and import to DB9 pg_dump --no-owner --no-privileges temp_db > converted.sql db9 db sql my-app -f converted.sql ``` 3. **Add Foreign Keys** PlanetScale (Vitess) does not support foreign keys, so your application likely enforces referential integrity in application code. Now you can add proper foreign keys: SQL ```sql ALTER TABLE posts ADD CONSTRAINT fk_posts_user FOREIGN KEY (user_id) REFERENCES users(id); ALTER TABLE comments ADD CONSTRAINT fk_comments_post FOREIGN KEY (post_id) REFERENCES posts(id); ALTER TABLE comments ADD CONSTRAINT fk_comments_author FOREIGN KEY (author_id) REFERENCES users(id); ``` Review your schema and add foreign keys where appropriate. This gives you database-level referential integrity that PlanetScale could not provide. 4. **Create the DB9 Database** Terminal ```bash db9 create --name my-app --show-connection-string ``` ▶ Run 5. **Import into DB9** Terminal ```bash # For small to medium databases db9 db sql my-app -f import.sql # For larger databases psql "$(db9 db connect my-app --output quiet)" -f import.sql ``` If you separated schema and data: Terminal ```bash # Schema first (includes foreign keys) psql "$(db9 db connect my-app --output quiet)" -f schema.sql # Then data psql "$(db9 db connect my-app --output quiet)" -f data.sql ``` 6. **Update Your Application** **Replace the MySQL driver with a PostgreSQL driver** Diff ```diff import mysql from 'mysql2/promise'; const pool = mysql.createPool(process.env.DATABASE_URL); const [rows] = await pool.execute('SELECT * FROM users WHERE id = ?', [userId]); import pg from 'pg'; const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [userId]); ``` **Key query syntax changes** | MySQL (PlanetScale) | PostgreSQL (DB9) | | ------------------------------------ | --------------------------------------------------- | | `SELECT * FROM users LIMIT 10, 20` | `SELECT * FROM users LIMIT 20 OFFSET 10` | | `IFNULL(col, default)` | `COALESCE(col, default)` | | `NOW()` | `now()` (same, but returns TIMESTAMPTZ) | | `GROUP_CONCAT(col)` | `string_agg(col, ',')` | | `INSERT ... ON DUPLICATE KEY UPDATE` | `INSERT ... ON CONFLICT DO UPDATE` | | Backtick identifiers `` `col` `` | Double-quote identifiers `"col"` (or just unquoted) | | `?` parameter placeholders | `$1, $2, $3` numbered placeholders | **Update your ORM** If you use an ORM, switch the database provider: Diff ```diff // Prisma provider = "mysql" provider = "postgresql" // Drizzle import { mysqlTable } from 'drizzle-orm/mysql-core'; import { pgTable } from 'drizzle-orm/pg-core'; ``` For ORM-specific setup, see: [Prisma](/docs/guides/prisma/), [Drizzle](/docs/guides/drizzle/), [SQLAlchemy](/docs/guides/python-sqlalchemy/). **Connection string** Diff ```diff DATABASE_URL=mysql://user:pass@host.connect.psdb.cloud/mydb?ssl={"rejectUnauthorized":true} DATABASE_URL=postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=require ``` 7. **Validate** **Check schema** Terminal ```bash db9 db dump my-app --ddl-only ``` ▶ Run Verify tables, columns, types, and constraints match your expectations. **Check row counts** Terminal ```bash db9 db sql my-app -q "SELECT count(*) FROM users" db9 db sql my-app -q "SELECT count(*) FROM posts" ``` ▶ Run Compare against the source PlanetScale database. **Run your test suite** Terminal ```bash DATABASE_URL="$(db9 db connect my-app --output quiet)" npm test ``` **Common test failures** * **MySQL-specific syntax** — backtick identifiers, `LIMIT offset, count`, `IFNULL`, `GROUP_CONCAT` * **Parameter placeholders** — `?` must become `$1, $2, ...` * **Boolean handling** — MySQL uses `0`/`1`; PostgreSQL uses `true`/`false` * **Date formatting** — MySQL `DATE_FORMAT()` → PostgreSQL `to_char()` ## Rollback Plan [Section titled “Rollback Plan”](#rollback-plan) Your PlanetScale database is unchanged. To revert: 1. Switch `DATABASE_URL` back to the PlanetScale connection string and revert driver/ORM changes. 2. If you need to export data created in DB9 back to PlanetScale, export from DB9 and convert PostgreSQL SQL back to MySQL syntax. ## Caveats [Section titled “Caveats”](#caveats) * **Full SQL dialect migration** — This is not a simple connection string swap. Every raw SQL query must be converted from MySQL to PostgreSQL syntax. ORMs reduce this effort significantly. * **Driver change required** — MySQL drivers (`mysql2`, `pymysql`, `go-sql-driver/mysql`) must be replaced with PostgreSQL drivers (`pg`, `psycopg`, `pgx`). * **Type differences** — MySQL and PostgreSQL handle types differently (TINYINT vs BOOLEAN, DATETIME vs TIMESTAMPTZ, ENUM handling). Review all column types. * **No schema branching** — PlanetScale’s git-like branching with deploy requests has no direct equivalent. DB9 has data-copy branches, but no schema-diff deploy workflow. * **Foreign keys are now available** — PlanetScale (Vitess) does not support foreign keys. Take advantage of this by adding proper referential integrity constraints. * **Stored procedure differences** — MySQL stored procedures must be rewritten in PL/pgSQL. DB9 supports basic PL/pgSQL but not `WHILE` loops, `CONTINUE`, or cursors. `EXECUTE` and exception handling work only inside a `DO` block, not in a `CREATE FUNCTION` body. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full list of supported and unsupported PostgreSQL features * [Connect](/docs/connect/) — connection string format and authentication options * [Prisma Guide](/docs/guides/prisma/) — type-safe ORM (handles dialect differences) * [Drizzle Guide](/docs/guides/drizzle/) — lightweight TypeScript ORM * [Production Checklist](/docs/production-checklist/) — deployment readiness # Migrate from PostgreSQL > Move a self-hosted or managed PostgreSQL database to DB9 — export with pg_dump, verify compatibility, import with the DB9 CLI, and validate. This guide covers migrating from any PostgreSQL installation — self-hosted, AWS RDS, Google Cloud SQL, Azure Database for PostgreSQL, or other managed services — to DB9. The process uses standard `pg_dump` for export and the DB9 CLI or `psql` for import. For platform-specific guides, see [Migrate from Neon](/docs/migrations/from-neon/) or [Migrate from Supabase](/docs/migrations/from-supabase/). ## Before You Start: Compatibility Check [Section titled “Before You Start: Compatibility Check”](#before-you-start-compatibility-check) Run the compatibility scan before starting DB9 supports most PostgreSQL workloads, but some features are not available. Running these checks first avoids surprises mid-migration. DB9 supports most PostgreSQL workloads, but some features are not available. Run these checks against your existing database before migrating. ### Quick compatibility scan [Section titled “Quick compatibility scan”](#quick-compatibility-scan) Connect to your source database and check for unsupported features: SQL ```sql -- Table partitioning (not supported) SELECT count(*) AS partitioned_tables FROM pg_partitioned_table; -- Table inheritance (not supported) SELECT count(*) AS inherited_tables FROM pg_inherits; -- Row-level security policies (not supported) SELECT count(*) AS rls_policies FROM pg_policies; -- Foreign data wrappers (not supported) SELECT count(*) AS fdw_servers FROM pg_foreign_server; -- Logical replication (not supported) SELECT count(*) AS publications FROM pg_publication; -- Advisory locks in use (supported, but semantics differ) SELECT count(*) AS advisory_locks FROM pg_locks WHERE locktype = 'advisory'; ``` If any of these return non-zero counts, review whether your application depends on them. Most checks above target unsupported features that need refactoring before migration; advisory lock usage needs a separate semantic review because DB9 advisory locks are node-local. ### Extension check [Section titled “Extension check”](#extension-check) SQL ```sql SELECT extname FROM pg_extension WHERE extname NOT IN ( 'plpgsql', 'uuid-ossp', 'hstore', 'vector' ) ORDER BY extname; ``` ▶ Run DB9 supports 9 built-in extensions: `http`, `uuid-ossp`, `hstore`, `fs9`, `pg_cron`, `parquet`, `zhparser`, `vector`, `embedding`. Extensions not in this list (PostGIS, pg\_trgm, ltree, citext, etc.) are not available and raise `42704 extension is not available`. Two further names — `pgcrypto` and `plpgsql` — are accepted as metadata shims so dumps containing them restore cleanly, but neither adds any functions. In particular `CREATE EXTENSION pgcrypto` succeeds while `crypt()`, `gen_salt()`, `hmac()`, `encrypt()` and `pgp_sym_encrypt()` remain unavailable (`42883`). See [Extensions](/docs/extensions/#metadata-shims-built-in-create-extension-optional). Note: `gen_random_uuid()` works in DB9 without any extension — no need for `pgcrypto`. ### PL/pgSQL check [Section titled “PL/pgSQL check”](#plpgsql-check) DB9 Difference: PL/pgSQL limitations DB9 supports basic PL/pgSQL but **not** `WHILE` loops, `CONTINUE`, or cursor operations. `EXECUTE` (dynamic SQL), exception handling (`BEGIN...EXCEPTION`), and nested blocks work only inside a `DO` block, not in a `CREATE FUNCTION` body. Review all stored functions and procedures before migrating. DB9 supports basic PL/pgSQL: variable declarations, IF/ELSIF, `CASE`, FOR loops (including `EXIT WHEN`), PERFORM, RAISE, and RETURN. It does not support: * `WHILE` loops * `CONTINUE` / `CONTINUE WHEN` * Cursor operations * `EXECUTE` (dynamic SQL) inside a `CREATE FUNCTION` body — use a `DO` block, where the command string may be a literal or a variable * Exception handling (`BEGIN...EXCEPTION`) inside a `CREATE FUNCTION` body — use a `DO` block * Nested `BEGIN...END` blocks inside a `CREATE FUNCTION` body — use a `DO` block All of these surface at call time, not at `CREATE FUNCTION` time `CREATE FUNCTION` **succeeds** for every item above and the function is registered in `pg_proc` — the error is raised on each call: * `EXECUTE`, exception handling, nested `BEGIN...END` → `0A000` (`... requires a Session-owned interactive DO host`) * `WHILE` loops, `CONTINUE`, cursor operations → `42601` (`syntax error: sql parser error: ...`) A migration that only replays DDL will therefore look completely clean. Exercise each migrated function before cutting over. See [Advanced SQL — PL/pgSQL](/docs/sql/advanced/) for the full host comparison. SQL ```sql -- Find functions that may use unsupported PL/pgSQL features SELECT proname, prosrc FROM pg_proc WHERE prolang = (SELECT oid FROM pg_language WHERE lanname = 'plpgsql') AND (prosrc ILIKE '%WHILE%' OR prosrc ILIKE '%EXECUTE%' OR prosrc ILIKE '%EXCEPTION%'); ``` Review any matches and rewrite them before migrating. ## What Changes [Section titled “What Changes”](#what-changes) | Area | Standard PostgreSQL | DB9 | | ------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Connection string** | `postgresql://user:pass@host:5432/dbname` | `postgresql://tenant.role@pg.db9.io:5433/postgres` | | **Port** | 5432 (default) | 5433 | | **Database name** | Custom | Always `postgres` | | **Username** | Standard roles | `tenant_id.role` format (e.g., `a1b2c3d4e5f6.admin`) | | **Transaction isolation** | SERIALIZABLE fully enforced | READ COMMITTED and REPEATABLE READ enforced; SERIALIZABLE is downgraded to REPEATABLE READ with a warning on the wire protocol, and rejected with an error over the HTTP SQL API | | **Connection pooling** | External (PgBouncer, pgpool) | Application-side pooling | | **Replication** | Logical and streaming | Not supported | | **LISTEN/NOTIFY** | Supported | Supported over pgwire (delivered on commit); the stateless HTTP SQL API can `NOTIFY` but cannot `LISTEN` | | **Extensions** | Community ecosystem | 9 built-in only | | **Indexes** | All types fully functional | B-tree and GIN full; HNSW is disabled in the current release; GiST/Hash/SP-GiST/BRIN rejected | See the [Compatibility Matrix](/docs/platform/compatibility-matrix/) for the complete list. ## What Stays the Same [Section titled “What Stays the Same”](#what-stays-the-same) * **SQL** — DML (SELECT, INSERT, UPDATE, DELETE, UPSERT), DDL (CREATE TABLE, ALTER, DROP), JOINs, CTEs, window functions, subqueries, and RETURNING all work without changes. * **Data types** — TEXT, INTEGER, BIGINT, BOOLEAN, TIMESTAMPTZ, UUID, JSONB, arrays, FLOAT8, NUMERIC, BYTEA, and vectors. * **Wire protocol** — pgwire v3 (Simple Query, Extended Query, COPY, prepared statements). Any PostgreSQL driver works. * **ORMs** — Prisma, Drizzle, SQLAlchemy, TypeORM, Sequelize, Knex, and GORM are tested at 98-100% compatibility. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Access to your source PostgreSQL database * `pg_dump` installed locally (should match or be close to your source PostgreSQL version) * DB9 CLI installed: `curl -fsSL https://db9.ai/install | sh` * A DB9 account: `db9 create --name my-app` to create your target database 1. **Export from PostgreSQL** **Schema and data (plain SQL)** Terminal ```bash pg_dump --no-owner --no-privileges --no-comments \ "postgresql://user:pass@your-host:5432/your_database" \ > export.sql ``` **Schema only** Terminal ```bash pg_dump --schema-only --no-owner --no-privileges \ "postgresql://user:pass@your-host:5432/your_database" \ > schema.sql ``` **Specific tables** Terminal ```bash pg_dump --no-owner --no-privileges -t users -t orders -t products \ "postgresql://user:pass@your-host:5432/your_database" \ > tables.sql ``` Use **plain SQL format** (default). DB9 does not support `pg_restore` with the custom (`-Fc`) or directory (`-Fd`) formats — import via SQL text only. Flags explained: * `--no-owner` — omits `ALTER ... OWNER TO` statements that reference source-specific roles * `--no-privileges` — omits `GRANT`/`REVOKE` statements * `--no-comments` — omits `COMMENT ON` statements Locale and encoding settings in the pg\_dump output (like `SET client_encoding`) are accepted and safely ignored by DB9, which operates in UTF-8 only. **Managed PostgreSQL notes** | Provider | Connection notes | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | **AWS RDS** | Use the endpoint hostname and master user credentials. Ensure the security group allows outbound connections from your machine. | | **Google Cloud SQL** | Use Cloud SQL Auth Proxy or allowlist your IP. Direct connection: `host:5432/dbname`. | | **Azure Database** | Use the `{user}@{server}` username format Azure requires. | | **DigitalOcean** | Use the connection string from the database dashboard. Requires `sslmode=require`. | 2. **Clean the Export** Review the export for features DB9 does not support: Terminal ```bash # Unsupported extensions grep "CREATE EXTENSION" export.sql # Table partitioning grep -i "PARTITION BY\|PARTITION OF" export.sql # Row-level security grep -i "ROW LEVEL SECURITY\|CREATE POLICY" export.sql # Table inheritance grep -i "INHERITS" export.sql # Foreign data wrappers grep -i "CREATE SERVER\|CREATE FOREIGN TABLE" export.sql # Replication grep -i "CREATE PUBLICATION\|CREATE SUBSCRIPTION" export.sql # Rules grep -i "CREATE RULE" export.sql ``` Remove or comment out any matches. For extensions, keep only those DB9 supports: `uuid-ossp`, `hstore`, `vector`, plus `pgcrypto` and `plpgsql`, which DB9 accepts as metadata shims. **Common cleanup patterns** Terminal ```bash # Remove all CREATE EXTENSION except supported ones sed -i.bak '/CREATE EXTENSION/!b; /uuid-ossp\|hstore\|vector\|pgcrypto\|plpgsql/!d' export.sql # Remove RLS sed -i.bak '/ENABLE ROW LEVEL SECURITY/d; /CREATE POLICY/,/;$/d' export.sql ``` Or manually review and remove the flagged lines. 3. **Create the DB9 Database** Terminal ```bash db9 create --name my-app --show-connection-string ``` ▶ Run Database creation is synchronous and completes in under a second. 4. **Import into DB9** Choose the method based on your database size. **Small databases (under 16 MB)** Terminal ```bash db9 db sql my-app -f export.sql ``` Uses the DB9 API. Limited to 50,000 rows or 16 MB per table. **Medium to large databases** Use `psql` with a direct pgwire connection — no size limits: Terminal ```bash psql "$(db9 db connect my-app --output quiet)" -f export.sql ``` **Large databases (streaming COPY)** For the fastest import of large datasets, split schema and data: Terminal ```bash # 1. Import schema psql "$(db9 db connect my-app --output quiet)" -f schema.sql # 2. Stream data directly from source to DB9 pg_dump --data-only --no-owner \ "postgresql://user:pass@your-host:5432/your_database" \ | psql "$(db9 db connect my-app --output quiet)" ``` This pipes `COPY` statements through pgwire without intermediate files. DB9 supports COPY in TEXT and CSV formats. `db9 db connect` returns a temporary DSN The DSN that `db9 db connect` prints carries a short-lived token (10 minutes). That is ample for a schema load or a CI job, but for a multi-hour bulk import use a static password instead — `db9 db reset-password my-app`, or create a dedicated user with `db9 db users my-app create --username importer --password `. Do not substitute `db9 db status --json | jq -r .connection_string` here: that field deliberately omits the password, so `psql` fails with `FATAL: Password authentication failed`. **Import errors** If import fails partway through: * **Unsupported DDL** — check the error message for the specific statement, remove it from the SQL file, and re-run. * **Data type mismatch** — DB9 does not support XML, CIDR/MACADDR, or most range types (INET is supported). Cast or remove these columns. Bit-string and composite columns fail in a less obvious way and are worth handling before you export. A `bit(n)` column with n of 2 or more checks that its input is a bit string, but does not enforce the declared length: short values are silently right-padded with zeros and long ones truncated, where PostgreSQL raises `22026` — and `bit(n)` is exactly what `pg_dump` emits for it, so nothing in the dump path flags the problem. Convert those to `BYTEA` or an integer bitmask before exporting. Single-flag `bit(1)` columns are fine as they are: they become `BOOLEAN` and round-trip `'1'`/`'0'` correctly. A `varbit` column happens to be safer only because `pg_dump` writes it as `bit varying(n)`, which DB9 cannot parse. A composite column accepts a text literal without validating it and can never be read back field-wise — flatten it into scalar columns. See the [compatibility matrix](/docs/platform/compatibility-matrix/#data-types). * **Encoding errors** — DB9 is UTF-8 only. Non-UTF-8 data will fail with “invalid byte sequence for encoding UTF8”. Convert the source data to UTF-8 before export. 5. **Update Your Application** **Connection string** Diff ```diff DATABASE_URL=postgresql://user:password@your-host:5432/your_database DATABASE_URL=postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=require ``` Key differences: * **Username**: `{tenant_id}.{role}` format * **Port**: 5433 * **Database**: Always `postgres` * **TLS**: Required (`sslmode=require`) **Connection pooling** If you use an external connection pooler (PgBouncer, pgpool), remove it and configure pooling in your application: TypeScript ```typescript // node-postgres const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10, idleTimeoutMillis: 30000, }); ``` Python ```python # SQLAlchemy engine = create_engine( DATABASE_URL, pool_size=10, pool_pre_ping=True, ) ``` For ORM-specific connection setup, see the integration guides: [Prisma](/docs/guides/prisma/), [Drizzle](/docs/guides/drizzle/), [SQLAlchemy](/docs/guides/python-sqlalchemy/), [GORM](/docs/guides/gorm/). **LISTEN/NOTIFY** `LISTEN` and `NOTIFY` work as they do in PostgreSQL: notifications are queued inside the transaction and delivered to listening sessions on commit. SQL ```sql -- session A LISTEN order_events; -- session B NOTIFY order_events, 'order-1234'; ``` `LISTEN` requires a session that stays open, so it must run over a direct pgwire connection (`db9 db connect `, or any Postgres driver). The stateless HTTP SQL API — `db9 db sql` and the SDK’s `sql()` method — can issue `NOTIFY`, but cannot hold a subscription to receive. **Sequences and SERIAL columns** `SERIAL`, `BIGSERIAL`, and `GENERATED ALWAYS AS IDENTITY` columns work in DB9. After importing data, verify sequences are set correctly: Terminal ```bash db9 db sql my-app -q "SELECT setval('users_id_seq', (SELECT max(id) FROM users))" ``` ▶ Run Run this for each table with auto-incrementing columns to avoid primary key conflicts on new inserts. 6. **Validate** **Check schema** Terminal ```bash db9 db dump my-app --ddl-only ``` Compare with your original schema. **Check row counts** Terminal ```bash db9 db sql my-app -q "SELECT count(*) FROM users" db9 db sql my-app -q "SELECT count(*) FROM orders" ``` ▶ Run Compare against the source database. **Run your test suite** Terminal ```bash DATABASE_URL="$(db9 db connect my-app --output quiet)" npm test ``` **Common differences to watch for** * **SERIALIZABLE isolation** — DB9 does not implement SERIALIZABLE. On the wire protocol it is accepted with a `WARNING` and silently downgraded to REPEATABLE READ, so code that relies on serializable guarantees (write-skew prevention) keeps running without failing. Audit those transactions and add explicit `SELECT ... FOR UPDATE` locks or unique constraints. * **Index access methods** — only `btree` and `gin` can actually be created; `hnsw` is a recognized method but index building is gated off in the current release, so replaying a dump over pgwire fails those statements with `55000` (`feature "hnsw_index" is unavailable`), while over the HTTP SQL API (`db9 db sql -f`) they report `CREATE INDEX` and leave an index the planner never uses. `GiST`, `Hash`, `SP-GiST`, and `BRIN` are rejected at `CREATE INDEX` time with `access method "..." is not supported` (0A000), so a dump that contains them will fail to restore those statements too. GIN itself is fully functional — full-text search and JSONB containment use index scans. * **Advisory locks** — `pg_advisory_lock()` and related functions are available, but coordination is node-local (not cross-process/global). For strict row-level coordination semantics, use `SELECT ... FOR UPDATE`. * **Interval negation** — unary `-INTERVAL '1 day'` is rejected; multiply by `-1` instead. ## Rollback Plan [Section titled “Rollback Plan”](#rollback-plan) Your source database is unchanged by the migration. To revert: 1. Switch `DATABASE_URL` back to the original PostgreSQL connection string. 2. If you need to export data created in DB9: Terminal ```bash # Small databases db9 db dump my-app -o db9-export.sql psql "postgresql://user:pass@your-host:5432/your_database" -f db9-export.sql # Large databases — use COPY per table, over pgwire psql "$(db9 db connect my-app --output quiet)" \ -c "COPY users TO STDOUT WITH (FORMAT csv, HEADER)" > users.csv psql "postgresql://user:pass@your-host:5432/your_database" \ -c "COPY users FROM STDIN WITH (FORMAT csv, HEADER)" < users.csv ``` `COPY ... TO STDOUT` requires pgwire, and only the table form Run `COPY ... TO STDOUT` through `psql`, not through `db9 db sql -q`. The HTTP SQL API cannot carry a COPY stream and fails with `error: unexpected message from server`. Only `COPY
[(columns)] TO STDOUT` is supported. The query form — `COPY (SELECT ...) TO STDOUT`, the usual way to export a filtered or joined result — is rejected on both transports with `0A000`: ```plaintext ERROR: Unsupported COPY TO STDOUT syntax. Supported: COPY [schema.]table [(col1, col2, ...)] TO STDOUT [WITH (options)] ``` To export a subset, wrap the query in a view and COPY the view — `COPY TO STDOUT` works, including `HEADER` and a column list: Terminal ```bash psql "$(db9 db connect my-app --output quiet)" -q \ -c "CREATE VIEW export_v AS SELECT id, email FROM users WHERE active" \ -c "COPY export_v TO STDOUT WITH (FORMAT csv, HEADER)" > active_users.csv ``` `-q` matters here: without it psql writes the `CREATE VIEW` command tag to stdout and it lands in the CSV. Keep `COPY` as the only statement in its own `-c` as well — a `COPY` sent in a multi-statement batch (`SELECT 1; COPY ...`) fails with `COPY not supported`. The `db9 db dump` command outputs plain SQL (up to 50,000 rows or 16 MB per table). For larger databases, export individual tables with COPY. ## Caveats [Section titled “Caveats”](#caveats) * **No zero-downtime migration** — DB9 does not support logical replication. Plan a maintenance window for the cutover, or accept a brief period of dual-writes. * **UTF-8 only** — DB9 does not support other encodings. Ensure your data is UTF-8 before export. * **Plain SQL import only** — DB9 does not support `pg_restore` with custom or directory formats. Always use the default plain-text format with `pg_dump`. * **Dump size limits** — The `db9 db sql -f` API has per-table limits (50,000 rows, 16 MB). For larger databases, use direct `psql` over pgwire. * **No custom extensions** — only the 9 built-in extensions are available. If your application depends on PostGIS, ltree, pg\_trgm, or other community extensions, those features will not be available. * **No custom index access methods** — only `btree` and `gin` can be created (`hnsw` is recognized but disabled in the current release). ## Next Pages [Section titled “Next Pages”](#next-pages) * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full list of supported and unsupported features * [Connect](/docs/connect/) — connection string format and authentication * [Migrate from Neon](/docs/migrations/from-neon/) — Neon-specific migration * [Migrate from Supabase](/docs/migrations/from-supabase/) — Supabase-specific migration * [Production Checklist](/docs/production-checklist/) — deployment readiness * [Limits and Quotas](/docs/platform/limits-and-quotas/) — operational limits # Migrate from Railway > Move your database from Railway PostgreSQL to DB9 — export with pg_dump, import with the DB9 CLI, and update your application's connection string. Railway provides managed PostgreSQL databases as part of its application hosting platform. Since Railway runs standard PostgreSQL, migration to DB9 uses the same `pg_dump` / import workflow as any PostgreSQL migration. For the general PostgreSQL migration guide, see [Migrate from PostgreSQL](/docs/migrations/from-postgres/). ## What Changes and What Stays the Same [Section titled “What Changes and What Stays the Same”](#what-changes-and-what-stays-the-same) ### Stays the same [Section titled “Stays the same”](#stays-the-same) * **SQL compatibility** — DB9 supports the same DML, DDL, joins, CTEs, window functions, and subqueries you use in Railway PostgreSQL. Most queries work without changes. * **PostgreSQL drivers** — Any driver that connects via pgwire (node-postgres, psycopg, pgx, JDBC) works with DB9. * **ORM compatibility** — Prisma, Drizzle, SQLAlchemy, TypeORM, Sequelize, Knex, and GORM are tested and supported. * **Data types** — Common types (TEXT, INTEGER, BIGINT, BOOLEAN, TIMESTAMPTZ, UUID, JSONB, arrays, vectors) work identically. ### Changes [Section titled “Changes”](#changes) | Area | Railway PostgreSQL | DB9 | | ----------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | **Connection string** | `postgresql://postgres:pass@host.railway.app:port/railway` | `postgresql://tenant.role@pg.db9.io:5433/postgres` | | **Port** | Varies per deployment | 5433 | | **Database name** | `railway` (default) | Always `postgres` | | **Connection pooling** | No built-in pooler | No built-in pooler — use application-side pooling | | **Extensions** | Most community extensions available | 9 built-in (http, vector, fs9, pg\_cron, embedding, hstore, uuid-ossp, parquet, zhparser) | | **Replication** | Logical replication supported | Not supported | | **Table partitioning** | Supported | Not supported | | **LISTEN/NOTIFY** | Supported | Supported over pgwire (delivered on commit); the stateless HTTP SQL API can `NOTIFY` but cannot `LISTEN` | | **Deployment coupling** | Tight integration with Railway services via `$DATABASE_URL` | Standalone — connect from any platform | Review the [Compatibility Matrix](/docs/platform/compatibility-matrix/) for the full list of supported and unsupported features. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Access to your Railway project and database credentials * Railway CLI installed (`npm install -g @railway/cli`) or connection string from the Railway dashboard * `pg_dump` installed locally (comes with PostgreSQL client tools) * DB9 CLI installed: `curl -fsSL https://db9.ai/install | sh` * A DB9 account: `db9 create --name my-app` to create your target database 1. **Get Your Railway Connection String** **Option A: Railway dashboard** Go to your project → PostgreSQL service → Variables tab → copy `DATABASE_URL`. **Option B: Railway CLI** Terminal ```bash railway login railway link # Link to your project railway variables get DATABASE_URL ``` The connection string looks like: ```plaintext postgresql://postgres:password@host.railway.app:12345/railway ``` 2. **Export from Railway** Use `pg_dump` with the Railway connection string. **Schema and data (plain SQL format)** Terminal ```bash pg_dump --no-owner --no-privileges --no-comments \ "postgresql://postgres:password@host.railway.app:12345/railway" \ > export.sql ``` **Schema only** Terminal ```bash pg_dump --schema-only --no-owner --no-privileges \ "postgresql://postgres:password@host.railway.app:12345/railway" \ > schema.sql ``` Flags explained: * `--no-owner` — omits `ALTER ... OWNER TO` statements that reference Railway-specific roles * `--no-privileges` — omits `GRANT`/`REVOKE` statements * `--no-comments` — omits `COMMENT ON` statements Use **plain SQL format** (default). DB9 does not support `pg_restore` with the custom (`-Fc`) or directory (`-Fd`) formats — import via SQL text only. 3. **Clean the Export** The `pg_dump` output may contain statements that DB9 does not support. Remove or comment out: * **`CREATE EXTENSION`** for extensions DB9 does not have — DB9 supports 9 built-in extensions. Remove any `CREATE EXTENSION` for extensions not in: `http`, `uuid-ossp`, `hstore`, `fs9`, `pg_cron`, `parquet`, `zhparser`, `vector`, `embedding`. * **`CREATE PUBLICATION` / `CREATE SUBSCRIPTION`** — DB9 does not support logical replication. * **Row-level security policies** — `CREATE POLICY`, `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`. * **Table partitioning** — `PARTITION BY`, `CREATE TABLE ... PARTITION OF`. A quick way to identify issues: Terminal ```bash # Check for unsupported extensions grep "CREATE EXTENSION" export.sql # Check for partitioning grep -i "PARTITION" export.sql # Check for RLS grep -i "ROW LEVEL SECURITY\|CREATE POLICY" export.sql # Check for replication grep -i "PUBLICATION\|SUBSCRIPTION" export.sql ``` 4. **Create the DB9 Database** Terminal ```bash db9 create --name my-app --show-connection-string ``` ▶ Run This returns immediately with the connection string and credentials. Save them for your application config. 5. **Import into DB9** **Option A: CLI import (recommended for most databases)** Terminal ```bash db9 db sql my-app -f export.sql ``` Suitable for databases up to the API import limits (50,000 rows or 16 MB per table). **Option B: Direct psql import (for larger databases)** Terminal ```bash psql "$(db9 db connect my-app --output quiet)" -f export.sql ``` Streams SQL through pgwire without API size limits. **Option C: COPY for bulk data** Terminal ```bash # Import schema first psql "$(db9 db connect my-app --output quiet)" -f schema.sql # Then stream data directly from Railway into DB9 pg_dump --data-only --no-owner \ "postgresql://postgres:password@host.railway.app:12345/railway" \ | psql "$(db9 db connect my-app --output quiet)" ``` DB9 supports `COPY` in CSV and TEXT formats over pgwire. 6. **Update Your Application** **Connection string** Replace the Railway connection string with DB9’s: Diff ```diff DATABASE_URL=postgresql://postgres:password@host.railway.app:12345/railway DATABASE_URL=postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=require ``` Key differences: * **Username**: DB9 uses `{tenant_id}.{role}` format (e.g., `a1b2c3d4e5f6.admin`) * **Port**: 5433 * **Database**: Always `postgres` * **Host**: `pg.db9.io` **Railway service variables** If your Railway services reference `$DATABASE_URL` as a shared variable, update the variable in each service that connects to the database, or set a new variable pointing to DB9 and update your code to use it. **Connection pooling** Railway does not include a built-in connection pooler, so your application likely already handles pooling. Verify your pool settings work with DB9: TypeScript ```typescript const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10, idleTimeoutMillis: 30000, }); ``` For ORMs, see the integration guides: [Prisma](/docs/guides/prisma/), [Drizzle](/docs/guides/drizzle/), [SQLAlchemy](/docs/guides/python-sqlalchemy/). 7. **Validate** **Check schema** Terminal ```bash db9 db dump my-app --ddl-only ``` ▶ Run Compare the output with your original schema to confirm all tables, indexes, and constraints were created. **Check row counts** Terminal ```bash db9 db sql my-app -q "SELECT count(*) FROM users" db9 db sql my-app -q "SELECT count(*) FROM orders" ``` ▶ Run Compare row counts against the source Railway database. **Run your test suite** Terminal ```bash DATABASE_URL="$(db9 db connect my-app --output quiet)" npm test ``` **Check for unsupported features** If your tests fail, check these common differences: * **SERIALIZABLE isolation** — DB9 does not implement SERIALIZABLE. Requesting it on the wire protocol raises a `WARNING` and silently downgrades the transaction to REPEATABLE READ, so it will not fail loudly. Audit transactions that relied on serializability and add explicit `SELECT ... FOR UPDATE` locks or unique constraints * **LISTEN/NOTIFY** — supported over a direct pgwire connection; the stateless HTTP SQL API can `NOTIFY` but cannot `LISTEN` * **Advisory locks** — available, but coordination is node-local. For strict row-level coordination, use `SELECT ... FOR UPDATE` ## Rollback Plan [Section titled “Rollback Plan”](#rollback-plan) If you need to revert: 1. Your Railway database is unchanged — switch `DATABASE_URL` back to the Railway connection string. 2. If you need to export data created in DB9 back to Railway: Terminal ```bash # Export from DB9 db9 db dump my-app -o db9-export.sql # Import to Railway psql "postgresql://postgres:password@host.railway.app:12345/railway" \ -f db9-export.sql ``` The `db9 db dump` command outputs plain SQL (up to 50,000 rows or 16 MB per table). For larger databases, use `psql` to stream individual tables with `COPY`. ## Caveats [Section titled “Caveats”](#caveats) * **No zero-downtime migration** — DB9 does not support logical replication, so you cannot stream changes from Railway in real time. Plan a maintenance window or accept a brief cutover period. * **Extension gaps** — If your Railway database uses extensions not in DB9’s built-in set (e.g., `PostGIS`, `pg_trgm`, `pgcrypto`), those features will not be available. Check your `CREATE EXTENSION` statements. * **Dump size limits** — The `db9 db sql -f` API import has limits (50,000 rows, 16 MB per table). For larger databases, use direct `psql` connection for import. * **Railway integration loss** — Railway’s tight coupling between services (automatic `DATABASE_URL` injection, private networking) will not apply to DB9. Update each service’s environment variables manually. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full list of supported and unsupported PostgreSQL features * [Connect](/docs/connect/) — connection string format and authentication options * [Migrate from PostgreSQL](/docs/migrations/from-postgres/) — general PostgreSQL migration path * [Production Checklist](/docs/production-checklist/) — deployment readiness # Migrate from Supabase > Move your database layer from Supabase to DB9 — export with pg_dump, import with the DB9 CLI, and replace Supabase-specific features with standard PostgreSQL patterns. Supabase is a full application platform — PostgreSQL database, authentication, file storage, realtime subscriptions, edge functions, and an auto-generated REST API. DB9 replaces **only the database layer**. Everything else needs a separate solution or stays with Supabase. This guide covers exporting your schema and data, importing into DB9, and replacing Supabase-specific database features in your application. ## What DB9 Replaces and What It Does Not [Section titled “What DB9 Replaces and What It Does Not”](#what-db9-replaces-and-what-it-does-not) ### DB9 replaces [Section titled “DB9 replaces”](#db9-replaces) * **PostgreSQL database** — SQL queries, tables, indexes, functions, triggers * **Connection pooling endpoint** — use application-side pooling instead * **pg\_cron** — DB9 has a built-in pg\_cron extension * **pgvector** — DB9 has built-in vector search with HNSW indexes and native embeddings ### DB9 does not replace [Section titled “DB9 does not replace”](#db9-does-not-replace) | Supabase feature | What to use instead | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **Auth (GoTrue)** | Third-party auth (Auth0, Clerk, Firebase Auth) or custom JWT | | **Storage (S3-backed)** | AWS S3, GCS, or DB9’s fs9 extension for file-as-SQL workflows | | **Realtime (CDC + LISTEN/NOTIFY)** | `LISTEN`/`NOTIFY` with your own triggers (no automatic CDC), fanned out by an application WebSocket layer or a message queue | | **Edge Functions (Deno)** | Cloudflare Workers, Vercel Functions, AWS Lambda | | **PostgREST (auto-generated API)** | Build API routes in your framework (Next.js, Express, FastAPI) | | **Row-Level Security (RLS)** | Enforce access control in your application layer | | **Dashboard and SQL editor** | DB9 CLI (`db9 db sql`) and standard tools (psql, pgAdmin) | If you only need to migrate the database and plan to rebuild or replace the other services, continue with this guide. ## What Changes and What Stays the Same [Section titled “What Changes and What Stays the Same”](#what-changes-and-what-stays-the-same) ### Stays the same [Section titled “Stays the same”](#stays-the-same) * **SQL queries** — DML, DDL, joins, CTEs, window functions, and subqueries work without changes. * **PostgreSQL drivers** — node-postgres, psycopg, pgx, JDBC, and other pgwire drivers work with DB9. * **ORM compatibility** — Prisma, Drizzle, SQLAlchemy, TypeORM, Sequelize, Knex, and GORM are tested. * **Common data types** — TEXT, INTEGER, BIGINT, BOOLEAN, TIMESTAMPTZ, UUID, JSONB, arrays, vectors. ### Changes [Section titled “Changes”](#changes) | Area | Supabase | DB9 | | ---------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | **Connection string** | `postgresql://postgres.[ref]:pass@pooler.supabase.com:6543/postgres` | `postgresql://tenant.role@pg.db9.io:5433/postgres` | | **Port** | 5432 (direct) or 6543 (pooled) | 5433 | | **Username** | `postgres` or `postgres.[ref]` | `tenant_id.role` (e.g., `a1b2c3d4e5f6.admin`) | | **Connection pooling** | Built-in Supavisor (transaction mode) | No built-in pooler — use application-side pooling | | **Row-Level Security** | Supported and heavily used | Browser HTTP scoped support (phase-1) | | **LISTEN/NOTIFY** | Supported (powers Realtime) | Supported over pgwire (delivered on commit); the stateless HTTP SQL API can `NOTIFY` but cannot `LISTEN` | | **Extensions** | 40+ (PostGIS, pg\_graphql, pgsodium, etc.) | 9 built-in (http, vector, fs9, pg\_cron, embedding, hstore, uuid-ossp, parquet, zhparser) | | **Table partitioning** | Supported | Not supported | | **Replication** | Logical replication supported | Not supported | Review the [Compatibility Matrix](/docs/platform/compatibility-matrix/) for the full list. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Access to your Supabase project (direct database connection string from Settings > Database) * `pg_dump` installed locally (comes with PostgreSQL client tools) * DB9 CLI installed: `curl -fsSL https://db9.ai/install | sh` * A DB9 account: `db9 create --name my-app` to create your target database 1. **Export from Supabase** Use the **direct connection** string from your Supabase dashboard (Settings > Database > Connection string > URI, **not** the pooled connection). `pg_dump` does not work over pooled connections. **Schema and data** Terminal ```bash pg_dump --no-owner --no-privileges --no-comments \ -N auth -N storage -N realtime -N extensions -N supabase_functions -N supabase_migrations \ "postgresql://postgres.[your-ref]:[password]@db.[your-ref].supabase.co:5432/postgres?sslmode=require" \ > export.sql ``` The `-N` flags exclude Supabase’s internal schemas. You want your application’s `public` schema (and any custom schemas), not Supabase’s platform tables. **Schema only** Terminal ```bash pg_dump --schema-only --no-owner --no-privileges \ -N auth -N storage -N realtime -N extensions -N supabase_functions -N supabase_migrations \ "postgresql://postgres.[your-ref]:[password]@db.[your-ref].supabase.co:5432/postgres?sslmode=require" \ > schema.sql ``` Flags explained: * `--no-owner` — omits `ALTER ... OWNER TO` statements referencing Supabase-specific roles * `--no-privileges` — omits `GRANT`/`REVOKE` for Supabase’s role hierarchy * `-N auth -N storage ...` — excludes Supabase platform schemas 2. **Clean the Export** Supabase exports commonly include features DB9 does not support. Review and remove: **Row-Level Security (most common)** Supabase projects heavily use RLS. Remove all policy definitions: Terminal ```bash # Find RLS statements grep -n "ROW LEVEL SECURITY\|CREATE POLICY\|ALTER POLICY" export.sql ``` Remove lines like: SQL ```sql -- Remove these ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users can view own posts" ON public.posts FOR SELECT USING (auth.uid() = user_id); ``` You will need to enforce these access rules in your application layer instead (see Step 5). **Supabase-specific extensions** Terminal ```bash grep "CREATE EXTENSION" export.sql ``` Keep extensions that DB9 supports: `uuid-ossp`, `hstore`, `vector` (mapped to DB9’s built-in vector). Remove others — common Supabase extensions not in DB9 include `pg_graphql`, `pgsodium`, `pg_net`, `pg_stat_statements`, `PostGIS`, `pg_trgm`. `CREATE EXTENSION pgcrypto` can stay: DB9 accepts it as a metadata shim so Supabase bootstrap scripts restore unchanged. It does not, however, bring pgcrypto’s functions with it. `gen_random_uuid()` and `digest()` are DB9 built-ins and keep working, but `crypt()`, `gen_salt()`, `hmac()`, `encrypt()`/`decrypt()`, `pgp_sym_encrypt()` and `gen_random_bytes()` all fail with `42883 function ... does not exist`. If your schema hashes passwords with `crypt()`/`gen_salt()`, move that into your application layer before migrating. **Auth schema references** If your SQL references `auth.uid()` or `auth.jwt()` (common in RLS policies and triggers), remove those references: Terminal ```bash grep -n "auth\.uid\|auth\.jwt\|auth\.role" export.sql ``` **Triggers that reference Supabase internals** Terminal ```bash grep -n "supabase_functions\|extensions\.\|realtime\." export.sql ``` Remove triggers that call into Supabase’s internal schemas. **Other unsupported features** Terminal ```bash # Table partitioning grep -i "PARTITION" export.sql # Logical replication grep -i "PUBLICATION\|SUBSCRIPTION" export.sql # Advisory locks grep -i "advisory_lock" export.sql ``` 3. **Create the DB9 Database** Terminal ```bash db9 create --name my-app --show-connection-string ``` ▶ Run Returns immediately with the connection string and credentials. 4. **Import into DB9** **Option A: CLI import (recommended for most databases)** Terminal ```bash db9 db sql my-app -f export.sql ``` Suitable for databases up to the API import limits (50,000 rows or 16 MB per table). **Option B: Direct psql import (for larger databases)** Terminal ```bash psql "$(db9 db connect my-app --output quiet)" -f export.sql ``` Streams SQL through pgwire without API size limits. **Option C: COPY for bulk data** Split schema and data imports for large databases: Terminal ```bash # Import schema psql "$(db9 db connect my-app --output quiet)" -f schema.sql # Stream data from Supabase directly into DB9 pg_dump --data-only --no-owner \ -N auth -N storage -N realtime -N extensions -N supabase_functions -N supabase_migrations \ "postgresql://postgres.[your-ref]:[password]@db.[your-ref].supabase.co:5432/postgres?sslmode=require" \ | psql "$(db9 db connect my-app --output quiet)" ``` DB9 supports `COPY` in CSV and TEXT formats over pgwire. 5. **Update Your Application** **Connection string** Replace the Supabase connection string: Diff ```diff DATABASE_URL=postgresql://postgres.[ref]:[password]@db.[ref].supabase.co:5432/postgres DATABASE_URL=postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=require ``` **Replace the Supabase client** If you use `@supabase/supabase-js` for database queries, switch to a standard PostgreSQL driver: Diff ```diff import { createClient } from '@supabase/supabase-js'; const supabase = createClient(url, anonKey); const { data } = await supabase.from('posts').select('*').eq('published', true); import pg from 'pg'; const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10 }); const { rows: data } = await pool.query('SELECT * FROM posts WHERE published = true'); ``` Or use an ORM: [Prisma](/docs/guides/prisma/), [Drizzle](/docs/guides/drizzle/), [SQLAlchemy](/docs/guides/python-sqlalchemy/). **Replace Row-Level Security** Supabase RLS policies run inside the database. With DB9, enforce access control in your API or middleware: TypeScript ```typescript // Before (Supabase RLS): // CREATE POLICY "own posts" ON posts FOR SELECT USING (auth.uid() = user_id); // const { data } = await supabase.from('posts').select('*'); // After (application-level): const userId = req.auth.userId; // From your auth middleware const { rows } = await pool.query( 'SELECT * FROM posts WHERE user_id = $1', [userId] ); ``` Every query that previously relied on RLS needs an explicit `WHERE` clause or middleware check. **Replace Realtime** Supabase Realtime combines LISTEN/NOTIFY with change-data-capture over logical replication. DB9 supports `LISTEN`/`NOTIFY` but not logical replication, so table changes are not broadcast automatically — you emit the events yourself: SQL ```sql CREATE FUNCTION notify_order_change() RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify('order_events', NEW.id::text); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER order_change AFTER INSERT OR UPDATE ON orders FOR EACH ROW EXECUTE FUNCTION notify_order_change(); ``` Subscribers must hold an open pgwire connection to receive; the stateless HTTP SQL API cannot `LISTEN`. If you need browser clients or automatic CDC, keep an application-side fan-out: * **Application WebSockets** — a backend service holds the `LISTEN` connection and pushes to clients * **External message queue** — publish change events to Redis Pub/Sub, AWS SQS, or similar * **Polling** — query the database on an interval for changes **Replace Storage** For file operations, consider: * **AWS S3 / GCS** — direct replacement for Supabase Storage * **DB9 fs9 extension** — store and query files directly in SQL (see [fs9 guide](/docs/extensions/fs9/)) **Replace Edge Functions** Supabase Edge Functions (Deno) have no DB9 equivalent. Use your preferred serverless platform (Cloudflare Workers, Vercel Functions, AWS Lambda) and connect to DB9 with a standard PostgreSQL driver. 6. **Validate** **Check schema** Terminal ```bash db9 db dump my-app --ddl-only ``` ▶ Run Compare with your original schema to confirm tables, indexes, and constraints were created. **Check row counts** Terminal ```bash db9 db sql my-app -q "SELECT count(*) FROM posts" db9 db sql my-app -q "SELECT count(*) FROM users" ``` ▶ Run Compare against Supabase. **Run your test suite** Terminal ```bash DATABASE_URL="$(db9 db connect my-app --output quiet)" npm test ``` **Common test failures after migration** * **`auth.uid()` calls** — these no longer exist. Replace with application-provided user IDs. * **RLS-dependent queries** — queries that relied on RLS returning filtered results now return all rows unless you add explicit `WHERE` clauses. * **`gen_random_uuid()` without extension** — DB9 supports this as a built-in function. `CREATE EXTENSION pgcrypto` in your schema is accepted as a metadata shim and can stay, but it registers no functions: calls to `crypt()`, `gen_salt()`, `hmac()`, `encrypt()` or `pgp_sym_encrypt()` fail at runtime with `42883`, not at restore time. * **SERIALIZABLE isolation** — DB9 does not implement SERIALIZABLE. Requesting it on the wire protocol raises a `WARNING` and silently downgrades the transaction to REPEATABLE READ, so it will not fail loudly. Audit transactions that relied on serializability and add explicit `SELECT ... FOR UPDATE` locks or unique constraints. ## Rollback Plan [Section titled “Rollback Plan”](#rollback-plan) Keep your Supabase project running during migration. If you need to revert: 1. Switch `DATABASE_URL` back to the Supabase connection string. 2. If you need to export data created in DB9: Terminal ```bash db9 db dump my-app -o db9-export.sql psql "postgresql://postgres.[ref]:[password]@db.[ref].supabase.co:5432/postgres?sslmode=require" \ -f db9-export.sql ``` The `db9 db dump` command outputs plain SQL (up to 50,000 rows or 16 MB per table). For larger databases, use `psql` with `COPY` to stream individual tables. ## Caveats [Section titled “Caveats”](#caveats) * **Database only** — DB9 does not replace Supabase Auth, Storage, Realtime, Edge Functions, or the PostgREST API. Plan replacements for each service you use. * **No zero-downtime migration** — DB9 does not support logical replication. Plan a maintenance window for the cutover. * **RLS must move to application code** — this is typically the largest refactoring effort. Audit every table that had RLS enabled. * **Extension gaps** — PostGIS, pg\_graphql, pgsodium, pg\_net, and other Supabase extensions are not available. Check your `CREATE EXTENSION` statements. * **Dump size limits** — The `db9 db sql -f` API import has limits (50,000 rows, 16 MB per table). Use direct `psql` for larger databases. * **No built-in REST API** — Supabase auto-generates a REST API via PostgREST. With DB9, build your own API layer or use an ORM. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full list of supported and unsupported PostgreSQL features * [Connect](/docs/connect/) — connection string format and authentication * [Migrate from Neon](/docs/migrations/from-neon/) — Neon-specific migration guide * [Migrate from PostgreSQL](/docs/migrations/from-postgres/) — general PostgreSQL migration path * [fs9 Extension](/docs/extensions/fs9/) — file operations in SQL (alternative to Supabase Storage) * [Production Checklist](/docs/production-checklist/) — deployment readiness # Migrate from Turso > Move your database from Turso (libSQL/SQLite) to DB9 — export your data, convert from SQLite to PostgreSQL, and import with the DB9 CLI. Turso is a managed database built on libSQL, a fork of SQLite. DB9 is PostgreSQL-compatible. Migrating from Turso requires converting your schema and queries from SQLite to PostgreSQL syntax. This guide covers exporting from Turso, converting the SQLite dump to PostgreSQL-compatible SQL, importing into DB9, and updating your application. ## Key Differences [Section titled “Key Differences”](#key-differences) | Area | Turso (libSQL/SQLite) | DB9 (PostgreSQL) | | ---------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | **SQL dialect** | SQLite | PostgreSQL | | **Connection** | HTTP (libSQL protocol) or embedded | pgwire (TCP) — standard PostgreSQL drivers | | **Type system** | Dynamic typing (type affinity) | Strict static typing | | **Data types** | TEXT, INTEGER, REAL, BLOB, NULL | Full PostgreSQL type system (TEXT, INTEGER, BIGINT, BOOLEAN, TIMESTAMPTZ, UUID, JSONB, arrays, vectors, etc.) | | **Auto-increment** | `INTEGER PRIMARY KEY` (implicit ROWID) | `GENERATED ALWAYS AS IDENTITY` or `SERIAL` | | **Boolean** | Stored as `0`/`1` (INTEGER) | Native `BOOLEAN` (`true`/`false`) | | **Date/time** | Stored as TEXT or INTEGER (no native type) | Native `TIMESTAMPTZ`, `DATE`, `TIME` | | **JSON** | `json()` / `json_extract()` functions | `JSONB` type with operators (`->`, `->>`, `@>`) | | **Concurrent writes** | Single-writer (WAL mode) | Full multi-writer MVCC | | **Joins** | Supported | Full JOIN support with more advanced options (lateral, full outer) | | **Transactions** | SERIALIZABLE (default) | READ COMMITTED / REPEATABLE READ (SERIALIZABLE downgraded to REPEATABLE READ on the wire protocol; rejected with an error over the HTTP SQL API) | | **Replication** | Embedded replicas (edge) | Not supported | | **Foreign keys** | Supported (must be enabled per-connection) | Always enforced | | **Connection pooling** | Not applicable (HTTP) | Application-side pooling | ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Access to your Turso database (Turso CLI or connection URL and auth token) * Turso CLI installed (`curl -sSfL https://get.tur.so/install.sh | bash`) * DB9 CLI installed: `curl -fsSL https://db9.ai/install | sh` * A DB9 account: `db9 create --name my-app` to create your target database 1. **Export from Turso** **Option A: Turso CLI dump** Terminal ```bash turso db shell your-database .dump > export.sql ``` This produces a SQLite-format SQL dump. **Option B: Export as SQL via the shell** Terminal ```bash turso db shell your-database <<'EOF' .mode insert .output users.sql SELECT * FROM users; .output posts.sql SELECT * FROM posts; EOF ``` **Option C: Download the database file** Terminal ```bash # Create a local copy turso db shell your-database .dump > dump.sql sqlite3 local-copy.db < dump.sql ``` 2. **Convert SQLite to PostgreSQL** SQLite and PostgreSQL have different SQL dialects. Convert the dump: **Common SQLite → PostgreSQL conversions:** ```plaintext SQLite → PostgreSQL ────────────────────────────────────────────────────────── INTEGER PRIMARY KEY (autoincrement) → BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY REAL → DOUBLE PRECISION BLOB → BYTEA 0 / 1 (boolean) → FALSE / TRUE datetime('now') → now() strftime(...) → to_char(...) json_extract(col, '$.key') → col->>'key' GROUP_CONCAT(col) → string_agg(col, ',') IFNULL(a, b) → COALESCE(a, b) || (string concat) → || (same in PostgreSQL) AUTOINCREMENT → GENERATED ALWAYS AS IDENTITY ``` **Example table conversion:** SQL ```sql -- SQLite (Turso) CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, active INTEGER DEFAULT 1, metadata TEXT, -- JSON stored as text created_at TEXT DEFAULT (datetime('now')) ); -- PostgreSQL (DB9) CREATE TABLE users ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, active BOOLEAN DEFAULT true, metadata JSONB, created_at TIMESTAMPTZ DEFAULT now() ); ``` **Conversion script for the dump:** Terminal ```bash cat export.sql \ | sed 's/INTEGER PRIMARY KEY AUTOINCREMENT/BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY/g' \ | sed 's/INTEGER PRIMARY KEY/BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY/g' \ | sed "s/datetime('now')/now()/g" \ | sed 's/REAL/DOUBLE PRECISION/g' \ | sed 's/BLOB/BYTEA/g' \ | grep -v "^BEGIN TRANSACTION" \ | grep -v "^COMMIT" \ | grep -v "^CREATE INDEX.*sqlite" \ > import.sql ``` Review the output manually — automated conversion misses edge cases, especially around boolean values and date handling. **Convert boolean data:** SQLite stores booleans as `0`/`1`. After importing, convert to proper booleans: SQL ```sql -- If you kept the column as BOOLEAN, SQLite's 0/1 may need conversion -- PostgreSQL accepts 0/1 as boolean in most contexts, but verify your data ``` **Convert date/time data:** SQLite stores dates as TEXT (e.g., `"2026-01-15 10:30:00"`). If you changed the column type to TIMESTAMPTZ, PostgreSQL will parse ISO 8601 strings automatically. Verify timezone handling: SQL ```sql -- SQLite dates are typically UTC with no timezone info -- Ensure your application interprets them correctly as UTC in DB9 ``` 3. **Create the DB9 Database** Terminal ```bash db9 create --name my-app --show-connection-string ``` ▶ Run 4. **Import into DB9** Terminal ```bash # For small to medium databases db9 db sql my-app -f import.sql # For larger databases psql "$(db9 db connect my-app --output quiet)" -f import.sql ``` 5. **Update Your Application** **Replace the Turso/libSQL driver with a PostgreSQL driver** Diff ```diff import { createClient } from '@libsql/client'; const db = createClient({ url: process.env.TURSO_URL, authToken: process.env.TURSO_TOKEN }); const result = await db.execute('SELECT * FROM users WHERE id = ?', [userId]); import pg from 'pg'; const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [userId]); ``` **Key query syntax changes:** | SQLite (Turso) | PostgreSQL (DB9) | | ---------------------------- | ---------------------------------- | | `?` parameter placeholders | `$1, $2, $3` numbered placeholders | | `IFNULL(col, default)` | `COALESCE(col, default)` | | `GROUP_CONCAT(col, ',')` | `string_agg(col, ',')` | | `json_extract(col, '$.key')` | `col->>'key'` | | `datetime('now')` | `now()` | | `strftime('%Y-%m-%d', col)` | `to_char(col, 'YYYY-MM-DD')` | | `typeof(col)` | `pg_typeof(col)` | | `LIMIT count OFFSET offset` | `LIMIT count OFFSET offset` (same) | **Update your ORM** If you use an ORM, switch the database provider: Diff ```diff // Prisma provider = "sqlite" provider = "postgresql" // Drizzle import { sqliteTable } from 'drizzle-orm/sqlite-core'; import { pgTable } from 'drizzle-orm/pg-core'; ``` For ORM-specific setup, see: [Prisma](/docs/guides/prisma/), [Drizzle](/docs/guides/drizzle/), [SQLAlchemy](/docs/guides/python-sqlalchemy/). **Connection string** Diff ```diff TURSO_URL=libsql://your-db-name-your-org.turso.io TURSO_TOKEN=your-auth-token DATABASE_URL=postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=require ``` **Remove embedded replica logic** If you use Turso’s embedded replicas for edge reads, remove that logic. DB9 uses a single connection endpoint: Diff ```diff const db = createClient({ url: process.env.TURSO_URL, authToken: process.env.TURSO_TOKEN, syncUrl: process.env.TURSO_SYNC_URL, }); const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); ``` 6. **Validate** **Check schema** Terminal ```bash db9 db dump my-app --ddl-only ``` ▶ Run Verify tables, columns, types, and constraints match your expectations. **Check row counts** Terminal ```bash db9 db sql my-app -q "SELECT count(*) FROM users" db9 db sql my-app -q "SELECT count(*) FROM posts" ``` ▶ Run Compare against the source Turso database. **Run your test suite** Terminal ```bash DATABASE_URL="$(db9 db connect my-app --output quiet)" npm test ``` **Common test failures** * **Parameter placeholders** — `?` must become `$1, $2, ...` * **Boolean values** — `0`/`1` vs `true`/`false` * **Date handling** — text dates vs native TIMESTAMPTZ * **Dynamic typing** — SQLite allows any value in any column; PostgreSQL enforces types strictly * **SERIALIZABLE transactions** — Turso defaults to SERIALIZABLE; DB9 does not implement it and silently downgrades to REPEATABLE READ with a warning. Use REPEATABLE READ explicitly. ## Rollback Plan [Section titled “Rollback Plan”](#rollback-plan) Your Turso database is unchanged. To revert: 1. Switch back to the Turso connection URL and auth token, and revert driver/ORM changes. 2. If you need to export data created in DB9 back to Turso, export from DB9 and convert PostgreSQL SQL back to SQLite-compatible format. ## Caveats [Section titled “Caveats”](#caveats) * **Full SQL dialect migration** — Every raw SQL query must be converted from SQLite to PostgreSQL syntax. ORMs reduce this effort significantly. * **Driver change required** — The libSQL client (`@libsql/client`) must be replaced with a PostgreSQL driver (`pg`, `psycopg`, `pgx`). * **Type system change** — SQLite’s dynamic typing means any column can hold any type. PostgreSQL enforces types strictly. Review your data for type mismatches before importing. * **No embedded replicas** — Turso’s embedded replicas for edge reads have no DB9 equivalent. All reads go through the single DB9 endpoint. * **Boolean and date conversion** — SQLite stores booleans as integers and dates as text. These need explicit type conversion during migration. * **Transaction isolation change** — Turso/SQLite defaults to SERIALIZABLE. DB9 does not implement SERIALIZABLE; requesting it downgrades the transaction to REPEATABLE READ with a `WARNING` rather than failing. Applications that depend on SERIALIZABLE behavior need logic changes, and nothing will error to remind you. * **No HTTP protocol** — Turso uses HTTP (libSQL protocol). DB9 uses pgwire (TCP). This affects environments like Cloudflare Workers that only support HTTP — move database queries to a Node.js runtime. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — full list of supported and unsupported PostgreSQL features * [Connect](/docs/connect/) — connection string format and authentication options * [Prisma Guide](/docs/guides/prisma/) — type-safe ORM (handles dialect differences) * [Drizzle Guide](/docs/guides/drizzle/) — lightweight TypeScript ORM * [Production Checklist](/docs/production-checklist/) — deployment readiness # What is DB9? > DB9 is a PostgreSQL-compatible serverless database built on TiKV, designed for AI agents and developers who need instant provisioning, built-in embeddings, a file system, HTTP from SQL, branching, and scheduled jobs — all inside one database. DB9 is a PostgreSQL-compatible serverless database built on [TiKV](https://tikv.org/). It speaks the PostgreSQL wire protocol, so any tool that connects to Postgres — `psql`, Prisma, Drizzle, SQLAlchemy, or a raw driver — connects to DB9 with no adapter needed. What makes DB9 different is what ships inside the database: vector search and built-in embeddings, a queryable file system (fs9), HTTP calls from SQL, scheduled jobs via pg\_cron, and zero-copy branching. These capabilities are compiled into the server, not bolted on through external services. ## Who is DB9 for? [Section titled “Who is DB9 for?”](#who-is-db9-for) **AI agents and agent frameworks.** An agent can provision a database in under a second using the CLI or TypeScript SDK, store structured data alongside embeddings and files, call external APIs from SQL, and tear the database down when the task is done — all without leaving the SQL layer. **Developers building with ORMs and frameworks.** If you use Prisma, Drizzle, TypeORM, Sequelize, Knex, or SQLAlchemy, DB9 works as your Postgres backend with no driver changes. You get instant creation and branching on top of the ORM workflow you already have. **Teams that need disposable or per-tenant databases.** DB9 provisions databases programmatically, so patterns like database-per-user, database-per-test-run, or ephemeral preview databases are first-class, not workarounds. ## Core capabilities [Section titled “Core capabilities”](#core-capabilities) ### Instant provisioning [Section titled “Instant provisioning”](#instant-provisioning) Create a database in under a second with zero configuration. No signup required Anonymous trial databases work immediately — no account, credit card, or email needed. Run `db9 claim` to remove the 5-database limit when you’re ready. No signup is required — anonymous trial databases work immediately: Terminal ```bash db9 create --name myapp ``` ▶ Run Or from TypeScript: TypeScript ```typescript import { instantDatabase } from 'get-db9'; const db = await instantDatabase({ name: 'myapp' }); console.log(db.connectionString); ``` ▶ Run ### Built-in vector search and embeddings [Section titled “Built-in vector search and embeddings”](#built-in-vector-search-and-embeddings) DB9 includes a pgvector-compatible vector type with HNSW indexing, plus a built-in `embedding()` function that generates and caches embeddings without external infrastructure. Enable it once per database with `CREATE EXTENSION embedding`: SQL ```sql -- Generate an embedding and store it INSERT INTO docs (content, vec) VALUES ('DB9 overview', embedding('DB9 overview')::vector); -- Semantic search SELECT content FROM docs ORDER BY vec <-> embedding('search query')::vector LIMIT 5; ``` ▶ Run ### fs9 — queryable file system [Section titled “fs9 — queryable file system”](#fs9--queryable-file-system) Store and query files — CSV, JSONL, Parquet — directly from SQL: SQL ```sql -- Read a CSV file as a table SELECT * FROM extensions.fs9('/data/users.csv'); -- Copy local files into the database filesystem -- (via CLI: db9 fs cp ./report.csv myapp:/data/report.csv) ``` ▶ Run The file system is also accessible as an interactive shell (`db9 fs sh myapp`) or a FUSE mount. ### HTTP from SQL [Section titled “HTTP from SQL”](#http-from-sql) Call external APIs without leaving a SQL query: SQL ```sql SELECT * FROM http('https://api.example.com/data'); ``` Supports GET, POST, PUT, and DELETE. Useful for enrichment pipelines, webhook triggers, and agent tool integrations. ### Database branching [Section titled “Database branching”](#database-branching) Create a branch of any database for safe testing, preview environments, or rollback points: Terminal ```bash db9 branch create myapp --name preview ``` ▶ Run Branches are full copies of the parent’s schema and data. Creation is asynchronous — poll for `ACTIVE` before connecting — and at most 2 branches can be created concurrently. ### Scheduled jobs with pg\_cron [Section titled “Scheduled jobs with pg\_cron”](#scheduled-jobs-with-pg_cron) Run periodic SQL inside the database with no external scheduler: SQL ```sql SELECT cron.schedule('refresh-stats', '*/5 * * * *', 'REFRESH MATERIALIZED VIEW stats'); ``` Manage jobs via SQL or the CLI (`db9 db cron myapp list`). ### Serverless functions [Section titled “Serverless functions”](#serverless-functions) Deploy JavaScript/TypeScript code that runs with native SQL and filesystem access — no separate compute layer needed. See [Serverless Functions](/docs/functions/). ### Migrations, type generation, and observability [Section titled “Migrations, type generation, and observability”](#migrations-type-generation-and-observability) * **Migrations:** version-controlled SQL files applied with `db9 migration up`. * **Type generation:** `db9 gen types myapp --lang typescript` produces type definitions from live schema. * **Observability:** `db9 db inspect myapp` shows query samples, slow queries, schemas, tables, and indexes. ## When to choose DB9 [Section titled “When to choose DB9”](#when-to-choose-db9) * You need a Postgres-compatible database that provisions instantly for agents, tests, or per-tenant isolation. * You want vector search, file ingestion, HTTP calls, and cron in one database instead of separate services. * You are building AI agent workflows where the database is a tool the agent creates, uses, and discards. * You want branching for preview environments or safe schema experiments. ## When DB9 may not be the right fit [Section titled “When DB9 may not be the right fit”](#when-db9-may-not-be-the-right-fit) * You need a full application platform with auth, storage, edge functions, and a dashboard (consider Supabase). * Your workload depends on PostgreSQL extensions that DB9 does not yet support — check the [extensions overview](/docs/extensions/) and [SQL limits](/docs/sql/limits/) first. * You need an on-premises or self-hosted deployment today. ## Architecture at a glance [Section titled “Architecture at a glance”](#architecture-at-a-glance) DB9 separates the **control plane** from the **data plane**: * **Control plane** — the API server, CLI, and SDK handle database creation, user management, token lifecycle, branching, and observability. You interact with it through `db9` commands or the TypeScript SDK. * **Data plane** — the `db9-server` process speaks the PostgreSQL wire protocol and executes SQL against TiKV. Each database gets its own isolated keyspace in TiKV, so tenants share infrastructure but never data. Extensions like fs9, HTTP, embedding, vector, and pg\_cron run inside the data plane as compiled-in capabilities, not separate microservices. For a deeper look, see the [Architecture](/docs/architecture/) page. ## Next steps [Section titled “Next steps”](#next-steps) * [Why DB9 for AI Agents](/docs/why-db9-for-ai-agents/) — the agent-native capabilities that make DB9 different * [Connect to DB9](/docs/connect/) — connection strings, psql, ORMs, drivers, and authentication * [Quick Start](/docs/quickstart/) — install the CLI and create your first database in under a minute * [CLI Reference](/docs/cli/) — full command reference for `db9` * [TypeScript SDK](/docs/sdk/) — `instantDatabase()`, client API, and credential management * [Extensions](/docs/extensions/) — fs9, HTTP, pg\_cron, vector search, full-text search, and more * [SQL Reference](/docs/sql/) — data types, DDL/DML, functions, and compatibility notes # Anonymous and Claimed Databases > How DB9's zero-setup trial works — anonymous accounts, database limits, claiming with SSO, and what happens to your data when you upgrade. DB9 lets you start without signing up. The first time you run `db9 create` without credentials, the CLI auto-registers an anonymous account, gives you a bearer token, and creates your database. When you are ready, run `db9 claim` to link an SSO identity and remove the trial limits. This page explains the full lifecycle: how anonymous accounts work, what limits apply, how claiming works, and what happens to your databases. ## Decision Summary [Section titled “Decision Summary”](#decision-summary) | | Anonymous | Claimed | | -------------- | ----------------------------- | --------------------------- | | Database limit | 5 | Unlimited | | Auth method | Bearer token (auto-refreshed) | SSO (Auth0) or API key | | Token lifetime | 90 days (auto-refresh) | 90 days (re-login to renew) | | Features | Full access to all features | Full access to all features | | Upgrade path | `db9 claim` | Already upgraded | **Key point:** anonymous accounts have full feature access — SQL, extensions, branching, cron, tokens, and SDK access all work. The only restriction is the 5-database limit. ## How Anonymous Accounts Work [Section titled “How Anonymous Accounts Work”](#how-anonymous-accounts-work) ### Automatic registration [Section titled “Automatic registration”](#automatic-registration) When you run `db9 create` without credentials (no `DB9_API_KEY` environment variable and no stored token), the CLI: 1. Calls the anonymous registration endpoint 2. Receives a bearer token (90-day TTL) and an anonymous secret 3. Stores both in `~/.db9/credentials` (file permissions: owner-only, `0600`) 4. Creates your database No email, password, or browser interaction required. ### Credential storage [Section titled “Credential storage”](#credential-storage) The CLI stores credentials locally: Output ```text ~/.db9/credentials ``` This file contains the bearer token, customer ID, anonymous ID, and anonymous secret. It is created with owner-only read/write permissions. ### Automatic token refresh [Section titled “Automatic token refresh”](#automatic-token-refresh) When your bearer token expires (after 90 days), the CLI automatically refreshes it using your anonymous secret. This happens transparently on the next command — you do not need to re-register or re-create anything. The refresh endpoint issues a new 90-day token each time, so as long as you use the CLI at least once every 90 days, your session persists indefinitely. Your session persists automatically Anonymous credentials auto-refresh on each CLI invocation. You don’t need to re-register or re-create anything as long as you run a CLI command at least once every 90 days. ## Limits [Section titled “Limits”](#limits) Branches count toward the 5-database limit Anonymous accounts are limited to **5 databases**. Branches are full databases and count toward this limit. Delete branches you no longer need to free up slots, or run `db9 claim` to remove the limit entirely. Anonymous accounts are limited to **5 databases** (branches count toward this limit). Attempting to create a sixth database returns an error: Output ```text Anonymous account database limit reached (max 5). Run 'db9 claim' to upgrade your account. ``` All other features are unrestricted: * SQL execution, transactions, and all data types * All 9 extensions (http, fs9, embedding, vector, pg\_cron, etc.) * Database branching (branches count toward the 5-database limit) * Cron jobs, connect tokens, and user management * TypeScript SDK access * CLI output formats (`--output json`, `--output csv`) ## Claiming Your Account [Section titled “Claiming Your Account”](#claiming-your-account) Claiming links your anonymous account to a verified SSO identity (Auth0), removes the database limit, and preserves all existing databases. ### Interactive claim [Section titled “Interactive claim”](#interactive-claim) Terminal ```bash db9 claim ``` This opens a browser for Auth0 authentication. After you sign in, the account is claimed. ### Non-interactive claim (CI/CD) [Section titled “Non-interactive claim (CI/CD)”](#non-interactive-claim-cicd) Terminal ```bash db9 claim --id-token ``` Use this when you already have an Auth0 ID token (e.g., from a prior authentication flow). ### Auto-claim on login [Section titled “Auto-claim on login”](#auto-claim-on-login) If you have an anonymous account and run `db9 login`, the CLI automatically detects the anonymous credentials and claims the account before completing login. No separate `db9 claim` step needed. Terminal ```bash db9 login # Detects anonymous account → auto-claims → completes SSO login ``` ## What Happens When You Claim [Section titled “What Happens When You Claim”](#what-happens-when-you-claim) The claim operation is atomic. In a single update: 1. **Email** is set to your verified SSO email 2. **Database limit** is removed (set to unlimited) 3. **Anonymous secret** is deleted (no longer needed) 4. **SSO identity** is linked (issuer + subject ID) 5. **Anonymous flag** is cleared ### Your databases stay [Section titled “Your databases stay”](#your-databases-stay) All databases created under the anonymous account remain owned by the same customer ID. Nothing is deleted, migrated, or interrupted. Your connection strings and credentials continue to work. ### Credentials change [Section titled “Credentials change”](#credentials-change) After claiming, the anonymous refresh mechanism is disabled (the secret is deleted). To get new tokens: * Run `db9 login` for interactive SSO authentication * Use `db9 login --api-key ` with a pre-created API token for automation ### Token continuity [Section titled “Token continuity”](#token-continuity) Your existing bearer token continues to work until it expires (90 days from last refresh). After expiry, use `db9 login` to get a new SSO-based token. ## Edge Cases [Section titled “Edge Cases”](#edge-cases) ### SSO identity already linked [Section titled “SSO identity already linked”](#sso-identity-already-linked) If the SSO identity you authenticate with is already linked to a different DB9 account, the claim fails with an error. You cannot merge two separate accounts. ### Email conflict [Section titled “Email conflict”](#email-conflict) If another account already uses the same email address, the claim fails. Each email can be associated with only one DB9 account. ### No anonymous account to claim [Section titled “No anonymous account to claim”](#no-anonymous-account-to-claim) Running `db9 claim` without an existing anonymous account (e.g., after `db9 logout`) returns an error. You must have anonymous credentials stored locally. ### Check your current state [Section titled “Check your current state”](#check-your-current-state) Run `db9 status` to see whether you are anonymous, claimed, or logged out. ### Lost credentials [Section titled “Lost credentials”](#lost-credentials) If you delete `~/.db9/credentials` before claiming, the anonymous account still exists on the server but you cannot access it. There is currently no recovery mechanism for lost anonymous credentials. ## Authentication for Automation [Section titled “Authentication for Automation”](#authentication-for-automation) For CI/CD or headless environments where browser-based SSO is not practical: 1. **First run:** Let `db9 create` auto-register an anonymous account 2. **Claim later:** Run `db9 claim --id-token ` with a pre-obtained token 3. **Ongoing:** Use `db9 login --api-key ` with a named API token Or skip anonymous entirely: Terminal ```bash # Create an API token from a claimed account, then use it in CI export DB9_API_KEY="your-api-token" db9 create --name ci-test-db ``` ## Adopting Databases from Another Account [Section titled “Adopting Databases from Another Account”](#adopting-databases-from-another-account) If you have databases created under a different anonymous account, you can transfer them to your current verified account using `db9 adopt`. Terminal ```bash db9 adopt ``` The interactive flow: 1. **Preflight check** — verifies ownership of the source anonymous account (via its `anonymous_secret`), lists adoptable databases, and checks your account’s quota. 2. **Execute adoption** — transfers selected databases to your current account. ### REST API [Section titled “REST API”](#rest-api) The CLI calls two endpoints under the hood: | Method | Path | Description | | ------ | ----------------------------------------------- | ------------------------------------------------------------------------ | | POST | `/customer/adopt-anonymous-databases/preflight` | Validate the anonymous secret, list adoptable databases, and check quota | | POST | `/customer/adopt-anonymous-databases` | Execute the transfer | Both require a verified account bearer token. ### Requirements [Section titled “Requirements”](#requirements) * Your current account must be **verified** (claimed via SSO, not anonymous). * The source account must be **anonymous**. * You cannot adopt from your own account. * Your database quota must accommodate the transferred databases. ### What Gets Transferred [Section titled “What Gets Transferred”](#what-gets-transferred) * Only `ACTIVE` and `CREATING` databases are transferred. * Databases in `DISABLED` or `CREATE_FAILED` state are skipped. * Connection strings, credentials, and data are preserved — nothing changes at the database level. ## Account Lifecycle Summary [Section titled “Account Lifecycle Summary”](#account-lifecycle-summary) Output ```text +----------------------------------------------------+ | No credentials | | db9 create --> anonymous-register | | bearer token (90 days) | | anonymous secret | | database limit: 5 | +----------------------------------------------------+ | Anonymous account | | All features available | | Auto-refresh on token expiry | | db9 claim --> SSO verification | | email set | | limit removed | | secret deleted | +----------------------------------------------------+ | Claimed account | | Unlimited databases | | SSO login (db9 login) | | API key auth (db9 login --api-key) | | All existing databases preserved | +----------------------------------------------------+ ``` ## Next Pages [Section titled “Next Pages”](#next-pages) * [CLI Reference](/docs/cli/) — `db9 login`, `db9 claim`, and credential management * [TypeScript SDK](/docs/sdk/) — authentication and token handling in the SDK * [Branching Workflows](/docs/guides/branching-workflows/) — branches count toward database limits * [Platform: Provisioning](/docs/platform/provisioning/) — database creation and lifecycle * [Production Checklist](/docs/production-checklist/) — verify auth before going live # Compatibility Matrix > What DB9 supports, partially supports, and does not support compared to PostgreSQL — covering SQL, data types, indexes, protocol, ORMs, extensions, and system catalogs. DB9 implements a PostgreSQL-compatible SQL engine over TiKV distributed storage. Most PostgreSQL clients, ORMs, and drivers work without changes. This page documents where DB9 matches PostgreSQL, where it diverges, and what is not available. Use this matrix when evaluating DB9 for a new project or migrating an existing PostgreSQL application. ## Summary [Section titled “Summary”](#summary) | Category | Coverage | Notes | | ---------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | SQL DML (SELECT, INSERT, UPDATE, DELETE) | Full | JOINs, CTEs, window functions, subqueries, upsert, RETURNING | | SQL DDL (CREATE, ALTER, DROP) | Near-full | No partitioning, table inheritance, or foreign data wrappers | | Data types | 20+ types | All common types incl. INET; no XML, CIDR/MACADDR; range types limited to a partial `INT4RANGE` | | Indexes | B-tree + GIN (HNSW is disabled in the current release) | GiST/Hash/SP-GiST/BRIN are rejected | | Transactions | Full | READ COMMITTED and REPEATABLE READ enforced; SERIALIZABLE is downgraded to REPEATABLE READ | | Built-in functions | 200+ | String, math, date/time, JSON/JSONB, array, aggregate, window, FTS | | Wire protocol | pgwire v3 | Simple Query, Extended Query, COPY, prepared statements | | ORM compatibility | 99%+ | Prisma, Drizzle, Sequelize, Knex, TypeORM, GORM, SQLAlchemy tested | | System catalogs | 50+ views | pg\_catalog, information\_schema, cron schema | | Extensions | 9 built-in | http, fs9, pg\_cron, vector, embedding, uuid-ossp, hstore, parquet, zhparser (plus `pgcrypto` and `plpgsql` as metadata-only shims) | | PL/pgSQL | Partial | Basics supported; `EXECUTE`, exception handling, and nested blocks require a `DO` block; no WHILE loops, CONTINUE, or cursors | | Replication | None | No logical or streaming replication | ## SQL [Section titled “SQL”](#sql) ### DML and Queries [Section titled “DML and Queries”](#dml-and-queries) | Feature | Status | | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | SELECT with FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT/OFFSET | Supported | | DISTINCT / DISTINCT ON | Supported | | JOINs (INNER, LEFT, RIGHT, FULL OUTER, CROSS) | Supported | | Subqueries (correlated, scalar, EXISTS, IN) | Supported, including inside a `LATERAL` body | | CTEs (WITH … AS) | Supported, including a `WITH` inside a `LATERAL` body and a name reused across scopes of one statement | | Recursive CTEs (WITH RECURSIVE) | Supported, up to 1,000 iterations | | Data-modifying CTEs (`WITH w AS (INSERT ... RETURNING ...)`) | Supported over the PostgreSQL wire protocol. Over the HTTP SQL API the data-modifying CTE must be the **first** definition in the `WITH` list, otherwise the connection is dropped; see the caution below | | Set operations (UNION, INTERSECT, EXCEPT) | Supported | | Window functions (ROW\_NUMBER, RANK, LAG, LEAD, etc.) | Supported | | INSERT with VALUES, SELECT, DEFAULT, RETURNING | Supported | | INSERT ON CONFLICT (upsert) | Supported | | UPDATE with WHERE, subqueries, RETURNING | Supported | | DELETE with WHERE, subqueries, RETURNING | Supported | | CASE expressions | Supported | | COPY (CSV, TEXT) | Supported for the `COPY
[(columns)]` form (BINARY format not supported). The query form `COPY (SELECT ...) TO STDOUT` is rejected with `0A000` | | EXPLAIN / EXPLAIN ANALYZE | Supported (ANALYZE adds summary runtime stats — actual rows, execution time, KV counters — but no per-operator timing; `FORMAT JSON` is honored). Plan rows are returned over pgwire only — the HTTP SQL API returns an empty result | | LATERAL joins | Supported, including correlated bodies, a `WITH` clause inside the body, and correlated subqueries inside the body | A data-modifying CTE must come first over the HTTP SQL API Over the [HTTP SQL API](/docs/api/) — which is what `db9 db sql` uses — a `WITH` list containing a data-modifying CTE (`INSERT`/`UPDATE`/`DELETE` … `RETURNING`) works only when that CTE is the **first** definition. If any other definition precedes it, the request fails with `error: connection closed`: SQL ```sql -- Fails over the HTTP SQL API WITH src AS (SELECT id FROM staging WHERE ready), ins AS (INSERT INTO target SELECT id FROM src RETURNING id) SELECT count(*) FROM ins; -- error: connection closed -- Works: the data-modifying CTE is first, with the helper inlined into it WITH ins AS (INSERT INTO target SELECT id FROM staging WHERE ready RETURNING id) SELECT count(*) FROM ins; ``` The statement is aborted, so no rows are written — but no SQLSTATE is returned either. Note that simply moving the definition later is not a fix: a `WITH` definition cannot reference one declared after it, so `ins` would then fail with `42P01 relation "src" does not exist`. Inline the helper into the data-modifying CTE as above, or run the statement over the PostgreSQL wire protocol (psql, ORMs, any pg driver), where every ordering works. ### DDL [Section titled “DDL”](#ddl) | Feature | Status | | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CREATE/ALTER/DROP TABLE | Supported | | CREATE TABLE AS / SELECT INTO | Supported — but the new table gets an extra `_rowid` primary key column that is visible in `SELECT *`, see [DDL](/docs/sql/ddl/) | | Column constraints (PRIMARY KEY, UNIQUE, NOT NULL, DEFAULT, CHECK) | Supported | | Foreign keys (CASCADE, SET NULL, SET DEFAULT, RESTRICT, NO ACTION) | Supported | | Generated columns | Supported | | SERIAL / BIGSERIAL | Supported | | CREATE/DROP INDEX (B-tree, GIN) | Supported | | CREATE INDEX CONCURRENTLY | Supported | | Expression indexes, partial indexes | Supported | | CREATE/DROP VIEW | Supported | | CREATE/DROP MATERIALIZED VIEW, REFRESH MATERIALIZED VIEW | Supported | | CREATE/DROP SEQUENCE | Supported | | ALTER SEQUENCE | Partial (only `OWNER TO` / `OWNED BY`; all other clauses — `RESTART`, `INCREMENT BY`, `RENAME TO`, `SET SCHEMA`, … — raise `0A000`. Use `SETVAL()` to reposition) | | DROP SEQUENCE dependency checks | Not supported (a sequence used by a column default can be dropped, breaking the table) | | CREATE/ALTER TYPE (enum) | Supported | | CREATE/DROP SCHEMA | Supported | | CREATE/DROP FUNCTION (PL/pgSQL) | Supported | | CREATE/DROP TRIGGER (BEFORE/AFTER, INSERT/UPDATE/DELETE) | Supported — but `FOR EACH STATEMENT` runs once per row, see [Advanced SQL — Triggers](/docs/sql/advanced/#triggers) | | TRUNCATE | Supported | | Row-Level Security (ENABLE/DISABLE/FORCE RLS) | Supported | | CREATE/ALTER/DROP POLICY | Supported | | Table partitioning (RANGE, LIST, HASH) | Not supported | | Table inheritance | Not supported | | Foreign data wrappers (FDW) | Not supported | | Tablespaces | Not supported (TiKV manages storage placement) | ### Transactions [Section titled “Transactions”](#transactions) | Feature | Status | | ------------------------------------------ | ---------------------------------------------------------------------------- | | BEGIN / COMMIT / ROLLBACK | Supported | | SAVEPOINT / RELEASE / ROLLBACK TO | Supported | | READ COMMITTED isolation | Supported — statement-level snapshots, as in PostgreSQL. Default level. | | REPEATABLE READ isolation | Supported — transaction-level snapshot | | SERIALIZABLE isolation | Downgraded to REPEATABLE READ, see note below | | READ UNCOMMITTED isolation | Supported — behaves as READ COMMITTED, as in PostgreSQL | | SET LOCAL (transaction-scoped settings) | Supported | | READ ONLY transactions | Supported — DML and DDL writes are rejected with SQLSTATE `25006` | | DEFERRABLE transactions | Not supported (requires SERIALIZABLE) | | Advisory locks (`pg_advisory_lock` family) | Supported (node-local; not coordinated across multiple db9-server processes) | SERIALIZABLE is silently downgraded `READ COMMITTED` and `REPEATABLE READ` behave as they do in PostgreSQL — a `READ COMMITTED` transaction takes a fresh snapshot per statement and sees concurrent commits, and a `REPEATABLE READ` transaction holds one snapshot for its lifetime. `SERIALIZABLE` is **not** implemented. TiKV provides snapshot isolation, not PostgreSQL’s serializable snapshot isolation (SSI), so anomalies that only SSI prevents — write skew in particular — are **not** detected. What happens when you ask for it depends on how you connect: SQL ```sql -- PostgreSQL wire protocol (psql, ORMs, any pg driver): accepted with a warning BEGIN ISOLATION LEVEL SERIALIZABLE; -- WARNING: TiKV provides snapshot isolation; SERIALIZABLE has been downgraded to REPEATABLE READ SHOW transaction_isolation; -- repeatable read — the level actually in effect ``` `SHOW transaction_isolation` reports the level DB9 applied, not the one you asked for, so reading it back is a reliable runtime check for the downgrade. The same holds for `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE` inside an open transaction. Over the [HTTP SQL API](/docs/api/) (as used by `db9 db sql`), the same request is rejected outright instead: ```plaintext ERROR: SERIALIZABLE isolation level is not supported. Use REPEATABLE READ or READ COMMITTED instead. ``` If your application depends on true serializability, enforce it yourself with explicit row locks (`SELECT ... FOR UPDATE`) or a unique constraint that makes the conflicting write fail. ## Data Types [Section titled “Data Types”](#data-types) ### Supported [Section titled “Supported”](#supported) | Type | Aliases | Notes | | ------------------------------------ | ----------------- | ------------------------------------------------------------------------------------------------------- | | BOOLEAN | BOOL | | | SMALLINT | INT2 | Stored as INT4 internally | | INTEGER | INT, INT4 | | | BIGINT | INT8 | | | REAL | FLOAT4 | Stored as FLOAT8 internally | | DOUBLE PRECISION | FLOAT8 | | | NUMERIC | DECIMAL | With precision and scale, up to precision 1000 | | TEXT | | Variable-length, no limit | | VARCHAR(n) | CHARACTER VARYING | | | CHAR(n) | CHARACTER | Stored as VARCHAR internally | | BYTEA | | Binary data | | DATE | | | | TIME | | Without time zone | | TIMESTAMP | | Without time zone, microsecond precision | | TIMESTAMPTZ | | With time zone, microsecond precision | | INTERVAL | | Unary negation is unsupported (`-INTERVAL '1 day'` fails) — multiply by `-1` instead | | JSON | | Stored as text | | JSONB | | Canonicalized (sorted keys, normalized whitespace) | | UUID | | | | INET | | IPv4/IPv6 host or network address; equality and ordering only — no network operators/functions, no CIDR | | BOOLEAN\[] / INT\[] / TEXT\[] / etc. | | 1-dimensional arrays of any supported type | | TSVECTOR | | Full-text search document representation | | TSQUERY | | Full-text search query | | VECTOR(n) | | pgvector-compatible; for HNSW indexes and distance operators | | NAME | | PostgreSQL identifier type | ### Not Supported [Section titled “Not Supported”](#not-supported) | Type | Notes | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | XML | | | CIDR / MACADDR | Network address types; INET is supported (equality/ordering only) | | Range types (INT4RANGE, TSRANGE, etc.) | `INT4RANGE` is accepted but only partly implemented — see the caution below. `INT8RANGE`, `NUMRANGE`, `DATERANGE`, `TSRANGE`, and `TSTZRANGE` do not exist | | Composite types (user-defined row types) | Declarable and writable via a text literal, but unreadable field-wise — see the caution below. Enum types are supported via CREATE TYPE | | Large objects (OID-based streaming) | BYTEA is available for binary data | | Multi-dimensional arrays | Only 1-dimensional arrays; an `INT[][]` column is catalogued as `text[]` and rejects every `INSERT` (42846) | | Money | | | Bit string (BIT, VARBIT) | Accepted: `BIT(n)` and `VARBIT` store and return bit strings, but an over-length value is silently truncated instead of raising; `BIT(1)` becomes `BOOLEAN`. Both `BIT VARYING` spellings are rejected (42601) — see the caution below | Three of these are written without complaint Most rows above fail loudly. `XML`, `MONEY`, `CIDR`, `MACADDR`, and every range type except `INT4RANGE` are rejected at `CREATE TABLE` with `type "..." does not exist` (42704). Multi-dimensional arrays get as far as `CREATE TABLE` — the column is catalogued as `text[]` — but every `INSERT` into such a column is rejected (42846), one-dimensional values included, so nothing wrong can be stored. Three entries have neither safeguard. The DDL succeeds and so does the write, so the problem surfaces later as wrong data or a misleading error. **`BIT` / `VARBIT`** — bit-string *content* is validated: a value that is not made up of `0`s and `1`s is rejected with `invalid input syntax for type bit(n)` / `varbit` (22P02). The declared *length* never raises. `BIT(n)` silently reshapes every value to exactly n bits — short input is right-padded with zeros, long input truncated — where PostgreSQL raises `22026` for both. `VARBIT(n)` leaves a short value alone, as PostgreSQL does, but silently truncates anything longer than n, where PostgreSQL raises `22001`: SQL ```sql CREATE TABLE b (id INT PRIMARY KEY, v BIT(8)); INSERT INTO b VALUES (1, '10101010'); -- stored as 10101010 INSERT INTO b VALUES (2, '101'); -- accepted, stored as 10100000 (PostgreSQL: 22026) INSERT INTO b VALUES (3, '1010101010101'); -- accepted, stored as 10101010 (PostgreSQL: 22026) INSERT INTO b VALUES (4, 'hello!!!'); -- rejected, 22P02 CREATE TABLE vb (id INT PRIMARY KEY, v VARBIT(8)); INSERT INTO vb VALUES (1, '101'); -- stored as 101, same as PostgreSQL INSERT INTO vb VALUES (2, '1010101010101'); -- accepted, stored as 10101010 (PostgreSQL: 22001) ``` Silent truncation is the hazard to plan around: every row survives the load and the corruption only shows up as wrong bit patterns later. A `varbit` column is safe by accident: `pg_dump` writes it as `bit varying(n)`, which DB9 cannot parse (42601 — both the bare `BIT VARYING` and the `BIT VARYING(n)` spellings are rejected). A `bit(n)` column is not — `pg_dump` writes it as `bit(n)` verbatim, exactly the spelling that is accepted and silently reshaped. `B'1010'` bit-string literals, as emitted by `pg_dump --inserts`, are accepted. One catalog view disagrees with the others. `information_schema.columns` and `pg_typeof()` both report the true type (`bit` / `bit varying`), but `format_type()` reports `text` — so `psql`’s `\d` and anything else built on `format_type()` will not show these columns as bit strings. Use `information_schema.columns` to find them. The bytes on disk are the ASCII characters of the bit string, which surfaces if you cast: `v` renders as `10101010` but `v::text` renders as `\x3130313031303130`. `BIT(1)` is the exception, and a benign one: it is created as `BOOLEAN`, round-trips `'1'` and `'0'` as `true`/`false`, and rejects anything longer at 22P02. Bare `BIT` behaves the same way, and is what `pg_dump` writes as `bit(1)` — so single-flag bit columns migrate cleanly and need no conversion. Convert the wider bit-string columns before exporting: use `BYTEA` for binary data, or an `INTEGER`/`BIGINT` bitmask for flags. **`INT4RANGE`** — this type does exist. It validates its input, round-trips correctly, and both the `int4range(lo, hi)` constructor and the overlap operator `&&` work as expected. The gaps are in everything else: the containment operator `@>` fails with `Invalid JSON` (XX000), `isempty()` is unavailable, and `lower()` / `upper()` resolve to the **text** case-folding functions instead of the range bound accessors — so they silently return the range unchanged rather than its bounds: SQL ```sql SELECT lower('[1,10)'::int4range); -- [1,10) PostgreSQL returns 1 SELECT upper('[1,10)'::int4range); -- [1,10) PostgreSQL returns 10 ``` Because the failure is silent, prefer two plain columns (`lo`, `hi`) over `INT4RANGE`. **Composite types** — `CREATE TYPE ... AS (...)` and a column of that type both succeed. Only the `ROW(...)` constructor is rejected (42804); a composite *text literal* — the form `pg_dump` emits — is accepted without any validation of shape, arity, or content, and the value can then never be read back field-wise: SQL ```sql CREATE TYPE ct AS (a TEXT, b INT); CREATE TABLE ctab (id INT PRIMARY KEY, c ct); INSERT INTO ctab VALUES (1, ROW('x', 1)); -- ERROR (42804) INSERT INTO ctab VALUES (2, '("x",1)'); -- accepted INSERT INTO ctab VALUES (3, 'zzz not composite'); -- accepted INSERT INTO ctab VALUES (4, '(1,2,3,4,5)'); -- accepted, wrong arity SELECT (c).a FROM ctab; -- ERROR: expression type not yet supported (0A000) ``` Flatten composite columns into ordinary scalar columns before migrating. ## Indexes [Section titled “Indexes”](#indexes) | Type | Status | Notes | | ------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | B-tree | Full | Default. Point, range, bounded-range, in-list, expression, partial indexes | | GIN | Full | JSONB containment (`@>`), full-text search (`@@`), array operators | | HNSW | Disabled in the current release | Approximate k-NN over `VECTOR` columns. Index building is gated off server-side — rejected over the wire protocol with `55000` (`feature "hnsw_index" is unavailable`), and silently unused when created over the HTTP SQL API. Exact vector search is unaffected. Requires a single-column primary key — without one you get a bare `XX000` instead, on both paths; see the note below | | GiST | Not supported | `CREATE INDEX` is rejected | | Hash | Not supported | `CREATE INDEX` is rejected | | SP-GiST | Not supported | `CREATE INDEX` is rejected | | BRIN | Not supported | `CREATE INDEX` is rejected | Only `btree`, `gin`, and `hnsw` are recognized access methods. Any other method is rejected at `CREATE INDEX` time — it is not accepted-then-ignored: ```plaintext ERROR: access method "gist" is not supported (0A000) HINT: Only btree, gin, and hnsw indexes are currently supported. ``` Note that the hint lists `hnsw` even though HNSW index building is currently gated off — see the HNSW row above. `IVFFlat` is not among them — `CREATE INDEX ... USING ivfflat` fails with `42704` (`access method "ivfflat" does not exist`). HNSW is the only vector index type. HNSW indexes carry extra structural requirements: the table needs a **single-column primary key** (no-PK and composite-PK tables are rejected), and an `INTEGER`/`BIGINT` primary key must hold only non-negative values. `UUID` and `TEXT` primary keys are fine. Partial (`WHERE`) and multi-column HNSW indexes are rejected. Queries fall back to an exact sequential scan when no usable index exists, so all distance operators (`<->`, `<=>`, `<#>`) and functions return correct results either way. See [pgvector](/docs/extensions/vector/) for details. GIN indexes on JSONB columns are fully functional for containment queries (`@>`). Queries using `@>` on GIN-indexed columns use index scans instead of sequential scans. ## Wire Protocol [Section titled “Wire Protocol”](#wire-protocol) | Feature | Status | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | pgwire v3 | Supported | | Simple Query (text) | Supported | | Extended Query (Parse, Bind, Describe, Execute) | Supported | | Binary parameter encoding | Supported | | COPY (CSV, TEXT) | Supported for the `COPY
[(columns)]` form (BINARY format not supported); `COPY (SELECT ...) TO STDOUT` is rejected | | Prepared statements | Supported | | Portals | Supported | | Multiple result sets | Supported | | SCRAM-SHA-256 authentication | Supported (at pgwire layer) | | LISTEN / NOTIFY | Supported (notifications are delivered on commit to sessions holding an open connection; `LISTEN` requires a persistent pgwire session, so the stateless HTTP SQL API can send `NOTIFY` but cannot receive) | | Logical replication protocol | Not supported | | Streaming replication | Not supported | ## Functions [Section titled “Functions”](#functions) DB9 implements 200+ built-in functions across these categories: | Category | Examples | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | String | `upper`, `lower`, `concat`, `substring`, `replace`, `trim`, `split_part`, `regexp_match`, `format` | | Math | `abs`, `ceil`, `floor`, `round`, `sqrt`, `power`, `log`, `random`, `trunc`, trig functions | | Date/Time | `now`, `date_trunc`, `date_part`, `extract`, `age`, `to_char`, `to_timestamp`, `make_date` | | Aggregate | `count`, `sum`, `avg`, `min`, `max`, `string_agg`, `array_agg`, `json_agg`, `bool_and/or` | | Window | `row_number`, `rank`, `dense_rank`, `lag`, `lead`, `first_value`, `last_value`, `ntile` | | JSON/JSONB | `jsonb_build_object`, `jsonb_set`, `jsonb_extract_path`, `jsonb_array_elements`, `jsonb_each`, `jsonb_typeof`, `to_jsonb`, `row_to_json` | | Array | `array_length`, `array_agg`, `unnest`, `array_append`, `array_cat`, `array_position`, `string_to_array` | | Full-text search | `to_tsvector`, `to_tsquery`, `plainto_tsquery`, `ts_rank`, `ts_headline`, `setweight` | | UUID | `uuid_generate_v4` and related functions | | Type conversion | `cast`, `to_char`, `to_number`, `to_date`, `to_timestamp` | | Conditional | `coalesce`, `nullif`, `greatest`, `least` | | HTTP (scalar) | `http_get`, `http_post`, `http_put`, `http_delete` returning JSONB | | Document chunking | `CHUNK_TEXT` — table-valued function for RAG pipelines | | Storage | `db9_refresh_storage_stats` — trigger storage scan | | System | `current_user`, `current_database`, `current_schema`, `pg_typeof`, `version` | ### JSON/JSONB Operators [Section titled “JSON/JSONB Operators”](#jsonjsonb-operators) | Operator | Description | Status | | -------- | ------------------------------------------- | -------------- | | `->` | Get JSON object field by key (returns JSON) | Supported | | `->>` | Get JSON object field by key (returns text) | Supported | | `@>` | Contains | Supported | | `<@` | Contained by | Supported | | `?` | Key exists | Supported | | \`? | \` | Any key exists | | `?&` | All keys exist | Supported | | \` | | \` | | `#-` | Delete path | Supported | ## PL/pgSQL [Section titled “PL/pgSQL”](#plpgsql) | Feature | Status | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | DECLARE / BEGIN / END blocks | Supported | | Variable assignment (`:=`) | Supported | | SELECT INTO | Supported — but an earlier `into` in the statement mis-parses, see caution below | | IF / THEN / ELSIF / ELSE / END IF | Supported | | FOR loops (query iteration and integer range) | Supported | | PERFORM (execute without result) | Supported | | EXIT (loop termination, plain and `EXIT WHEN `) | Supported | | CONTINUE / `CONTINUE WHEN ` | Not supported — `42601` (`syntax error: sql parser error: Expected an SQL statement, found: CONTINUE`) | | RAISE (NOTICE, WARNING, ERROR) | Supported | | RETURN / RETURN NEXT / RETURN QUERY | Supported | | RETURNS TABLE syntax | Supported | | CASE statements (in PL/pgSQL) | Supported | | WHILE loops | Not supported | | Cursor operations (FOR…IN CURSOR) | Not supported | | Exception handling (BEGIN…EXCEPTION) | `DO` blocks only | | Dynamic SQL (EXECUTE) | `DO` blocks only; a literal or a variable command string, but not `EXECUTE ... INTO` or `USING` | | Nested BEGIN…END blocks | `DO` blocks only | `SELECT ... INTO` mis-parses when `into` appears earlier in the statement DB9 locates the `INTO` target by scanning the statement text for the first case-insensitive occurrence of `into`, without skipping string literals, comments, or identifiers. When one of those precedes the real keyword, the statement is split in the wrong place. The failure mode depends on where that earlier `into` sits: | Earlier `into` appears in | Result | | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | A string literal — `SELECT 'into' INTO r` | `42601` — `syntax error: sql parser error: Unterminated string literal` | | A block comment — `SELECT /* into */ 7 INTO r` | `42601` — `syntax error: sql parser error: Unexpected EOF while in a multi-line comment` | | An identifier or alias — `SELECT into_col INTO r FROM ...` | **Silent.** The statement runs as SQL `SELECT ... INTO
`: it creates a table named after the target variable, leaves the variable `NULL`, and the next call fails with `relation "r" already exists` | The identifier case is the dangerous one — nothing is raised, the function returns `NULL`, and an unintended table is left behind in the schema. SQL ```sql -- Fails: the 'into' inside the literal precedes the keyword DO $$ DECLARE r text; BEGIN SELECT 'into' INTO r; END $$; -- ERROR: syntax error: sql parser error: Unterminated string literal (SQLSTATE 42601) -- Silently wrong: 'into' inside the alias precedes the keyword. -- No error, q is NULL, and a table named q is left behind. DO $$ DECLARE q int; BEGIN SELECT into_c INTO q FROM (SELECT 5 AS into_c) s; RAISE NOTICE 'q=%', q; END $$; -- NOTICE: q= -- Works: nothing resembling 'into' before the keyword DO $$ DECLARE r text; BEGIN SELECT 'ok' INTO r; END $$; -- Works: the literal follows the keyword DO $$ DECLARE r text; BEGIN SELECT s.c INTO r FROM (SELECT 'zinto' AS c) s; END $$; ``` Note that these must be run inside a PL/pgSQL host. At the top level, `SELECT ... INTO r` is SQL’s create-table form, which succeeds and creates a table regardless of the `into` placement. Only PL/pgSQL `SELECT ... INTO` is affected. Variable assignment (`:=`), `RETURN`, `INSERT INTO ... VALUES ('...into...')` and `RETURNING ... INTO` all handle `into` correctly, as does any occurrence that follows the keyword. Reproduces in both a `CREATE FUNCTION` body and a `DO` block, on the pgwire protocol and the HTTP SQL API. ### PL/pgSQL host differences [Section titled “PL/pgSQL host differences”](#plpgsql-host-differences) A `DO` block is a more capable PL/pgSQL host than a `CREATE FUNCTION` body. Exception handling, dynamic SQL, and nested blocks run inside `DO` but are rejected in a function body with `0A000` (`... requires a Session-owned interactive DO host`). | Feature | `CREATE FUNCTION` body | `DO` block | | ------------------------------------------ | ---------------------- | --------------------------------------------------------------- | | CASE statements | Supported | Supported | | Exception handling (`BEGIN ... EXCEPTION`) | `0A000` | Supported | | Dynamic SQL (`EXECUTE`) | `0A000` | Command string only (literal or variable); no `INTO` or `USING` | | Nested `BEGIN ... END` blocks | `0A000` | Supported | | WHILE loops | `42601` | `42601` | | Cursor operations | `42601` | `42601` | Within a `DO` block, `EXECUTE` runs a command string that may be either a literal or a variable. Capturing a result is not supported: `EXECUTE ... INTO` fails with `XX000` (`internal error`), and `EXECUTE ... USING` is rejected at parse time with `42601` — `syntax error: sql parser error: Expected end of statement, found: ...`, where the reported token is `USING` when `INTO` is also present and the first bound argument otherwise. Use a plain `SELECT ... INTO` for the value you need. Exception handlers in a `DO` block follow PostgreSQL rollback semantics: statements that ran before the exception are rolled back, and only the handler’s effects persist. Multi-line `DECLARE` entries parse correctly in both hosts: a declaration whose type or value spans several lines yields the same value as PostgreSQL (`v int := 1` / `+ 2;` is `3`, and `v int` / `:= 42;` is `42`). CASE with no matching branch PostgreSQL raises `CASE_NOT_FOUND` (`20000`) when no `CASE` branch matches and no `ELSE` is present. DB9 falls through silently instead, leaving the target variable `NULL`. Always include an explicit `ELSE` branch. Other procedural languages (PL/Python, PL/Perl, PL/v8) are not supported. ## ORM and Driver Compatibility [Section titled “ORM and Driver Compatibility”](#orm-and-driver-compatibility) DB9 is tested against major ORMs with a combined pass rate above 99%: | ORM / Driver | Tested Version | Pass Rate | Notes | | ------------------ | -------------- | ------------- | -------------------------------------------------- | | Prisma | 5.7+ | 100% (89/89) | Binary wire protocol; `$queryRaw` for advanced SQL | | Drizzle | 0.29+ | 100% (75/75) | Type-safe queries; full query builder support | | Sequelize | 6.35+ | 100% (87/87) | Raw queries for window/CTE features | | Knex.js | 3.1+ | 100% (97/97) | Full query builder, window functions, CTEs | | TypeORM | 0.3.17+ | 98% (147/150) | 3 tests skipped (schema introspection edge cases) | | node-postgres (pg) | 8.11+ | Full | Native pgwire client | | SQLAlchemy | 2.0+ | Tested | JSONB operators, RETURNING, transaction patterns | | GORM (Go) | 1.25+ | Tested | CRUD, transactions, foreign keys | **Known ORM limitations:** * Schema introspection queries may return incomplete results for some ORMs that rely heavily on `information_schema` * Some ORMs assume PostgreSQL-specific system functions that are not yet implemented ## System Catalogs [Section titled “System Catalogs”](#system-catalogs) DB9 implements 50+ virtual tables across `pg_catalog`, `information_schema`, and extension schemas. ### pg\_catalog [Section titled “pg\_catalog”](#pg_catalog) | View | Status | Notes | | ------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | pg\_class | Supported | Relations (tables, views, indexes, sequences) | | pg\_attribute | Supported | Column definitions | | pg\_index | Supported | Index metadata | | pg\_constraint | Supported | Constraints (PK, FK, UNIQUE, CHECK) | | pg\_type | Supported | Data types | | pg\_proc | Supported | Functions and procedures | | pg\_namespace | Supported | Schemas | | pg\_roles / pg\_user | Supported | Users and roles | | pg\_database | Supported | Database metadata | | pg\_sequence | Supported | Sequence state | | pg\_attrdef | Supported | Column defaults | | pg\_extension | Supported | Installed extensions | | pg\_am | Supported | Access methods | | pg\_trigger | Supported | Trigger definitions | | pg\_depend / pg\_description | Supported | Object dependencies and comments | | pg\_settings | Supported | Server and `db9.*` runtime parameters; `category` is always NULL and `short_desc` is sparse | | pg\_stat\_user\_tables | Stub | Returns rows but statistics columns are zeros | | pg\_stat\_statements | Not available | | | pg\_publication | Writable, but inert | A bare `CREATE PUBLICATION p;` succeeds on both transports and the row persists here — but nothing is replicated. `FOR`/`WITH` clauses are rejected with `0A000`, and `ALTER PUBLICATION` is a syntax error. See the caution below | | pg\_publication\_rel / pg\_publication\_namespace | Stub (empty) | Publication membership is never recorded | | pg\_subscription | Not available | `CREATE SUBSCRIPTION` is not parsed at all | A publication can be created, and it does nothing `CREATE PUBLICATION my_pub;` returns `CREATE PUBLICATION` and the row appears in `pg_publication` — but DB9 has no logical replication, so no changes are ever streamed and `pg_publication_rel` stays empty. Do not read the success of that statement as working CDC. `DROP PUBLICATION` works if you need to remove the inert object. The clauses `pg_dump` actually emits are rejected, so a restored dump fails loudly rather than silently: ```plaintext ERROR: CREATE PUBLICATION FOR/WITH clauses are not supported yet (0A000) ``` `ALTER PUBLICATION ... ADD TABLE` and `ALTER TABLE ... REPLICA IDENTITY` are syntax errors, and `pg_replication_slots` and `pg_create_logical_replication_slot()` do not exist. ### information\_schema [Section titled “information\_schema”](#information_schema) | View | Status | | ------------------------- | ------------- | | columns | Supported | | tables | Supported | | schemata | Supported | | table\_constraints | Supported | | key\_column\_usage | Supported | | referential\_constraints | Supported | | check\_constraints | Supported | | constraint\_column\_usage | Supported | | table\_privileges | Supported | | sequences | Supported | | routines | Supported | | views | Not available | `information_schema.views` is not implemented — querying it fails with `relation "views" does not exist` (`42P01`). To list views, use `pg_views`, or filter `information_schema.tables` on `table_type`: SQL ```sql SELECT viewname FROM pg_views WHERE schemaname = 'public'; -- Portable alternative: SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'VIEW'; ``` The eleven `Supported` views above are the complete `information_schema` surface in DB9. ## Extensions [Section titled “Extensions”](#extensions) DB9 includes 9 built-in extensions. Custom or third-party extensions cannot be installed. | Extension | Version | Default | Description | | --------- | ------- | ------- | -------------------------------------------------------------------------------- | | http | 1.0.0 | Yes | HTTP client (GET, POST, PUT, DELETE, PATCH, HEAD) | | pg\_cron | 1.0.0 | Yes | Job scheduler with cron expressions | | fs9 | 1.0.0 | No | File system operations (read, write, list, glob) | | vector | 0.8.1 | No | pgvector-compatible vector type and HNSW indexes | | embedding | 1.0.0 | No | Built-in text embedding generation | | uuid-ossp | 1.1 | No | UUID generation functions (functions are built-in; extension is a metadata shim) | | hstore | 1.0 | No | Key-value store type (metadata shim with limited semantics) | | parquet | 1.0.0 | No | Parquet file import | | zhparser | 2.0.0 | No | Chinese full-text search tokenizer | | pgcrypto | 1.3 | No | Metadata shim only — registers no functions (see below) | | plpgsql | 1.0 | No | Metadata shim; PL/pgSQL itself is compiled in | `CREATE EXTENSION pgcrypto` is accepted so Supabase and ORM bootstrap scripts run unchanged, but it provides no functions. `crypt()`, `gen_salt()`, `hmac()`, `encrypt()`/`decrypt()`, `pgp_sym_encrypt()` and `gen_random_bytes()` all fail with `42883`. `gen_random_uuid()` and `digest()` work, but they are DB9 built-ins and need no extension. Extensions not available: PostGIS, pg\_partman, pg\_stat\_statements, pg\_trgm, ltree, citext, and all other PostgreSQL contrib extensions. These raise `42704 extension is not available` at `CREATE EXTENSION` time. ## Not Supported [Section titled “Not Supported”](#not-supported-1) These PostgreSQL features are not available in DB9: | Feature | Category | | --------------------------------------------------------------------------------------------------------------------------------------- | ------------ | | Table partitioning (RANGE, LIST, HASH) | DDL | | Table inheritance | DDL | | Foreign data wrappers (FDW) | DDL | | Tablespaces | DDL | | Rules (CREATE RULE) | DDL | | Logical replication (`CREATE SUBSCRIPTION` is unparsed; a bare `CREATE PUBLICATION` is accepted but inert — see `pg_publication` above) | Replication | | Streaming replication | Replication | | SERIALIZABLE isolation (true serializable) | Transactions | | DEFERRABLE transactions | Transactions | | Large objects (OID-based) | Data | | XML type | Data | | Network types (CIDR, MACADDR) | Data | | Range types (except a partial `INT4RANGE`) | Data | | PL/Python, PL/Perl, PL/v8 | Languages | | Dynamic SQL in PL/pgSQL (EXECUTE) inside a function body | Languages | | Custom extensions | Extensions | | pg\_dump / pg\_restore (native format) | Tools | | pg\_basebackup | Tools | ## Validation [Section titled “Validation”](#validation) You can verify compatibility for your specific use case: SQL ```sql -- Check supported types SELECT typname FROM pg_type WHERE typnamespace = 11 ORDER BY typname; -- Check available extensions SELECT * FROM pg_extension; -- Check installed functions ('f' = function, 'p' = procedure, 'w' = window). -- Note: pg_proc lists only catalog-registered functions. Many built-ins (ABS, SUM, -- GENERATE_SERIES, ...) run in the executor and never appear here — see /docs/sql/catalog/ SELECT proname, prokind FROM pg_proc WHERE pronamespace = 11 ORDER BY proname; -- Check catalog coverage. pg_tables lists user schemas only, so query pg_class -- to enumerate the pg_catalog relations: SELECT n.nspname AS schema, c.relname AS catalog_table FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'pg_catalog' ORDER BY c.relname; -- Verify transaction isolation SHOW transaction_isolation; -- returns 'read committed' (the default) -- SERIALIZABLE is not implemented. On the PostgreSQL wire protocol it is accepted -- with a warning and downgraded; on the HTTP SQL API it returns an error: SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- WARNING: TiKV provides snapshot isolation; SERIALIZABLE has been downgraded to REPEATABLE READ ``` ## Next Pages [Section titled “Next Pages”](#next-pages) * [SQL Reference](/docs/sql/) — detailed SQL syntax and function reference * [Architecture](/docs/architecture/) — how DB9’s SQL engine connects to TiKV storage * [Extensions](/docs/extensions/) — guide to all 9 built-in extensions * [Limits and Quotas](/docs/platform/limits-and-quotas/) — operational limits and safety boundaries * [Production Checklist](/docs/production-checklist/) — evaluate compatibility gaps before going live * [Migrate from Neon](/docs/migrations/from-neon/) — step-by-step Neon to DB9 migration * [Migrate from PostgreSQL](/docs/migrations/from-postgres/) — general PostgreSQL migration path # Limits and Quotas > All DB9 limits in one place — connections, queries, extensions, storage, branching, and account quotas. This page consolidates all operational limits, quotas, and safety boundaries across DB9. Most limits are fixed. Where a limit is configurable by the platform operator (not the end user), it is marked accordingly. ## Account Quotas [Section titled “Account Quotas”](#account-quotas) | Limit | Anonymous | Claimed | | ---------------------------------- | --------- | --------- | | Max databases (including branches) | 5 | Unlimited | Remove the 5-database limit Anonymous accounts are limited to 5 databases (including branches). Run `db9 claim` to upgrade to a verified account and remove this limit — no payment required. \| Bearer token TTL | 90 days (auto-refresh) | 90 days (re-login to renew) | | API token TTL | 365 days (default) | 365 days (default) | | Feature access | Full | Full | See [Anonymous and Claimed Databases](/docs/platform/anonymous-and-claimed-databases/) for the upgrade path. ## Connections [Section titled “Connections”](#connections) | Limit | Value | Notes | | --------------------------------- | ---------- | -------------------------------------------------------------------------- | | Max server connections | 1,000 | Configurable by operator | | Statement timeout | 60 seconds | Configurable by operator; per-session override via `SET statement_timeout` | | Idle-in-transaction timeout | 60 seconds | Configurable by operator | | Idle tenant cache eviction | 5 minutes | Inactive tenants are unloaded from memory | | Max advisory locks per connection | 4,096 | Configurable by operator | ## SQL and Query Limits [Section titled “SQL and Query Limits”](#sql-and-query-limits) | Limit | Value | | ---------------------------------- | ------------------------------------------------------------------ | | Max identifier length | 63 bytes | | Max recursive CTE iterations | 1,000 | | Max view expansion depth | 64 | | Max generate\_series rows | 1,000,000 | | Max NUMERIC precision | 1,000 | | Max NUMERIC display scale | 28 | | Max string output | \~1 GB | | Sort memory (`db9.max_sort_bytes`) | 128 MB default (configurable via GUC; `work_mem` defaults to 4 MB) | | DML table scan max rows | 10,000 default (configurable via GUC) | | DPCCP join reorder max relations | 8 | | Max COPY FROM STDIN line | 32 MB | ## Vector and Embedding [Section titled “Vector and Embedding”](#vector-and-embedding) | Limit | Value | | -------------------------------- | ---------------------------------------------- | | Max vector dimensions | 16,384 | | Default embedding dimensions | 1,024 | | Embedding model | `text-embedding-v4` (configurable by operator) | | Embedding request timeout | 30 seconds | | Embedding connect timeout | 5 seconds | | Embedding concurrency per tenant | 5 | | HNSW default M | 16 | | HNSW default ef\_construction | 64 | | HNSW default ef\_search | 40 | ## HTTP Extension [Section titled “HTTP Extension”](#http-extension) | Limit | Value | | ------------------------------ | ---------------------------------------- | | Max requests per SQL statement | 100 | | Concurrent requests per tenant | 20 (5 reserved for interactive queries) | | Request timeout | 5 seconds (1 second connect timeout) | | Max response body | 1 MB | | Max request body | 256 KB | | Max redirects | 3 | | HTTPS only | Yes (SSRF protection blocks private IPs) | SSRF protection The HTTP extension only allows outbound HTTPS requests to public IP addresses. Private IP ranges (RFC 1918: `10.x`, `172.16.x`, `192.168.x`) and loopback addresses are blocked to prevent Server-Side Request Forgery attacks. See [HTTP from SQL](/docs/guides/http-from-sql/) for details. ## fs9 File System [Section titled “fs9 File System”](#fs9-file-system) | Limit | Value | | --------------------------------- | ---------- | | Max file size | 100 MB | | Max total bytes per glob query | 100 MB | | Max files per glob | 10,000 | | Max directory entries (recursive) | 100,000 | | Max recursive depth | 10 | | WebSocket connections per tenant | 50 | | WebSocket auth timeout | 10 seconds | | WebSocket idle timeout | 5 minutes | | WebSocket max JSON frame | 2 MB | See [Analyze Agent Logs with fs9](/docs/guides/analyze-agent-logs-with-fs9/) for usage patterns. ## Parquet Import [Section titled “Parquet Import”](#parquet-import) | Limit | Value | | ----------------------------- | ---------- | | Global memory budget | 512 MB | | Concurrent imports per tenant | 4 | | Max file size (via fs9) | 100 MB | | HTTP fetch timeout | 60 seconds | | HTTP max redirects | 5 | ## pg\_cron Scheduled Jobs [Section titled “pg\_cron Scheduled Jobs”](#pg_cron-scheduled-jobs) | Limit | Value | | ---------------------------------- | ------------------------------------------- | | Max jobs per database | 50 | | Max concurrent executions (global) | 32 | | Poll interval | 60 seconds (minimum scheduling granularity) | | Default job timeout | 5 minutes (orphan timeout) | | Max job timeout (cron-specific) | 30 minutes | | Run history retention | 7 days | See [Scheduled Jobs with pg\_cron](/docs/guides/scheduled-jobs-with-pg-cron/) for details. ## Branching [Section titled “Branching”](#branching) | Limit | Value | | ------------------------------------ | ---------- | | Max concurrent branch creations | 2 | | Branch clone timeout | 5 minutes | | Branch clone max runtime | 10 minutes | | Branches count toward database quota | Yes | Branches count toward your database quota Every branch is a full database and counts against your account’s database limit. Anonymous accounts can quickly hit the 5-database cap if branches are not cleaned up after use. See [Branching Workflows](/docs/guides/branching-workflows/) for details. ## Connect Tokens [Section titled “Connect Tokens”](#connect-tokens) | Limit | Value | | --------- | ------------------------------------- | | TTL range | 5–15 minutes (default 10 minutes) | | Signing | RS256 (public keys at JWKS endpoint) | | Scope | `db:connect` (single database + role) | See [Security and Auth](/docs/platform/security-and-auth/) for the full auth model. ## Portal and Cursor Limits [Section titled “Portal and Cursor Limits”](#portal-and-cursor-limits) | Limit | Value | | ------------------------------------ | ------ | | Max suspended portals per connection | 32 | | Max suspended portal buffer rows | 10,000 | | Max suspended portal buffer bytes | 16 MB | These apply to PostgreSQL Extended Query protocol cursors and named portals. ## Observability [Section titled “Observability”](#observability) | Limit | Value | | ------------------------ | ------------------------------------ | | Slow query threshold | 200 ms (configurable by operator) | | Max sample events | 20,000 (configurable by operator) | | Max sample groups | 50 (configurable by operator) | | Max SQL length in traces | 512 bytes (configurable by operator) | | Audit log retention | 90 days | ## Not Currently Limited [Section titled “Not Currently Limited”](#not-currently-limited) These resources do not have explicit limits in the current release: * **Database size** — no per-database storage cap (bounded by TiKV cluster capacity) * **Table count** — no limit on tables per database * **Column count** — no per-table column limit beyond PostgreSQL conventions * **Row size** — no explicit row size limit * **Transaction size** — no limit on transaction data volume * **Query parameter count** — no explicit limit on bind parameters These may change in future releases. For production capacity planning, monitor TiKV cluster metrics. ## Operator-Configurable Limits [Section titled “Operator-Configurable Limits”](#operator-configurable-limits) Platform operators can adjust these via environment variables on the db9-server: | Variable | Default | Description | | -------------------------------------------- | ------------- | --------------------------------- | | `DB9_MAX_CONNECTIONS` | 1,000 | Max concurrent pgwire connections | | `DB9_STATEMENT_TIMEOUT_MS` | 60,000 | Default statement timeout | | `DB9_IDLE_IN_TRANSACTION_SESSION_TIMEOUT_MS` | 60,000 | Idle-in-transaction timeout | | `DB9_TENANT_MEMORY_QUOTA_BYTES` | 0 (unlimited) | Per-tenant memory quota | | `DB9_TENANT_QPS_LIMIT` | 0 (disabled) | Per-tenant queries per second | | `DB9_MAX_ADVISORY_LOCKS_PER_CONNECTION` | 4,096 | Advisory lock budget | Extension and worker limits are fixed and cannot be changed without redeploying the server. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Security and Auth](/docs/platform/security-and-auth/) — token types, roles, and auth boundaries * [Anonymous and Claimed Databases](/docs/platform/anonymous-and-claimed-databases/) — trial limits and upgrade * [Production Checklist](/docs/production-checklist/) — verify limits before going live * [SQL Reference](/docs/sql/) — SQL engine compatibility and features * [Extensions Overview](/docs/extensions/) — all 9 built-in extensions # Multi-Tenant Patterns > How to structure DB9 databases for multi-tenant workloads — database-per-user, database-per-app, ephemeral-per-task, and branch-per-preview. DB9 provisions databases in under a second, each fully isolated with its own TiKV keyspace, credentials, and resource quotas. This makes it practical to use separate databases as the unit of tenancy — something that is usually too expensive or slow with traditional Postgres hosting. This page compares the four main patterns for organizing tenant data in DB9 and helps you choose the right one for your workload. ## Mental model [Section titled “Mental model”](#mental-model) Every DB9 database is an isolated tenant. The server routes connections by parsing the tenant ID from the username (`{id}.admin`), resolves the tenant to a dedicated TiKV keyspace, and applies per-tenant resource limits (QPS, memory, connections) independently. DB9 uses database-level isolation, not schema-level There is no shared-schema multi-tenancy inside a single DB9 database. If you need strict tenant isolation, create separate databases. Schemas within a single DB9 database are not isolated from each other. There is no shared-schema multi-tenancy inside a single DB9 database — if you need tenant isolation, you create separate databases. Branches inherit the parent’s data at a point in time but run as independent tenants with their own keyspace. The four patterns below use this isolation primitive in different ways. ## Pattern 1: Database-per-user [Section titled “Pattern 1: Database-per-user”](#pattern-1-database-per-user) **What it is:** Each end user or customer gets their own database, named deterministically from their identity. **Best for:** SaaS platforms, per-user AI agents, personalized workspaces. TypeScript ```typescript import { instantDatabase } from 'get-db9'; const db = await instantDatabase({ name: `user-${userId}`, seed: ` CREATE TABLE preferences (key TEXT PRIMARY KEY, value JSONB); CREATE TABLE history (id SERIAL, action TEXT, ts TIMESTAMPTZ DEFAULT now()); `, }); // db.connectionString — ready to use // db.databaseId — 12-character tenant ID ``` **How it works:** * `instantDatabase()` checks for an existing database with the same name and returns it if found. No duplicates on restart. * The `seed` SQL runs only on first creation, so schema setup is idempotent. * Each user’s data is physically isolated — no row-level filtering, no shared pools. **Operational notes:** * **Naming convention matters.** Use a prefix like `user-` or `tenant-` so you can filter by name when listing or cleaning up. * **Credential rotation** is per-database. Use `db9 db reset-password ` or the SDK equivalent. * **Fleet cleanup** — list all databases and filter by prefix: TypeScript ```typescript const all = await client.databases.list(); const userDbs = all.filter(db => db.name.startsWith('user-')); ``` **Limits to consider:** * Anonymous accounts are limited to 5 databases. For production use, [claim your account](/docs/platform/provisioning/#anonymous-and-claimed-accounts) first. * Each database carries its own resource quotas (QPS, memory). Under heavy concurrent usage, monitor per-tenant resource consumption. ## Pattern 2: Database-per-app [Section titled “Pattern 2: Database-per-app”](#pattern-2-database-per-app) **What it is:** Each application, service, or environment gets one shared database. All users of that application share the same database. **Best for:** Monolithic apps, shared data models, internal tools, prototypes. TypeScript ```typescript import { instantDatabase } from 'get-db9'; const db = await instantDatabase({ name: 'myapp-production', seed: ` CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT UNIQUE); CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id)); `, }); ``` ▶ Run **How it works:** * One database per logical environment (dev, staging, production). * All application users connect through the same credentials. * Schema and data are shared — traditional application-level multi-tenancy applies if needed. **Operational notes:** * This is the simplest pattern. Start here if you don’t need per-user isolation. * You can still use DB9 [branching](/docs/platform/multi-tenant-patterns/#pattern-4-branch-per-preview) for safe testing against production data. * Resource limits (QPS, memory, connections) apply to the single database, so all users share the quota. **When this pattern breaks down:** * When you need hard isolation between customers for compliance or security. * When per-user resource limits matter — a noisy user affects everyone in the same database. ## Pattern 3: Ephemeral-per-task [Section titled “Pattern 3: Ephemeral-per-task”](#pattern-3-ephemeral-per-task) **What it is:** A database is created for a specific task (CI run, agent session, one-shot job) and deleted when the task finishes. **Best for:** CI/CD test isolation, agent task runners, batch processing, disposable sandboxes. ### CLI pattern [Section titled “CLI pattern”](#cli-pattern) Terminal ```bash # Create a database for this CI run RESULT=$(db9 create --name "ci-${BUILD_ID}" --show-connection-string --json) DB_ID=$(echo "$RESULT" | jq -r '.id') CONN=$(echo "$RESULT" | jq -r '.connection_string') # Run tests DATABASE_URL="$CONN" npm test # Clean up db9 delete "$DB_ID" --yes ``` ### SDK pattern [Section titled “SDK pattern”](#sdk-pattern) TypeScript ```typescript import { instantDatabase, createDb9Client } from 'get-db9'; const taskId = crypto.randomUUID(); const db = await instantDatabase({ name: `task-${taskId}`, seed: 'CREATE TABLE results (id SERIAL, data JSONB)', }); try { // ... do work ... } finally { const client = createDb9Client(); await client.databases.delete(db.databaseId); } ``` ▶ Run **Operational notes:** * **Always clean up.** DB9 does not automatically delete idle databases. Build cleanup into your task lifecycle. * **Naming conventions help cleanup.** If tasks crash before deletion, you can sweep stale databases by prefix and age: TypeScript ```typescript const client = createDb9Client(); const all = await client.databases.list(); const stale = all.filter(db => db.name.startsWith('ci-') && new Date(db.created_at) < oneDayAgo ); for (const db of stale) { await client.databases.delete(db.id); } ``` * **Anonymous account limit:** If running many concurrent tasks from an anonymous account, you may hit the 5-database limit. Claim your account or clean up between runs. ## Pattern 4: Branch-per-preview [Section titled “Pattern 4: Branch-per-preview”](#pattern-4-branch-per-preview) **What it is:** A branch is created from a parent database to test changes against a copy of real data, then deleted when the preview is done. **Best for:** Preview environments, schema migration testing, rollback validation, feature development. ### CLI pattern [Section titled “CLI pattern”](#cli-pattern-1) Terminal ```bash # Create a branch from the production database db9 branch create production --name "preview-pr-42" # Test migrations against the branch db9 db sql preview-pr-42 -f migration.sql # Verify db9 db sql preview-pr-42 -q "SELECT count(*) FROM users" # Clean up db9 delete preview-pr-42 --yes ``` ### REST API pattern [Section titled “REST API pattern”](#rest-api-pattern) Terminal ```bash # Create a branch via the REST API curl -X POST "https://api.db9.ai/customer/databases/${PARENT_DB_ID}/branch" \ -H "Authorization: Bearer $DB9_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "preview-pr-42"}' # Returns a DatabaseResponse with its own connection_string and ID ``` **How branching works:** * A branch creates a new database that starts with a snapshot of the parent’s data. * The branch runs as an independent tenant — writes to the branch do not affect the parent. * Branch creation transitions through `CREATING → CLONING → ACTIVE`. * Once active, the branch behaves exactly like any other database. * Branches count toward your database limit. **Operational notes:** * **No merge-back.** Branches are one-way copies. There is no built-in mechanism to merge branch changes back to the parent. * **No live sync.** The branch is a point-in-time snapshot. Changes to the parent after branching are not reflected. * **Use for validation, not long-lived environments.** Branches are best for short-lived testing. For persistent environments, use separate named databases. ## Choosing a pattern [Section titled “Choosing a pattern”](#choosing-a-pattern) | Concern | Database-per-user | Database-per-app | Ephemeral-per-task | Branch-per-preview | | --------------------- | --------------------- | ------------------- | ------------------- | --------------------- | | **Isolation** | Full | None (shared DB) | Full | Full (snapshot copy) | | **Setup cost** | One per user | One per environment | One per task | One per preview | | **Data lifecycle** | Persistent | Persistent | Disposable | Disposable | | **Cleanup needed** | On user deletion | On app shutdown | On task completion | On preview close | | **Schema management** | Per-database seeds | Shared migrations | Per-task seeds | Inherited from parent | | **Best scale** | Hundreds to thousands | One to a few | Hundreds concurrent | Tens concurrent | ### Mixing patterns [Section titled “Mixing patterns”](#mixing-patterns) Most production deployments combine two or more patterns: * **Database-per-user + branch-per-preview:** Each user has a persistent database; branches test schema changes before applying to user databases. * **Database-per-app + ephemeral-per-task:** The application uses one shared database, but CI runs use disposable databases for test isolation. * **Database-per-user + ephemeral-per-task:** Each user has a persistent database, but agent tasks create temporary databases for sandboxed work. ## Isolation and resource boundaries [Section titled “Isolation and resource boundaries”](#isolation-and-resource-boundaries) Every DB9 database — whether created directly, per-user, or as a branch — runs with these isolation properties: | Boundary | Scope | Detail | | --------------------- | ------------------------ | ------------------------------------------------------------- | | **TiKV keyspace** | Per-database | Each database writes to `db9_tenant_{id}` — no shared storage | | **Credentials** | Per-database | Each database has its own admin password | | **Connection limits** | Per-user within database | PostgreSQL `rolconnlimit` enforced per user | | **QPS limit** | Per-database | Token-bucket rate limiter (configurable, disabled by default) | | **Memory quota** | Per-database | Per-statement memory accounting with RAII cleanup | | **Idle eviction** | Per-database | Tenant state evicted from server memory after 5 minutes idle | **No cross-database queries.** Each database is a separate tenant. There is no `dblink`, foreign data wrapper, or cross-database join support. ## Constraints and boundaries [Section titled “Constraints and boundaries”](#constraints-and-boundaries) * **No shared-schema multi-tenancy.** DB9 does not support schema-per-tenant or row-level security within a single database as a tenancy mechanism. Use separate databases instead. * **No database rename.** Names are permanent. Choose naming conventions carefully. * **No region migration.** Databases cannot be moved between regions after creation. * **Branch limits.** Branches count toward your database limit. Clean up unused branches to stay within quota. * **No merge-back from branches.** Branch changes cannot be automatically applied to the parent database. * **Idle eviction is server-side only.** A database’s in-memory state (connection pool, caches) is evicted after 5 minutes of inactivity. The data persists in TiKV — only the server-side tenant handle is released. ## Next steps [Section titled “Next steps”](#next-steps) * [Provisioning](/docs/platform/provisioning/) — how databases are created, listed, and deleted * [Architecture](/docs/architecture/) — how tenant isolation works at the TiKV and pgwire level * [Agent Workflows](/docs/agent-workflows/overview/) — how provisioning and tenancy fit the agent lifecycle * [Production Checklist](/docs/production-checklist/) — secrets, auth, and operational readiness * [Anonymous and Claimed Databases](/docs/platform/anonymous-and-claimed-databases/) — trial-first usage and account upgrade paths (coming soon) * [Branching Workflows](/docs/guides/branching-workflows/) — deeper guide on preview, rollback, and task-isolation flows (coming soon) # Observability > What you can see in DB9 today — query sampling, slow query detection, latency percentiles, schema introspection, and what's not yet available. DB9 includes built-in observability that is always on. Query performance is sampled automatically, slow queries are always captured, and the data is available through the CLI, REST API, and direct SQL — no extensions to install. This page covers what you can observe, how to access it, and where the current boundaries are compared to standard PostgreSQL. ## What You Can See [Section titled “What You Can See”](#what-you-can-see) | Metric | Available | Access | | ------------------------------- | --------- | ------------- | | QPS and TPS | Yes | CLI, API, SQL | | Latency (avg, p99) | Yes | CLI, API, SQL | | Active connections (count) | Yes | CLI, API, SQL | | Query samples with latency | Yes | CLI, API, SQL | | Slow queries (p99-sorted) | Yes | CLI | | Error count and failed queries | Yes | CLI, API, SQL | | Write-conflict retries (TiKV) | Yes | SQL | | HNSW index build metrics | Yes | SQL | | EXPLAIN query plans | Yes | SQL | | Schema, tables, indexes | Yes | CLI, SQL | | Per-connection details | No | — | | Index usage stats | No | — | | Memory/cache stats | No | — | | Prometheus/OpenTelemetry export | No | — | ## CLI: db9 db inspect [Section titled “CLI: db9 db inspect”](#cli-db9-db-inspect) The primary observability tool is `db9 db inspect`: Terminal ```bash # Summary dashboard (QPS, TPS, latency, connections, errors) db9 db inspect # Query samples with latency breakdown db9 db inspect queries # Combined summary + queries db9 db inspect report # Top slow queries sorted by p99 latency db9 db inspect slow-queries ``` ### Schema introspection [Section titled “Schema introspection”](#schema-introspection) Terminal ```bash # List schemas, tables, and indexes db9 db inspect schemas db9 db inspect tables db9 db inspect indexes ``` All commands support `--json` and `--output csv` for programmatic use. ### Example output [Section titled “Example output”](#example-output) Terminal ```bash db9 db inspect mydb ``` Output ```text Summary (60-minute window) QPS: 41.7 TPS: 20.8 Latency avg: 12.5 ms Latency p99: 45.2 ms Connections: 8 Statements: 150,000 Commits: 75,000 Errors: 3 ``` ## SQL: System Functions [Section titled “SQL: System Functions”](#sql-system-functions) Two built-in table functions provide observability data directly in SQL: ### Summary metrics [Section titled “Summary metrics”](#summary-metrics) SQL ```sql SELECT qps, tps, latency_avg_ms, latency_p99_ms, active_connections, statement_count, txn_commit_count, error_count FROM _db9_sys_observability(); ``` ▶ Run Returns a single row with the rolling 60-minute summary. Additional columns include write-conflict retry counts (`retry_attempts`, `retry_budget_exhausted`, `retry_timeout_aborts`) and HNSW index metrics (`hnsw_graph_bytes_written`, `hnsw_serialize_duration_us`). ### Query samples [Section titled “Query samples”](#query-samples) SQL ```sql SELECT query, sample_count, error_count, latency_avg_ms, latency_p99_ms, latency_max_ms, last_seen_ms_ago FROM _db9_sys_query_samples() ORDER BY latency_p99_ms DESC LIMIT 10; ``` ▶ Run Returns per-query aggregates for sampled queries in the current window. Up to 50 unique query groups are tracked. ### Find failing queries [Section titled “Find failing queries”](#find-failing-queries) SQL ```sql SELECT query, error_count, sample_count FROM _db9_sys_query_samples() WHERE error_count > 0 ORDER BY error_count DESC; ``` ▶ Run ## REST API [Section titled “REST API”](#rest-api) For automation, call the observability endpoint directly: Terminal ```bash curl -s "https://api.db9.ai/customer/databases//observability" \ -H "Authorization: Bearer $TOKEN" | jq . ``` Returns JSON with `summary` (QPS, TPS, latency, connections) and `samples` (per-query metrics). ## EXPLAIN [Section titled “EXPLAIN”](#explain) DB9 supports `EXPLAIN` for query plan inspection: SQL ```sql EXPLAIN SELECT * FROM users WHERE email = 'test@example.com'; ``` Plan nodes include SeqScan, IndexScan, HnswScan, NestedLoop, HashJoin, Sort, Limit, Aggregate, and more. `EXPLAIN ANALYZE` executes the query and appends summary runtime statistics to the plan — actual row count, execution time, and KV scan counters. Per-operator timing is not yet available. `FORMAT JSON` is supported and returns a PostgreSQL-compatible plan document: SQL ```sql EXPLAIN (FORMAT JSON) SELECT 1; -- [{"Plan":{"Node Type":"Result","Parallel Aware":false,"Async Capable":false, -- "One-Time Filter":"false","Startup Cost":0.0,"Total Cost":0.01, -- "Plan Rows":1,"Plan Width":0}}] ``` Combining it with `ANALYZE` adds `Actual Rows`, `Actual Loops`, and `Execution Time` to the document. EXPLAIN output requires a direct connection `db9 db sql`, the SDK’s `sql()` method, and the runnable examples on this site currently return an empty result for `EXPLAIN` statements. To see plan output, generate a DSN with `db9 db connect ` and run `EXPLAIN` from any Postgres client. ## How Sampling Works [Section titled “How Sampling Works”](#how-sampling-works) DB9 collects observability data using in-memory sampling: * **Window**: 60-minute rolling window * **Default rate**: 1 in 1,000 queries sampled (0.1%) * **Always captured**: errors and queries exceeding the slow threshold (200 ms default) * **Max query groups**: 50 unique query fingerprints * **Max sample events**: 20,000 in memory * **SQL normalization**: whitespace reduced, text truncated to 512 characters * **Redaction**: `PASSWORD` literals replaced with `'***'` This is process-level, in-memory data. It is not persisted to TiKV and resets when the server restarts. ## Database Status [Section titled “Database Status”](#database-status) For database metadata (not performance metrics), use: Terminal ```bash db9 db status ``` Shows database name, ID, state (`ACTIVE`, `CLONING`, `CREATE_FAILED`), region, creation time, endpoints, and connection string. ## PostgreSQL Compatibility [Section titled “PostgreSQL Compatibility”](#postgresql-compatibility) DB9 implements some `pg_catalog` views as stubs for tool compatibility: | View | Status | Notes | | --------------------------- | ------------- | --------------------------------------------------- | | `information_schema.tables` | Functional | Standard schema introspection | | `pg_indexes` | Functional | Index definitions | | `pg_stat_user_tables` | Stub (zeros) | All counters return 0 | | `pg_statistic_ext` | Stub (empty) | No extended statistics | | `pg_stat_statements` | Not available | Use `_db9_sys_query_samples()` | | `pg_stat_activity` | Not available | Use `_db9_sys_observability()` for connection count | Tools like Prisma, Drizzle, and psql that query `information_schema` and `pg_indexes` work normally. Tools that depend on `pg_stat_statements` or `pg_stat_activity` for monitoring need to use DB9’s native functions instead. ## What Is Not Available Today [Section titled “What Is Not Available Today”](#what-is-not-available-today) * **Per-connection activity** — only aggregate connection count, no per-session details * **Index usage statistics** — `pg_stat_user_tables.idx_scan` returns 0 * **Memory and cache metrics** — shared buffers, work memory usage not exposed * **Slow query log to file** — observability data is query-based only, not logged to disk * **Prometheus/OpenTelemetry export** — no `/metrics` endpoint or trace export No external metrics export yet DB9 does not currently export metrics to Prometheus, Datadog, or OpenTelemetry. Use `db9 db inspect` as your primary observability tool and build alerting around application-level metrics in your own monitoring stack. * **Full query text with parameters** — only normalized SQL stored (bind values not captured) * **EXPLAIN ANALYZE per-operator timing** — summary runtime stats are reported (actual rows, execution time, KV scan counters), but not per-operator timing ## Storage Metrics [Section titled “Storage Metrics”](#storage-metrics) DB9 provides built-in storage accounting through virtual tables: SQL ```sql -- Database-level storage breakdown SELECT database_name, data_bytes, index_bytes, total_bytes FROM _DB9_SYS_STORAGE_STATS; -- Per-table storage breakdown SELECT table_name, data_bytes, index_bytes, total_bytes FROM _DB9_SYS_TABLE_STORAGE_STATS ORDER BY total_bytes DESC; ``` ▶ Run Storage stats are automatically refreshed every 30 minutes. Manual refresh via `db9_refresh_storage_stats()` is **not available in the current release** — it returns `ERROR: feature "storage_size_scan" is unavailable (PreActivationSeal)` (`55000`). See [Storage Accounting](/docs/platform/storage/) for full details. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Storage Accounting](/docs/platform/storage/) — database and table storage usage * [Limits and Quotas](/docs/platform/limits-and-quotas/) — all operational limits in one place * [Security and Auth](/docs/platform/security-and-auth/) — token types and role model * [Production Checklist](/docs/production-checklist/) — verify observability before going live * [CLI Reference](/docs/cli/) — `db9 db inspect` command reference * [SQL Reference](/docs/sql/) — SQL engine compatibility # Provisioning > How to create, manage, and delete DB9 databases programmatically — through the CLI, TypeScript SDK, or REST API. DB9 databases are created synchronously and return a usable connection string in under a second. This makes DB9 suitable for workflows where databases are created on demand — per agent, per user, per task, or per CI run. This page explains the provisioning model: how databases are created, what metadata they carry, how their lifecycle works, and how to manage fleets of databases programmatically. ## Mental model [Section titled “Mental model”](#mental-model) A DB9 database is an isolated tenant backed by a dedicated TiKV keyspace. Creating a database provisions the keyspace, bootstraps an admin user, installs default extensions (`http` and `pg_cron`), and returns connection credentials — all in a single synchronous API call. Each database is identified by a 12-character opaque ID (e.g., `t1a2b3c4d5e6`) and an optional human-readable name. The ID is permanent. The name must be unique within your account. ## Creating a database [Section titled “Creating a database”](#creating-a-database) ### CLI [Section titled “CLI”](#cli) Terminal ```bash # Minimal — auto-generates a name like "brave-tiger-42" db9 create # Named db9 create --name my-agent-db # With region hint db9 create --name my-agent-db --region us-east # Show connection string in output db9 create --name my-agent-db --show-connection-string ``` Available flags: | Flag | Description | | -------------------------- | ------------------------------------------------------------------- | | `--name ` | Human-readable name (auto-generated if omitted) | | `--region ` | Region hint (stored as metadata) | | `--password ` | Set a specific admin password (random if omitted) | | `--show-password` | Print admin password in output | | `--show-connection-string` | Print the full PostgreSQL DSN | | `--show-secrets` | Shorthand for both `--show-password` and `--show-connection-string` | | `--output json` | Machine-readable JSON output | ### TypeScript SDK [Section titled “TypeScript SDK”](#typescript-sdk) The SDK offers two creation patterns: **`instantDatabase()` — idempotent, agent-friendly:** TypeScript ```typescript import { instantDatabase } from 'get-db9'; const db = await instantDatabase({ name: 'agent-workspace', seed: ` CREATE TABLE context (id SERIAL, key TEXT, value JSONB); CREATE TABLE artifacts (id SERIAL, path TEXT, content TEXT); `, }); console.log(db.databaseId); // "t1a2b3c4d5e6" console.log(db.connectionString); // "postgresql://..." console.log(db.adminUser); // "admin" ``` ▶ Run `instantDatabase()` checks for an existing database with the same name and returns it if found. If no match exists, it creates a new one. This makes agent restarts safe — the same code path won’t duplicate databases. **Note:** The `seed` SQL only runs when a new database is created. If an existing database is returned, the seed is skipped. Options: | Option | Type | Description | | ------------ | -------- | -------------------------------------------- | | `name` | `string` | Database name (default: `'default'`) | | `seed` | `string` | SQL to execute after creation | | `seedFile` | `string` | SQL file content to execute after creation | | `baseUrl` | `string` | API base URL (default: `https://api.db9.ai`) | | `timeout` | `number` | Request timeout in milliseconds | | `maxRetries` | `number` | Retry count for transient failures | **`databases.create()` — direct API call:** TypeScript ```typescript import { createDb9Client } from 'get-db9'; const client = createDb9Client(); const db = await client.databases.create({ name: 'my-agent-db' }); ``` ▶ Run The create request accepts `name` (required), `region` (optional), and `admin_password` (optional). ### REST API [Section titled “REST API”](#rest-api) Terminal ```bash curl -X POST https://api.db9.ai/customer/databases \ -H "Authorization: Bearer $DB9_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "my-agent-db"}' ``` Response (HTTP 201): JSON ```json { "id": "t1a2b3c4d5e6", "name": "my-agent-db", "state": "ACTIVE", "admin_user": "admin", "admin_password": "generated-password", "created_at": "2025-01-15T10:30:00Z", "connection_string": "postgresql://t1a2b3c4d5e6.admin@pg.db9.io:5433/postgres" } ``` ## Database metadata [Section titled “Database metadata”](#database-metadata) Every database carries these fields: | Field | Description | | -------------------- | ------------------------------------------------------------------------ | | `id` | 12-character opaque identifier, permanent | | `name` | Human-readable name, unique per account | | `state` | Current lifecycle state (see below) | | `state_reason` | Error message when state is not `ACTIVE` | | `admin_user` | Always `"admin"` | | `admin_password` | Plaintext password (only returned at create time or after reset) | | `connection_string` | PostgreSQL DSN | | `region` | Region hint as supplied at creation | | `endpoints` | Host, port, and type for each endpoint (only on single-database queries) | | `created_at` | RFC 3339 timestamp | | `parent_database_id` | Source database ID (branches only) | | `snapshot_at` | Snapshot timestamp (branches only) | **Connection string format:** `postgresql://{id}.admin@{host}:{port}/postgres` The database ID is embedded in the username (`{id}.admin`), which is how the server routes connections to the correct tenant. ## Lifecycle states [Section titled “Lifecycle states”](#lifecycle-states) A database transitions through these states: | State | Meaning | Terminal? | | --------------- | --------------------------- | ----------------------------------------------- | | `CREATING` | Keyspace being provisioned | No — transitions to `ACTIVE` or `CREATE_FAILED` | | `CLONING` | Branch copy in progress | No — transitions to `ACTIVE` or `CREATE_FAILED` | | `ACTIVE` | Normal operating state | No | | `DISABLING` | Delete in progress | No — transitions to `DISABLED` | | `DISABLED` | Deleted | Yes | | `CREATE_FAILED` | Provisioning failed | Yes | | `SUSPENDED` | Operator-imposed suspension | Yes (requires operator action to resume) | **Normal lifecycle:** `CREATING → ACTIVE → DISABLING → DISABLED` **Branch lifecycle:** `CREATING → CLONING → ACTIVE → DISABLING → DISABLED` **Recovery:** A background reconciler recovers databases stuck in `CREATING` or `DISABLING` for more than 10 minutes. If recovery fails, the database moves to `CREATE_FAILED`. `DISABLED` databases are excluded from list responses. ## Anonymous and claimed accounts [Section titled “Anonymous and claimed accounts”](#anonymous-and-claimed-accounts) DB9 supports a trial-first model where databases can be created without signing up. **Anonymous accounts:** * Created automatically on first `db9 create` when no credentials are present * Limited to **5 databases** (including branches) * Receive a 90-day bearer token * Can be refreshed using the stored anonymous credentials **Claiming an account:** * Run `db9 claim` to upgrade via Auth0 SSO * The database limit is removed after claiming * All existing databases are retained under the claimed account If an anonymous account hits the 5-database limit, the CLI returns an error directing you to run `db9 claim`. → *Deeper guide: [Anonymous and Claimed Databases](/docs/platform/anonymous-and-claimed-databases/) (coming soon)* ## Listing databases [Section titled “Listing databases”](#listing-databases) ### CLI [Section titled “CLI”](#cli-1) Terminal ```bash # Table output db9 list # JSON output db9 list --json ``` ▶ Run Table columns: `ID`, `NAME`, `STATE`, `REGION`, `CREATED`. ### SDK [Section titled “SDK”](#sdk) TypeScript ```typescript const databases = await client.databases.list(); // Returns DatabaseResponse[] — excludes DISABLED databases ``` ### REST API [Section titled “REST API”](#rest-api-1) Terminal ```bash curl https://api.db9.ai/customer/databases \ -H "Authorization: Bearer $DB9_TOKEN" ``` ## Getting database details [Section titled “Getting database details”](#getting-database-details) A single-database query returns the full metadata including endpoints and connection string: Terminal ```bash db9 db status my-agent-db ``` Or via the SDK: TypeScript ```typescript const db = await client.databases.get('t1a2b3c4d5e6'); console.log(db.endpoints); // [{ host, port, type, enabled }] console.log(db.connectionString); ``` ## Deleting databases [Section titled “Deleting databases”](#deleting-databases) ### CLI [Section titled “CLI”](#cli-2) Terminal ```bash # Interactive confirmation db9 delete my-agent-db # Skip confirmation db9 delete my-agent-db --yes ``` ### SDK [Section titled “SDK”](#sdk-1) TypeScript ```typescript await client.databases.delete('t1a2b3c4d5e6'); ``` Deletion transitions the database through `DISABLING → DISABLED`. Once disabled, the database is excluded from list results and its TiKV keyspace is cleaned up. ## Credential management [Section titled “Credential management”](#credential-management) **Password reset:** Terminal ```bash db9 db reset-password my-agent-db ``` Returns a new random password and updated connection string. **Connect tokens** (short-lived, for sharing or CI): Terminal ```bash db9 db connect my-agent-db ``` Returns a token usable as `PGPASSWORD` with `psql`. Connect tokens expire in 10 minutes by default. **SDK equivalent for credentials:** TypeScript ```typescript const creds = await client.databases.credentials('t1a2b3c4d5e6'); ``` The SDK does not yet publish a native `connectToken()` method — call the REST endpoint directly, or use the CLI as shown above: TypeScript ```typescript const res = await fetch('https://api.db9.ai/customer/databases/t1a2b3c4d5e6/connect-token', { method: 'POST', headers: { Authorization: `Bearer ${process.env.DB9_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ role: 'admin' }), }); const token = await res.json(); ``` ## Fleet patterns [Section titled “Fleet patterns”](#fleet-patterns) When managing many databases programmatically, consider these patterns: ### Database-per-agent [Section titled “Database-per-agent”](#database-per-agent) Each agent instance creates its own database on startup and uses it for the duration of its lifecycle. Use `instantDatabase()` with a deterministic name derived from the agent’s identity. TypeScript ```typescript const db = await instantDatabase({ name: `agent-${agentId}`, seed: 'CREATE TABLE state (key TEXT PRIMARY KEY, value JSONB)', }); ``` ### Database-per-user [Section titled “Database-per-user”](#database-per-user) Multi-tenant applications create a database for each end user. Name databases with a user identifier to make them idempotent. TypeScript ```typescript const db = await instantDatabase({ name: `user-${userId}` }); ``` ### Ephemeral databases [Section titled “Ephemeral databases”](#ephemeral-databases) CI pipelines or one-shot tasks create a database, run their work, and delete it: Terminal ```bash ID=$(db9 create --name "ci-run-$BUILD_ID" --json | jq -r '.id') # ... run tests against the database ... db9 delete "$ID" --yes ``` ### Fleet listing and cleanup [Section titled “Fleet listing and cleanup”](#fleet-listing-and-cleanup) Use the SDK or CLI to list and clean up databases that match a naming pattern: TypeScript ```typescript const all = await client.databases.list(); const stale = all.filter(db => db.name.startsWith('ci-run-') && new Date(db.created_at) < oneDayAgo ); for (const db of stale) { await client.databases.delete(db.id); } ``` ## Constraints and boundaries [Section titled “Constraints and boundaries”](#constraints-and-boundaries) * **Name uniqueness:** Database names must be unique within an account. Duplicate names return HTTP 409. * **Anonymous limit:** 5 databases per anonymous account (including branches). Claim to remove the limit. * **No rename:** Database names cannot be changed after creation. * **No region migration:** The region hint is metadata only — databases cannot be moved between regions after creation. * **Synchronous creation:** Database creation blocks until the keyspace is provisioned. This typically completes in under a second, but can take longer under load. * **State recovery:** Databases stuck in `CREATING` or `DISABLING` for more than 10 minutes are automatically recovered by a background reconciler. ## Next steps [Section titled “Next steps”](#next-steps) * [Connect](/docs/connect/) — connection strings, TLS, and authentication options * [Agent Workflows](/docs/agent-workflows/overview/) — how provisioning fits into the full agent lifecycle * [Multi-Tenant Patterns](/docs/platform/multi-tenant-patterns/) — database-per-user, database-per-app, ephemeral-per-task, and branch-per-preview * [Anonymous and Claimed Databases](/docs/platform/anonymous-and-claimed-databases/) — trial-first usage and account upgrade paths (coming soon) * [Recovery and Branch Lifecycle](/docs/platform/recovery-and-branch-lifecycle/) — database states, branch phases, deletion, and disaster recovery expectations * [Production Checklist](/docs/production-checklist/) — secrets, auth, and operational readiness * [TypeScript SDK](/docs/sdk/) — full SDK reference for `instantDatabase()` and `createDb9Client()` # Recovery and Branch Lifecycle > Database states, branch lifecycle, deletion behavior, automatic recovery, and what DB9 does and does not provide for backup and disaster recovery. DB9 databases move through a defined set of states from creation to deletion. Branches follow a similar lifecycle with additional clone-specific phases. This page explains the state machine, what happens when things fail, how automatic recovery works, and where the boundaries are for backup and disaster recovery. ## Database States [Section titled “Database States”](#database-states) Every database is in exactly one of these states: | State | Meaning | | --------------- | -------------------------------------------------------------------------- | | `CREATING` | Keyspace is being provisioned in TiKV and admin user is being bootstrapped | | `ACTIVE` | Database is ready for connections | | `CLONING` | Branch clone is in progress (from a parent database) | | `DISABLING` | Deletion is in progress; keyspace is being disabled | | `DISABLED` | Deleted; keyspace has been removed from the cluster | | `CREATE_FAILED` | Provisioning or clone failed; terminal state | ### Normal transitions [Section titled “Normal transitions”](#normal-transitions) Output ```text Creation: CREATING → ACTIVE Branching: CLONING → ACTIVE Deletion: ACTIVE → DISABLING → DISABLED Failure: CREATING → CREATE_FAILED CLONING → CREATE_FAILED ``` `DISABLED` and `CREATE_FAILED` are terminal states. A database in either state cannot be recovered or reactivated. ### Check database state [Section titled “Check database state”](#check-database-state) Terminal ```bash db9 db status ``` Returns the database name, ID, state, region, creation time, endpoints, and connection string. For branches, also shows the parent database ID. ## Branch Lifecycle [Section titled “Branch Lifecycle”](#branch-lifecycle) Branches are independent databases created from a point-in-time snapshot of a parent. Once created, a branch has its own keyspace, credentials, and lifecycle — it does not share storage with the parent. ### How branch creation works [Section titled “How branch creation works”](#how-branch-creation-works) 1. **Concurrency check** — at most 2 branches can be created concurrently (across your account). Additional requests are rejected with HTTP 429. 2. **Snapshot capture** — the state of the parent database is captured as a timestamp. By default this is the current time; pass `--snapshot-at ` to use a specific past timestamp. 3. **Keyspace creation** — a new TiKV keyspace is provisioned for the branch. 4. **Data transfer** — data is copied from the parent to the new keyspace using one of two methods: * **TiKV restore**: point-in-time snapshot restore at the storage level (used when the TiKV restore API is available) * **Logical clone**: SQL-level dump and restore (fallback) 5. **Finalization** — admin credentials are set, extensions are verified, and the branch moves to `ACTIVE`. During this process, the branch is in `CLONING` state. ### Branch phases [Section titled “Branch phases”](#branch-phases) While in `CLONING`, the branch progresses through internal phases visible in the API response: | Phase | Description | | -------------------- | ----------------------------------------------------- | | `PREPARING` | Setting up source connection and metadata | | `KEYSPACE_READY` | Target keyspace created (logical clone path) | | `RESTORE_SUBMITTING` | Submitting restore task to TiKV | | `RESTORE_RUNNING` | TiKV restore in progress | | `FINALIZING` | Post-restore cleanup and credential setup | | `VERIFYING` | Data integrity checks | | `SUCCEEDED` | Clone completed; database transitions to `ACTIVE` | | `FAILED` | Clone failed; database transitions to `CREATE_FAILED` | ### Poll for completion [Section titled “Poll for completion”](#poll-for-completion) Branch creation is asynchronous. Poll the database status until the state changes from `CLONING`: Terminal ```bash # Check branch status db9 db status ``` TypeScript ```typescript // SDK polling const branch = await client.databases.branch(parentId, { name: 'feature-test' }); let status = await client.databases.get(branch.id); while (status.state === 'CLONING') { await new Promise(r => setTimeout(r, 2000)); status = await client.databases.get(branch.id); } // status.state is now ACTIVE or CREATE_FAILED ``` ### Parent-branch relationship [Section titled “Parent-branch relationship”](#parent-branch-relationship) * Branches store a reference to their parent database ID * The parent can be deleted while branches still exist — branches become orphaned but continue to function normally * Orphaned branches retain all their data and can be used and deleted independently * There is no automatic cascade delete; deleting a parent does not delete its branches ## Database Deletion [Section titled “Database Deletion”](#database-deletion) Deletion is permanent and cannot be undone. ### CLI [Section titled “CLI”](#cli) Terminal ```bash # Interactive confirmation db9 delete # Skip confirmation (CI/automation) db9 delete --yes ``` ### What happens during deletion [Section titled “What happens during deletion”](#what-happens-during-deletion) 1. The database state changes to `DISABLING` 2. The TiKV keyspace is disabled via the PD API 3. The database state changes to `DISABLED` 4. The database remains in the metadata store (for audit trail) but is no longer accessible If the keyspace disable fails, the database rolls back to its previous state. You can retry the deletion. ### Deleting databases in non-terminal states [Section titled “Deleting databases in non-terminal states”](#deleting-databases-in-non-terminal-states) | State | Can delete? | Notes | | --------------- | ----------- | ------------------------------------------------ | | `ACTIVE` | Yes | Standard deletion | | `CLONING` | Yes | Cancels the in-progress branch job, then deletes | | `CREATE_FAILED` | Yes | Cleans up the failed provisioning | | `DISABLING` | No | Already being deleted | | `DISABLED` | No | Already deleted | ## Automatic Recovery [Section titled “Automatic Recovery”](#automatic-recovery) DB9 runs a background reconciler that detects and recovers databases stuck in intermediate states. This handles cases like process crashes during provisioning or network interruptions during branch creation. ### What the reconciler does [Section titled “What the reconciler does”](#what-the-reconciler-does) | Stuck state | Recovery action | | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | | `CREATING` for >10 minutes | Checks if keyspace exists in TiKV; transitions to `ACTIVE` if it does, `CREATE_FAILED` if it does not | | `CLONING` for >10 minutes (no active branch job) | Disables the orphaned keyspace; transitions to `CREATE_FAILED` | | `DISABLING` for >10 minutes | Force-disables the keyspace; transitions to `DISABLED` | The reconciler runs every 5 minutes. It also cleans up orphaned branch keyspaces in TiKV that belong to databases in terminal states (`CREATE_FAILED` or `DISABLED`). ### Branch job recovery [Section titled “Branch job recovery”](#branch-job-recovery) Branch clone jobs use a distributed lease system for coordination across multiple backend workers: * Each job holds a lease that must be periodically renewed * If a worker crashes, the lease expires and another worker picks up the job * Transient errors (network timeouts, temporary unavailability) are retried automatically * Permanent errors move the job to `FAILED` and the branch to `CREATE_FAILED` ## Backup and Disaster Recovery [Section titled “Backup and Disaster Recovery”](#backup-and-disaster-recovery) ### What DB9 provides [Section titled “What DB9 provides”](#what-db9-provides) * **Point-in-time branching** — you can create a branch from any active database. By default this captures the current state; with `--snapshot-at` you can branch from a specific past timestamp (requires TiKV snapshot restore). * **TiKV durability** — data is stored in TiKV with replication across the cluster. Individual node failures do not cause data loss. * **Automatic stuck-state recovery** — the reconciler handles process-level failures during provisioning and deletion. ### What DB9 does not provide [Section titled “What DB9 does not provide”](#what-db9-does-not-provide) No automated backup or PITR — plan manually DB9 has a [backup REST API](/docs/api/#backups--restores), but it is not yet generally available — it requires backend infrastructure configuration and currently returns 503 on the shared production environment. There is no WAL archiving or automated point-in-time recovery. Branching is the primary built-in snapshot mechanism. Create a branch before any risky operation (migration, bulk delete) and maintain application-level exports for critical data. * **User-initiated backups** — there is no CLI backup command. Branching is the primary way to capture a point-in-time copy. See the [API reference](/docs/api/#backups--restores) for the REST backup endpoints once they reach GA. * **Point-in-time recovery (PITR)** — you can branch from a past timestamp using `db9 branch create --name rollback --snapshot-at `, but this creates a new branch rather than restoring the original database in-place. Full in-place PITR is not supported. * **Cross-region replication** — databases exist in a single region. There is no built-in geo-replication. * **Backup export** — there is no way to export a backup file (pg\_dump equivalent) from DB9. Use `db9 db dump` for logical SQL export, but this is a live dump, not a consistent snapshot. * **Retention policies** — deleted databases cannot be recovered. There is no soft-delete window or trash/recycle bin. Deleted databases are permanently gone There is no recycle bin or soft-delete. Once you delete a database or branch, it is gone immediately and cannot be restored. Always create a branch snapshot before deleting anything in production. * **WAL archiving** — TiKV does not expose a WAL archive interface. Standard PostgreSQL backup tools (pg\_basebackup, pgBackRest) are not compatible. ### Practical recovery strategies [Section titled “Practical recovery strategies”](#practical-recovery-strategies) 1. **Regular branching** — create periodic branches as checkpoints. Each branch is a full, independent copy of the database at the time of creation. 2. **Logical export** — run `db9 db dump ` to export SQL that can be used to recreate the schema and data in another database. 3. **Application-level backup** — for critical data, write periodic exports to an external system (S3, another database) using the HTTP extension or application code. ## Lifecycle Limits [Section titled “Lifecycle Limits”](#lifecycle-limits) | Limit | Value | | ------------------------------- | ---------- | | Max concurrent branch creations | 2 | | Branch clone timeout (per step) | 5 minutes | | Branch clone max runtime | 10 minutes | | Reconciler cycle interval | 5 minutes | | Stuck-state recovery threshold | 10 minutes | | Audit log retention | 90 days | See [Limits and Quotas](/docs/platform/limits-and-quotas/) for the complete list. ## Next Pages [Section titled “Next Pages”](#next-pages) * [Provisioning](/docs/platform/provisioning/) — database creation and fleet management * [Branching Workflows](/docs/guides/branching-workflows/) — practical branch patterns for CI, preview, and isolation * [Limits and Quotas](/docs/platform/limits-and-quotas/) — all operational limits * [Observability](/docs/platform/observability/) — monitoring database health and performance * [Production Checklist](/docs/production-checklist/) — verify recovery strategy before going live # Security and Auth > DB9's authentication layers, token types, role model, credential storage, and security boundaries — what is protected, how, and what is not. DB9 has multiple authentication layers: API-level bearer tokens for managing databases, database-level roles for SQL access, and short-lived connect tokens for secure credential handoff. This page explains each layer, how they interact, and where the current boundaries are. ## Auth Model at a Glance [Section titled “Auth Model at a Glance”](#auth-model-at-a-glance) | Layer | Mechanism | Lifetime | Purpose | | ------------------ | ------------------------------------------ | ------------------------------ | ------------------------------------------ | | Customer API token | Bearer token (128-char hex) | 365 days (configurable) | Manage databases, users, branches, tokens | | Anonymous token | Bearer token (auto-refreshed) | 90 days (renewable) | Trial access before SSO claim | | API key login | `DB9_API_KEY` env or `db9 login --api-key` | Same as token | CI/CD and headless automation | | Connect token | JWT (RS256-signed) | 5–15 minutes | Short-lived database connection credential | | Connect key | `db9ck_`-prefixed key | Configurable (up to permanent) | Long-lived programmatic database access | | Database password | SCRAM-SHA-256 | Permanent until reset | Direct pgwire authentication | ## API Authentication [Section titled “API Authentication”](#api-authentication) All database management goes through the customer API, protected by bearer tokens. ### Bearer tokens [Section titled “Bearer tokens”](#bearer-tokens) Every API request requires an `Authorization: Bearer ` header. Tokens are: * 128-character hex strings (64 random bytes) * Stored as SHA-256 hashes in the metadata database (one-way; cannot be recovered) * Created with an expiry (default: 365 days) Terminal ```bash # Login via SSO (browser-based Auth0 device flow) db9 login # Login with a pre-created API key (headless) db9 login --api-key # Check current auth state db9 status ``` ### Named API tokens [Section titled “Named API tokens”](#named-api-tokens) Create additional tokens for automation, CI/CD, or team members: Terminal ```bash # Via CLI (uses the SDK under the hood) db9 db connect ``` Tokens can be listed and revoked through the API. The token value is only returned on creation — store it securely. ### Anonymous tokens [Section titled “Anonymous tokens”](#anonymous-tokens) First-time users get an anonymous bearer token automatically when running `db9 create` without credentials. These auto-refresh using an anonymous secret stored locally. See [Anonymous and Claimed Databases](/docs/platform/anonymous-and-claimed-databases/) for the full lifecycle. ## Database Authentication [Section titled “Database Authentication”](#database-authentication) ### Admin user [Section titled “Admin user”](#admin-user) Every database is bootstrapped with an `admin` user that has superuser privileges: * `SUPERUSER`, `LOGIN`, `CREATEDB`, `CREATEROLE` * Password is randomly generated and stored encrypted (AES-256-GCM) in the backend The admin password is used when you run `db9 db connect ` — the CLI retrieves it from the backend automatically. ### Additional users [Section titled “Additional users”](#additional-users) Create regular (non-superuser) database users through the CLI or API: Terminal ```bash db9 db users create --username --password ``` Regular users: * Can `LOGIN` to the database * Cannot create other users, alter the admin password, or perform superuser-only operations * Access is controlled by standard PostgreSQL `GRANT` statements Usernames must be 1–63 characters, alphanumeric plus underscore, and cannot start with `_db9_sys_` (reserved for internal system users). ### Superuser vs. regular user [Section titled “Superuser vs. regular user”](#superuser-vs-regular-user) | Capability | Superuser (admin) | Regular user | | ----------------------------------------------- | ----------------- | ----------------------------------- | | Run any SQL | Yes | Subject to GRANTs | | Use extensions (http, fs9, embedding, pg\_cron) | Yes | Restricted (most require superuser) | | Create/drop users | Yes | No | | Create/drop tables | Yes | Only if granted | | View all cron jobs | Yes | Own jobs only | | Cancel running cron jobs | Yes | No | | Use `cron.running_jobs` | Yes | No | ## Connect Tokens [Section titled “Connect Tokens”](#connect-tokens) Connect tokens are short-lived JWTs for establishing database connections from external clients, ORMs, or tools that need temporary credentials. Terminal ```bash db9 db connect db9 db connect --user ``` Returns (with `--json`): JSON ```json { "connection_string": "postgresql://.:@pg.db9.io:5433/postgres", "database": "postgres", "expires_at": "2026-03-12T12:10:00Z", "expires_in": "10min", "expires_in_seconds": 600, "host": "pg.db9.io", "port": 5433, "user": "." } ``` The JWT is embedded in `connection_string` as the password; the CLI does not emit it as a separate `token` field. If you need the raw token on its own, call the [REST endpoint](/docs/api/) `POST /customer/databases/{id}/connect-token`, which returns `token` separately. ### Properties [Section titled “Properties”](#properties) * **Signing**: RS256 (RSA private key), verifiable via public JWKS endpoint * **TTL**: 5–15 minutes (default 10 minutes), not configurable per-request * **Scope**: `db:connect` — limited to establishing a database connection * **Role binding**: Each token is bound to a specific database and role * **JWKS verification**: Public keys available at `/.well-known/db9-connect-jwks.json` for external token validation * **Single use intent**: Designed for handoff to a driver; create a new token for each connection ### When to use connect tokens [Section titled “When to use connect tokens”](#when-to-use-connect-tokens) * Passing credentials to an ORM or migration tool without exposing the admin password * Short-lived CI/CD database access * Programmatic connection from serverless functions ## Connect Keys [Section titled “Connect Keys”](#connect-keys) Connect keys are long-lived, scoped API keys for programmatic database access (especially the fs9 filesystem): * Prefixed with `db9ck_` for easy identification * SHA-256 hashed in storage (like bearer tokens) * Support scopes: `fs9:ro` (read-only filesystem), `fs9:rw` (read-write filesystem) * Configurable expiry or permanent * Revocable at any time ### When to use connect keys [Section titled “When to use connect keys”](#when-to-use-connect-keys) Use connect keys when you need long-lived, revocable credentials for automation that touches the filesystem path. For short-lived SQL/ORM handoff, prefer [Connect Tokens](#connect-tokens). ### REST API (create, list, revoke) [Section titled “REST API (create, list, revoke)”](#rest-api-create-list-revoke) All connect key APIs require a customer bearer token: Terminal ```bash AUTH_TOKEN="" DB_ID="" ``` #### Create a connect key [Section titled “Create a connect key”](#create-a-connect-key) `POST /customer/databases/{database_id}/connect-keys` Terminal ```bash curl -s -X POST "https://api.db9.ai/customer/databases/$DB_ID/connect-keys" \ -H "Authorization: Bearer $AUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "ci-fs", "role": "admin", "scopes": ["fs9:rw"], "expires_in_days": 30 }' ``` Example response (`201 Created`): JSON ```json { "id": "f8f5f11a-0f9f-4f6d-bf04-a69de8d5f3b5", "name": "ci-fs", "connect_key": "db9ck_...", "role": "admin", "scopes": ["fs9:rw"], "created_at": "2026-03-16T18:00:00Z", "expires_at": "2026-04-15T18:00:00Z" } ``` Store the connect key immediately `connect_key` is only returned once at creation time and cannot be retrieved again. Store it in a secrets manager or environment variable before proceeding. Notes: * `scopes` currently allow `fs9:ro` and `fs9:rw`. * Omit `expires_in_days` for a non-expiring key. #### List connect keys [Section titled “List connect keys”](#list-connect-keys) `GET /customer/databases/{database_id}/connect-keys` Terminal ```bash curl -s "https://api.db9.ai/customer/databases/$DB_ID/connect-keys" \ -H "Authorization: Bearer $AUTH_TOKEN" ``` Example response (`200 OK`): JSON ```json [ { "id": "f8f5f11a-0f9f-4f6d-bf04-a69de8d5f3b5", "name": "ci-fs", "role": "admin", "scopes": ["fs9:rw"], "created_at": "2026-03-16T18:00:00Z", "expires_at": "2026-04-15T18:00:00Z", "revoked_at": null } ] ``` The list endpoint returns metadata only and never returns `connect_key` secrets. #### Revoke a connect key [Section titled “Revoke a connect key”](#revoke-a-connect-key) `DELETE /customer/databases/{database_id}/connect-keys/{key_id}` Terminal ```bash KEY_ID="" curl -s -X DELETE "https://api.db9.ai/customer/databases/$DB_ID/connect-keys/$KEY_ID" \ -H "Authorization: Bearer $AUTH_TOKEN" ``` Example response (`200 OK`): JSON ```json { "message": "Connect key revoked" } ``` ## Publishable Keys [Section titled “Publishable Keys”](#publishable-keys) Publishable keys (`db9pk_...`) are browser-safe API keys for the [Browser SDK](/docs/sdk-browser/). Unlike bearer tokens, they are designed to be embedded in client-side code. ### Creating Publishable Keys [Section titled “Creating Publishable Keys”](#creating-publishable-keys) Terminal ```bash curl -X POST https://api.db9.ai/customer/databases//publishable-keys \ -H "Authorization: Bearer $DB9_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "web-app", "allowed_origins": ["https://myapp.com"], "exposed_schemas": ["public"], "exposed_tables": [], "rate_limit": { "rps": 100, "burst": 200 } }' ``` | Field | Type | Description | | ----------------- | ---------- | ------------------------------------------------------------------------------ | | `name` | `string` | Optional key name for identification. | | `allowed_origins` | `string[]` | CORS whitelist. Empty array allows all origins. | | `exposed_schemas` | `string[]` | Required. Schemas accessible through this key. | | `exposed_tables` | `string[]` | Optional. Restrict to specific tables (empty = all tables in exposed schemas). | | `rate_limit` | `object` | Optional. `{ rps, burst }` for request throttling. | | `expires_in_days` | `number` | Optional. Key expiry in days. | ### Managing Keys [Section titled “Managing Keys”](#managing-keys) Terminal ```bash # List all publishable keys curl https://api.db9.ai/customer/databases//publishable-keys \ -H "Authorization: Bearer $DB9_TOKEN" # Revoke a key curl -X DELETE https://api.db9.ai/customer/databases//publishable-keys/ \ -H "Authorization: Bearer $DB9_TOKEN" ``` The key value (`db9pk_...`) is only returned at creation time. Store it in your environment variables or deployment config. ### Security Properties [Section titled “Security Properties”](#security-properties) * Keys are SHA-256 hashed in storage (like bearer tokens) * Scoped to specific schemas and tables * Origin-restricted via `allowed_origins` * Rate-limited per key * Revocable at any time ## Bring Your Own JWT [Section titled “Bring Your Own JWT”](#bring-your-own-jwt) For per-user Row-Level Security with the Browser SDK, configure BYO JWT authentication on your database: Terminal ```bash curl -X PUT https://api.db9.ai/customer/databases//auth-config \ -H "Authorization: Bearer $DB9_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "auth_mode": "byo_jwt", "byo_jwt": { "jwks_url": "https://your-auth-provider.com/.well-known/jwks.json", "issuer": "https://your-auth-provider.com", "audience": "your-app-id", "subject_claim": "sub" } }' ``` | Field | Type | Description | | --------------- | -------- | -------------------------------------------------------- | | `jwks_url` | `string` | Required. URL to your JWKS endpoint (RS256 keys). | | `issuer` | `string` | Optional. Expected `iss` claim value. | | `audience` | `string` | Optional. Expected `aud` claim value. | | `subject_claim` | `string` | Which JWT claim holds the user identity. Default: `sub`. | When a browser SDK request includes a `Bearer ` header: 1. DB9 fetches your JWKS endpoint (cached for 1 hour) 2. Validates the JWT signature (RS256), expiry, issuer, and audience 3. Resolves the user’s database role from the `sub` claim 4. Issues a connect token with that role 5. Queries execute under that role — [RLS policies](/docs/sql/rls/) filter results Compatible with Auth0, Clerk, Supabase Auth, Firebase Auth, and any provider that exposes a JWKS endpoint. ## Credential Storage [Section titled “Credential Storage”](#credential-storage) ### Server side [Section titled “Server side”](#server-side) | Credential | Storage method | | ------------------------ | ----------------------------------------------------- | | Customer bearer tokens | SHA-256 hash (irreversible) | | Connect keys | SHA-256 hash (irreversible) | | Database admin passwords | AES-256-GCM encrypted (requires `DB9_CREDENTIAL_KEY`) | | Auth0 ID tokens | Validated and discarded (not stored) | ### Client side [Section titled “Client side”](#client-side) The CLI stores credentials in `~/.db9/credentials` with owner-only permissions (`0600`). This file contains: * Bearer token * Customer ID * Anonymous ID and secret (if anonymous) Running `db9 logout` clears this file. ## Wire Protocol Security [Section titled “Wire Protocol Security”](#wire-protocol-security) DB9 uses the PostgreSQL wire protocol (pgwire v3) for SQL connections: * **Authentication**: Password-based (SCRAM-SHA-256 or plain, depending on configuration) * **Multi-tenant isolation**: Connection usernames are prefixed with the tenant ID (`tenant_id.username`), enforcing keyspace isolation at the wire level * **Encryption**: Wire-level TLS is not currently enforced between client and db9-server. Network-level encryption (VPN, private networking) is recommended for production. Wire encryption is not enforced TLS between the client and db9-server is not currently mandatory. For production deployments, use a VPN or private network to protect data in transit until wire-level TLS enforcement is available. ## Audit Trail [Section titled “Audit Trail”](#audit-trail) All privilege-changing operations are logged to an audit trail: * Token creation and revocation * User creation and deletion * Connect key operations * Database lifecycle events Audit logs are retained for 90 days by default. ## What Is Not Currently Supported [Section titled “What Is Not Currently Supported”](#what-is-not-currently-supported) Enterprise features not yet available The following features are common in enterprise database platforms but are not yet available in DB9. Plan your security architecture around these gaps. These features are common in enterprise database platforms but are not available in DB9 today: * **IP allowlisting** — no built-in IP restrictions; use network-level controls * **Wire-level TLS** — pgwire connections are not encrypted; rely on network security * **Multi-factor authentication** — SSO (Auth0) is the only identity verification layer * **Password complexity rules** — no enforcement of length, character, or rotation policies * **Automated credential rotation** — revoke and re-create tokens manually * **Column-level encryption** — encrypt sensitive data at the application level ## Next Pages [Section titled “Next Pages”](#next-pages) * [Anonymous and Claimed Databases](/docs/platform/anonymous-and-claimed-databases/) — trial accounts, claiming, adoption, and upgrade flow * [Browser SDK](/docs/sdk-browser/) — client-side data access with publishable keys and RLS * [Row-Level Security](/docs/sql/rls/) — RLS policies, enforcement, and bypass mechanisms * [Production Checklist](/docs/production-checklist/) — verify auth and security before going live * [CLI Reference](/docs/cli/) — `db9 login`, `db9 db connect`, `db9 db users` * [TypeScript SDK](/docs/sdk/) — programmatic auth (native `connectToken()` not yet published; use the REST API or CLI for connect tokens) * [SQL Reference: Auth and Roles](/docs/sql/auth/) — `CREATE ROLE`, `GRANT`, and role management in SQL # Storage Accounting > Monitor database and table storage usage with virtual tables and automatic background scans. DB9 provides built-in storage accounting through virtual tables that show how much space your data, indexes, and metadata consume. Storage stats are updated automatically in the background and can be refreshed on demand. ## Virtual Tables [Section titled “Virtual Tables”](#virtual-tables) ### `_DB9_SYS_STORAGE_STATS` [Section titled “\_DB9\_SYS\_STORAGE\_STATS”](#_db9_sys_storage_stats) Database-level storage breakdown. SQL ```sql SELECT database_name, data_bytes, index_bytes, metadata_bytes, total_bytes, scanned_at FROM _DB9_SYS_STORAGE_STATS; ``` ▶ Run | Column | Type | Description | | ------------------ | ----------- | ----------------------------------------------------- | | `database_id` | `TEXT` | Database identifier. | | `database_name` | `TEXT` | Database name. | | `data_bytes` | `BIGINT` | Bytes used by table data. | | `index_bytes` | `BIGINT` | Bytes used by indexes. | | `metadata_bytes` | `BIGINT` | Bytes used by system metadata. | | `total_bytes` | `BIGINT` | Sum of data + index + metadata bytes. | | `scanned_at` | `TIMESTAMP` | When the last scan completed (NULL if never scanned). | | `scan_duration_ms` | `BIGINT` | Duration of the last scan in milliseconds. | ### `_DB9_SYS_TABLE_STORAGE_STATS` [Section titled “\_DB9\_SYS\_TABLE\_STORAGE\_STATS”](#_db9_sys_table_storage_stats) Per-table storage breakdown. SQL ```sql SELECT table_name, data_bytes, index_bytes, total_bytes FROM _DB9_SYS_TABLE_STORAGE_STATS ORDER BY total_bytes DESC; ``` ▶ Run | Column | Type | Description | | ------------- | ----------- | ----------------------------------- | | `database_id` | `TEXT` | Database identifier. | | `table_id` | `TEXT` | Internal table identifier. | | `table_name` | `TEXT` | Table name. | | `data_bytes` | `BIGINT` | Bytes used by this table’s data. | | `index_bytes` | `BIGINT` | Bytes used by this table’s indexes. | | `total_bytes` | `BIGINT` | Sum of data + index bytes. | | `scanned_at` | `TIMESTAMP` | When the last scan completed. | ## Manual Refresh [Section titled “Manual Refresh”](#manual-refresh) PostgreSQL-style manual scans are requested with: SQL ```sql SELECT db9_refresh_storage_stats(); ``` Not available in the current release `DB9_REFRESH_STORAGE_STATS()` requires the background worker subsystem, which is sealed off in the current release. Calling it returns: Output ```text ERROR: feature "storage_size_scan" is unavailable (PreActivationSeal) SQLSTATE: 55000 ``` There is currently no way to trigger a scan on demand. The virtual tables above are still populated by the automatic reconciliation cycle described below, so query them directly. ## Automatic Reconciliation [Section titled “Automatic Reconciliation”](#automatic-reconciliation) Storage stats are automatically refreshed by a background worker on a 30-minute cycle. No configuration is required — the system keeps stats reasonably up-to-date without manual intervention. ## Practical Examples [Section titled “Practical Examples”](#practical-examples) **Check total database size:** SQL ```sql SELECT database_name, total_bytes, round(total_bytes::numeric / 1024 / 1024, 2) AS total_mb, scanned_at FROM _DB9_SYS_STORAGE_STATS; ``` ▶ Run DB9 Difference: `pg_size_pretty()` is not implemented PostgreSQL’s size-formatting and size-inspection functions — `pg_size_pretty()`, `pg_total_relation_size()`, `pg_relation_size()`, `pg_table_size()`, `pg_indexes_size()` — do not exist in DB9 and fail with `function (...) does not exist` (`42883`), in both the bare and `extensions.`-qualified forms. The virtual tables report raw `BIGINT` byte counts; divide by `1024` as shown above to format them. **Find the largest tables:** SQL ```sql SELECT table_name, round(data_bytes::numeric / 1024 / 1024, 2) AS data_mb, round(index_bytes::numeric / 1024 / 1024, 2) AS index_mb, round(total_bytes::numeric / 1024 / 1024, 2) AS total_mb FROM _DB9_SYS_TABLE_STORAGE_STATS ORDER BY total_bytes DESC LIMIT 10; ``` ▶ Run **Monitor index-to-data ratio:** SQL ```sql SELECT table_name, CASE WHEN data_bytes > 0 THEN round(index_bytes::numeric / data_bytes, 2) ELSE 0 END AS index_ratio FROM _DB9_SYS_TABLE_STORAGE_STATS WHERE total_bytes > 0 ORDER BY index_ratio DESC; ``` ▶ Run ## Next Steps [Section titled “Next Steps”](#next-steps) * [Observability](/docs/platform/observability/) — Query metrics and live inspection * [Limits & Quotas](/docs/platform/limits-and-quotas/) — Storage limits per tenant * [Built-in Functions](/docs/sql/functions/) — db9\_refresh\_storage\_stats() reference # Production Checklist > Minimum guidance for running DB9 in production — authentication, secrets, connection management, branching strategy, observability, recovery expectations, and operational limits. Use this checklist before promoting a DB9 database from development to production. Each section covers what to do, why it matters, and how to verify. ## Decision summary [Section titled “Decision summary”](#decision-summary) | Area | Recommended default | | ---------------------- | ----------------------------------------------------------------- | | **Authentication** | Short-lived connect tokens, rotated per deployment | | **Secrets** | Never commit credentials; pass via environment variables | | **Connection strings** | Use `sslmode=require` and port `5433` | | **Branching** | One main database for production; branches for preview and CI | | **Observability** | `db9 db inspect` for live metrics; periodic slow query review | | **Recovery** | Branches as snapshots; application-level backup for critical data | | **Limits** | Know the per-tenant boundaries before launch | ## Authentication [Section titled “Authentication”](#authentication) ### Use connect tokens in production [Section titled “Use connect tokens in production”](#use-connect-tokens-in-production) Connect tokens expire automatically (default: 10 minutes) and are the recommended credential for production workloads. Generate them at deploy time or on a short rotation schedule. Terminal ```bash # Generate a connect token db9 db connect myapp ``` The token is used as the PostgreSQL password. Your application should fetch a fresh token at startup or on reconnect. For the TypeScript SDK, call the REST endpoint directly — the SDK does not yet publish a native `connectToken()` method: TypeScript ```typescript const res = await fetch(`https://api.db9.ai/customer/databases/${databaseId}/connect-token`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DB9_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ role: 'admin' }), }); const { token, host, port, user, expires_at } = await res.json(); ``` ### Avoid static passwords in production [Section titled “Avoid static passwords in production”](#avoid-static-passwords-in-production) Static passwords set with `db9 db reset-password` do not expire. They are convenient during development but create a credential that lives forever unless manually rotated. If you must use a static password, rotate it regularly and store it in a secrets manager. ### API tokens for automation [Section titled “API tokens for automation”](#api-tokens-for-automation) Use named API tokens for CI pipelines and infrastructure automation that manage databases through the REST API or SDK — not for direct SQL connections. Terminal ```bash db9 token create --name ci-deploy --expires-in-days 90 ``` Set the resulting token as `DB9_API_KEY` in your CI environment. List and revoke tokens with `db9 token list` and `db9 token revoke`. ### Publishable keys for browser access [Section titled “Publishable keys for browser access”](#publishable-keys-for-browser-access) If your application exposes data to browsers via the [Browser SDK](/docs/sdk-browser/), create scoped publishable keys: Terminal ```bash # Create via REST API — scope to specific schemas and tables curl -X POST https://api.db9.ai/customer/databases//publishable-keys \ -H "Authorization: Bearer $DB9_TOKEN" \ -H "Content-Type: application/json" \ -d '{"exposed_schemas": ["public"], "allowed_origins": ["https://myapp.com"]}' ``` Key security practices: * Restrict `allowed_origins` to your production domains * Limit `exposed_schemas` and `exposed_tables` to the minimum required * Set a `rate_limit` to prevent abuse * Enable [RLS policies](/docs/sql/rls/) on all exposed tables to enforce per-user access ### Row-Level Security for multi-tenant access [Section titled “Row-Level Security for multi-tenant access”](#row-level-security-for-multi-tenant-access) If browser clients access user-specific data, enable RLS on every exposed table: SQL ```sql ALTER TABLE todos ENABLE ROW LEVEL SECURITY; CREATE POLICY user_access ON todos FOR ALL USING (user_id = current_user) WITH CHECK (user_id = current_user); ``` ▶ Run Configure [BYO JWT](/docs/platform/security-and-auth/#bring-your-own-jwt) to map your auth provider’s JWTs to database roles. ## Secrets management [Section titled “Secrets management”](#secrets-management) Never commit credentials to source control Connection strings, API tokens, and passwords must never be committed to git. Use environment variables, `.env` files excluded via `.gitignore`, or a dedicated secrets manager. A leaked token gives full access to all your databases. | Rule | Details | | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | Never commit credentials | Connection strings, API tokens, and passwords belong in environment variables or a secrets manager — not in source control. | | Use `PGPASSWORD` or `DATABASE_URL` | Standard PostgreSQL environment variables work with DB9. | | Rotate connect tokens per deployment | Generate a fresh token each time your application starts. | | Scope API tokens narrowly | Create separate tokens for CI, monitoring, and admin tasks with appropriate expiration. | ## Connection configuration [Section titled “Connection configuration”](#connection-configuration) ### Required settings [Section titled “Required settings”](#required-settings) Output ```text Host: pg.db9.io Port: 5433 Database: postgres Username: .admin SSL: sslmode=require ``` DB9 uses port `5433`, not the PostgreSQL default `5432`. All connections use the `postgres` database name. The username format is `.` — see [Connect to DB9](/docs/connect/) for details. ### TLS [Section titled “TLS”](#tls) DB9’s hosted service supports TLS with SCRAM-SHA-256 authentication. Always set `sslmode=require` in production connection strings: Output ```text postgresql://a1b2c3d4e5f6.admin:token@pg.db9.io:5433/postgres?sslmode=require ``` ### Connection pooling [Section titled “Connection pooling”](#connection-pooling) db9-server manages per-tenant connection pooling internally. Idle tenant connections are evicted after 5 minutes by default. For applications with bursty traffic, keep connections alive with a lightweight health check query rather than relying on reconnection. If your application framework provides its own connection pool (most ORMs do), configure it with: * A reasonable pool size (start with 5–10 connections) * A connection timeout that accounts for TLS handshake latency * Reconnection logic that fetches a fresh connect token on auth failure ## Branching strategy [Section titled “Branching strategy”](#branching-strategy) DB9 branches create isolated copies of a database at a point in time. Use them to separate production from non-production workloads. ### Recommended pattern [Section titled “Recommended pattern”](#recommended-pattern) | Environment | Approach | | --------------------- | --------------------------------------------------------------------------------------------------- | | **Production** | One main database. No branches on this database during normal operation. | | **Staging / Preview** | Branch from production for realistic preview environments. Delete branches when the preview closes. | | **CI / Testing** | Create ephemeral branches per test run. Delete after tests complete. | | **Development** | Each developer can create personal branches for isolated experimentation. | ### Branch lifecycle [Section titled “Branch lifecycle”](#branch-lifecycle) Branches do not auto-expire. Clean up branches you no longer need: Terminal ```bash # List branches db9 branch list myapp # Delete a branch db9 branch delete preview-42 ``` For CI workflows, script branch creation and deletion as part of your pipeline: Terminal ```bash # Create a branch for this CI run db9 branch create myapp --name "ci-${CI_BUILD_ID}" # Run tests against the branch # ... # Clean up db9 branch delete "ci-${CI_BUILD_ID}" ``` Branch creation has a timeout of 5 minutes (300 seconds) by default. For large databases, factor this into your CI pipeline timing. ## Observability [Section titled “Observability”](#observability) ### Live metrics with `db9 db inspect` [Section titled “Live metrics with db9 db inspect”](#live-metrics-with-db9-db-inspect) The `inspect` command provides real-time metrics for a running database: Terminal ```bash # Summary: QPS, TPS, latency, active connections db9 db inspect myapp # Recent query samples with latency db9 db inspect myapp queries # Full report (summary + query samples) db9 db inspect myapp report # Schema, table, and index information db9 db inspect myapp schemas db9 db inspect myapp tables db9 db inspect myapp indexes ``` ### Slow query review [Section titled “Slow query review”](#slow-query-review) Identify slow queries with: Terminal ```bash db9 db inspect myapp slow-queries ``` Review slow queries periodically — especially after schema changes, new feature deployments, or load increases. Use `EXPLAIN` against the database to investigate query plans. ### What you can see today [Section titled “What you can see today”](#what-you-can-see-today) | Metric | Available | | --------------------------------------------- | --------- | | Queries per second (QPS) | Yes | | Transactions per second (TPS) | Yes | | Average and p99 latency | Yes | | Active connection count | Yes | | Query samples with individual latency | Yes | | Slow query log | Yes | | Schema / table / index inspection | Yes | | External metrics export (Prometheus, Datadog) | Not yet | | Alerting | Not yet | DB9 does not currently export metrics to external monitoring systems. Use `db9 db inspect` as your primary observability tool and build alerting around your application-level metrics for now. ## Recovery expectations [Section titled “Recovery expectations”](#recovery-expectations) ### What DB9 provides [Section titled “What DB9 provides”](#what-db9-provides) * **Branch-based snapshots**: Create a branch at any time to capture a point-in-time copy of your database. This is the primary mechanism for creating restore points. * **TiKV durability**: Data is replicated across TiKV nodes with Raft consensus. Individual node failures do not cause data loss. * **Reconciler**: A background process detects and recovers failed provisioning or deletion operations (stuck states recover within \~10 minutes). ### What DB9 does not provide today [Section titled “What DB9 does not provide today”](#what-db9-does-not-provide-today) No automated backups or PITR DB9 does not provide automated point-in-time recovery. Create a branch before any destructive operation (migration, bulk delete, schema change) to serve as a manual snapshot. Application-level exports are required for critical data that cannot be reconstructed. * **Automated point-in-time recovery (PITR)**: There is no built-in continuous backup with arbitrary restore points. Use branches as manual snapshots before risky operations. * **Cross-region replication**: Data lives in one region. Plan accordingly for disaster recovery. * **Self-service backup export**: Database dumps are limited to 50,000 rows and 16 MB. For larger datasets, export data through your application logic or use `COPY` over a pgwire connection. ### Recommended practices [Section titled “Recommended practices”](#recommended-practices) 1. **Before migrations**: Create a branch as a snapshot before running schema changes. Terminal ```bash db9 branch create myapp --name "pre-migration-$(date +%Y%m%d)" # Run migration # Verify # Delete snapshot branch when confident ``` 2. **Critical data**: Maintain application-level backups (periodic `COPY TO` or scheduled exports) for data that cannot be reconstructed. 3. **Test recovery**: Periodically create a branch, connect to it, and verify your data is accessible. Do not assume recovery works without testing it. ## Limits and quotas [Section titled “Limits and quotas”](#limits-and-quotas) Know these boundaries before launching: | Limit | Value | Notes | | -------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Database count** | Varies by account tier | Anonymous accounts: 5 databases. Verified accounts: higher limits. | | **Connection limit** | 1000 per db9-server instance | Per-user connection limits enforced via PostgreSQL role settings. | | **Database dump** | 50,000 rows / 16 MB | Via the REST API dump endpoint. No limit on `COPY` over pgwire. | | **Connect token TTL** | 10 minutes (default) | Configurable by the platform operator. | | **Branch clone timeout** | 5 minutes (default) | Large databases may need more time. | | **Serializable isolation** | Not implemented — downgraded to REPEATABLE READ | READ COMMITTED and REPEATABLE READ behave as in PostgreSQL. On the wire protocol SERIALIZABLE is accepted with a warning and runs at REPEATABLE READ; over the HTTP SQL API it is rejected with an error. | DB9 Difference: SERIALIZABLE is downgraded, not rejected If your application uses `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`, it will **not** fail. On the wire protocol DB9 emits `WARNING: TiKV provides snapshot isolation; SERIALIZABLE has been downgraded to REPEATABLE READ` and continues. Any correctness guarantee you were getting from true serializability — write-skew prevention above all — is gone without an error to tell you. Audit those transactions and enforce the invariant explicitly with `SELECT ... FOR UPDATE` or a unique constraint. (Over the HTTP SQL API the request errors out instead.) To detect the downgrade at runtime, read the isolation level back — DB9 reports the level it actually applied, not the one you asked for: SQL ```sql BEGIN ISOLATION LEVEL SERIALIZABLE; SHOW transaction_isolation; -- repeatable read ``` \| **Per-tenant QPS limit** | Disabled by default | Can be configured by the platform operator. | For detailed SQL compatibility limits, see [SQL Limits & Constraints](/docs/sql/limits/). ## Serverless Functions [Section titled “Serverless Functions”](#serverless-functions) If you are deploying [Serverless Functions](/docs/functions/), review these items before going live: * [ ] Grant the `authenticated` role access to tables your functions need * [ ] Set appropriate timeout limits for long-running functions — see [Functions configuration](/docs/functions/configuration/) * [ ] Configure `network_allowlist` for any outbound HTTP calls your functions make ## Pre-launch verification [Section titled “Pre-launch verification”](#pre-launch-verification) Run through these checks before going live: Terminal ```bash # 1. Verify you can connect with a fresh connect token db9 db connect myapp # 2. Check database health db9 db inspect myapp # 3. Verify TLS psql "postgresql://TENANT.admin@pg.db9.io:5433/postgres?sslmode=require" # 4. Run your application's health check against the database # (application-specific) # 5. Confirm branches are cleaned up db9 branch list myapp # 6. Review slow queries from recent testing db9 db inspect myapp slow-queries ``` ## Checklist summary [Section titled “Checklist summary”](#checklist-summary) * [ ] Using connect tokens (not static passwords) for application auth * [ ] Credentials stored in environment variables or secrets manager * [ ] Connection string uses port `5433`, database `postgres`, `sslmode=require` * [ ] Branching strategy documented: production database, preview branches, CI branches * [ ] Stale branches cleaned up * [ ] `db9 db inspect` reviewed for baseline metrics * [ ] Slow queries checked and addressed * [ ] Recovery plan documented: pre-migration branches, application-level backups for critical data * [ ] Account limits understood (database count, connection limit, dump size) * [ ] Serializable isolation caveat understood if your application uses it * [ ] API tokens scoped and expiring for CI and automation * [ ] Publishable keys scoped to specific schemas/tables with origin restrictions (if using Browser SDK) * [ ] RLS policies enabled on all browser-accessible tables (if using Browser SDK) ## Next steps [Section titled “Next steps”](#next-steps) * [Connect to DB9](/docs/connect/) — connection strings, drivers, and authentication details * [Architecture](/docs/architecture/) — how DB9 isolates tenants and processes queries * [CLI Reference](/docs/cli/) — full command reference including `db inspect`, `db branch`, and `token` * [SQL Limits & Constraints](/docs/sql/limits/) — detailed compatibility notes * [Extensions](/docs/extensions/) — enable vector search, fs9, HTTP from SQL, and pg\_cron # Quick Start > Get started with DB9 in under a minute — install the CLI or TypeScript SDK and create your first database. Pick the path that matches how you work: the **CLI** for terminal-first workflows, the **TypeScript SDK** for programmatic access, or the **agent onboarding** flow to give an AI coding agent its own database. ## Path 1: CLI [Section titled “Path 1: CLI”](#path-1-cli) ### Install [Section titled “Install”](#install) Terminal ```bash curl -fsSL https://db9.ai/install | sh ``` The installer downloads the `db9` binary for your platform (macOS or Linux, x64 or arm64) and places it in `/usr/local/bin` by default. To install elsewhere: Terminal ```bash DB9_INSTALL_DIR="$HOME/.local/bin" curl -fsSL https://db9.ai/install | sh ``` Verify: Terminal ```bash db9 --version ``` ▶ Run If you plan to use `db9 fs mount` on macOS, install macFUSE first: Terminal ```bash brew install --cask macfuse ``` On Apple Silicon/macOS, you may also need to approve the macFUSE system extension in **System Settings > Privacy & Security** before mounts succeed. ### Create a database [Section titled “Create a database”](#create-a-database) No signup needed — the CLI auto-registers an anonymous account on first use. Terminal ```bash db9 create --name myapp ``` ▶ Run You’ll see the database ID, name, and state. The database is ready to use immediately. ### Run SQL [Section titled “Run SQL”](#run-sql) Use the built-in SQL interface to run queries: Terminal ```bash # One-shot query db9 db sql myapp -q "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)" db9 db sql myapp -q "INSERT INTO users (name) VALUES ('alice'), ('bob')" db9 db sql myapp -q "SELECT * FROM users" ``` ▶ Run Or open an interactive REPL: Terminal ```bash db9 db sql myapp ``` The REPL supports multi-line SQL, tab completion, and `\`-backslash commands similar to `psql`. ### Get connection info [Section titled “Get connection info”](#get-connection-info) To connect from any PostgreSQL client (`psql`, an ORM, or a driver): Terminal ```bash db9 db connect myapp ``` ▶ Run This prints a temporary token-based connection string (valid for up to 10 minutes). To get a full connection string with credentials: Terminal ```bash db9 create --name myapp --show-connection-string ``` ▶ Run ### Verify it works [Section titled “Verify it works”](#verify-it-works) Terminal ```bash # Check account status db9 status # List your databases db9 list # Run a round-trip test db9 db sql myapp -q "SELECT 'hello from db9' AS greeting" ``` ▶ Run Expected output: Output ```text greeting ───────────────── hello from db9 ``` ## Path 2: TypeScript SDK [Section titled “Path 2: TypeScript SDK”](#path-2-typescript-sdk) ### Install [Section titled “Install”](#install-1) Terminal ```bash npm install get-db9 ``` ### Create a database and run SQL [Section titled “Create a database and run SQL”](#create-a-database-and-run-sql) * TypeScript TypeScript ```typescript import { instantDatabase } from 'get-db9'; const db = await instantDatabase({ name: 'myapp', seed: ` CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT); INSERT INTO users (name) VALUES ('alice'), ('bob'); `, }); console.log(db.databaseId); // "t1a2b3c4d5e6" console.log(db.connectionString); // "postgresql://..." console.log(db.adminUser); // "admin" ``` ▶ Run `instantDatabase()` is idempotent — if a database named `myapp` already exists, it returns the existing one without re-running the seed SQL. * Python Python ```python import requests import psycopg2 import os headers = {"Authorization": f"Bearer {os.environ['DB9_API_KEY']}"} # Create database resp = requests.post( "https://api.db9.ai/customer/databases", json={"name": "myapp"}, headers=headers, ) db = resp.json() # Run seed SQL conn = psycopg2.connect(db["connection_string"]) conn.autocommit = True cur = conn.cursor() cur.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT)") cur.execute("INSERT INTO users (name) VALUES ('alice'), ('bob')") cur.close() conn.close() print(db["id"]) # "t1a2b3c4d5e6" print(db["connection_string"]) # "postgresql://..." ``` * cURL Terminal ```bash # Create database curl -X POST https://api.db9.ai/customer/databases \ -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "myapp"}' # Run SQL (use the database_id from the response above) curl -X POST "https://api.db9.ai/customer/databases/$DATABASE_ID/sql" \ -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)"}' curl -X POST "https://api.db9.ai/customer/databases/$DATABASE_ID/sql" \ -H "Authorization: Bearer $DB9_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "INSERT INTO users (name) VALUES ('"'"'alice'"'"'), ('"'"'bob'"'"')"}' ``` ### Connect from your application [Section titled “Connect from your application”](#connect-from-your-application) Use the returned `connectionString` with any PostgreSQL client library: * TypeScript TypeScript ```typescript import pg from 'pg'; const pool = new pg.Pool({ connectionString: db.connectionString }); const result = await pool.query('SELECT * FROM users'); console.log(result.rows); // [{ id: 1, name: 'alice' }, { id: 2, name: 'bob' }] ``` * Python Python ```python import psycopg2 conn = psycopg2.connect(db["connection_string"]) cur = conn.cursor() cur.execute("SELECT * FROM users") print(cur.fetchall()) # [(1, 'alice'), (2, 'bob')] cur.close() conn.close() ``` ## Path 3: Agent onboarding [Section titled “Path 3: Agent onboarding”](#path-3-agent-onboarding) Give an AI coding agent (Claude Code, Codex, OpenCode) access to DB9 with a single command: Terminal ```bash # Install DB9 skills for Claude Code db9 onboard --agent claude # Or for all supported agents db9 onboard --all # Preview what will be installed without making changes db9 onboard --agent claude --dry-run ``` The onboard command installs a DB9 skill file that teaches the agent how to create databases, run SQL, manage files, and use branching — all through the DB9 CLI. Supported agents: `claude`, `codex`, `opencode`, `agents`. Scope options: | Flag | Effect | | ----------------- | ------------------------------------ | | `--scope user` | Install for all projects (default) | | `--scope project` | Install for the current project only | | `--scope both` | Install at both levels | ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) **`db9: command not found`** — The binary isn’t on your PATH. Either move it or add the install directory: Terminal ```bash export PATH="$HOME/.local/bin:$PATH" ``` **`Anonymous account database limit reached`** — Anonymous accounts can create up to 5 databases. Run `db9 claim` to upgrade your account via SSO and remove the limit. **`Connection refused` or timeouts** — DB9 requires TLS. Make sure your client supports `sslmode=require` or equivalent. See [Connect](/docs/connect/) for details. **SDK authentication** — The SDK auto-registers anonymously on first use, similar to the CLI. Credentials are stored locally and reused across runs. ## Next steps [Section titled “Next steps”](#next-steps) * [Serverless Functions](/docs/functions/) — deploy JavaScript/TypeScript with native SQL access * [Connect](/docs/connect/) — connection strings, psql, ORMs, drivers, and TLS options * [Why DB9 for AI Agents](/docs/why-db9-for-ai-agents/) — built-in embeddings, file system, HTTP, branching * [Agent Workflows](/docs/agent-workflows/overview/) — the full agent lifecycle with DB9 * [CLI Reference](/docs/cli/) — complete command reference * [TypeScript SDK](/docs/sdk/) — server-side SDK API and types * [Browser SDK](/docs/sdk-browser/) — client-side data access with RLS * [Extensions](/docs/extensions/) — fs9, HTTP, pg\_cron, vector search, and more * [Architecture](/docs/architecture/) — how DB9 works under the hood # TypeScript SDK > Complete API reference for the get-db9 TypeScript SDK — provisioning, SQL, filesystem, branching, auth lifecycle, and all exported interfaces. The `get-db9` SDK provides typed TypeScript access to DB9 databases — provisioning, SQL execution, filesystem operations, branching, and token management. This is the **server-side** SDK for Node.js environments. > **Client-side?** For browser and edge environments, use the [`@db9/browser` SDK](/docs/sdk-browser/) — it provides a query builder with Row-Level Security enforcement and publishable key authentication. ## When to Use the SDK [Section titled “When to Use the SDK”](#when-to-use-the-sdk) | Scenario | Recommended tool | | ------------------------------------------------------ | ---------------------------------------------------------------------------- | | One-liner database provisioning in a script or test | **SDK** — `instantDatabase()` | | Programmatic fleet management (create, branch, delete) | **SDK** — `createDb9Client()` | | File read/write from Node.js (RAG, ingestion) | **SDK** — `client.fs.*` | | Interactive SQL from a terminal | **CLI** — `db9 db sql` ([CLI Reference](/docs/cli/)) | | ORM or driver connection to an existing database | **Raw pgwire** — use the connection string ([Connect](/docs/connect/)) | | Agent onboarding (Codex, Claude Code) | **CLI** — `db9 onboard` ([Agent Workflows](/docs/agent-workflows/overview/)) | ## Installation [Section titled “Installation”](#installation) Runtime requirement: Node.js 18+ (native `fetch`). TypeScript 5+ for full type exports. Terminal ```bash npm install get-db9 ``` Also available via `yarn add get-db9`, `pnpm add get-db9`, or `bun add get-db9`. View the package on [npm](https://www.npmjs.com/package/get-db9). The SDK shares the credential store (`~/.db9/credentials`) with the `db9` CLI. If you have logged in via the CLI, the SDK picks up the token automatically. ### Framework Integrations [Section titled “Framework Integrations”](#framework-integrations) The SDK works with any Node.js framework. For framework-specific setup guides with connection patterns and best practices: * [Next.js](/docs/guides/nextjs/) — Server Components, Server Actions, Route Handlers * [Express / Hono](/docs/guides/express/) — middleware and route patterns * [SvelteKit](/docs/guides/sveltekit/) — server-only load functions * [Nuxt](/docs/guides/nuxt/) — server API routes * [Remix](/docs/guides/remix/) — loaders and actions * [Astro](/docs/guides/astro/) — SSR mode with Node adapter For Python, Ruby, Go, and PHP frameworks, use the connection string from `instantDatabase()` or `createDb9Client()` with your language’s PostgreSQL driver. See [Connect](/docs/connect/) for driver-specific examples. ## Quick Start [Section titled “Quick Start”](#quick-start) `instantDatabase()` creates or reuses a database by name. If no name is provided, it defaults to `"default"`. TypeScript ```typescript import { instantDatabase } from 'get-db9'; const db = await instantDatabase({ name: 'myapp', seed: 'CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT)' }); console.log(db.databaseId); console.log(db.connectionString); console.log(db.adminUser, db.adminPassword); console.log(db.state, db.createdAt); ``` ▶ Run Authentication required The SDK requires a valid token in `~/.db9/credentials` or passed via `Db9ClientOptions.token`. If no token is available, the client throws an error. Run `db9 login` or `db9 create` (which auto-registers an anonymous account) before using the SDK, or pass a token explicitly. Result shape: TypeScript ```typescript interface InstantDatabaseResult { databaseId: string; connectionString: string; adminUser: string; adminPassword: string; state: string; createdAt: string; } ``` ## `instantDatabase(options?)` [Section titled “instantDatabase(options?)”](#instantdatabaseoptions) High-level API that wraps `createDb9Client()`, checks for an existing database by name, creates one if missing, and optionally executes seed SQL. Seed SQL runs only on initial creation — if the database already exists, seed is skipped. TypeScript ```typescript function instantDatabase( options?: InstantDatabaseOptions ): Promise; ``` | Option | Type | Description | | ----------------- | ----------------- | ---------------------------------------------------------------------------- | | `name` | `string` | Database name. Default: `'default'`. | | `baseUrl` | `string` | Override API endpoint. Default: `https://api.db9.ai` or `DB9_API_URL` env. | | `fetch` | `FetchFn` | Custom fetch implementation. | | `credentialStore` | `CredentialStore` | Token load/save strategy. | | `seed` | `string` | SQL text executed via `client.databases.sql()` on creation only. | | `seedFile` | `string` | SQL file content executed via `client.databases.sqlFile()` on creation only. | | `timeout` | `number` | Request timeout in milliseconds. | | `maxRetries` | `number` | Retry count for failed requests (capped at 3). | | `retryDelay` | `number` | Delay between retries in milliseconds. | TypeScript ```typescript const db = await instantDatabase({ name: 'analytics', seedFile: ` CREATE TABLE events ( id BIGSERIAL PRIMARY KEY, user_id TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); ` }); ``` ## `createDb9Client(options?)` [Section titled “createDb9Client(options?)”](#createdb9clientoptions) Low-level typed client exposing grouped APIs: `auth`, `tokens`, `databases`, and `fs`. The client lazy-loads the token from the credential store on the first protected call. If no token is found, it throws `Db9Error` (status 401). Use `db9 login` or pass a token via `options.token` before calling protected methods. TypeScript ```typescript function createDb9Client(options?: Db9ClientOptions): Db9Client; ``` | Option | Type | Description | | ----------------- | ---------------------- | --------------------------------------------------------------------------------------- | | `baseUrl` | `string` | Default: `https://api.db9.ai` (or `DB9_API_URL` env). | | `token` | `string` | Bearer token; skips credential store lookup. | | `fetch` | `FetchFn` | Custom HTTP implementation. | | `credentialStore` | `CredentialStore` | Load/save token state. | | `timeout` | `number` | Request timeout in milliseconds. | | `maxRetries` | `number` | Retry count for failed requests (capped at 3). | | `retryDelay` | `number` | Delay between retries in milliseconds. | | `WebSocket` | `WebSocketConstructor` | Override WebSocket for `client.fs` operations (see [Filesystem](#filesystem-clientfs)). | | `wsPort` | `number` | Override the fs9 WebSocket port (default: `5480`). | TypeScript ```typescript import { createDb9Client, MemoryCredentialStore } from 'get-db9'; const client = createDb9Client({ baseUrl: 'https://api.db9.ai', credentialStore: new MemoryCredentialStore() }); ``` ▶ Run ## Authentication (`client.auth`) [Section titled “Authentication (client.auth)”](#authentication-clientauth) * `me(): Promise` — Get current user profile The SDK itself does not handle login or registration. Use the CLI for auth: Terminal ```bash # Zero-setup trial (auto creates anonymous account + token) db9 create --name quickstart # Upgrade anonymous account to verified SSO identity db9 claim # Human operator login (browser-based) db9 login # API key login (CI/CD, agents) db9 login --api-key # Create an automation token db9 token create --name my-agent --expires-in-days 365 # Agent runtime export DB9_API_KEY= ``` Once authenticated via CLI, the SDK picks up the stored token automatically. ## Token Management (`client.tokens`) [Section titled “Token Management (client.tokens)”](#token-management-clienttokens) Create, inspect, and revoke API tokens for CI/CD and programmatic access. * `create(req: CreateTokenRequest): Promise` — Create named token with optional expiry * `list(): Promise` * `revoke(tokenId: string): Promise` TypeScript ```typescript const newToken = await client.tokens.create({ name: 'ci-deploy', expires_in_days: 90 }); console.log(newToken.token); // Store in secret manager const tokens = await client.tokens.list(); for (const token of tokens) { console.log(token.id, token.name, token.created_at, token.expires_at); } await client.tokens.revoke(tokens[0].id); ``` ## Database Management (`client.databases`) [Section titled “Database Management (client.databases)”](#database-management-clientdatabases) Core lifecycle APIs: create, list, get, delete, reset password, retrieve credentials, and read observability metrics. * `create(req: CreateDatabaseRequest): Promise` * `list(): Promise` * `get(databaseId: string): Promise` * `delete(databaseId: string): Promise` * `resetPassword(databaseId: string): Promise` * `credentials(databaseId: string): Promise` — Get stored admin credentials without resetting * `observability(databaseId: string): Promise` TypeScript ```typescript const db = await client.databases.create({ name: 'billing', region: 'us-west', admin_password: 'StrongAdminPass1' }); const all = await client.databases.list(); const current = await client.databases.get(db.id); const rotated = await client.databases.resetPassword(db.id); const creds = await client.databases.credentials(db.id); const metrics = await client.databases.observability(db.id); await client.databases.delete(db.id); ``` For programmatic provisioning patterns, see [Provisioning](/docs/platform/provisioning/). ## SQL Execution [Section titled “SQL Execution”](#sql-execution) Execute SQL strings or SQL file content through the customer API. Both methods return `SqlResult`. * `sql(databaseId: string, query: string): Promise` * `sqlFile(databaseId: string, fileContent: string): Promise` TypeScript ```typescript const result = await client.databases.sql( databaseId, 'SELECT id, email FROM users ORDER BY id LIMIT 10' ); console.log(result.columns); console.log(result.rows); console.log(result.row_count, result.command, result.error); const fromFile = await client.databases.sqlFile(databaseId, ` CREATE TABLE audit_log (id BIGSERIAL PRIMARY KEY, event TEXT); INSERT INTO audit_log(event) VALUES ('created'); `); ``` | `SqlResult` Field | Type | Description | | ----------------- | -------------------------- | ---------------------------------------------------------------- | | `columns` | `ColumnInfo[]` | Column metadata for result rows. | | `rows` | `unknown[][]` | Result values matrix. | | `row_count` | `number` | Rows affected/returned. | | `command` | `string` | Executed command label (`SELECT`, `INSERT`, etc.). | | `error` | `string \| SqlErrorDetail` | Structured error with message, code, detail, hint, and position. | ## Schema and Dump [Section titled “Schema and Dump”](#schema-and-dump) Introspect schema objects or export SQL dump payloads. * `schema(databaseId: string): Promise` * `dump(databaseId: string, req?: DumpRequest): Promise` TypeScript ```typescript const schema = await client.databases.schema(databaseId); for (const table of schema.tables) { console.log(table.schema, table.name); } const ddlOnly = await client.databases.dump(databaseId, { ddl_only: true }); console.log(ddlOnly.object_count); console.log(ddlOnly.sql); ``` ## Migrations [Section titled “Migrations”](#migrations) Apply SQL migrations with checksums and inspect migration history. * `applyMigration(databaseId: string, req: MigrationApplyRequest): Promise` * `listMigrations(databaseId: string): Promise` TypeScript ```typescript await client.databases.applyMigration(databaseId, { name: '20260218_add_users', sql: 'CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL);', checksum: 'f0b9c43b' }); const applied = await client.databases.listMigrations(databaseId); for (const migration of applied) { console.log(migration.name, migration.applied_at, migration.checksum); } ``` ## Branching [Section titled “Branching”](#branching) Create a database branch from an existing database. Branch creation starts in `CLONING` state — poll with `get()` until it reaches `ACTIVE` or `CREATE_FAILED`. * `branch(databaseId: string, req: BranchRequest): Promise` TypeScript ```typescript const featureDb = await client.databases.branch(databaseId, { name: 'feature-auth' }); let current = featureDb; while (current.state === 'CLONING') { await new Promise((resolve) => setTimeout(resolve, 1000)); current = await client.databases.get(featureDb.id); } if (current.state === 'CREATE_FAILED') { throw new Error('Branch clone failed'); } console.log('Branch ready:', current.id, current.name); ``` For branch workflow patterns, see [Multi-Tenant Patterns](/docs/platform/multi-tenant-patterns/). ## Database Users (`client.databases.users`) [Section titled “Database Users (client.databases.users)”](#database-users-clientdatabasesusers) Manage Postgres users inside a database. * `list(databaseId: string): Promise` * `create(databaseId: string, req: CreateUserRequest): Promise` * `delete(databaseId: string, username: string): Promise` TypeScript ```typescript await client.databases.users.create(databaseId, { username: 'app_user', password: 'AppUserPass!' }); const users = await client.databases.users.list(databaseId); users.forEach((u) => { console.log(u.name, u.can_login, u.can_create_db, u.is_superuser); }); await client.databases.users.delete(databaseId, 'app_user'); ``` ## Filesystem (`client.fs`) [Section titled “Filesystem (client.fs)”](#filesystem-clientfs) Cloud filesystem operations for reading, writing, and managing files attached to each database. Built for RAG pipelines, document ingestion, and agent workflows. WebSocket requirement Filesystem operations use WebSocket connections. Node.js 21+ and browsers have native WebSocket support. On Node.js 18–20, install the `ws` package or pass a `WebSocket` constructor via `Db9ClientOptions.WebSocket`. * `connect(dbId): Promise` — Open a persistent WebSocket connection (caller must call `close()`) * `list(dbId, path): Promise` — List directory contents * `read(dbId, path): Promise` — Read file content as text * `readBinary(dbId, path): Promise` — Read file as binary * `write(dbId, path, content): Promise` — Write a file * `append(dbId, path, content): Promise` — Append to a file, returns bytes written * `stat(dbId, path): Promise` — Get file metadata * `exists(dbId, path): Promise` — Check if file exists * `mkdir(dbId, path): Promise` — Create a directory recursively * `remove(dbId, path, opts?): Promise` — Delete a file or directory * `rename(dbId, oldPath, newPath): Promise` — Move or rename a path TypeScript ```typescript import { createDb9Client } from 'get-db9'; const client = createDb9Client(); const dbId = 'my-database-id'; // Create directory and write a file await client.fs.mkdir(dbId, '/data'); await client.fs.write(dbId, '/data/hello.txt', 'Hello from db9!'); // Read file content const content = await client.fs.read(dbId, '/data/hello.txt'); // List directory const files = await client.fs.list(dbId, '/data/'); for (const file of files) { console.log(file.path, file.type, file.size); } // Stat and check existence const info = await client.fs.stat(dbId, '/data/hello.txt'); console.log(info.type, info.size, info.mtime); const exists = await client.fs.exists(dbId, '/data/hello.txt'); console.log('File exists:', exists); // Append and rename await client.fs.append(dbId, '/data/hello.txt', '\nHello again'); await client.fs.rename(dbId, '/data/hello.txt', '/data/hello-2.txt'); // Cleanup await client.fs.remove(dbId, '/data/hello-2.txt'); await client.fs.remove(dbId, '/data'); ``` ### FileInfo [Section titled “FileInfo”](#fileinfo) | Field | Type | Description | | ------- | ----------------- | ------------------------------------ | | `path` | `string` | Full file path. | | `size` | `number` | File size in bytes. | | `type` | `'file' \| 'dir'` | Entry type. | | `mode` | `number` | Unix file mode. | | `mtime` | `string` | Last modified time as RFC 3339 text. | ### FsRemoveOptions [Section titled “FsRemoveOptions”](#fsremoveoptions) | Option | Type | Description | | ----------- | --------- | -------------------------------------------------- | | `recursive` | `boolean` | Remove directories recursively (default: `false`). | Batch operations: use a persistent connection Convenience methods like `read()`, `write()`, and `list()` each open and close a WebSocket connection per call. For batch operations, use `client.fs.connect(dbId)` to open a persistent connection and reuse it. ### FsClient [Section titled “FsClient”](#fsclient) `client.fs.connect(dbId)` returns an `FsClient` bound to a single persistent WebSocket connection. Its method names differ slightly from the `client.fs.*` convenience methods above: * `authenticate(username: string, password: string): Promise` — Authenticate the connection. Must be called first after connecting. * `stat(path: string): Promise` — Get file metadata * `readdir(path: string): Promise` — List directory contents (note: `readdir`, not `list`) * `mkdir(path: string, recursive?: boolean): Promise` — Create a directory * `readFile(path: string): Promise` — Read an entire file as **raw bytes** (not a string) * `writeFile(path: string, data: Uint8Array | ArrayBuffer | string): Promise` — Overwrite a file, returns bytes written * `appendFile(path: string, data: Uint8Array | ArrayBuffer | string): Promise` — Append to a file, returns bytes written * `rm(path: string, recursive?: boolean): Promise` — Delete a file or directory (note: `rm`, not `remove`, and it takes a boolean — not the options object that `client.fs.remove()` accepts) * `rename(oldPath: string, newPath: string): Promise` — Move or rename a path * `close(): Promise` — Gracefully close the WebSocket connection TypeScript ```typescript const fs = await client.fs.connect(dbId); await fs.mkdir('/data'); await fs.writeFile('/data/hello.txt', 'Hello from db9!'); // readFile returns bytes — decode if you want a string const bytes = await fs.readFile('/data/hello.txt'); const content = new TextDecoder().decode(bytes); const entries = await fs.readdir('/data'); await fs.close(); ``` ## Credential Storage [Section titled “Credential Storage”](#credential-storage) Credential stores implement a shared async interface used by client auto-auth. * `FileCredentialStore(path?)` — TOML file store at `~/.db9/credentials` (shared with `db9` CLI) * `MemoryCredentialStore` — Volatile in-memory store for tests and serverless * `defaultCredentialStore()` — Factory that returns `new FileCredentialStore()` TypeScript ```typescript import { createDb9Client, FileCredentialStore, MemoryCredentialStore, defaultCredentialStore } from 'get-db9'; const fileStore = new FileCredentialStore(); const customStore = new FileCredentialStore('/tmp/db9-credentials.toml'); const memStore = new MemoryCredentialStore(); const client = createDb9Client({ credentialStore: fileStore }); ``` ## Error Handling [Section titled “Error Handling”](#error-handling) API failures throw named `Db9Error` subclasses based on HTTP status code. * `Db9Error` — Base class with `statusCode`, `message`, and optional `response` * `Db9AuthError` — Status `401` * `Db9NotFoundError` — Status `404` * `Db9ConflictError` — Status `409` (e.g., duplicate database name) Filesystem operations (`client.fs.*` and `FsClient`) throw `FsError` instead — it is a separate class, not a `Db9Error` subclass. TypeScript ```typescript import { createDb9Client, Db9Error, Db9AuthError, Db9NotFoundError, Db9ConflictError } from 'get-db9'; const client = createDb9Client(); try { await client.databases.get('missing-id'); } catch (error) { if (error instanceof Db9NotFoundError) { console.error('Database not found'); } else if (error instanceof Db9AuthError) { console.error('Authentication required — run `db9 login` first'); } else if (error instanceof Db9ConflictError) { console.error('Conflict (e.g., duplicate name)'); } else if (error instanceof Db9Error) { console.error(`db9 API error ${error.statusCode}: ${error.message}`); } else { throw error; } } ``` ## TypeScript Types Reference [Section titled “TypeScript Types Reference”](#typescript-types-reference) The package re-exports all interfaces from `./types` in addition to client, credential, and filesystem types. > **Already documented inline:** `InstantDatabaseResult` (see [Quick Start](#quick-start)), `SqlResult` and `ColumnInfo` (see [SQL Execution](#sql-execution)), `FileInfo` and `FsRemoveOptions` (see [Filesystem](#filesystem-clientfs)). ### DatabaseResponse [Section titled “DatabaseResponse”](#databaseresponse) Returned by `create()`, `list()`, `get()`, and `branch()`. TypeScript ```typescript interface DatabaseResponse { id: string; name: string; state: string; parent_database_id?: string; region?: string; endpoints?: Endpoint[]; admin_user?: string; admin_password?: string; created_at: string; connection_string?: string; } ``` | Field | Type | Description | | -------------------- | ------------- | ------------------------------------------------------------------------------------------- | | `id` | `string` | Unique database identifier. | | `name` | `string` | Human-readable database name. | | `state` | `string` | Lifecycle state: `CREATING`, `ACTIVE`, `CLONING`, `DISABLING`, `DISABLED`, `CREATE_FAILED`. | | `parent_database_id` | `string?` | Source database ID for branch databases. | | `region` | `string?` | Deployment region (e.g. `us-west`). | | `endpoints` | `Endpoint[]?` | Connection endpoints (host, port, type). | | `admin_user` | `string?` | Admin Postgres username. | | `admin_password` | `string?` | Admin Postgres password. Only present on create. | | `created_at` | `string` | ISO 8601 creation timestamp. | | `connection_string` | `string?` | Full `postgresql://` connection URI. | ### CustomerResponse [Section titled “CustomerResponse”](#customerresponse) Returned by `client.auth.me()`. TypeScript ```typescript interface CustomerResponse { id: string; email: string; created_at: string; status: string; } ``` | Field | Type | Description | | ------------ | -------- | ---------------------------------------------------- | | `id` | `string` | Customer account identifier. | | `email` | `string` | Account email address. Empty for anonymous accounts. | | `created_at` | `string` | ISO 8601 account creation timestamp. | | `status` | `string` | Account status: `active`, `anonymous`. | ### TokenResponse [Section titled “TokenResponse”](#tokenresponse) Returned by `client.tokens.list()`. Metadata only — does not include the token secret. TypeScript ```typescript interface TokenResponse { id: string; name: string; created_at: string; expires_at?: string; } ``` | Field | Type | Description | | ------------ | --------- | ----------------------------------------------------------- | | `id` | `string` | Token identifier. Use this to revoke the token. | | `name` | `string` | Human-readable token label. | | `created_at` | `string` | ISO 8601 creation timestamp. | | `expires_at` | `string?` | ISO 8601 expiry timestamp. `undefined` means never expires. | ### CreateTokenResponse [Section titled “CreateTokenResponse”](#createtokenresponse) Returned by `client.tokens.create()`. Includes the token secret — store it immediately. TypeScript ```typescript interface CreateTokenResponse { id: string; name: string; token: string; created_at: string; expires_at?: string; } ``` | Field | Type | Description | | ------------ | --------- | ------------------------------------------------------------------------ | | `id` | `string` | Token identifier. | | `name` | `string` | Human-readable token label. | | `token` | `string` | **The secret token value.** Shown only once — store in a secret manager. | | `created_at` | `string` | ISO 8601 creation timestamp. | | `expires_at` | `string?` | ISO 8601 expiry timestamp. | ### SchemaResponse [Section titled “SchemaResponse”](#schemaresponse) Returned by `client.databases.schema()`. TypeScript ```typescript interface SchemaResponse { tables: TableMetadata[]; views: ViewMetadata[]; } ``` | Field | Type | Description | | -------- | ----------------- | --------------------------- | | `tables` | `TableMetadata[]` | All tables in the database. | | `views` | `ViewMetadata[]` | All views in the database. | ### TableMetadata [Section titled “TableMetadata”](#tablemetadata) TypeScript ```typescript interface TableMetadata { name: string; schema: string; columns: ColumnMetadata[]; } ``` | Field | Type | Description | | --------- | ------------------ | ------------------------------------- | | `name` | `string` | Table name. | | `schema` | `string` | Postgres schema name (e.g. `public`). | | `columns` | `ColumnMetadata[]` | Column definitions for this table. | ### ColumnMetadata [Section titled “ColumnMetadata”](#columnmetadata) TypeScript ```typescript interface ColumnMetadata { name: string; type: string; nullable: boolean; default_value?: string; } ``` | Field | Type | Description | | --------------- | --------- | --------------------------------------------------------- | | `name` | `string` | Column name. | | `type` | `string` | Postgres data type (e.g. `text`, `integer`, `timestamp`). | | `nullable` | `boolean` | Whether the column accepts `NULL`. | | `default_value` | `string?` | Default expression, if defined. | ### ViewMetadata [Section titled “ViewMetadata”](#viewmetadata) TypeScript ```typescript interface ViewMetadata { name: string; schema: string; } ``` | Field | Type | Description | | -------- | -------- | --------------------- | | `name` | `string` | View name. | | `schema` | `string` | Postgres schema name. | ### DumpResponse [Section titled “DumpResponse”](#dumpresponse) Returned by `client.databases.dump()`. TypeScript ```typescript interface DumpResponse { sql: string; object_count: number; } ``` | Field | Type | Description | | -------------- | -------- | ------------------------------------------------ | | `sql` | `string` | Full SQL dump text. | | `object_count` | `number` | Number of database objects included in the dump. | ### TenantObservabilityResponse [Section titled “TenantObservabilityResponse”](#tenantobservabilityresponse) Returned by `client.databases.observability()`. TypeScript ```typescript interface TenantObservabilityResponse { summary: ObservabilitySummary; samples: QuerySample[]; } ``` | Field | Type | Description | | --------- | ---------------------- | --------------------------------------------- | | `summary` | `ObservabilitySummary` | Aggregate metrics for the observation window. | | `samples` | `QuerySample[]` | Per-query performance samples. | ### ObservabilitySummary [Section titled “ObservabilitySummary”](#observabilitysummary) TypeScript ```typescript interface ObservabilitySummary { window_seconds: number; statement_count: number; txn_commit_count: number; error_count: number; qps: number; tps: number; latency_avg_ms: number; latency_p99_ms: number; active_connections: number; } ``` | Field | Type | Description | | -------------------- | -------- | ---------------------------------------------- | | `window_seconds` | `number` | Observation window length in seconds. | | `statement_count` | `number` | Total statements executed in the window. | | `txn_commit_count` | `number` | Total committed transactions. | | `error_count` | `number` | Total statement errors. | | `qps` | `number` | Average queries per second. | | `tps` | `number` | Average transactions per second. | | `latency_avg_ms` | `number` | Mean query latency in milliseconds. | | `latency_p99_ms` | `number` | 99th-percentile query latency in milliseconds. | | `active_connections` | `number` | Current active connection count. | ### QuerySample [Section titled “QuerySample”](#querysample) TypeScript ```typescript interface QuerySample { query: string; sample_count: number; error_count: number; latency_avg_ms: number; latency_p99_ms: number; latency_max_ms: number; last_seen_ms_ago: number; } ``` | Field | Type | Description | | ------------------ | -------- | ----------------------------------------------------- | | `query` | `string` | Normalized query text (literals replaced with `$N`). | | `sample_count` | `number` | Number of times this query pattern was observed. | | `error_count` | `number` | Number of times this query pattern produced an error. | | `latency_avg_ms` | `number` | Mean execution latency in milliseconds. | | `latency_p99_ms` | `number` | 99th-percentile latency in milliseconds. | | `latency_max_ms` | `number` | Maximum observed latency in milliseconds. | | `last_seen_ms_ago` | `number` | Milliseconds since this query was last observed. | ### CustomerPasswordResetResponse [Section titled “CustomerPasswordResetResponse”](#customerpasswordresetresponse) Returned by `client.databases.resetPassword()` and `client.databases.credentials()`. TypeScript ```typescript interface CustomerPasswordResetResponse { admin_user: string; admin_password: string; connection_string: string; } ``` | Field | Type | Description | | ------------------- | -------- | --------------------------------------------------- | | `admin_user` | `string` | Admin Postgres username. | | `admin_password` | `string` | Admin Postgres password (new password after reset). | | `connection_string` | `string` | Full `postgresql://` connection URI. | ### UserResponse [Section titled “UserResponse”](#userresponse) Returned by `client.databases.users.list()`. TypeScript ```typescript interface UserResponse { name: string; is_superuser: boolean; can_login: boolean; can_create_db: boolean; can_create_role: boolean; } ``` | Field | Type | Description | | ----------------- | --------- | ------------------------------------------ | | `name` | `string` | Postgres username. | | `is_superuser` | `boolean` | Whether the user has superuser privileges. | | `can_login` | `boolean` | Whether the user can open connections. | | `can_create_db` | `boolean` | Whether the user can create databases. | | `can_create_role` | `boolean` | Whether the user can create roles. | ### MigrationApplyResponse [Section titled “MigrationApplyResponse”](#migrationapplyresponse) Returned by `client.databases.applyMigration()`. TypeScript ```typescript interface MigrationApplyResponse { status: string; name: string; } ``` | Field | Type | Description | | -------- | -------- | ----------------------------------------------------------- | | `status` | `string` | Migration result: `applied` or `skipped` (already applied). | | `name` | `string` | Migration name as provided in the request. | ### MigrationMetadata [Section titled “MigrationMetadata”](#migrationmetadata) Returned by `client.databases.listMigrations()`. TypeScript ```typescript interface MigrationMetadata { name: string; checksum: string; applied_at: string; sql_preview: string; } ``` | Field | Type | Description | | ------------- | -------- | -------------------------------------------------- | | `name` | `string` | Migration name. | | `checksum` | `string` | Checksum provided at apply time. | | `applied_at` | `string` | ISO 8601 timestamp when the migration was applied. | | `sql_preview` | `string` | Truncated SQL for display purposes. | ### MessageResponse [Section titled “MessageResponse”](#messageresponse) Returned by delete and revoke operations. TypeScript ```typescript interface MessageResponse { message: string; } ``` | Field | Type | Description | | --------- | -------- | ------------------------------------ | | `message` | `string` | Human-readable confirmation message. | ### Request Types [Section titled “Request Types”](#request-types) | Type | Key Fields | | ----------------------- | ------------------------------------------------------------ | | `CreateDatabaseRequest` | `name: string`, `region?: string`, `admin_password?: string` | | `CreateTokenRequest` | `name?: string`, `expires_in_days?: number` | | `BranchRequest` | `name: string` | | `DumpRequest` | `ddl_only?: boolean` | | `CreateUserRequest` | `username: string`, `password: string` | | `MigrationApplyRequest` | `name: string`, `sql: string`, `checksum: string` | ### Other Exported Types [Section titled “Other Exported Types”](#other-exported-types) | Type | Description | | ----------------------------------- | --------------------------------------------------------------------------------------------- | | `TenantState` | Union: `'CREATING' \| 'ACTIVE' \| 'DISABLING' \| 'DISABLED' \| 'CREATE_FAILED'` | | `SqlErrorDetail` | Structured SQL error: `message`, `code?`, `position?`, `hint?`, `detail?` | | `Endpoint` | Connection endpoint: `host`, `port`, `type`, `region?`, `priority`, `enabled`, `description?` | | `InstantDatabaseOptions` | Options for `instantDatabase()` — see [Quick Start](#quick-start) | | `Db9ClientOptions` | Options for `createDb9Client()` — see [createDb9Client](#createdb9clientoptions) | | `CredentialStore` | Interface for token load/save — see [Credential Storage](#credential-storage) | | `FetchFn` | Custom fetch implementation type | | `FsListOptions`, `FsConnectOptions` | Filesystem operation options | ## Next Steps [Section titled “Next Steps”](#next-steps) * [Browser SDK](/docs/sdk-browser/) — Client-side data access with RLS * [CLI Reference](/docs/cli/) — Terminal-based database management * [Connect](/docs/connect/) — Connection strings, TLS, and driver configuration * [Provisioning](/docs/platform/provisioning/) — Programmatic fleet management patterns * [Agent Workflows](/docs/agent-workflows/overview/) — SDK usage in agent pipelines * [Extensions](/docs/extensions/) — fs9, HTTP, vector, pg\_cron, and more # Browser SDK > API reference for the @db9/browser TypeScript SDK — query databases from client-side code with Row-Level Security, publishable keys, and a chainable query builder. Preview: package not yet published to npm The `@db9/browser` package is in active development (`v0.1.0`) and is **not yet published to npm**. `npm install @db9/browser` will return `404 Not Found`. This page documents the upcoming API surface so you can preview it; the install and import snippets will start working once the first release ships. Until then, query DB9 from the browser using the [REST API](/docs/api/) or proxy through a server route that uses the [Node.js SDK](/docs/sdk/). Never expose connection strings or admin credentials to the browser The browser SDK uses scoped publishable keys (`db9pk_...`), not connection strings or admin tokens. Never embed `postgresql://...` connection strings or API bearer tokens in client-side code — use a publishable key with `allowed_origins` and `exposed_tables` restrictions instead. The `@db9/browser` SDK lets you query DB9 databases directly from browser and edge environments. All queries flow through the public data API with **Row-Level Security (RLS)** enforcement — you never expose raw connection strings or admin credentials to the client. ## When to Use the Browser SDK [Section titled “When to Use the Browser SDK”](#when-to-use-the-browser-sdk) | Scenario | Recommended tool | | -------------------------------------------- | ---------------------------------------------------------------------- | | Client-side data access with per-user RLS | **Browser SDK** — `@db9/browser` | | Invoke serverless functions from the browser | **Browser SDK** — `client.functions.invoke()` | | Server-side provisioning, migrations, fs9 | **Node.js SDK** — `get-db9` ([TypeScript SDK](/docs/sdk/)) | | Terminal database management | **CLI** — `db9` ([CLI Reference](/docs/cli/)) | | ORM or driver connection from a backend | **Raw pgwire** — use the connection string ([Connect](/docs/connect/)) | ## Installation [Section titled “Installation”](#installation) Works in any environment with `fetch` — browsers, Deno, Cloudflare Workers, Next.js client components. Terminal ```bash npm install @db9/browser ``` Also available via `pnpm add @db9/browser`, `yarn add @db9/browser`, or `bun add @db9/browser`. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) Before using the browser SDK you need: 1. **A database** — create one with `db9 create` or the Node.js SDK. 2. **A publishable key** — create one via the REST API (see [Security & Auth — Publishable Keys](/docs/platform/security-and-auth/#publishable-keys)). 3. **(Optional) Auth config** — if you want per-user RLS, configure BYO JWT on the database (see [Security & Auth — BYO JWT](/docs/platform/security-and-auth/#bring-your-own-jwt)). ## Quick Start [Section titled “Quick Start”](#quick-start) TypeScript ```typescript import { createDb9BrowserClient } from '@db9/browser'; const db9 = createDb9BrowserClient({ apiUrl: 'https://api.db9.ai', databaseId: 'your-database-id', anonKey: 'db9pk_your_publishable_key', }); // Read rows const { data, error } = await db9.from('todos').select('*'); if (error) { console.error(error.message, error.statusCode); } else { console.log(data); // [{ id: 1, task: '...', done: false }, ...] } ``` ▶ Run ## `createDb9BrowserClient(options)` [Section titled “createDb9BrowserClient(options)”](#createdb9browserclientoptions) Creates a client instance. The client is stateless — no persistent connections are opened until a query executes. TypeScript ```typescript function createDb9BrowserClient(options: Db9BrowserClientOptions): Db9BrowserClient; ``` | Option | Type | Required | Description | | ------------ | -------------- | -------- | ----------------------------------------------------------------- | | `apiUrl` | `string` | Yes | Base URL of the DB9 API (e.g., `https://api.db9.ai`). | | `databaseId` | `string` | Yes | Target database ID. | | `anonKey` | `string` | Yes | Publishable key (`db9pk_...`). Safe to embed in client-side code. | | `fetch` | `typeof fetch` | No | Custom fetch implementation (e.g., for testing or polyfills). | | `timeout` | `number` | No | Request timeout in milliseconds. | ## Authentication (`client.auth`) [Section titled “Authentication (client.auth)”](#authentication-clientauth) Without authentication, queries run under the **anonymous** role. To enable per-user RLS enforcement, attach a JWT from your auth provider. ### Static Token [Section titled “Static Token”](#static-token) Set a token you already have (e.g., after login): TypeScript ```typescript db9.auth.setSession({ accessToken: userJwt }); ``` ### Token Provider (Auto-Refresh) [Section titled “Token Provider (Auto-Refresh)”](#token-provider-auto-refresh) Supply an async function that is called before each request — ideal for expiring tokens: TypeScript ```typescript db9.auth.setAccessTokenProvider(async () => { return await myAuthService.getAccessToken(); }); ``` ### Inspect or Clear Session [Section titled “Inspect or Clear Session”](#inspect-or-clear-session) TypeScript ```typescript const session = db9.auth.getSession(); // { accessToken: '...' } or null db9.auth.clearSession(); // reverts to anonymous role ``` ### Auth Reference [Section titled “Auth Reference”](#auth-reference) | Method | Description | | ----------------------------- | ------------------------------------------------------- | | `setSession({ accessToken })` | Set a static JWT token. | | `setAccessTokenProvider(fn)` | Set an async token provider called before each request. | | `getSession()` | Returns `{ accessToken }` or `null`. | | `clearSession()` | Removes token and provider; reverts to anonymous role. | ## Query Builder [Section titled “Query Builder”](#query-builder) All queries start with `db9.from(table)`, which returns a `TableRef`. From there, chain into one of four operations: `select`, `insert`, `update`, or `delete`. Every query returns `Db9Result` — an object with `{ data, error }`. The SDK **never throws** on API errors; check `error` instead. TypeScript ```typescript type Row = Record; interface Db9Result { data: T | null; error: Db9BrowserError | null; } ``` ### SELECT [Section titled “SELECT”](#select) TypeScript ```typescript // All columns, all rows (up to server default limit of 100) const { data } = await db9.from('todos').select('*'); // Specific columns const { data } = await db9.from('todos').select('id', 'task'); // With filters, ordering, and pagination const { data } = await db9 .from('todos') .select('*') .eq('status', 'active') .order('created_at', 'desc') .limit(20) .offset(40); ``` ▶ Run #### SelectBuilder Methods [Section titled “SelectBuilder Methods”](#selectbuilder-methods) | Method | Signature | Description | | -------- | --------------------------- | ---------------------------------------------------------------- | | `select` | `(...columns: string[])` | Columns to return. `'*'` or omit for all. | | `eq` | `(column, value)` | `column = value` | | `neq` | `(column, value)` | `column != value` | | `gt` | `(column, value)` | `column > value` | | `gte` | `(column, value)` | `column >= value` | | `lt` | `(column, value)` | `column < value` | | `lte` | `(column, value)` | `column <= value` | | `like` | `(column, pattern)` | `column LIKE pattern` | | `ilike` | `(column, pattern)` | `column ILIKE pattern` (case-insensitive) | | `in` | `(column, values[])` | `column IN (...)` | | `is` | `(column, null \| boolean)` | `column IS NULL` / `column IS NOT NULL` | | `filter` | `(column, op, value)` | Generic filter with any `FilterOperator`. | | `order` | `(column, 'asc' \| 'desc')` | Sort results. Default: `'asc'`. Chainable for multi-column sort. | | `limit` | `(count)` | Max rows to return (server max: 1000). | | `offset` | `(count)` | Skip rows for pagination. | ### INSERT [Section titled “INSERT”](#insert) TypeScript ```typescript const { data, error } = await db9 .from('todos') .insert({ task: 'Ship browser SDK', status: 'active' }) .returning('*'); ``` ▶ Run | Method | Signature | Description | | ----------- | -------------- | ----------------------------------------------------------- | | `values` | `(record)` | Set column values. Also accepted as argument to `insert()`. | | `returning` | `(...columns)` | Columns to return from the inserted row. `'*'` for all. | ### UPDATE [Section titled “UPDATE”](#update) TypeScript ```typescript const { data, error } = await db9 .from('todos') .update({ status: 'done' }) .eq('id', 42); ``` ▶ Run | Method | Signature | Description | | -------- | --------------------- | ----------------------------------------------------------- | | `set` | `(record)` | Set column values. Also accepted as argument to `update()`. | | `eq` | `(column, value)` | Filter rows to update. | | `filter` | `(column, op, value)` | Generic filter. | ### DELETE [Section titled “DELETE”](#delete) TypeScript ```typescript const { data, error } = await db9 .from('todos') .delete() .eq('id', 42); ``` ▶ Run | Method | Signature | Description | | -------- | --------------------- | ---------------------- | | `eq` | `(column, value)` | Filter rows to delete. | | `filter` | `(column, op, value)` | Generic filter. | ## Schema-Qualified Tables [Section titled “Schema-Qualified Tables”](#schema-qualified-tables) To query tables outside the `public` schema, use dot notation: TypeScript ```typescript const { data } = await db9.from('analytics.events').select('*'); ``` ▶ Run The schema must be listed in the publishable key’s `exposed_schemas`. ## Error Handling [Section titled “Error Handling”](#error-handling) The SDK returns errors in `Db9Result.error` instead of throwing. The error object includes a human-readable message and the HTTP status code. TypeScript ```typescript const { data, error } = await db9.from('todos').select('*'); if (error) { console.error(`[${error.statusCode}] ${error.message}`); // e.g., [403] "table 'secrets' is not exposed by this key" // e.g., [401] "invalid or expired publishable key" } ``` ▶ Run ### Db9BrowserError [Section titled “Db9BrowserError”](#db9browsererror) TypeScript ```typescript class Db9BrowserError extends Error { readonly statusCode: number; } ``` | Status Code | Meaning | | ----------- | ------------------------------------------------------------- | | `400` | Invalid request (bad filter, missing values). | | `401` | Invalid publishable key or expired JWT. | | `403` | Table or schema not exposed by the key, or RLS denied access. | | `404` | Database not found. | | `429` | Rate limit exceeded (configure via publishable key). | ## Serverless Functions (`client.functions`) [Section titled “Serverless Functions (client.functions)”](#serverless-functions-clientfunctions) Invoke [DB9 Serverless Functions](/docs/functions/) directly from the browser. The function must be deployed and configured to accept public invocations. HTTP-level failures are returned in `error`; function-level failures (non-zero exit, thrown error) remain in the response body with `status: "error"`. TypeScript ```typescript const { data, error } = await db9.functions.invoke('greet', { name: 'world' }); if (error) { console.error(`[${error.statusCode}] ${error.message}`); } else { const result = JSON.parse(data.result_json ?? 'null'); console.log(result); // function return value } ``` ▶ Run ### `functions.invoke(name, input?, options?)` [Section titled “functions.invoke(name, input?, options?)”](#functionsinvokename-input-options) TypeScript ```typescript functions.invoke( functionName: string, input?: unknown, options?: FunctionInvokeOptions, ): Promise> ``` | Parameter | Type | Required | Description | | ------------------------ | --------- | -------- | ----------------------------------------------------------- | | `functionName` | `string` | Yes | Name of the function to invoke. | | `input` | `unknown` | No | Input payload passed to the function as `ctx.input`. | | `options.idempotencyKey` | `string` | No | Key for deduplicating function runs within the same window. | The result `data` is a `FunctionInvokeResponse`: | Field | Type | Description | | --------------- | --------- | ----------------------------------------- | | `run_id` | `string` | Unique ID for this invocation. | | `function_id` | `string` | Function ID. | | `version_id` | `string` | Version that ran. | | `status` | `string` | Outcome: `success`, `error`, etc. | | `result_json` | `string?` | Function return value serialized as JSON. | | `error_code` | `string?` | Error code if `status` is `"error"`. | | `error_message` | `string?` | Human-readable error message. | | `logs_tail` | `string?` | Last log lines from the function run. | See [Serverless Functions](/docs/functions/) for how to create and deploy functions. ## TypeScript Types Reference [Section titled “TypeScript Types Reference”](#typescript-types-reference) All types are exported from the package entry point: TypeScript ```typescript import type { Db9BrowserClientOptions, Db9BrowserClient, Db9Auth, AccessTokenProvider, QueryOperation, FilterOperator, QueryFilter, QueryOrder, QueryRequest, QueryResponse, ColumnInfo, QueryError, Db9Result, Row, FunctionInvokeOptions, FunctionInvokeResponse, Db9Functions, } from '@db9/browser'; import { createDb9BrowserClient, Db9BrowserError, SelectBuilder, InsertBuilder, UpdateBuilder, DeleteBuilder, TableRef, } from '@db9/browser'; ``` ## Full Example: Next.js with Auth [Section titled “Full Example: Next.js with Auth”](#full-example-nextjs-with-auth) TypeScript ```typescript // lib/db9.ts import { createDb9BrowserClient } from '@db9/browser'; export const db9 = createDb9BrowserClient({ apiUrl: process.env.NEXT_PUBLIC_DB9_API_URL!, databaseId: process.env.NEXT_PUBLIC_DB9_DATABASE_ID!, anonKey: process.env.NEXT_PUBLIC_DB9_ANON_KEY!, }); ``` TypeScript ```typescript // components/TodoList.tsx 'use client'; import { useEffect, useState } from 'react'; import { db9 } from '../lib/db9'; import { useAuth } from '../hooks/useAuth'; export function TodoList() { const { jwt } = useAuth(); const [todos, setTodos] = useState([]); useEffect(() => { if (jwt) { db9.auth.setSession({ accessToken: jwt }); } else { db9.auth.clearSession(); } }, [jwt]); useEffect(() => { async function load() { const { data, error } = await db9 .from('todos') .select('*') .order('created_at', 'desc') .limit(50); if (data) setTodos(data); } load(); }, [jwt]); return (
    {todos.map((t) => (
  • {t.task}
  • ))}
); } ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * [TypeScript SDK](/docs/sdk/) — Server-side Node.js SDK for provisioning and admin * [Security & Auth](/docs/platform/security-and-auth/) — Publishable keys, BYO JWT configuration * [Row-Level Security](/docs/sql/rls/) — RLS policies and enforcement * [CLI Reference](/docs/cli/) — Terminal-based database management # SQL Reference > DB9 SQL engine overview — PostgreSQL compatibility, supported features, known boundaries, and links to detailed reference pages. DB9 implements a PostgreSQL-compatible SQL engine with pgwire protocol v3, a cost-based optimizer, and TiKV-backed distributed storage. Most PostgreSQL clients, ORMs, and drivers work without changes. ## What You Can Do [Section titled “What You Can Do”](#what-you-can-do) | Task | Where to look | | ------------------------------------------- | ------------------------------------------ | | Create tables, indexes, views, types | [DDL](/docs/sql/ddl/) | | Insert, update, delete, upsert | [DML and Queries](/docs/sql/dml/) | | JOINs, CTEs, window functions, subqueries | [DML and Queries](/docs/sql/dml/) | | Transactions, savepoints, isolation levels | [Transactions](/docs/sql/transactions/) | | Data types and coercion rules | [Data Types](/docs/sql/data-types/) | | 100+ built-in functions | [Built-in Functions](/docs/sql/functions/) | | PL/pgSQL, triggers, sequences, custom types | [Advanced SQL](/docs/sql/advanced/) | | Roles, users, grants | [Auth and Roles](/docs/sql/auth/) | | Session parameters and GUC settings | [Session Parameters](/docs/sql/session/) | | Engine and extension limits | [Limits](/docs/sql/limits/) | | System catalog tables | [System Catalog](/docs/sql/catalog/) | ## PostgreSQL Compatibility at a Glance [Section titled “PostgreSQL Compatibility at a Glance”](#postgresql-compatibility-at-a-glance) ### Fully supported [Section titled “Fully supported”](#fully-supported) * **Wire protocol** — pgwire v3 with Simple Query and Extended Query (Parse/Bind/Describe/Execute) * **DDL** — `CREATE/ALTER/DROP TABLE`, indexes (btree, GIN; HNSW is recognized but disabled in the current release), views, materialized views, schemas, sequences, functions, triggers, types, collations * **DML** — `INSERT`, `UPDATE`, `DELETE` with `RETURNING`; `INSERT ON CONFLICT` (upsert) * **Queries** — `JOIN` (inner, left, right, full outer, cross, lateral), CTEs (including recursive), window functions, subqueries (correlated, EXISTS, IN/ANY/ALL), set operations (UNION/INTERSECT/EXCEPT) * **Transactions** — `BEGIN/COMMIT/ROLLBACK`, savepoints, autocommit * **Data types** — boolean, integer, bigint, double precision, numeric, text, varchar, bytea, timestamp/timestamptz, date, time, interval, uuid, json/jsonb, arrays, serial/bigserial, vector, tsvector/tsquery * **PL/pgSQL** — functions, procedures, and control flow (`WHILE`, `CONTINUE`, and cursors are not supported; `EXCEPTION` and `EXECUTE` require a `DO` block) * **Triggers** — BEFORE/AFTER on INSERT/UPDATE/DELETE. `FOR EACH STATEMENT` is accepted but runs once per row; `RETURN NEW;` must be alone on its line — see [Advanced SQL](/docs/sql/advanced/#triggers) * **Indexes** — btree (default), GIN, partial indexes, expression indexes, `CREATE INDEX CONCURRENTLY`. HNSW (vector) is a recognized access method but index building is disabled in the current release; exact vector search is unaffected. `GiST`, `Hash`, `SP-GiST`, and `BRIN` are rejected at `CREATE INDEX` time — see [DDL — CREATE INDEX](/docs/sql/ddl/#create-index) ### Partial or different from PostgreSQL [Section titled “Partial or different from PostgreSQL”](#partial-or-different-from-postgresql) DB9 Difference: SERIALIZABLE isolation `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE` does not error on the wire protocol — it emits `WARNING: TiKV provides snapshot isolation; SERIALIZABLE has been downgraded to REPEATABLE READ` and runs the transaction at `REPEATABLE READ`. Applications that require Serializable Snapshot Isolation (SSI) to prevent write-skew anomalies are not supported, and nothing will fail to tell you so — enforce the invariant with `SELECT ... FOR UPDATE` or a unique constraint. | Feature | DB9 behavior | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SERIALIZABLE` isolation | Not implemented — downgraded to `REPEATABLE READ` with a warning. | | `SMALLINT` / `INT2` | Aliased to `INTEGER` (32-bit). | | `REAL` / `FLOAT4` | Aliased to `DOUBLE PRECISION` (64-bit). | | `CHAR(n)` fixed-length | Accepted but silently aliased to `VARCHAR(n)` with the same length. Padding semantics differ from standard PostgreSQL. | | Timestamp precision | Microsecond precision, matching PostgreSQL. `TIMESTAMP(n)` / `TIMESTAMPTZ(n)` **columns** round to the declared precision on insert and on `ALTER COLUMN ... TYPE`. The parameter is ignored in cast expressions and on `TIME(n)` / `TIMETZ(n)` columns. | | GIN index execution | Functional. The planner produces GIN scan plans and the executor uses the index — `EXPLAIN ANALYZE` reports `KV Table Scan Pairs: 0`. | | Advisory locks | `pg_advisory_lock()` family is supported, but lock coordination is node-local (not cross-process/global). | | Encoding | UTF-8 only. No other server or client encoding. | GIN indexes are used at runtime GIN index execution works: the planner produces a GIN scan plan and the executor reads the index rather than the table. `EXPLAIN ANALYZE` makes this checkable — with the index present the plan is an `Index Scan` reporting `KV Table Scan Pairs: 0`, and dropping the index turns the same query into a `Seq Scan` that reads every row. ```plaintext -- with a GIN index on tags (1000 rows, 20 matches) Index Scan using arr_idx on arr_t Index Cond: (tags @> ARRAY[t7]) KV Table Scan Pairs: 0 KV Index Scan Pairs: 40 -- same query after DROP INDEX Seq Scan on arr_t KV Table Scan Pairs: 1000 ``` Selectivity depends on the operator class. Array and full-text predicates probe only the matching entries; `jsonb_ops` containment on a column of highly distinct values still reads a large share of the index, so the speedup there is smaller than in PostgreSQL. ### Not supported [Section titled “Not supported”](#not-supported) These PostgreSQL features are not available in DB9: * Logical replication * Foreign data wrappers (`postgres_fdw`, etc.) * Tablespaces (all storage is TiKV-managed) `LISTEN` / `NOTIFY` **is** supported, but only over a direct pgwire connection — a session has to stay open to receive notifications. The stateless HTTP SQL API can `NOTIFY` but cannot `LISTEN`. ## Query Execution Pipeline [Section titled “Query Execution Pipeline”](#query-execution-pipeline) When you send a SQL query to DB9, it follows this pipeline: 1. **Parse** — SQL text is parsed into an AST 2. **Route** — Statement classified as DDL, DML, transaction control, or settings 3. **Expand views** — View references inlined recursively (up to 64 levels) 4. **Analyze** — Semantic analysis resolves names, infers types, checks scope 5. **Optimize** — Cost-based optimizer (DPccp algorithm) chooses join order, access methods, and physical operators 6. **Execute** — Pull-based (Volcano) operator tree streams results row by row 7. **Return** — Results sent over pgwire The optimizer uses table statistics from `ANALYZE` for cardinality estimation, with predicate pushdown and subquery decorrelation. ## Key Limits [Section titled “Key Limits”](#key-limits) | Limit | Value | | ------------------------------ | ----------------------- | | Statement timeout | 60s (default) | | Idle transaction timeout | 600s / 10 min (default) | | View nesting depth | 64 levels | | Pending portals per connection | 32 | | Timestamp precision | Milliseconds | | Encoding | UTF-8 only | See [Limits](/docs/sql/limits/) for extension-specific limits (HTTP, fs9, pg\_cron). ## Connecting [Section titled “Connecting”](#connecting) DB9 uses standard PostgreSQL connection strings: Terminal ```bash # psql psql "postgresql://dbname.admin:password@pg.db9.io:5433/postgres" # Or via the CLI db9 db sql -q "SELECT now()" ``` Any PostgreSQL client library works: psycopg2, node-postgres, JDBC, Go pgx, etc. See [Connect](/docs/connect/) for full details. ## Reference Pages [Section titled “Reference Pages”](#reference-pages) * [Data Types](/docs/sql/data-types/) — 23+ supported types with coercion and casting rules * [DDL](/docs/sql/ddl/) — CREATE, ALTER, DROP for tables, indexes, views, schemas, types, triggers * [DML and Queries](/docs/sql/dml/) — INSERT, UPDATE, DELETE, JOINs, CTEs, window functions, subqueries * [Transactions](/docs/sql/transactions/) — isolation levels, savepoints, COPY * [Built-in Functions](/docs/sql/functions/) — string, math, date/time, JSON, array, aggregate, window, vector, FTS * [System Catalog](/docs/sql/catalog/) — pg\_class, pg\_attribute, information\_schema, and more * [Auth and Roles](/docs/sql/auth/) — users, roles, grants, and permissions * [Advanced SQL](/docs/sql/advanced/) — PL/pgSQL, triggers, sequences, custom types, collations * [Session Parameters](/docs/sql/session/) — SET/SHOW/RESET and GUC compatibility * [Limits](/docs/sql/limits/) — engine, extension, and timing constraints ## Next Steps [Section titled “Next Steps”](#next-steps) * [Connect](/docs/connect/) — Connection strings, TLS, and driver configuration * [Extensions](/docs/extensions/) — fs9, HTTP, vector, pg\_cron, embedding, and more * [Architecture](/docs/architecture/) — TiKV storage, pgwire, and query engine internals * [CLI Reference](/docs/cli/) — db9 db sql for terminal SQL execution * [Compatibility Matrix](/docs/platform/compatibility-matrix/) — Full supported/unsupported feature matrix across SQL, types, indexes, ORMs, and extensions # Advanced SQL > PL/pgSQL, sequences, triggers, and other advanced SQL features in DB9. ## Advanced SQL [Section titled “Advanced SQL”](#advanced-sql) ### PL/pgSQL [Section titled “PL/pgSQL”](#plpgsql) DB9 Difference: PL/pgSQL limitations DB9 supports PL/pgSQL with variable declarations, `IF`/`ELSIF`, `CASE`, `FOR` loops, `PERFORM`, `RAISE`, and `RETURN`. `WHILE` loops, `CONTINUE`, and cursor operations are **not supported** anywhere. `EXECUTE` (dynamic SQL), `BEGIN ... EXCEPTION` blocks, and nested `BEGIN ... END` blocks work **only inside a `DO` block**. Inside a `CREATE FUNCTION` body they fail with `0A000` — but **at call time, not at create time**: `CREATE FUNCTION` succeeds and the function is registered in `pg_proc`, then every call raises `PL/pgSQL EXECUTE requires a Session-owned interactive DO host` or `nested PL/pgSQL blocks require a Session-owned interactive DO host`. A migration that only replays DDL will therefore look clean. Migrate stored procedures that rely on these features before switching to DB9. DB9 supports PL/pgSQL functions and procedures with variable declarations and standard control flow. Multi-line `DECLARE` entries parse correctly — a declaration whose type or value spans several lines produces the same value as PostgreSQL, in both a `DO` block and a `CREATE FUNCTION` body: SQL ```sql DECLARE v int := 1 + 2; -- 3 DECLARE s text := 'abc' || 'def'; -- 'abcdef' DECLARE v int := 42; -- 42 ``` #### Dynamic SQL and exception handling [Section titled “Dynamic SQL and exception handling”](#dynamic-sql-and-exception-handling) Both require a `DO` block. Inside one, `EXECUTE` runs a command string that may be either a literal or a variable, but it cannot capture a result or bind parameters: `EXECUTE ... INTO` fails with `XX000` (`internal error`), and `EXECUTE ... USING` is rejected at parse time with `42601`. SQL ```sql DO $$ BEGIN EXECUTE 'CREATE TABLE audit_snapshot(x int)'; EXCEPTION WHEN others THEN RAISE NOTICE 'setup skipped'; END; $$; ``` Exception handlers follow PostgreSQL rollback semantics: statements that ran before the exception are rolled back, and only the handler’s effects persist. #### CASE statements [Section titled “CASE statements”](#case-statements) `CASE` works in both function bodies and `DO` blocks, in simple and searched form: SQL ```sql CREATE FUNCTION size_label(n INT) RETURNS TEXT AS $$ BEGIN CASE WHEN n > 100 THEN RETURN 'large'; WHEN n > 10 THEN RETURN 'medium'; ELSE RETURN 'small'; END CASE; END; $$ LANGUAGE plpgsql; ``` Always include ELSE PostgreSQL raises `CASE_NOT_FOUND` (`20000`) when no branch matches and no `ELSE` is present. DB9 falls through silently, leaving the target variable `NULL`. SQL ```sql CREATE FUNCTION increment(val INT) RETURNS INT AS $$ BEGIN RETURN val + 1; END; $$ LANGUAGE plpgsql; CREATE FUNCTION safe_divide(a NUMERIC, b NUMERIC) RETURNS NUMERIC AS $$ BEGIN RETURN a / NULLIF(b, 0); END; $$ LANGUAGE plpgsql; ``` #### SELECT … INTO [Section titled “SELECT … INTO”](#select--into) `SELECT ... INTO variable` assigns a query result to a declared variable. SQL ```sql CREATE FUNCTION user_count() RETURNS INT AS $$ DECLARE n INT; BEGIN SELECT count(*) INTO n FROM users; RETURN n; END; $$ LANGUAGE plpgsql; ``` DB9 Difference: an earlier `into` in the statement breaks the parse DB9 finds the `INTO` target by scanning for the first case-insensitive occurrence of `into` in the statement text, without skipping string literals, comments, or identifiers. If one precedes the real keyword, the statement is split in the wrong place: | Earlier `into` appears in | Result | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | A string literal — `SELECT 'into' INTO r` | `42601` — `Unterminated string literal` | | A block comment — `SELECT /* into */ 7 INTO r` | `42601` — `Unexpected EOF while in a multi-line comment` | | An identifier or alias — `SELECT into_col INTO r FROM ...` | **Silent.** Runs as SQL `SELECT ... INTO
`, creating a table named after the target variable and leaving the variable `NULL`; the next call fails with `relation "r" already exists` | Substrings count: `'xintoy'` and `'staticinto'` fail just as `'into'` does. Rename the offending identifier, or move the value after the keyword — `SELECT s.c INTO r FROM (SELECT 'zinto' AS c) s` works. Variable assignment (`:=`), `RETURN`, `INSERT INTO ... VALUES ('...into...')` and `RETURNING ... INTO` are unaffected. #### RETURNING … INTO [Section titled “RETURNING … INTO”](#returning--into) DML statements inside PL/pgSQL functions can capture returned values into variables using `RETURNING ... INTO`. Works with `INSERT`, `UPDATE`, and `DELETE`. SQL ```sql CREATE FUNCTION create_order(p_item TEXT) RETURNS INT AS $$ DECLARE new_id INT; BEGIN INSERT INTO orders (item) VALUES (p_item) RETURNING id INTO new_id; RETURN new_id; END; $$ LANGUAGE plpgsql; CREATE FUNCTION archive_user(p_id INT) RETURNS TEXT AS $$ DECLARE removed_name TEXT; BEGIN DELETE FROM users WHERE id = p_id RETURNING name INTO removed_name; RETURN COALESCE(removed_name, 'not found'); END; $$ LANGUAGE plpgsql; ``` Multiple columns can be captured into separate variables: SQL ```sql RETURNING id, name INTO v_id, v_name; ``` If the DML statement affects zero rows, the target variables are set to `NULL`. Expressions (not just column names) are supported in the `RETURNING` list. Following PostgreSQL semantics, `RETURNING ... INTO` with a statement that returns more than one row will capture the first row. ### Triggers [Section titled “Triggers”](#triggers) Row-level triggers on INSERT, UPDATE, and DELETE. `FOR EACH STATEMENT` is accepted but does not get statement-level semantics — see the caution below. SQL ```sql CREATE FUNCTION audit_trigger() RETURNS TRIGGER AS $$ BEGIN INSERT INTO audit_log (table_name, action, changed_at) VALUES (TG_TABLE_NAME, TG_OP, NOW()); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER users_audit AFTER INSERT OR UPDATE ON users FOR EACH ROW EXECUTE FUNCTION audit_trigger(); ``` Supported trigger timing: `BEFORE`, `AFTER`. Supported events: `INSERT`, `UPDATE`, `DELETE`. A `BEFORE` trigger may assign to `NEW` (`NEW.name := upper(NEW.name);`) and the modified row is what gets stored. `RETURN NEW;` and `RETURN OLD;` must be alone on their line DB9 scans a trigger body line by line and only recognises `NEW` / `OLD` in a `RETURN` when that `RETURN` is the whole line. Share the line with anything else and `NEW` falls through to normal name resolution, so the trigger fails at run time — on both the wire protocol and the HTTP SQL API — with: ```plaintext ERROR: column "NEW" does not exist -- SQLSTATE 42703 ``` The statement that fired the trigger is rolled back with it, and the error names a column you never wrote, so it points nowhere near the real cause. SQL ```sql -- Fails: RETURN NEW shares its line CREATE FUNCTION t_bad() RETURNS TRIGGER AS $$ BEGIN INSERT INTO audit_log (table_name, action, changed_at) VALUES (TG_TABLE_NAME, TG_OP, NOW()); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Works: RETURN NEW is the only statement on its line CREATE FUNCTION t_ok() RETURNS TRIGGER AS $$ BEGIN INSERT INTO audit_log (table_name, action, changed_at) VALUES (TG_TABLE_NAME, TG_OP, NOW()); RETURN NEW; END; $$ LANGUAGE plpgsql; ``` Only the bare `RETURN NEW` / `RETURN OLD` form is affected. Qualified references such as `NEW.name` are resolved normally wherever they appear, and `RETURN NULL;` — the usual `AFTER` trigger ending — works on a shared line because `NULL` is a literal, not a pseudo-record. Formatters and ORMs that emit a compact one-line body are the common way to hit this. `FOR EACH STATEMENT` runs once per row `CREATE TRIGGER ... FOR EACH STATEMENT` is accepted, but DB9 does not model the statement level — the trigger executes with `FOR EACH ROW` semantics. Nothing warns you, and `pg_trigger` has no `tgtype` column to read the declared level back from. Measured against a table with one `AFTER ... FOR EACH STATEMENT` trigger per event: | Statement | PostgreSQL | DB9 | | ------------------------ | ----------- | ----- | | `INSERT` of 3 rows | 1 execution | **3** | | `UPDATE` matching 3 rows | 1 execution | **3** | | `UPDATE` matching 0 rows | 1 execution | **0** | | `DELETE` matching 0 rows | 1 execution | **0** | The zero-row rows are the ones that bite: a statement-level trigger written to record “this table was written to” never fires when the statement matches nothing, and over-counts by a factor of the row count when it does. Write the logic as an explicit `FOR EACH ROW` trigger so the per-row behaviour is intentional, or do the once-per-statement work in the application. ### Sequences [Section titled “Sequences”](#sequences) SQL ```sql CREATE SEQUENCE order_seq START WITH 1000 INCREMENT BY 1; SELECT NEXTVAL('order_seq'); SELECT CURRVAL('order_seq'); SELECT LASTVAL(); SELECT SETVAL('order_seq', 2000); DROP SEQUENCE order_seq; ``` ▶ Run Sequence options: `START`, `INCREMENT`, `MINVALUE`, `MAXVALUE`, `CACHE`, `CYCLE`. `ALTER SEQUENCE` cannot change any of them after creation — it supports only `OWNER TO` and `OWNED BY`, so `RESTART`, `INCREMENT BY`, `RENAME TO` and every other clause raise `ERROR: ALTER SEQUENCE not supported (0A000)`. Use `SETVAL()` to reposition a sequence. Note that `DROP SEQUENCE` does **not** check column defaults that depend on the sequence — see [DDL — Other DDL](/docs/sql/ddl/#other-ddl) for both caveats. See also: [DDL — Identity columns](/docs/sql/ddl/#create-table) for `GENERATED ALWAYS AS IDENTITY` and `GENERATED BY DEFAULT AS IDENTITY`. ### Custom Types [Section titled “Custom Types”](#custom-types) SQL ```sql -- Enum type CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral'); ALTER TYPE mood ADD VALUE 'excited'; -- Composite type CREATE TYPE address AS (street TEXT, city TEXT, zip TEXT); ``` ▶ Run ### Collations [Section titled “Collations”](#collations) SQL ```sql CREATE COLLATION my_collation (LOCALE = 'en_US.utf8'); DROP COLLATION my_collation; ``` ▶ Run A collation you create is real: it persists across sessions, and declaring it on a column changes how that column sorts and compares. SQL ```sql CREATE COLLATION my_collation (LOCALE = 'en_US.utf8'); CREATE TABLE coll_demo (t TEXT COLLATE my_collation); INSERT INTO coll_demo VALUES ('B'), ('a'); SELECT t FROM coll_demo ORDER BY t; -- a -- B ``` A `TEXT` column declared without a collation sorts the same way as `COLLATE "C"` — by byte value — so the same two rows come back in the opposite order (`B`, then `a`). The same applies to comparisons: `('a' COLLATE my_collation) < 'B'` is true, while an uncollated `'a' < 'B'` is false. The `LOCALE` you name is ignored Declaring *any* collation switches the column to one fixed locale-aware order. The `LOCALE` value has no effect — a collation created with `LOCALE = 'C'` sorts identically to one created with `LOCALE = 'en_US.utf8'`: SQL ```sql CREATE COLLATION coll_c (LOCALE = 'C'); CREATE COLLATION coll_en (LOCALE = 'en_US.utf8'); -- rows 'a', 'A', 'b', 'B' -- COLLATE coll_c -> a, A, b, B PostgreSQL's C collation gives A, B, a, b -- COLLATE coll_en -> a, A, b, B -- no collation -> A, B, a, b ``` So you can choose *between* byte order and locale order, but not between locales. DB9’s database-default ordering is bytewise where PostgreSQL follows the database locale — a known parity gap (DB9-FEAT-132). Aggregate and window `ORDER BY` drop the collation A collation applies to a plain `ORDER BY` on the column, but **not** to an `ORDER BY` written inside an aggregate, nor to a window’s `ORDER BY`. Those silently fall back to byte order on the same column, in the same query: SQL ```sql -- rows 'a', 'A', 'b', 'B' in a column declared COLLATE my_collation SELECT t FROM cx ORDER BY t; -- a, A, b, B (collated) SELECT string_agg(t, ',' ORDER BY t) FROM cx; -- A,B,a,b (byte order) SELECT array_agg(t ORDER BY t) FROM cx; -- {A,B,a,b} (byte order) SELECT t, row_number() OVER (ORDER BY t) FROM cx; -- A, B, a, b (byte order) ``` If you need collated ordering inside an aggregate, sort in a subquery first and aggregate over the result. The catalog, however, does not reflect any of this. Two lookups you might reach for are misleading: | Lookup | What it returns | Reality | | -------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------- | | `SELECT collname FROM pg_collation` | only `C`, `POSIX`, `default` | your collation exists | | `pg_attribute.attcollation` for a `COLLATE` column | `100`, the `default` OID — identical to a column declared with no collation | the column does carry the declared collation | So do not use either one to check whether a collation exists. Create it again instead: if it is already there, that fails with `ERROR: collation "my_collation" already exists (42710)` — and if it was not, remember to `DROP COLLATION` the one you just made. # Authentication & Roles > Connection format, TLS, and role-based access control in DB9. ## Authentication & Roles [Section titled “Authentication & Roles”](#authentication--roles) Connection format, TLS, and role-based access control. ### Connecting [Section titled “Connecting”](#connecting) Connect using the standard PostgreSQL connection string format: Terminal ```bash psql "postgresql://.:@pg.db9.io:5433/postgres" ``` All connections use TLS (`sslmode=require`). See [Connect](/docs/connect/) for full driver and ORM examples. ### Role Management [Section titled “Role Management”](#role-management) SQL ```sql -- Create a role with login CREATE ROLE app_user LOGIN PASSWORD 'SecurePass1'; -- Create a role with specific attributes CREATE ROLE admin_role LOGIN PASSWORD 'pw' BYPASSRLS; CREATE ROLE readonly_role LOGIN PASSWORD 'pw'; -- Alter role attributes ALTER ROLE app_user PASSWORD 'NewPass1'; ALTER ROLE admin_role BYPASSRLS; ALTER ROLE admin_role NOBYPASSRLS; -- Drop a role DROP ROLE app_user; ``` ### Role Attributes [Section titled “Role Attributes”](#role-attributes) | Attribute | Description | | ----------- | ------------------------------------------------------------------------ | | `LOGIN` | Role can connect (required for users). | | `SUPERUSER` | Bypasses all permission checks. The default `admin` role is a superuser. | | `CREATEDB` | Can create new databases. | | `BYPASSRLS` | Bypasses Row-Level Security policies (see [RLS](/docs/sql/rls/)). | ### Privileges [Section titled “Privileges”](#privileges) SQL ```sql -- Grant table access GRANT SELECT, INSERT ON todos TO app_user; GRANT ALL ON ALL TABLES IN SCHEMA public TO admin_role; -- Revoke access REVOKE INSERT ON todos FROM app_user; -- Schema-level grants GRANT USAGE ON SCHEMA analytics TO app_user; GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO app_user; ``` DB9 Difference: sequence privileges Table and sequence privileges are both enforced — an unprivileged role gets `permission denied for table` / `permission denied for sequence` (SQLSTATE `42501`). Calling `nextval()`, `currval()`, or `setval()` on a sequence requires a grant, and `setval()` needs `UPDATE` (a `USAGE, SELECT` grant is not sufficient). Two details differ from PostgreSQL: * **`SERIAL` defaults do not require a sequence grant.** An `INSERT` that relies on a `SERIAL` / `BIGSERIAL` column’s default succeeds with only the table grant, where PostgreSQL would also require `USAGE` on the backing sequence. * **Grants are not reflected in the catalog.** `GRANT ... ON SEQUENCE` / `ON ALL SEQUENCES` is enforced, but `pg_class.relacl` stays `NULL`, so you cannot audit sequence grants by reading the catalog. `ALTER DEFAULT PRIVILEGES ... ON SEQUENCES` applies only to sequences created explicitly with `CREATE SEQUENCE` — a sequence created implicitly by a `SERIAL` column does not inherit it. `ALTER DEFAULT PRIVILEGES ... ON FUNCTIONS` is not supported and raises `Invalid ALTER DEFAULT PRIVILEGES syntax`. ### Session Role Switching [Section titled “Session Role Switching”](#session-role-switching) SQL ```sql -- Switch to a different role within the session SET ROLE app_user; -- Reset to the original authenticated role RESET ROLE; ``` Role switching is enforced — you cannot `SET ROLE` to a role you haven’t been granted. ### Row-Level Security [Section titled “Row-Level Security”](#row-level-security) DB9 supports full PostgreSQL-compatible Row-Level Security. RLS policies filter rows based on the current role: SQL ```sql ALTER TABLE todos ENABLE ROW LEVEL SECURITY; CREATE POLICY user_todos ON todos FOR SELECT USING (user_id = current_user); ``` See [Row-Level Security](/docs/sql/rls/) for complete documentation on policies, permissive vs restrictive modes, bypass mechanisms, and Browser SDK integration. # System Catalog > System views for introspecting database objects in DB9. ## System Catalog [Section titled “System Catalog”](#system-catalog) DB9 implements PostgreSQL-compatible system catalog views for introspecting database objects. ### pg\_catalog Views [Section titled “pg\_catalog Views”](#pg_catalog-views) | View | Description | | ---------------------- | ---------------------------------------------------------------------------------------------------------- | | `pg_tables` | User tables | | `pg_views` | User views | | `pg_class` | Tables, indexes, sequences, views | | `pg_attribute` | Table columns | | `pg_attrdef` | Column default values | | `pg_namespace` | Schemas | | `pg_type` | Data types | | `pg_index` | Index metadata | | `pg_indexes` | Index definitions (`indexdef`) | | `pg_constraint` | Constraints (PK, FK, CHECK, UNIQUE, NOT NULL) | | `pg_proc` | Functions and procedures | | `pg_trigger` | Triggers — no `tgtype` column, so the declared BEFORE/AFTER and row/statement level cannot be read back | | `pg_enum` | Enum type values | | `pg_sequence` | Sequence metadata | | `pg_extension` | Installed extensions | | `pg_collation` | Collations — built-ins only; [collations you create work but never appear](/docs/sql/advanced/#collations) | | `pg_roles` / `pg_user` | User and role definitions | | `pg_database` | Databases | | `pg_description` | Object descriptions/comments | | `pg_depend` | Dependency tracking | | `pg_am` | Access methods | | `pg_stat_user_tables` | Table statistics | | `pg_inherits` | Table inheritance | | `pg_range` | Range type metadata | | `pg_opclass` | Operator classes | | `pg_policy` | Row-level security policies | | `pg_policies` | Readable RLS policy view (`qual`, `with_check`) | | `pg_settings` | Runtime parameters, including DB9’s `db9.*` tuning settings | | `pg_timezone_names` | Time zone names, abbreviations and UTC offsets (`name`, `abbrev`, `utc_offset`, `is_dst`) | | `pg_rewrite` | Rewrite rules — populated with the `_RETURN` rule backing each view | | `pg_aggregate` | Aggregate functions. Exposes only `aggfnoid`; the transition-function columns are absent | The following relations resolve but are always empty — they exist so that catalog-introspecting tools and ORMs do not error, not because the feature behind them is implemented: | Relation | Note | | ------------------------------------------------- | ------------------------------------------------------------------------------------- | | `pg_cast` | No user-defined casts | | `pg_auth_members` | Role membership is not exposed | | `pg_default_acl` | No default privilege rules | | `pg_db_role_setting` | No per-role/database settings | | `pg_shdescription` | No shared-object comments | | `pg_publication_namespace` / `pg_publication_rel` | No logical replication — see [`pg_publication`](/docs/platform/compatibility-matrix/) | ### information\_schema Views [Section titled “information\_schema Views”](#information_schema-views) | View | Description | | ------------------------- | --------------------------------------------------------------------------------------------------------------- | | `tables` | Tables | | `columns` | Table columns (includes `is_generated`, `generation_expression`) | | `schemata` | Schemas | | `sequences` | Sequences | | `routines` | Functions and procedures | | `table_constraints` | PRIMARY KEY, FOREIGN KEY, CHECK and UNIQUE constraints (never NOT NULL — see [below](#check-table-constraints)) | | `key_column_usage` | PRIMARY/FOREIGN KEY columns | | `referential_constraints` | Foreign key constraints | | `check_constraints` | CHECK constraints | | `constraint_column_usage` | Column constraint usage | | `table_privileges` | Table access privileges | These eleven views are the complete `information_schema` surface. Notably, `information_schema.views` is **not** implemented — it fails with `relation "views" does not exist` (`42P01`). List views through `pg_views`, or filter `information_schema.tables` on `table_type = 'VIEW'`. ### Cron Catalog [Section titled “Cron Catalog”](#cron-catalog) | View | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------ | | `cron.job` | Scheduled cron jobs | | `cron.job_run_details` | Job execution history | | `cron.running_jobs` | Currently executing jobs (superuser only) — see [pg\_cron](/docs/extensions/pg-cron/#cronrunning_jobs) | *** ## Query Recipes [Section titled “Query Recipes”](#query-recipes) Practical queries for introspecting your database. Replace `'public'` and `'my_table'` with your actual schema and table names. ### List all tables in a schema [Section titled “List all tables in a schema”](#list-all-tables-in-a-schema) Returns all user-defined tables in a schema with their owner and row estimate. SQL ```sql SELECT tablename AS table, tableowner AS owner, hasindexes AS indexed, hasrules AS has_rules, hastriggers AS has_triggers FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename; -- Alternative using information_schema (more portable): SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' ORDER BY table_name; ``` ▶ Run ### List columns of a table [Section titled “List columns of a table”](#list-columns-of-a-table) Returns all columns with their data type, nullability, and default value. SQL ```sql SELECT column_name, data_type, character_maximum_length, is_nullable, column_default, ordinal_position FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'my_table' ORDER BY ordinal_position; -- Using pg_attribute for lower-level detail (includes system columns): SELECT a.attname AS column, pg_catalog.format_type(a.atttypid, a.atttypmod) AS type, a.attnotnull AS not_null, a.attnum AS position FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND c.relname = 'my_table' AND a.attnum > 0 -- exclude system columns AND NOT a.attisdropped ORDER BY a.attnum; ``` ### Find all indexes on a table [Section titled “Find all indexes on a table”](#find-all-indexes-on-a-table) Lists every index on a table, including the columns covered and whether it is unique or primary. SQL ```sql SELECT indexname AS index, indexdef AS definition FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'my_table' ORDER BY indexname; -- More detailed view via pg_index + pg_class: SELECT i.relname AS index, ix.indisunique AS unique, ix.indisprimary AS primary, array_agg(a.attname ORDER BY a.attnum) AS columns FROM pg_index ix JOIN pg_class t ON t.oid = ix.indrelid JOIN pg_class i ON i.oid = ix.indexrelid JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) JOIN pg_namespace n ON n.oid = t.relnamespace WHERE n.nspname = 'public' AND t.relname = 'my_table' GROUP BY i.relname, ix.indisunique, ix.indisprimary ORDER BY i.relname; ``` ### Check table constraints [Section titled “Check table constraints”](#check-table-constraints) The `information_schema` query returns the PRIMARY KEY, UNIQUE, CHECK and FOREIGN KEY constraints on a table; the `pg_constraint` variant additionally returns NOT NULL. SQL ```sql SELECT tc.constraint_name, tc.constraint_type, kcu.column_name, cc.check_clause FROM information_schema.table_constraints tc LEFT JOIN information_schema.key_column_usage kcu ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema LEFT JOIN information_schema.check_constraints cc ON cc.constraint_name = tc.constraint_name AND cc.constraint_schema = tc.constraint_schema WHERE tc.table_schema = 'public' AND tc.table_name = 'my_table' ORDER BY tc.constraint_type, tc.constraint_name; -- Concise version using pg_constraint: SELECT conname AS constraint, contype AS type, -- p=PK, u=UNIQUE, c=CHECK, f=FK, n=NOT NULL pg_get_constraintdef(oid) AS definition FROM pg_constraint WHERE conrelid = 'public.my_table'::regclass ORDER BY contype, conname; ``` NOT NULL appears only in `pg_constraint` The two queries above do not return the same set. `information_schema.table_constraints` emits only `PRIMARY KEY`, `FOREIGN KEY`, `CHECK` and `UNIQUE` — DB9 never surfaces a `NOT NULL` row there, unlike PostgreSQL, which reports each one as a generated `CHECK`. `pg_constraint` does expose them, as `contype = 'n'` with a `
__not_null` name, so the concise query returns one extra row per `NOT NULL` column. Which PostgreSQL you are porting from matters here. PostgreSQL only records NOT NULL in `pg_constraint` as of version 18; PostgreSQL 16 — the version DB9 reports in `version()` — has no `contype = 'n'` rows at all, so a query calibrated against PG16 will see rows it does not expect. A query written for PG18 ports over as-is, with one difference: PG18 preserves a user-supplied NOT NULL constraint name, and DB9 does not. `CONSTRAINT my_nn NOT NULL` is stored as `
__not_null` regardless. Explicit `PRIMARY KEY`, `UNIQUE` and `CHECK` names are preserved as written. To list nullability, prefer `information_schema.columns.is_nullable` or `pg_attribute.attnotnull` over either constraint view. ### See RLS policies [Section titled “See RLS policies”](#see-rls-policies) Lists all row-level security policies on a table, including which roles they apply to and their USING / WITH CHECK expressions. SQL ```sql SELECT policyname AS policy, cmd AS command, -- ALL, SELECT, INSERT, UPDATE, DELETE roles AS applies_to, qual AS using_expr, with_check AS check_expr FROM pg_policies WHERE schemaname = 'public' AND tablename = 'my_table' ORDER BY policyname; -- Check if RLS is enabled on the table: SELECT relname, relrowsecurity, relforcerowsecurity FROM pg_class WHERE oid = 'public.my_table'::regclass; ``` ### Find foreign keys [Section titled “Find foreign keys”](#find-foreign-keys) Returns all foreign key relationships — both outbound (this table references another) and inbound (other tables reference this one). SQL ```sql -- Outbound: FK constraints on my_table SELECT tc.constraint_name, kcu.column_name AS fk_column, ccu.table_name AS references_table, ccu.column_name AS references_column, rc.update_rule, rc.delete_rule FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema JOIN information_schema.referential_constraints rc ON rc.constraint_name = tc.constraint_name AND rc.constraint_schema = tc.constraint_schema JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = rc.unique_constraint_name WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public' AND tc.table_name = 'my_table' ORDER BY tc.constraint_name; -- All FK constraints in the schema (both directions): SELECT c.conrelid::regclass::text AS from_table, c.confrelid::regclass::text AS to_table, c.conname AS constraint, pg_get_constraintdef(c.oid) AS definition FROM pg_constraint c JOIN pg_class t ON t.oid = c.conrelid JOIN pg_namespace n ON n.oid = t.relnamespace WHERE c.contype = 'f' AND n.nspname = 'public' ORDER BY from_table, c.conname; ``` Pitfall: cast `regclass` to `text`, and filter the schema by join Two things to watch when adapting `regclass` queries. Both fail *silently* rather than erroring: * **DB9 difference — `::regclass` alone renders the numeric OID**, not the relation name. `SELECT conrelid::regclass` returns `10000000005`, where PostgreSQL returns `child_t`. Add `::text` to get the name. * **`regclass::text` is schema-qualified only when the relation is not visible in `search_path`.** With the default `"$user", public`, a table in `public` renders bare as `child_t` while a table in another schema renders as `other_schema.t`. So the common idiom `WHERE conrelid::regclass::text LIKE 'public.%'` returns zero rows on a schema that does have foreign keys — and under a non-default `search_path` it is worse than useless: a `public` table shadowed by a same-named one earlier in the path renders *qualified* and does match, so the filter silently returns an arbitrary subset. Matching on the bare name instead is no better — it breaks as soon as a table lives outside `search_path`, or has a name that needs quoting (`public."Mix Ed"` renders as `"Mix Ed"`, quotes included). This idiom is equally broken on PostgreSQL; it is a widespread bug, not a DB9 limitation. Join to `pg_class` and `pg_namespace` and filter on `nspname` instead, as shown above. Casting in the other direction is unaffected — `WHERE conrelid = 'public.my_table'::regclass` works as written, including for tables outside `search_path`. ### List all functions [Section titled “List all functions”](#list-all-functions) Returns all user-defined functions in a schema with their return types. SQL ```sql SELECT routine_name AS function, routine_type AS type, data_type AS return_type FROM information_schema.routines WHERE routine_schema = 'public' ORDER BY routine_name; -- More detail via pg_proc: SELECT p.proname AS function, format_type(p.prorettype, NULL) AS return_type, p.prokind AS kind -- 'f' = function, 'p' = procedure, 'w' = window FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = 'public' ORDER BY p.proname; ``` Note `pg_proc` exposes 18 columns: `oid`, `proname`, `pronamespace`, `proowner`, `prorettype`, `prokind`, `prosecdef`, `proacl`, `pronargs`, `pronargdefaults`, `proargtypes`, `proargmodes`, `proargnames`, `proretset`, `prorows`, `provolatile`, `provariadic` and `proconfig`. Argument-level introspection works for **user-defined** functions — a function declared `add_two(a int, b int)` reports `pronargs = 2`, `proargtypes = 23 23` and `proargnames = {a,b}`: SQL ```sql SELECT proname, pronargs, proargtypes, proargnames FROM pg_proc WHERE proname = 'add_two'; ``` **This is reliable only for functions you created.** For catalog built-ins the argument columns are not trustworthy, in three separate ways: * `proargnames`, `proargmodes` and `provolatile` are **NULL** for every one of the 341 `pg_catalog` rows — test them with `IS NULL`, since `proargnames = '{}'` matches nothing. * `proargtypes` is frequently an **empty array** even when `pronargs` says there are arguments — 146 of the 341 rows, including `age`, `array_length`, `concat` and `array_to_string`. Test it with `cardinality(proargtypes) = 0`; the comparison `proargtypes = '{}'` fails with an `internal error`. * `pronargs` itself can be wrong. `count`, `make_interval` and the `json_build_*` / `jsonb_build_*` family report `pronargs = 0` while accepting arguments, and variadic functions under-report: `concat` says `1` but `concat('a','b','c','d','e')` works. The `extensions.http*` family (all `pronargs = 0`, taking one to four arguments) is one instance of this, not a special case. Where arity or argument types matter for a built-in, confirm them by calling the function. Still unavailable: `pg_get_function_arguments()`, `pg_get_function_result()`, `pg_proc.prosrc`, and the `pg_language` catalog. Use `information_schema.routines` for a portable summary. `pg_proc` is not a complete list of callable functions Many built-ins are implemented in the executor and never registered in the catalog. `ABS`, `SUM`, `GREATEST`, `SUBSTR`, `COALESCE`, `GENERATE_SERIES` and `UNNEST` are all absent from `pg_proc` yet work normally. Catalog absence is never evidence that a function is missing — call it instead. ### Check table sizes [Section titled “Check table sizes”](#check-table-sizes) Returns disk usage for every table, largest first. DB9 reports sizes through the [storage accounting](/docs/platform/storage/) virtual tables rather than the PostgreSQL size functions. SQL ```sql SELECT table_name, data_bytes, index_bytes, total_bytes, round(total_bytes::numeric / 1024 / 1024, 2) AS total_mb FROM _DB9_SYS_TABLE_STORAGE_STATS ORDER BY total_bytes DESC; -- Database-level totals: SELECT database_name, total_bytes, round(total_bytes::numeric / 1024 / 1024, 2) AS total_mb, scanned_at FROM _DB9_SYS_STORAGE_STATS; ``` DB9 Difference: PostgreSQL size functions do not exist `pg_size_pretty()`, `pg_total_relation_size()`, `pg_relation_size()`, `pg_table_size()` and `pg_indexes_size()` are not implemented — calling any of them fails with `function (...) does not exist` (`42883`), in both the bare and `extensions.`-qualified forms. Use the storage virtual tables above, which report raw `BIGINT` byte counts you can format yourself. # Data Types > All 24 data types supported by the DB9 SQL engine, including type coercion rules and cast contexts. ## Data Types [Section titled “Data Types”](#data-types) db9 supports 24 data types. All data is stored in UTF-8 encoding. | SQL Type | Aliases | Description | | ------------------ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BOOLEAN` | `BOOL` | True/false values | | `INTEGER` | `INT`, `INT4` | 4-byte signed integer | | `BIGINT` | `INT8` | 8-byte signed integer | | `DOUBLE PRECISION` | `FLOAT8` | 8-byte IEEE 754 floating point | | `NUMERIC` | `NUMERIC(p,s)`, `DECIMAL` | Arbitrary-precision number (up to 28-29 significant digits) | | `TEXT` | — | Variable-length UTF-8 string | | `VARCHAR(n)` | `CHARACTER VARYING(n)` | Length-limited text (max n characters) | | `BYTEA` | — | Raw byte array | | `TIMESTAMP` | `TIMESTAMP WITHOUT TIME ZONE` | Timestamp (microsecond precision) | | `TIMESTAMPTZ` | `TIMESTAMP WITH TIME ZONE` | Timestamp with timezone (microsecond precision) | | `DATE` | — | Calendar date (days) | | `TIME` | `TIME WITHOUT TIME ZONE` | Time of day (microsecond precision) | | `INTERVAL` | — | Time interval (months + microseconds). Operator restrictions apply — see below | | `UUID` | — | 128-bit UUID | | `INET` | — | IPv4/IPv6 address, host or network (`192.168.1.1`, `10.0.0.0/8`, `::1`). Equality and ordering supported; network operators (`<<`) and functions (`host()`, `netmask()`) not yet available, no `CIDR` type | | `JSON` | — | JSON data (original text preserved) | | `JSONB` | — | JSON data in canonical binary form | | `SERIAL` | — | Auto-incrementing 4-byte integer | | `BIGSERIAL` | — | Auto-incrementing 8-byte integer | | `type[]` | — | Array of any supported type | | `vector(N)` | — | Dense float vector of N dimensions (pgvector-compatible) | | `TSVECTOR` | — | Full-text search document representation | | `TSQUERY` | — | Full-text search query | | `NAME` | — | 63-byte identifier (PostgreSQL system type) | > **Not supported:** `SMALLINT` / `INT2`, `REAL` / `FLOAT4`, `CHAR(n)` (fixed-length). Use `INTEGER`, `DOUBLE PRECISION`, and `VARCHAR(n)` instead. > **Timestamp precision:** Timestamps are stored at **microsecond** precision, matching standard PostgreSQL. Precision parameters 0–6 are accepted in syntax but are not applied — `TIMESTAMP(3)` retains all six fractional digits rather than truncating. ### Interval Representation [Section titled “Interval Representation”](#interval-representation) Intervals store months and sub-month components separately, enabling calendar-aware arithmetic (e.g., adding 1 month to January 31 gives February 28/29). The sub-month component is stored in microseconds. DB9 Differences: interval operators Interval arithmetic has one remaining limitation compared with PostgreSQL. **Unary negation is not supported.** Both `-INTERVAL '1 day'` and `-i` (where `i` is an interval column) fail with `XX000` (`internal error`). Multiply by `-1` instead: SQL ```sql SELECT -1 * INTERVAL '1 day' AS negative_interval; ``` ▶ Run Everything else works as in PostgreSQL. An interval literal may appear on either side of a binary operator, intervals can be added to and subtracted from one another, and division is supported: SQL ```sql SELECT INTERVAL '1 day' * 2 AS literal_on_left, 2 * INTERVAL '1 day' AS literal_on_right, INTERVAL '1 day' / 2 AS divided, INTERVAL '1 day' + INTERVAL '1 hour' AS summed, INTERVAL '3 days' - INTERVAL '1 day' AS subtracted, NOW() - 7 * INTERVAL '1 day' AS a_week_ago; ``` ▶ Run Intervals held in a column or returned by an expression behave the same way: SQL ```sql SELECT i * 2 AS scaled, i / 2 AS halved, i + i AS summed, i - i AS zero, NOW() + i AS tomorrow FROM (SELECT 1 * INTERVAL '1 day' AS i) t; ``` ▶ Run ## Type Coercion [Section titled “Type Coercion”](#type-coercion) The type system uses two coercion strategies matching PostgreSQL behavior: ### UNION / CASE / COALESCE / VALUES (“Text wins”) [Section titled “UNION / CASE / COALESCE / VALUES (“Text wins”)”](#union--case--coalesce--values-text-wins) When mixing Text with a typed value, the result is Text. Example: `SELECT 1 UNION SELECT 'a'` resolves to `TEXT`. ### Comparisons (“Non-Text wins”) [Section titled “Comparisons (“Non-Text wins”)”](#comparisons-non-text-wins) When comparing Text with a typed value, the text literal is coerced to the typed side. Example: `WHERE col = '42'` coerces `'42'` to the column’s type. ### Numeric Promotion Hierarchy [Section titled “Numeric Promotion Hierarchy”](#numeric-promotion-hierarchy) `Int32` → `Int64` → `Float64` → `Numeric`. When two numeric types are mixed, the higher-precedence type wins. ### Temporal Promotion [Section titled “Temporal Promotion”](#temporal-promotion) * `Date` + `Timestamp` = `Timestamp` * `Timestamp` + `TimestampTz` = `TimestampTz` * `Date` + `TimestampTz` = `TimestampTz` ### CAST Contexts [Section titled “CAST Contexts”](#cast-contexts) | Context | When Used | Behavior | | -------------- | ------------------------------ | ---------------------------------------------------------------------------------- | | **Explicit** | `CAST(x AS type)` or `x::type` | Most permissive. Allows rounding (Float to Int), Bool↔Int, VARCHAR truncation | | **Assignment** | INSERT/UPDATE column coercion | Medium. Rejects fractional Float→Int, rejects Bool↔Int, errors on VARCHAR overflow | | **Implicit** | Comparison coercion | Strictest. Similar to Assignment | | Cast | Explicit | Assignment | | --------------------- | ------------------ | ------------------- | | `2.7::INTEGER` | Rounds to 3 | Rejects (fraction) | | `TRUE::INTEGER` | Returns 1 | Rejects | | `42::BOOLEAN` | Returns true | Rejects | | `'hello'::VARCHAR(3)` | Truncates to “hel” | Errors if > 3 chars | | `NULL` to any type | Always succeeds | Always succeeds | # DDL — Data Definition > Data Definition Language statements for creating and managing database objects in DB9. ## SQL Reference: DDL [Section titled “SQL Reference: DDL”](#sql-reference-ddl) Data Definition Language statements for creating and managing database objects. ### CREATE TABLE [Section titled “CREATE TABLE”](#create-table) SQL ```sql CREATE TABLE [IF NOT EXISTS] table_name ( column_name type [constraints], ... [table_constraints] ); -- Create from query result CREATE TABLE new_table AS SELECT ... FROM source; SELECT ... INTO new_table FROM source; ``` **Column constraints:** `NOT NULL`, `DEFAULT`, `PRIMARY KEY`, `UNIQUE`, `REFERENCES` (foreign key), `CHECK` **Table constraints:** `PRIMARY KEY (col, ...)`, `UNIQUE (col, ...)`, `FOREIGN KEY (col) REFERENCES table(col)` with `ON DELETE`/`ON UPDATE` actions, `CHECK (expr)` `SERIAL` and `BIGSERIAL` columns are supported for auto-incrementing IDs. Self-referential foreign keys are supported. `CREATE TABLE AS` adds a `_rowid` primary key column Tables created by `CREATE TABLE ... AS SELECT` or `SELECT ... INTO` get an extra internal `_rowid` column prepended to the projected columns. It is visible in `SELECT *` and in `information_schema.columns`, so `SELECT *` returns one more column than PostgreSQL would: SQL ```sql CREATE TABLE ctas AS SELECT 1 AS a, 'x' AS b; SELECT * FROM ctas; -- returns _rowid, a, b CREATE TABLE tgt (a INT, b TEXT); INSERT INTO tgt SELECT * FROM ctas; -- ERROR: INSERT has more target columns than expressions (2 columns, 3 values) (42601) ``` The error message’s wording is inverted — the surplus value is `_rowid`. `_rowid` becomes the table’s primary key and cannot be removed afterwards (`ALTER TABLE ctas DROP COLUMN _rowid` fails with `Cannot drop primary key column '_rowid'`). Selecting columns explicitly (`SELECT a, b FROM ctas`) avoids the problem per query; to avoid the column entirely, create the table first and populate it separately: SQL ```sql CREATE TABLE clean (a INT, b TEXT); INSERT INTO clean SELECT a, b FROM source; ``` Tables created with a plain `CREATE TABLE` are unaffected, with or without a primary key. **Identity columns:** SQL ```sql -- GENERATED ALWAYS AS IDENTITY (database controls the value) CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY); -- GENERATED BY DEFAULT AS IDENTITY (user can override) CREATE TABLE t (id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY); ``` Identity columns ignore sequence options Identity columns always start at 1 and increment by 1. Sequence options in an identity definition either fail to parse or have no effect: * More than one option is a parse error — the PostgreSQL form `IDENTITY (START WITH 100 INCREMENT BY 10)` fails with `SQL parse error: Expected ), found: INCREMENT` (42601). The comma-separated form fails too. * A *single* option such as `IDENTITY (START WITH 100)` or `IDENTITY (MINVALUE 5)` is accepted, but **silently ignored** — the generated values are still `1, 2, 3, …`. This applies to all of `START`, `INCREMENT`, `MINVALUE`, `MAXVALUE`, `CACHE`, and `CYCLE`. For a specific starting value or step, use an explicit sequence — `CREATE SEQUENCE` *does* honor `START` and `INCREMENT`: SQL ```sql CREATE SEQUENCE order_seq START WITH 1000 INCREMENT BY 5; CREATE TABLE orders (id BIGINT PRIMARY KEY DEFAULT nextval('order_seq'), tag TEXT); -- ids: 1000, 1005, 1010, … ``` **Generated (computed) columns:** SQL ```sql CREATE TABLE products ( price NUMERIC, tax NUMERIC, total NUMERIC GENERATED ALWAYS AS (price + tax) STORED ); ``` ### CREATE INDEX [Section titled “CREATE INDEX”](#create-index) SQL ```sql -- B-tree index (default) CREATE INDEX idx_name ON table (column); -- Unique index CREATE UNIQUE INDEX idx_name ON table (column); -- GIN index (for JSONB, full-text search, arrays) CREATE INDEX idx_name ON table USING GIN (column); -- Expression index CREATE INDEX idx_name ON table (lower(column)); -- Partial index CREATE INDEX idx_name ON table (column) WHERE active = true; -- Non-blocking index creation CREATE INDEX CONCURRENTLY idx_name ON table (column); ``` Only `btree` (default), `gin`, and `hnsw` are recognized index access methods, and `hnsw` index building is gated off in the current release — see the [pgvector page](/docs/extensions/vector/). `IF NOT EXISTS` is supported. Other index access methods are rejected `GiST`, `Hash`, `SP-GiST`, and `BRIN` are rejected at `CREATE INDEX` time — they are not accepted-then-ignored: ```plaintext ERROR: access method "gist" is not supported (0A000) HINT: Only btree, gin, and hnsw indexes are currently supported. ``` The hint lists `hnsw` even though HNSW index building is currently disabled. `hnsw` builds vector indexes and has extra structural requirements of its own — see [pgvector](/docs/extensions/vector/#index-constraints) and the [compatibility matrix](/docs/platform/compatibility-matrix/#indexes). **GIN index example (JSONB containment):** SQL ```sql CREATE TABLE documents (id SERIAL PRIMARY KEY, metadata JSONB); CREATE INDEX idx_metadata ON documents USING GIN (metadata); -- Use the @> containment operator (accelerated by GIN) SELECT * FROM documents WHERE metadata @> '{"type": "pdf"}'; ``` GIN indexes support JSONB containment (`@>`) and full-text search (`@@`) operators. See [Full-Text Search](/docs/extensions/fts/) for tsvector usage. ### CREATE VIEW [Section titled “CREATE VIEW”](#create-view) SQL ```sql CREATE VIEW view_name AS SELECT ...; CREATE OR REPLACE VIEW view_name AS SELECT ...; ``` `CREATE OR REPLACE VIEW` cannot drop or rename existing columns; only appending new columns is allowed. ### CREATE MATERIALIZED VIEW [Section titled “CREATE MATERIALIZED VIEW”](#create-materialized-view) SQL ```sql CREATE MATERIALIZED VIEW mv_name AS SELECT ...; REFRESH MATERIALIZED VIEW mv_name; ``` ### ALTER TABLE [Section titled “ALTER TABLE”](#alter-table) | Operation | Syntax | | ----------------- | ---------------------------------------------------------------- | | Add column | `ALTER TABLE t ADD COLUMN col type` | | Drop column | `ALTER TABLE t DROP COLUMN col` | | Rename column | `ALTER TABLE t RENAME COLUMN old TO new` | | Rename table | `ALTER TABLE t RENAME TO new_name` | | Set default | `ALTER TABLE t ALTER COLUMN col SET DEFAULT val` | | Drop default | `ALTER TABLE t ALTER COLUMN col DROP DEFAULT` | | Set not null | `ALTER TABLE t ALTER COLUMN col SET NOT NULL` | | Drop not null | `ALTER TABLE t ALTER COLUMN col DROP NOT NULL` | | Change type | `ALTER TABLE t ALTER COLUMN col SET DATA TYPE type [USING expr]` | | Add constraint | `ALTER TABLE t ADD CONSTRAINT name ...` | | Drop constraint | `ALTER TABLE t DROP CONSTRAINT name` | | Rename constraint | `ALTER TABLE t RENAME CONSTRAINT old TO new` | ### DROP Statements [Section titled “DROP Statements”](#drop-statements) SQL ```sql DROP TABLE [IF EXISTS] table_name [CASCADE]; DROP INDEX [IF EXISTS] index_name; DROP VIEW [IF EXISTS] view_name [CASCADE]; DROP MATERIALIZED VIEW [IF EXISTS] mv_name [CASCADE]; TRUNCATE TABLE table_name; ``` `CASCADE` transitively resolves and drops all dependent views and materialized views. Dropping a table without `CASCADE` does not refuse — it leaves dependents broken PostgreSQL refuses to drop a table that other objects depend on unless you pass `CASCADE`. DB9 drops it anyway, and dependents are neither dropped nor updated. A dependent **view** stays in `pg_class` / `pg_views` and fails only when queried: SQL ```sql CREATE TABLE d1 (a INT); CREATE VIEW dv1 AS SELECT * FROM d1; DROP TABLE d1; -- succeeds (PostgreSQL would raise a dependency error) SELECT * FROM dv1; -- ERROR: relation "d1" does not exist (42P01) ``` A dependent **materialized view** is worse — it keeps serving its last-materialized rows with no error — `REFRESH` is what surfaces it: SQL ```sql CREATE TABLE m1 (a INT); INSERT INTO m1 VALUES (1), (2), (3); CREATE MATERIALIZED VIEW mv1 AS SELECT * FROM m1; DROP TABLE m1; -- succeeds SELECT count(*) FROM mv1; -- still returns 3 — stale data, no error REFRESH MATERIALIZED VIEW mv1; -- ERROR: materialized view source relation 'public.m1' no longer exists (XX000) ``` Always pass `CASCADE` when you intend to remove dependents, and check for dependent views and materialized views before dropping a table. ### Other DDL [Section titled “Other DDL”](#other-ddl) | Statement | Supported | | ---------------------------------------------------------------- | -------------------------------- | | `CREATE SCHEMA` | Yes | | `CREATE SEQUENCE` / `DROP SEQUENCE` | Yes | | `ALTER SEQUENCE` | Ownership forms only — see below | | `CREATE TYPE` (enum, composite) / `ALTER TYPE` / `DROP TYPE` | Yes | | `CREATE FUNCTION` / `DROP FUNCTION` | Yes | | `CREATE TRIGGER` / `DROP TRIGGER` | Yes | | `CREATE COLLATION` / `DROP COLLATION` | Yes | | `CREATE DATABASE` / `DROP DATABASE` / `ALTER DATABASE` | Yes | | `CREATE EXTENSION` / `DROP EXTENSION` | Yes | | `COMMENT ON` (table, column, function, extension, index, schema) | Yes | `COMMENT ON` accepts only six object types `TABLE`, `COLUMN`, `FUNCTION`, `EXTENSION`, `INDEX`, and `SCHEMA` are supported. `VIEW`, `SEQUENCE`, `TYPE`, `DATABASE`, and `CONSTRAINT` are rejected: ```plaintext ERROR: Unsupported COMMENT ON statement (0A000) ``` Where the statement does succeed, the comment is written to `pg_description`, but `OBJ_DESCRIPTION()` and `COL_DESCRIPTION()` still return `NULL` — read `pg_description` directly. See [description functions](/docs/sql/functions/). **Sequence options:** `START`, `INCREMENT`, `MINVALUE`, `MAXVALUE`, `CACHE`, `CYCLE` are accepted in `CREATE SEQUENCE` (`START` and `INCREMENT` are confirmed to take effect). In *identity column* definitions a single option is accepted but ignored, and multiple options fail to parse — see [Identity columns](#create-table) above. `ALTER SEQUENCE` supports only the ownership forms These three are accepted and take effect: SQL ```sql ALTER SEQUENCE order_seq OWNER TO app_user; -- changes pg_class.relowner ALTER SEQUENCE order_seq OWNED BY orders.id; -- sequence is dropped with the table ALTER SEQUENCE order_seq OWNED BY NONE; -- clears it; sequence survives the table ``` Every other form is rejected: ```plaintext ERROR: ALTER SEQUENCE not supported (0A000) ``` That covers the parameter clauses (`RESTART`, `RESTART WITH`, `INCREMENT BY`, `START WITH`, `MINVALUE`, `NO MINVALUE`, `MAXVALUE`, `NO MAXVALUE`, `CACHE`, `CYCLE`, `NO CYCLE`), the type and storage clauses (`AS`, `SET LOGGED`, `SET UNLOGGED`), and `RENAME TO` and `SET SCHEMA`. A sequence’s parameters, name, and schema are therefore all fixed for its lifetime. To reposition a sequence, use [`SETVAL()`](/docs/sql/functions/#sequence-functions) instead of `RESTART`: SQL ```sql SELECT SETVAL('order_seq', 1000); -- next NEXTVAL returns 1001 (at INCREMENT 1) SELECT SETVAL('order_seq', 1000, false); -- next NEXTVAL returns 1000 ``` Changing `INCREMENT` or the bounds requires dropping and recreating the sequence — but read the warning below first if anything already references it. `DROP SEQUENCE` does not check column defaults Unlike PostgreSQL, DB9 does **not** refuse to drop a sequence that a column default still depends on. The `DROP` succeeds silently and leaves the table broken: SQL ```sql CREATE SEQUENCE ref_s; CREATE TABLE ref_t (id BIGINT PRIMARY KEY DEFAULT nextval('ref_s'), tag TEXT); DROP SEQUENCE ref_s; -- succeeds; PostgreSQL would refuse INSERT INTO ref_t (tag) VALUES ('x'); -- ERROR: relation "public.ref_s" does not exist (42P01) ``` PostgreSQL would reject that `DROP` with *“cannot drop sequence ref\_s because other objects depend on it”*. `SERIAL` columns are affected the same way, which is the more common case — the sequence `CREATE TABLE t (id SERIAL …)` creates implicitly can be dropped just as freely: SQL ```sql CREATE TABLE ser_t (id SERIAL PRIMARY KEY, tag TEXT); DROP SEQUENCE ser_t_id_seq; -- succeeds INSERT INTO ser_t (tag) VALUES ('x'); -- ERROR: relation "public.ser_t_id_seq" does not exist (42P01) ``` So check for column defaults, `SERIAL`, and identity columns before dropping a sequence. If you are recreating one to change its parameters, recreate it under the same name and restore its position with `SETVAL()` — reading `last_value` first, so you do not hand out duplicate keys: SQL ```sql SELECT last_value FROM order_seq; -- note this before dropping -- ... DROP SEQUENCE / CREATE SEQUENCE with the new parameters ... SELECT SETVAL('order_seq', ); -- restore the position ALTER SEQUENCE order_seq OWNED BY orders.id; -- re-establish the link, if it had one ``` That last line matters: a recreated sequence has no `OWNED BY` link, so without it the sequence will outlive the table it belongs to. ### Row-Level Security DDL [Section titled “Row-Level Security DDL”](#row-level-security-ddl) SQL ```sql -- Enable / disable RLS on a table ALTER TABLE t ENABLE ROW LEVEL SECURITY; ALTER TABLE t DISABLE ROW LEVEL SECURITY; -- Force RLS for table owner ALTER TABLE t FORCE ROW LEVEL SECURITY; ALTER TABLE t NO FORCE ROW LEVEL SECURITY; -- Create, alter, and drop policies CREATE POLICY name ON t FOR SELECT USING (expr); CREATE POLICY name ON t FOR INSERT WITH CHECK (expr); CREATE POLICY name ON t AS RESTRICTIVE FOR SELECT USING (expr); ALTER POLICY name ON t USING (new_expr); DROP POLICY [IF EXISTS] name ON t; ``` See [Row-Level Security](/docs/sql/rls/) for complete policy semantics and examples. # DML & Queries > Data Manipulation Language (INSERT, UPDATE, DELETE) and query features (SELECT, JOIN, CTEs, window functions, set operations) in DB9. ## SQL Reference: DML [Section titled “SQL Reference: DML”](#sql-reference-dml) Data Manipulation Language statements for inserting, updating, and deleting data. ### INSERT [Section titled “INSERT”](#insert) SQL ```sql -- With RETURNING INSERT INTO users (name, email) VALUES ('Dave', 'dave@example.com') ON CONFLICT (email) DO NOTHING RETURNING id, name, email; ``` ▶ Run SQL ```sql -- Single row INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'); -- Multi-row INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'), ('Bob', 'bob@example.com'); -- INSERT ... SELECT INSERT INTO archive SELECT * FROM users WHERE active = false; -- With RETURNING INSERT INTO users (name) VALUES ('Alice') RETURNING id, name; ``` ### INSERT ON CONFLICT (Upsert) [Section titled “INSERT ON CONFLICT (Upsert)”](#insert-on-conflict-upsert) SQL ```sql -- Skip conflicting rows INSERT INTO users (email, name) VALUES ('alice@example.com', 'Alice') ON CONFLICT (email) DO NOTHING; -- Upsert by column INSERT INTO users (email, name) VALUES ('alice@example.com', 'Alice Updated') ON CONFLICT (email) DO UPDATE SET name = excluded.name; -- Upsert by constraint name INSERT INTO users (email, name) VALUES ('alice@example.com', 'Alice') ON CONFLICT ON CONSTRAINT users_email_key DO UPDATE SET name = excluded.name; ``` ▶ Run The `excluded` pseudo-table is available in `DO UPDATE SET` clauses, referencing the row that was proposed for insertion. ### UPDATE [Section titled “UPDATE”](#update) SQL ```sql UPDATE users SET active = true WHERE name = 'Charlie' RETURNING *; ``` ▶ Run SQL ```sql UPDATE users SET name = 'Bob' WHERE id = 1; UPDATE users SET name = 'Bob' WHERE id = 1 RETURNING *; ``` ▶ Run ### DELETE [Section titled “DELETE”](#delete) SQL ```sql DELETE FROM users WHERE id = 1; DELETE FROM users WHERE id = 1 RETURNING *; ``` ### Foreign Key Referential Actions [Section titled “Foreign Key Referential Actions”](#foreign-key-referential-actions) Supported actions for `ON DELETE` and `ON UPDATE`: `CASCADE`, `SET NULL`, `SET DEFAULT`, `RESTRICT`, `NO ACTION`. Recursive cascade operations include cycle detection. MATCH SIMPLE semantics (PostgreSQL default): if any referencing column is NULL, the FK check is skipped. *** ## SQL Reference: Queries [Section titled “SQL Reference: Queries”](#sql-reference-queries) SELECT query features, JOINs, window functions, CTEs, subqueries, and set operations. ### JOINs [Section titled “JOINs”](#joins) | Join Type | Supported | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `INNER JOIN` | Yes | | `LEFT [OUTER] JOIN` | Yes | | `RIGHT [OUTER] JOIN` | Yes | | `FULL [OUTER] JOIN` | Yes | | `CROSS JOIN` | Yes | | `LATERAL JOIN` | Yes — but a `WITH` clause or a correlated scalar/`EXISTS`/`ARRAY` subquery *inside* the `LATERAL` body is restricted; see the [compatibility matrix](/docs/platform/compatibility-matrix/#dml-and-queries) | | Semi join (via `EXISTS`) | Yes | | Anti join (via `NOT EXISTS`) | Yes | Both hash join and nested-loop join implementations are available. The optimizer uses cost-based join reordering (DPccp algorithm). ### Aggregation [Section titled “Aggregation”](#aggregation) * `GROUP BY` with hash-based aggregation * `HAVING` clause * `DISTINCT` aggregates: `COUNT(DISTINCT col)` * `FILTER` clause: `COUNT(*) FILTER (WHERE x > 0)` * `GROUPING()` function ### Window Functions [Section titled “Window Functions”](#window-functions) SQL ```sql SELECT name, department, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rank FROM employees; ``` ▶ Run Supported ranking functions: `ROW_NUMBER`, `RANK`, `DENSE_RANK`, `NTILE`, `PERCENT_RANK`, `CUME_DIST` Supported access functions: `LAG`, `LEAD`, `FIRST_VALUE`, `LAST_VALUE`, `NTH_VALUE` All standard aggregate functions can also be used as window functions. ### Common Table Expressions (CTEs) [Section titled “Common Table Expressions (CTEs)”](#common-table-expressions-ctes) SQL ```sql -- Standard CTE WITH active_users AS ( SELECT * FROM users WHERE active = true ) SELECT * FROM active_users; -- Recursive CTE WITH RECURSIVE tree AS ( SELECT id, parent_id, name, 1 AS depth FROM categories WHERE parent_id IS NULL UNION ALL SELECT c.id, c.parent_id, c.name, t.depth + 1 FROM categories c JOIN tree t ON c.parent_id = t.id ) SELECT * FROM tree; ``` ▶ Run Recursive CTEs run for at most 1,000 iterations; beyond that the query fails with `54001` — `statement exceeds server complexity limit`. Put a data-modifying CTE first when using the HTTP SQL API A `WITH` list may contain `INSERT`/`UPDATE`/`DELETE` … `RETURNING` definitions. Over the [HTTP SQL API](/docs/api/) — which is what `db9 db sql` uses — such a definition must be the **first** one in the list; if any other definition precedes it the request fails with `error: connection closed` (the statement is aborted, so nothing is written): SQL ```sql -- Fails over the HTTP SQL API WITH src AS (SELECT id FROM staging WHERE ready), ins AS (INSERT INTO target SELECT id FROM src RETURNING id) SELECT count(*) FROM ins; -- Works: the data-modifying CTE comes first, with the helper inlined into it WITH ins AS (INSERT INTO target SELECT id FROM staging WHERE ready RETURNING id) SELECT count(*) FROM ins; ``` Moving the helper after the data-modifying CTE is not a fix — a `WITH` definition cannot reference one declared later, so it would fail with `42P01`. Inline it as above, or use the PostgreSQL wire protocol (psql, ORMs, any pg driver), where every ordering works. A `WITH` definition that is never referenced is not executed, as in PostgreSQL — a CTE that calls a volatile function such as `nextval()` has no effect when nothing selects from it. ### Subqueries [Section titled “Subqueries”](#subqueries) * Scalar subqueries * `EXISTS` / `NOT EXISTS` * `IN` / `NOT IN` with subquery * `ANY` / `ALL` with subquery * Array subqueries * Correlated subqueries (with subquery decorrelation optimization) All of these also work inside a `LATERAL` body, correlated or not, in any position — select list, `VALUES` element, `WHERE`, `GROUP BY`, `HAVING`, `ORDER BY` or a function argument. ### Set Operations [Section titled “Set Operations”](#set-operations) | Operation | Supported | | ----------------------------- | --------- | | `UNION` / `UNION ALL` | Yes | | `INTERSECT` / `INTERSECT ALL` | Yes | | `EXCEPT` / `EXCEPT ALL` | Yes | ### Other Query Features [Section titled “Other Query Features”](#other-query-features) * `SELECT DISTINCT` and `SELECT DISTINCT ON (expr)` * `ORDER BY` with `ASC`/`DESC` and `NULLS FIRST`/`NULLS LAST` * `LIMIT` and `OFFSET` * `EXPLAIN` and `EXPLAIN ANALYZE` for query plans * `PREPARE name AS ...` / `EXECUTE name (params)` / `DEALLOCATE name` * `generate_series()` table function * `SHOW TABLES` # Built-in Functions > Comprehensive reference of all built-in functions in DB9, organized by category. ## Built-in Functions [Section titled “Built-in Functions”](#built-in-functions) Comprehensive reference of all built-in functions, organized by category. *** ### String Functions [Section titled “String Functions”](#string-functions) #### UPPER / LOWER [Section titled “UPPER / LOWER”](#upper--lower) `UPPER(string TEXT) → TEXT` — Converts all characters to upper case. `LOWER(string TEXT) → TEXT` — Converts all characters to lower case. SQL ```sql SELECT UPPER('hello'); -- 'HELLO' SELECT LOWER('WORLD'); -- 'world' ``` ▶ Run #### LENGTH / CHAR\_LENGTH [Section titled “LENGTH / CHAR\_LENGTH”](#length--char_length) `LENGTH(string TEXT) → INT` — Returns the number of characters in the string. SQL ```sql SELECT LENGTH('hello'); -- 5 SELECT LENGTH(''); -- 0 ``` ▶ Run #### LEFT / RIGHT [Section titled “LEFT / RIGHT”](#left--right) `LEFT(string TEXT, n INT) → TEXT` — Returns the first n characters. If n is negative, returns all but the last |n| characters. `RIGHT(string TEXT, n INT) → TEXT` — Returns the last n characters. SQL ```sql SELECT LEFT('PostgreSQL', 4); -- 'Post' SELECT RIGHT('PostgreSQL', 3); -- 'SQL' SELECT LEFT('hello', -2); -- 'hel' ``` ▶ Run #### TRIM / BTRIM / LTRIM / RTRIM [Section titled “TRIM / BTRIM / LTRIM / RTRIM”](#trim--btrim--ltrim--rtrim) `TRIM([LEADING|TRAILING|BOTH] [chars TEXT] FROM string TEXT) → TEXT` — Removes characters (default: spaces) from the start, end, or both. `BTRIM(string TEXT [, chars TEXT]) → TEXT` — Removes chars from both ends. `LTRIM(string TEXT [, chars TEXT]) → TEXT` — Removes chars from the left. `RTRIM(string TEXT [, chars TEXT]) → TEXT` — Removes chars from the right. SQL ```sql SELECT TRIM(' hello '); -- 'hello' SELECT BTRIM('xxhelloxx', 'x'); -- 'hello' SELECT LTRIM('000123', '0'); -- '123' ``` ▶ Run #### LPAD / RPAD [Section titled “LPAD / RPAD”](#lpad--rpad) `LPAD(string TEXT, length INT [, fill TEXT]) → TEXT` — Left-pads string to length using fill (default: space). `RPAD(string TEXT, length INT [, fill TEXT]) → TEXT` — Right-pads string to length using fill. SQL ```sql SELECT LPAD('42', 5, '0'); -- '00042' SELECT RPAD('hi', 5, '.'); -- 'hi...' ``` ▶ Run #### REPEAT [Section titled “REPEAT”](#repeat) `REPEAT(string TEXT, number INT) → TEXT` — Repeats string the given number of times. SQL ```sql SELECT REPEAT('ab', 3); -- 'ababab' ``` ▶ Run #### REVERSE [Section titled “REVERSE”](#reverse) `REVERSE(string TEXT) → TEXT` — Reverses the characters of a string. SQL ```sql SELECT REVERSE('hello'); -- 'olleh' ``` ▶ Run #### INITCAP [Section titled “INITCAP”](#initcap) `INITCAP(string TEXT) → TEXT` — Converts the first letter of each word to upper case and the rest to lower case. SQL ```sql SELECT INITCAP('hello world'); -- 'Hello World' ``` ▶ Run #### ASCII / CHR [Section titled “ASCII / CHR”](#ascii--chr) `ASCII(string TEXT) → INT` — Returns the ASCII code of the first character. `CHR(code INT) → TEXT` — Returns the character with the given ASCII code. SQL ```sql SELECT ASCII('A'); -- 65 SELECT CHR(65); -- 'A' ``` ▶ Run #### STRPOS / POSITION [Section titled “STRPOS / POSITION”](#strpos--position) `STRPOS(string TEXT, substring TEXT) → INT` — Returns the position of the first occurrence of substring (1-based; 0 if not found). `POSITION(substring TEXT IN string TEXT) → INT` — SQL standard form of STRPOS. SQL ```sql SELECT STRPOS('hello world', 'world'); -- 7 SELECT POSITION('lo' IN 'hello'); -- 4 ``` ▶ Run #### SPLIT\_PART [Section titled “SPLIT\_PART”](#split_part) `SPLIT_PART(string TEXT, delimiter TEXT, field INT) → TEXT` — Splits string on delimiter and returns the nth field (1-based). SQL ```sql SELECT SPLIT_PART('a,b,c', ',', 2); -- 'b' SELECT SPLIT_PART('2024-03-15', '-', 1); -- '2024' ``` ▶ Run #### TRANSLATE [Section titled “TRANSLATE”](#translate) `TRANSLATE(string TEXT, from TEXT, to TEXT) → TEXT` — Replaces characters in from with corresponding characters in to. If to is shorter, extra from characters are deleted. SQL ```sql SELECT TRANSLATE('hello', 'el', 'ip'); -- 'hippo' SELECT TRANSLATE('123-456', '-', ''); -- '123456' ``` ▶ Run #### QUOTE\_IDENT / QUOTE\_LITERAL / QUOTE\_NULLABLE [Section titled “QUOTE\_IDENT / QUOTE\_LITERAL / QUOTE\_NULLABLE”](#quote_ident--quote_literal--quote_nullable) `QUOTE_IDENT(string TEXT) → TEXT` — Returns string as a safely quoted SQL identifier (double-quotes where needed). `QUOTE_LITERAL(string TEXT) → TEXT` — Returns string as a safely quoted SQL literal. `QUOTE_NULLABLE(value TEXT) → TEXT` — Like QUOTE\_LITERAL but returns `NULL` for NULL input. SQL ```sql SELECT QUOTE_IDENT('my table'); -- '"my table"' SELECT QUOTE_LITERAL('it''s'); -- 'it''s' SELECT QUOTE_NULLABLE(NULL); -- 'NULL' ``` ▶ Run #### OVERLAY [Section titled “OVERLAY”](#overlay) `OVERLAY(string TEXT PLACING replacement TEXT FROM start INT [FOR length INT]) → TEXT` — Replaces a substring within string. SQL ```sql SELECT OVERLAY('hello world' PLACING 'DB9' FROM 7); -- 'hello DB9ld' SELECT OVERLAY('hello world' PLACING 'DB9' FROM 7 FOR 5); -- 'hello DB9' ``` ▶ Run #### STARTS\_WITH [Section titled “STARTS\_WITH”](#starts_with) `STARTS_WITH` ``` STARTS_WITH(string TEXT, prefix TEXT)→ BOOLEAN ``` Returns true if string begins with prefix. Equivalent to the LIKE 'prefix%' pattern but without escaping concerns. SQL ```sql SELECT STARTS_WITH('hello world', 'hello'); -- true SELECT STARTS_WITH('hello world', 'world'); -- false ``` ▶ Run *** #### CONCAT [Section titled “CONCAT”](#concat) `CONCAT` ``` CONCAT(val1 TEXT, val2 TEXT, ...)→ TEXT ``` Concatenates all arguments. NULL arguments are silently ignored (unlike the || operator, which returns NULL if any operand is NULL). SQL ```sql SELECT CONCAT('DB', '9'); -- 'DB9' SELECT CONCAT('Hello', NULL, ' World'); -- 'Hello World' SELECT CONCAT(42, ' items'); -- '42 items' ``` ▶ Run #### CONCAT\_WS [Section titled “CONCAT\_WS”](#concat_ws) `CONCAT_WS` ``` CONCAT_WS(separator TEXT, val1 TEXT, val2 TEXT, ...)→ TEXT ``` Concatenates arguments with a separator between each. The first argument is the separator. NULL values in the list are skipped. | Parameter | Type | Required | Default | Description | | ----------------- | ------ | -------- | ------- | ----------------------------------------- | | `separator` | `TEXT` | Yes | — | String placed between each non-NULL value | | `val1, val2, ...` | `TEXT` | Yes | — | Values to join; NULLs are skipped | SQL ```sql SELECT CONCAT_WS(', ', 'Alice', 'Bob', 'Carol'); -- 'Alice, Bob, Carol' SELECT CONCAT_WS('-', '2024', '01', '15'); -- '2024-01-15' SELECT CONCAT_WS(', ', 'Alice', NULL, 'Carol'); -- 'Alice, Carol' ``` ▶ Run #### SUBSTRING [Section titled “SUBSTRING”](#substring) `SUBSTRING` ``` SUBSTRING(string TEXT, start INT [, length INT])→ TEXT ``` Extracts a substring starting at position start (1-based). Also supports the SQL standard form: SUBSTRING(string FROM start FOR length). Supports regex extraction: SUBSTRING(string FROM pattern). | Parameter | Type | Required | Default | Description | | --------- | ------ | -------- | ------- | ------------------------------- | | `string` | `TEXT` | Yes | — | Source string | | `start` | `INT` | Yes | — | Starting position (1-based) | | `length` | `INT` | No | to end | Number of characters to extract | SQL ```sql SELECT SUBSTRING('PostgreSQL', 1, 4); -- 'Post' SELECT SUBSTRING('PostgreSQL' FROM 5); -- 'greSQL' SELECT SUBSTRING('foo@bar.com' FROM '@(.*)'); -- 'bar.com' ``` ▶ Run #### REPLACE [Section titled “REPLACE”](#replace) `REPLACE` ``` REPLACE(string TEXT, from TEXT, to TEXT)→ TEXT ``` Replaces all occurrences of substring from in string with to. SQL ```sql SELECT REPLACE('Hello World', 'World', 'DB9'); -- 'Hello DB9' SELECT REPLACE('aabbcc', 'bb', 'XX'); -- 'aaXXcc' ``` ▶ Run #### FORMAT [Section titled “FORMAT”](#format) `FORMAT` ``` FORMAT(formatstr TEXT [, args ANY, ...])→ TEXT ``` Formats a string using printf-style format specifiers. Supports %s (string), %I (quoted identifier), %L (quoted literal), and %% (literal %). Particularly useful for safely building dynamic SQL. SQL ```sql SELECT FORMAT('Hello, %s!', 'World'); -- 'Hello, World!' SELECT FORMAT('SELECT * FROM %I WHERE id = %L', 'users', 42); -- 'SELECT * FROM users WHERE id = ''42''' ``` ▶ Run *** ### Mathematical Functions [Section titled “Mathematical Functions”](#mathematical-functions) #### ABS [Section titled “ABS”](#abs) `ABS(x NUMERIC) → NUMERIC` — Absolute value. SQL ```sql SELECT ABS(-42); -- 42 ``` ▶ Run #### CEIL / CEILING / FLOOR [Section titled “CEIL / CEILING / FLOOR”](#ceil--ceiling--floor) `CEIL(x NUMERIC) → NUMERIC` — Nearest integer greater than or equal to x. `CEILING` is an alias. `FLOOR(x NUMERIC) → NUMERIC` — Nearest integer less than or equal to x. SQL ```sql SELECT CEIL(4.2); -- 5 SELECT FLOOR(4.8); -- 4 ``` ▶ Run #### ROUND / TRUNC [Section titled “ROUND / TRUNC”](#round--trunc) `ROUND(x NUMERIC [, s INT]) → NUMERIC` — Rounds to s decimal places (default 0). `TRUNC(x NUMERIC [, s INT]) → NUMERIC` — Truncates to s decimal places (default 0). SQL ```sql SELECT ROUND(4.567, 2); -- 4.57 SELECT TRUNC(4.567, 2); -- 4.56 SELECT ROUND(4.5); -- 5 ``` ▶ Run #### SQRT / CBRT [Section titled “SQRT / CBRT”](#sqrt--cbrt) `SQRT(x NUMERIC) → NUMERIC` — Square root. `CBRT(x DOUBLE PRECISION) → DOUBLE PRECISION` — Cube root. SQL ```sql SELECT SQRT(16); -- 4 SELECT CBRT(8); -- 2 ``` ▶ Run #### POWER / EXP / LN / LOG [Section titled “POWER / EXP / LN / LOG”](#power--exp--ln--log) `POWER(base NUMERIC, exp NUMERIC) → NUMERIC` — Raises base to the power of exp. `EXP(x NUMERIC) → NUMERIC` — Exponential (e^x). `LN(x NUMERIC) → NUMERIC` — Natural logarithm. `LOG(x NUMERIC) → NUMERIC` — Logarithm base 10. SQL ```sql SELECT POWER(2, 10); -- 1024 SELECT EXP(1); -- 2.718281828... SELECT LN(EXP(1)); -- 1 SELECT LOG(100); -- 2 ``` ▶ Run DB9 Difference: two-argument LOG(b, x) not supported The two-argument form `LOG(b, x)` (logarithm of `x` to base `b`) is **not yet supported** in DB9 CLI 2.2.0. Calling it does not raise an error but silently returns `log10(b)` instead of the expected `log_b(x)`. Use `LN(x) / LN(b)` for arbitrary bases: SQL ```sql SELECT LN(8) / LN(2); -- 3 (log base 2 of 8) ``` #### MOD [Section titled “MOD”](#mod) `MOD(y NUMERIC, x NUMERIC) → NUMERIC` — Remainder of y / x. SQL ```sql SELECT MOD(10, 3); -- 1 ``` ▶ Run #### SIGN [Section titled “SIGN”](#sign) `SIGN(x) → DOUBLE PRECISION | NUMERIC` — Returns -1, 0, or 1 based on the sign of x. Following PostgreSQL, integer and floating-point inputs return `DOUBLE PRECISION`; a `NUMERIC` input returns `NUMERIC`. SQL ```sql SELECT SIGN(-5); -- -1.0 (double precision) SELECT SIGN(0); -- 0.0 SELECT SIGN(5.0::numeric); -- 1.0 (numeric) ``` ▶ Run #### PI / DEGREES / RADIANS [Section titled “PI / DEGREES / RADIANS”](#pi--degrees--radians) `PI() → DOUBLE PRECISION` — Returns π (3.14159…). `DEGREES(x DOUBLE PRECISION) → DOUBLE PRECISION` — Converts radians to degrees. `RADIANS(x DOUBLE PRECISION) → DOUBLE PRECISION` — Converts degrees to radians. SQL ```sql SELECT PI(); -- 3.14159265358979 SELECT DEGREES(PI()); -- 180 SELECT RADIANS(180); -- 3.14159265358979 ``` ▶ Run #### Trigonometric Functions [Section titled “Trigonometric Functions”](#trigonometric-functions) `SIN(x)`, `COS(x)`, `TAN(x)` — Sine, cosine, tangent (argument in radians). `ASIN(x)`, `ACOS(x)`, `ATAN(x)` — Inverse sine, cosine, tangent (result in radians). `ATAN2(y, x) → DOUBLE PRECISION` — Arc tangent of `y/x`, using the signs of both arguments to determine the quadrant. All trigonometric functions return `DOUBLE PRECISION`. SQL ```sql SELECT SIN(PI() / 2); -- 1.0 SELECT COS(0); -- 1.0 SELECT ASIN(1); -- 1.5707963267948966 (π/2) SELECT ATAN2(1, 1); -- 0.7853981633974483 (π/4) ``` ▶ Run #### RANDOM [Section titled “RANDOM”](#random) `RANDOM() → DOUBLE PRECISION` — Returns a random value in \[0.0, 1.0). SQL ```sql SELECT RANDOM(); -- e.g. 0.37428... SELECT FLOOR(RANDOM() * 100)::int AS rand; -- random int 0–99 ``` ▶ Run #### HASHTEXT [Section titled “HASHTEXT”](#hashtext) `HASHTEXT(string TEXT) → INT` — Returns an integer hash of the string. Useful for sharding or bucketing. SQL ```sql SELECT ABS(HASHTEXT('hello')) % 10 AS shard; -- consistent bucket 0–9 (HASHTEXT returns a signed int, so wrap with ABS()) ``` ▶ Run #### WIDTH\_BUCKET [Section titled “WIDTH\_BUCKET”](#width_bucket) `WIDTH_BUCKET(operand, low, high, count) → INTEGER` — Returns the bucket index (1 to `count`) that `operand` falls into for an equal-width histogram spanning `low` to `high`. Returns `0` for values below `low` and `count + 1` for values at or above `high`. SQL ```sql SELECT WIDTH_BUCKET(5, 0, 10, 5); -- 3 SELECT WIDTH_BUCKET(-1, 0, 10, 5); -- 0 (below range) SELECT WIDTH_BUCKET(99, 0, 10, 5); -- 6 (above range) ``` ▶ Run *** *** ### Date/Time Functions [Section titled “Date/Time Functions”](#datetime-functions) #### CURRENT\_DATE [Section titled “CURRENT\_DATE”](#current_date) `CURRENT_DATE → DATE` — Current date (no time component). SQL ```sql SELECT CURRENT_DATE; -- 2024-03-15 ``` ▶ Run #### AGE [Section titled “AGE”](#age) `AGE(timestamp1 TIMESTAMPTZ, timestamp2 TIMESTAMPTZ) → INTERVAL` — Computes the difference between two timestamps as a symbolic interval. `AGE(timestamp TIMESTAMPTZ) → INTERVAL` — Difference between NOW() and the argument. SQL ```sql SELECT AGE('2024-03-15'::date, '2020-01-01'::date); -- 4 years 2 mons 14 days SELECT AGE(NOW(), birth_date) AS age FROM users; -- Age of each user ``` DB9 Difference: single-argument AGE() is sign-inverted The single-argument form returns a sign-inverted interval: for a past timestamp, `AGE(ts)` returns a **negative** interval (e.g. `AGE('2020-01-01'::timestamptz)` → `-6 years -6 mons ...`), where PostgreSQL returns a positive one. Use the two-argument form `AGE(NOW(), ts)` instead, which behaves correctly. #### DATE\_TRUNC [Section titled “DATE\_TRUNC”](#date_trunc) `DATE_TRUNC` ``` DATE_TRUNC(field TEXT, source TIMESTAMP|TIMESTAMPTZ)→ TIMESTAMP|TIMESTAMPTZ ``` Truncates a timestamp to the specified precision. Returns the same type as the input. | Parameter | Type | Required | Default | Description | | --------- | -------------------------- | -------- | ------- | ------------------------------------------------- | | `field` | `TEXT` | Yes | — | Precision: second, minute, hour, day, month, year | | `source` | `TIMESTAMP \| TIMESTAMPTZ` | Yes | — | Value to truncate | SQL ```sql SELECT DATE_TRUNC('month', '2024-03-15 14:30:00'::timestamp); -- 2024-03-01 00:00:00 SELECT DATE_TRUNC('hour', NOW()); -- e.g. 2024-03-15 14:00:00+00 -- Group events by day SELECT DATE_TRUNC('day', created_at) AS day, COUNT(*) AS events FROM logs GROUP BY 1 ORDER BY 1; ``` ▶ Run DB9 Difference: timestamp literals require seconds Timestamp literals without a seconds component are rejected: `'2024-03-15 14:30'::timestamp` fails with `invalid input syntax`. Always include seconds, e.g. `'2024-03-15 14:30:00'::timestamp`. DB9 Difference: DATE\_TRUNC has no timezone argument, no INTERVAL support, and a reduced field list * **No 3-argument form**: PostgreSQL’s optional `timezone` argument is not supported — `DATE_TRUNC('day', NOW(), 'America/New_York')` fails with `function date_trunc(unknown, timestamptz, unknown) does not exist`. * **INTERVAL input is not supported**: passing an `INTERVAL` does not error — it silently returns `NULL` instead of a truncated interval, e.g. `DATE_TRUNC('hour', INTERVAL '3 days 4 hours')` returns `NULL`. * **Reduced field list**: only `second`, `minute`, `hour`, `day`, `month`, and `year` are supported. Other PostgreSQL fields (`microseconds`, `milliseconds`, `week`, `quarter`, `decade`, `century`, `millennium`) fail with `Unsupported DATE_TRUNC field`. #### EXTRACT / DATE\_PART [Section titled “EXTRACT / DATE\_PART”](#extract--date_part) `EXTRACT` ``` EXTRACT(field FROM source TIMESTAMP|TIMESTAMPTZ|INTERVAL|DATE)→ NUMERIC ``` Extracts a numeric subfield from a date/time value. DATE\_PART(field, source) is an equivalent functional form. Returns a NUMERIC (not integer). | Parameter | Type | Required | Default | Description | | --------- | ---------------------------------------------- | -------- | ------- | ------------------------------------------------------------------------------ | | `field` | `TEXT` | Yes | — | year, month, day, hour, minute, second, dow (0=Sun), doy, epoch, quarter, week | | `source` | `TIMESTAMP \| TIMESTAMPTZ \| INTERVAL \| DATE` | Yes | — | Value to extract from | SQL ```sql SELECT EXTRACT(year FROM '2024-03-15'::date); -- 2024 SELECT EXTRACT(month FROM NOW()); -- current month number SELECT EXTRACT(epoch FROM INTERVAL '1 day'); -- 86400 SELECT DATE_PART('dow', '2024-03-15'::date); -- 5 (Friday) ``` ▶ Run DB9 Difference: timezone fields not supported `timezone`, `timezone_hour`, and `timezone_minute` are **not supported** as EXTRACT/DATE\_PART fields — they fail with `Unsupported EXTRACT field`. #### NOW / CURRENT\_TIMESTAMP [Section titled “NOW / CURRENT\_TIMESTAMP”](#now--current_timestamp) `NOW` ``` NOW()→ TIMESTAMPTZ ``` Returns the current transaction start time as TIMESTAMPTZ. Within a transaction, NOW() always returns the same value — the time the transaction began. Use CLOCK\_TIMESTAMP() for the actual current wall-clock time that advances during a transaction. SQL ```sql SELECT NOW(); -- 2024-03-15 14:30:00.123456+00 (microsecond precision) SELECT CURRENT_TIMESTAMP; -- same as NOW() SELECT CLOCK_TIMESTAMP(); -- actual wall-clock time (changes within transaction) ``` ▶ Run #### TO\_CHAR [Section titled “TO\_CHAR”](#to_char) `TO_CHAR` ``` TO_CHAR(value TIMESTAMP|INTERVAL|NUMERIC, format TEXT)→ TEXT ``` Formats a timestamp, interval, or number as a string using a format template. Common date patterns: YYYY (4-digit year), MM (month 01-12), DD (day 01-31), HH24 (hour 0-23), MI (minute), SS (second). Common numeric patterns: 9 (digit), 0 (zero-padded digit), . (decimal point), , (group separator), FM (suppress padding). | Parameter | Type | Required | Default | Description | | --------- | ---------------------------------- | -------- | ------- | ---------------------- | | `value` | `TIMESTAMP \| INTERVAL \| NUMERIC` | Yes | — | Value to format | | `format` | `TEXT` | Yes | — | Format template string | SQL ```sql SELECT TO_CHAR(NOW(), 'YYYY-MM-DD'); -- '2024-03-15' SELECT TO_CHAR(NOW(), 'HH24:MI:SS'); -- '14:30:00' SELECT TO_CHAR(INTERVAL '2 hours 30 mins', 'HH24:MI'); -- '02:30' ``` ▶ Run Numeric formatting is supported, including rounding, zero padding, group separators, and the `PR` (parenthesized negative) pattern: SQL ```sql SELECT TO_CHAR(1234.56, 'FM9999') AS rounded, -- '1235' TO_CHAR(0.5, '0.00') AS padded, -- ' 0.50' TO_CHAR(1234567.891, 'FM999,999,999.99') AS grouped, -- '1,234,567.89' TO_CHAR(-5, '999PR') AS parenthesized; -- ' <5>' ``` ▶ Run DB9 Difference: name patterns **Day/month name patterns are not interpreted**: patterns such as `Day` and `Month` are copied to the output as literal text — `TO_CHAR(NOW(), 'Day, DD Month YYYY')` returns `'Day, 13 Month 2026'` instead of the spelled-out name. Use numeric patterns (`DD`, `MM`) instead. #### TO\_TIMESTAMP [Section titled “TO\_TIMESTAMP”](#to_timestamp) `TO_TIMESTAMP(epoch DOUBLE PRECISION) → TIMESTAMPTZ` — Converts a Unix epoch (seconds since 1970-01-01 UTC) to a `TIMESTAMP WITH TIME ZONE`. SQL ```sql SELECT TO_TIMESTAMP(0); -- 1970-01-01 00:00:00+00 SELECT TO_TIMESTAMP(1700000000); -- 2023-11-14 22:13:20+00 ``` ▶ Run #### MAKE\_DATE / MAKE\_TIME / MAKE\_TIMESTAMP / MAKE\_INTERVAL [Section titled “MAKE\_DATE / MAKE\_TIME / MAKE\_TIMESTAMP / MAKE\_INTERVAL”](#make_date--make_time--make_timestamp--make_interval) `MAKE_DATE(year INT, month INT, day INT) → DATE` — Builds a date from its parts. `MAKE_TIME(hour INT, min INT, sec DOUBLE PRECISION) → TIME` — Builds a time of day from its parts. `MAKE_TIMESTAMP(year INT, month INT, day INT, hour INT, min INT, sec DOUBLE PRECISION) → TIMESTAMP` — Builds a timestamp (without time zone) from its parts. `MAKE_INTERVAL([years INT [, months INT [, weeks INT [, days INT [, hours INT [, mins INT [, secs DOUBLE PRECISION]]]]]]]) → INTERVAL` — Builds an interval from individual fields (all default to 0). SQL ```sql SELECT MAKE_DATE(2024, 3, 15); -- 2024-03-15 SELECT MAKE_TIME(12, 30, 0); -- 12:30:00 SELECT MAKE_TIMESTAMP(2024, 3, 15, 12, 30, 0); -- 2024-03-15 12:30:00 SELECT MAKE_INTERVAL(0, 0, 0, 0, 1, 30, 0); -- 01:30:00 (1 hour 30 min) SELECT MAKE_INTERVAL(1, 2, 0, 3, 4, 5, 6); -- 1 year 2 mons 3 days 04:05:06 ``` ▶ Run #### CURRENT\_TIME [Section titled “CURRENT\_TIME”](#current_time) `CURRENT_TIME → TIME` — Returns the current time of day. (In DB9 this returns `TIME WITHOUT TIME ZONE`.) SQL ```sql SELECT CURRENT_TIME; -- e.g. 15:03:43.833000 ``` ▶ Run #### TIMEZONE [Section titled “TIMEZONE”](#timezone) `TIMEZONE(zone TEXT, timestamp TIMESTAMP) → TIMESTAMPTZ` — Treat timestamp as being in zone and convert to TIMESTAMPTZ. `TIMEZONE(zone TEXT, timestamptz TIMESTAMPTZ) → TIMESTAMP` — Convert TIMESTAMPTZ to local timestamp in zone. SQL ```sql SELECT TIMEZONE('America/New_York', NOW()); -- time in NY as local TIMESTAMP SELECT NOW() AT TIME ZONE 'America/New_York'; -- equivalent operator form ``` ▶ Run *** *** ### Aggregate Functions [Section titled “Aggregate Functions”](#aggregate-functions) #### COUNT [Section titled “COUNT”](#count) `COUNT(*) → BIGINT` — Counts all rows including NULLs. `COUNT(expression) → BIGINT` — Counts non-NULL values of expression. `COUNT(DISTINCT expression) → BIGINT` — Counts distinct non-NULL values. SQL ```sql SELECT COUNT(*) FROM users; SELECT COUNT(email) FROM users; -- excludes NULL emails SELECT COUNT(DISTINCT country) FROM users; ``` #### SUM / AVG / MIN / MAX [Section titled “SUM / AVG / MIN / MAX”](#sum--avg--min--max) `SUM(expression)` — Sum of non-NULL values. `AVG(expression)` — Average of non-NULL values. `MIN(expression)` — Minimum non-NULL value. `MAX(expression)` — Maximum non-NULL value. SQL ```sql SELECT SUM(amount), AVG(amount), MIN(amount), MAX(amount) FROM orders; ``` ▶ Run #### BOOL\_AND / BOOL\_OR / EVERY [Section titled “BOOL\_AND / BOOL\_OR / EVERY”](#bool_and--bool_or--every) `BOOL_AND(expression BOOLEAN) → BOOLEAN` — True if all non-NULL values are true. `BOOL_OR(expression BOOLEAN) → BOOLEAN` — True if any non-NULL value is true. `EVERY(expression BOOLEAN) → BOOLEAN` — Alias for BOOL\_AND. SQL ```sql SELECT BOOL_AND(active) FROM users; -- true only if all users are active SELECT BOOL_OR(has_premium) FROM users; -- true if any user has premium ``` #### JSON\_AGG / JSONB\_AGG [Section titled “JSON\_AGG / JSONB\_AGG”](#json_agg--jsonb_agg) `JSON_AGG(expression) → JSON` — Aggregates values into a JSON array, preserving NULL entries. `JSONB_AGG(expression) → JSONB` — Same but returns JSONB. SQL ```sql SELECT JSONB_AGG(ROW_TO_JSON(u)) FROM users u; -- [{"id":1,"name":"Alice"}, {"id":2,"name":"Bob"}, ...] ``` ▶ Run *** #### STRING\_AGG [Section titled “STRING\_AGG”](#string_agg) `STRING_AGG` ``` STRING_AGG(expression TEXT, delimiter TEXT [ORDER BY sort_expression])→ TEXT ``` Concatenates non-NULL string values with a delimiter. Supports ORDER BY within the aggregate to control concatenation order. Returns NULL when no rows match. | Parameter | Type | Required | Default | Description | | ------------ | ------ | -------- | ------- | -------------------------------------- | | `expression` | `TEXT` | Yes | — | Value to aggregate (NULLs are skipped) | | `delimiter` | `TEXT` | Yes | — | Separator placed between values | SQL ```sql SELECT STRING_AGG(name, ', ') FROM users; -- 'Alice, Bob, Carol' -- With ORDER BY for deterministic ordering SELECT STRING_AGG(name, ', ' ORDER BY name) FROM users; -- 'Alice, Bob, Carol' -- Per-group tags SELECT post_id, STRING_AGG(tag, ' | ' ORDER BY tag) AS tags FROM post_tags GROUP BY post_id; ``` ▶ Run #### ARRAY\_AGG [Section titled “ARRAY\_AGG”](#array_agg) `ARRAY_AGG` ``` ARRAY_AGG(expression ANY [ORDER BY sort_expression])→ ARRAY ``` Collects values into an array. NULLs are included by default. Supports ORDER BY within the aggregate. SQL ```sql SELECT ARRAY_AGG(id ORDER BY id) FROM users; -- {1,2,3,4,5} SELECT user_id, ARRAY_AGG(tag ORDER BY tag) AS tags FROM user_tags GROUP BY user_id; -- Exclude NULLs by filtering before aggregation SELECT ARRAY_AGG(email) FROM users WHERE email IS NOT NULL; ``` `ARRAY_AGG(expr) FILTER (WHERE ...)` is supported, as it is for `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `STRING_AGG`, `JSON_AGG`, `JSONB_AGG`, `BOOL_AND`, `BOOL_OR`, `EVERY`, `STDDEV`, `STDDEV_POP`, `STDDEV_SAMP`, `VARIANCE`, `VAR_POP`, `VAR_SAMP`, `BIT_AND`, `BIT_OR`, `BIT_XOR`, `CORR`, `COVAR_POP`, `COVAR_SAMP`, and the `REGR_*` functions: SQL ```sql SELECT ARRAY_AGG(id) FILTER (WHERE active) FROM users; ``` ▶ Run #### STDDEV / VARIANCE [Section titled “STDDEV / VARIANCE”](#stddev--variance) `STDDEV(expression)` — Sample standard deviation of non-NULL values. Alias for STDDEV\_SAMP. `STDDEV_POP(expression)` — Population standard deviation. `STDDEV_SAMP(expression)` — Sample standard deviation. `VARIANCE(expression)` — Sample variance of non-NULL values. Alias for VAR\_SAMP. `VAR_POP(expression)` — Population variance. `VAR_SAMP(expression)` — Sample variance. All six return `NUMERIC` for integer/numeric inputs and `DOUBLE PRECISION` for float inputs. SQL ```sql SELECT STDDEV(x), VAR_POP(x) FROM (VALUES (1.0), (2.0), (3.0), (4.0)) t(x); -- 1.2909944487358056 | 1.25 ``` ▶ Run #### CORR / COVAR\_POP / COVAR\_SAMP [Section titled “CORR / COVAR\_POP / COVAR\_SAMP”](#corr--covar_pop--covar_samp) `CORR(y, x) → DOUBLE PRECISION` — Pearson correlation coefficient between two numeric columns. `COVAR_POP(y, x) → DOUBLE PRECISION` — Population covariance. `COVAR_SAMP(y, x) → DOUBLE PRECISION` — Sample covariance. SQL ```sql SELECT CORR(y, x), COVAR_SAMP(y, x) FROM (VALUES (1.0, 2.0), (2.0, 4.0), (3.0, 6.1)) t(x, y); -- 0.9999008674099176 | 2.05 ``` ▶ Run #### REGR\_\* (Linear Regression) [Section titled “REGR\_\* (Linear Regression)”](#regr_-linear-regression) `REGR_SLOPE(y, x) → DOUBLE PRECISION` — Slope of the least-squares-fit line. `REGR_INTERCEPT(y, x) → DOUBLE PRECISION` — Y-intercept of the fit line. `REGR_R2(y, x) → DOUBLE PRECISION` — Square of the correlation coefficient. `REGR_AVGX(y, x)` / `REGR_AVGY(y, x)` → `DOUBLE PRECISION` — Average of the independent (x) / dependent (y) values. `REGR_COUNT(y, x) → BIGINT` — Number of rows where both inputs are non-NULL. `REGR_SXX(y, x)` / `REGR_SYY(y, x)` / `REGR_SXY(y, x)` → `DOUBLE PRECISION` — Sums of squares and products. SQL ```sql SELECT REGR_SLOPE(y, x), REGR_INTERCEPT(y, x), REGR_COUNT(y, x) FROM (VALUES (1.0, 2.0), (2.0, 4.0), (3.0, 6.1)) t(x, y); -- 2.05 | -0.06666666666666643 | 3 ``` ▶ Run #### BIT\_AND / BIT\_OR / BIT\_XOR [Section titled “BIT\_AND / BIT\_OR / BIT\_XOR”](#bit_and--bit_or--bit_xor) `BIT_AND(expression)` — Bitwise AND of all non-NULL input values. `BIT_OR(expression)` — Bitwise OR of all non-NULL input values. `BIT_XOR(expression)` — Bitwise XOR of all non-NULL input values. Bigint input returns `BIGINT`; smaller integer inputs (e.g. `SMALLINT`) return `INTEGER` rather than the input type. SQL ```sql SELECT BIT_AND(x), BIT_OR(x), BIT_XOR(x) FROM (VALUES (5), (3), (7)) t(x); -- 1 | 7 | 1 ``` ▶ Run #### PERCENTILE\_CONT / PERCENTILE\_DISC (Ordered-Set Aggregates) [Section titled “PERCENTILE\_CONT / PERCENTILE\_DISC (Ordered-Set Aggregates)”](#percentile_cont--percentile_disc-ordered-set-aggregates) `PERCENTILE_CONT(fraction) WITHIN GROUP (ORDER BY expr)` — Continuous percentile: interpolates between adjacent input values. Returns `DOUBLE PRECISION`. `PERCENTILE_DISC(fraction) WITHIN GROUP (ORDER BY expr)` — Discrete percentile: returns the first input value whose cumulative distribution is at least `fraction`. Returns the input type, so it also works on sortable non-numeric types such as `TEXT`. Both accept a single fraction or an array of fractions, honour `ORDER BY ... DESC`, and work with `GROUP BY`. SQL ```sql SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x) AS median, PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY x) AS median_disc, PERCENTILE_CONT(ARRAY[0.25, 0.75]) WITHIN GROUP (ORDER BY x) AS quartiles FROM generate_series(1, 10) x; -- 5.5 | 5 | {3.25,7.75} ``` ▶ Run DB9 Difference: MODE() is not available `MODE() WITHIN GROUP (ORDER BY expr)` is not supported — it fails with `mode is not an ordered-set aggregate, so it cannot have WITHIN GROUP`. Calling `MODE(expr)` as a plain aggregate fails with `function MODE(integer) does not exist` (`42883`). *** ### Window Functions [Section titled “Window Functions”](#window-functions) Only `SUM`, `AVG`, `COUNT`, `MIN`, `MAX`, and `STRING_AGG` may be used as window functions with an OVER clause. DB9 Difference: most aggregates are not supported as window functions Only `SUM`, `AVG`, `COUNT`, `MIN`, `MAX`, and `STRING_AGG` work with an `OVER` clause. The remaining aggregate functions fail with `Unsupported window function` when used this way: `JSON_AGG`, `JSONB_AGG`, `BOOL_AND`, `BOOL_OR`, `EVERY`, `STDDEV`, `STDDEV_POP`, `STDDEV_SAMP`, `VARIANCE`, `VAR_POP`, `VAR_SAMP`, `BIT_AND`, `BIT_OR`, `BIT_XOR`, `CORR`, `COVAR_POP`, `COVAR_SAMP`, and the `REGR_*` functions. `ARRAY_AGG(...) OVER (...)` fails even earlier, at parse time, with a SQL parse error. `STRING_AGG` supports a bare `OVER ()`, `PARTITION BY`, a window `ORDER BY` (running concatenation), and explicit `ROWS BETWEEN` frames. As in PostgreSQL, without a window `ORDER BY` the concatenation order within a partition is unspecified. SQL ```sql SELECT role, STRING_AGG(name, ', ') OVER (PARTITION BY role ORDER BY name) AS names FROM users ORDER BY role, names; ``` ▶ Run #### RANK / DENSE\_RANK [Section titled “RANK / DENSE\_RANK”](#rank--dense_rank) `RANK() OVER (...)` — Rank within partition, with gaps for ties (1, 1, 3…). `DENSE_RANK() OVER (...)` — Rank within partition, without gaps for ties (1, 1, 2…). SQL ```sql SELECT name, score, RANK() OVER (ORDER BY score DESC) AS rank, DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank FROM leaderboard; ``` #### NTILE [Section titled “NTILE”](#ntile) `NTILE(buckets INT) OVER ([PARTITION BY expr] ORDER BY expr) → BIGINT` — Divides rows into buckets number of approximately equal groups and assigns a bucket number to each row. SQL ```sql SELECT user_id, amount, NTILE(4) OVER (ORDER BY amount DESC) AS quartile FROM orders; ``` ▶ Run #### PERCENT\_RANK / CUME\_DIST [Section titled “PERCENT\_RANK / CUME\_DIST”](#percent_rank--cume_dist) `PERCENT_RANK() OVER (...)` — Relative rank: (rank - 1) / (total rows - 1). Returns 0 to 1. `CUME_DIST() OVER (...)` — Cumulative distribution: fraction of rows ≤ current row’s value. Returns (0, 1]. SQL ```sql SELECT score, PERCENT_RANK() OVER (ORDER BY score) AS pct_rank, CUME_DIST() OVER (ORDER BY score) AS cume_dist FROM test_results; ``` #### FIRST\_VALUE / LAST\_VALUE / NTH\_VALUE [Section titled “FIRST\_VALUE / LAST\_VALUE / NTH\_VALUE”](#first_value--last_value--nth_value) `FIRST_VALUE(value) OVER (...)` — Value at the first row of the window frame. `LAST_VALUE(value) OVER (...)` — Value at the last row of the window frame. `NTH_VALUE(value, n INT) OVER (...)` — Value at the nth row of the window frame (1-based). SQL ```sql SELECT user_id, amount, FIRST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS first_amount, LAST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_amount FROM orders; ``` ▶ Run *** #### ROW\_NUMBER [Section titled “ROW\_NUMBER”](#row_number) `ROW_NUMBER` ``` ROW_NUMBER() OVER ([PARTITION BY expr] ORDER BY expr)→ BIGINT ``` Assigns a sequential integer starting from 1 to each row within its window partition. Unlike RANK(), there are never gaps — every row gets a unique number within its partition. SQL ```sql -- Number rows per user SELECT id, user_id, created_at, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS rn FROM orders; -- Get the most recent order per user SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders ) t WHERE rn = 1; ``` ▶ Run #### LAG [Section titled “LAG”](#lag) `LAG` ``` LAG(value ANY [, offset INT [, default ANY]]) OVER ([PARTITION BY expr] ORDER BY expr)→ same as value ``` Returns the value from a row that is offset rows before the current row within the window partition. If no such row exists, returns default (or NULL if unspecified). | Parameter | Type | Required | Default | Description | | --------- | ----- | -------- | ------- | -------------------------------------------------- | | `value` | `ANY` | Yes | — | Expression to evaluate at the lagged row | | `offset` | `INT` | No | 1 | Number of rows back to look | | `default` | `ANY` | No | NULL | Value to return when the offset row does not exist | SQL ```sql -- Previous day's revenue and delta SELECT date, revenue, LAG(revenue) OVER (ORDER BY date) AS prev_revenue, revenue - LAG(revenue, 1, 0) OVER (ORDER BY date) AS delta FROM daily_sales; ``` #### LEAD [Section titled “LEAD”](#lead) `LEAD` ``` LEAD(value ANY [, offset INT [, default ANY]]) OVER ([PARTITION BY expr] ORDER BY expr)→ same as value ``` Returns the value from a row that is offset rows after the current row within the window partition. Mirror of LAG — same parameters, looks forward instead of backward. SQL ```sql -- Next scheduled event timestamp SELECT event_id, scheduled_at, LEAD(scheduled_at) OVER (ORDER BY scheduled_at) AS next_event FROM events; ``` #### FIRST\_VALUE [Section titled “FIRST\_VALUE”](#first_value) `FIRST_VALUE` ``` FIRST_VALUE(value ANY) OVER ([PARTITION BY expr] ORDER BY expr [frame])→ same as value ``` Returns the value evaluated at the first row of the window frame. The default frame is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW — use ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING to always get the first row of the entire partition. SQL ```sql -- First order amount per user alongside each order SELECT user_id, order_id, amount, FIRST_VALUE(amount) OVER ( PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS first_order_amount FROM orders; ``` *** ### JSON/JSONB Functions [Section titled “JSON/JSONB Functions”](#jsonjsonb-functions) #### JSONB\_TYPEOF [Section titled “JSONB\_TYPEOF”](#jsonb_typeof) `JSONB_TYPEOF(from_json JSONB) → TEXT` — Returns the type of the top-level JSON value: `object`, `array`, `string`, `number`, `boolean`, `null`. SQL ```sql SELECT JSONB_TYPEOF('{"a":1}'::jsonb); -- 'object' SELECT JSONB_TYPEOF('[1,2,3]'::jsonb); -- 'array' SELECT JSONB_TYPEOF('42'::jsonb); -- 'number' ``` ▶ Run #### JSONB\_ARRAY\_LENGTH [Section titled “JSONB\_ARRAY\_LENGTH”](#jsonb_array_length) `JSONB_ARRAY_LENGTH(from_json JSONB) → INT` — Returns the number of elements in a JSON array. SQL ```sql SELECT JSONB_ARRAY_LENGTH('[1,2,3,4]'::jsonb); -- 4 ``` ▶ Run #### JSONB\_EXISTS / JSONB\_EXISTS\_ANY / JSONB\_EXISTS\_ALL [Section titled “JSONB\_EXISTS / JSONB\_EXISTS\_ANY / JSONB\_EXISTS\_ALL”](#jsonb_exists--jsonb_exists_any--jsonb_exists_all) `JSONB_EXISTS(from_json JSONB, key TEXT) → BOOLEAN` — True if the given key exists at the top level. `JSONB_EXISTS_ANY(from_json JSONB, keys TEXT[]) → BOOLEAN` — True if any key from the array exists. `JSONB_EXISTS_ALL(from_json JSONB, keys TEXT[]) → BOOLEAN` — True if all keys from the array exist. SQL ```sql SELECT JSONB_EXISTS('{"a":1,"b":2}'::jsonb, 'a'); -- true SELECT JSONB_EXISTS_ANY('{"a":1}'::jsonb, ARRAY['a','c']); -- true SELECT JSONB_EXISTS_ALL('{"a":1,"b":2}'::jsonb, ARRAY['a','b']); -- true ``` ▶ Run #### JSONB\_OBJECT\_KEYS [Section titled “JSONB\_OBJECT\_KEYS”](#jsonb_object_keys) `JSONB_OBJECT_KEYS(from_json JSONB) → SETOF TEXT` — Returns the set of top-level keys of the JSON object. SQL ```sql SELECT JSONB_OBJECT_KEYS('{"name":"Alice","age":30}'::jsonb); -- 'name' / 'age' ``` ▶ Run #### JSONB\_PRETTY [Section titled “JSONB\_PRETTY”](#jsonb_pretty) `JSONB_PRETTY(from_json JSONB) → TEXT` — Returns a human-readable, indented JSON string. SQL ```sql SELECT JSONB_PRETTY('{"a":1,"b":{"c":2}}'::jsonb); -- { -- "a": 1, -- "b": { -- "c": 2 -- } -- } ``` ▶ Run #### TO\_JSON / TO\_JSONB / ROW\_TO\_JSON [Section titled “TO\_JSON / TO\_JSONB / ROW\_TO\_JSON”](#to_json--to_jsonb--row_to_json) `TO_JSON(any) → JSON` — Converts any SQL value to its JSON representation. `TO_JSONB(any) → JSONB` — Same but returns JSONB. `ROW_TO_JSON(record [, pretty_bool]) → JSON` — Converts a row to a JSON object. SQL ```sql SELECT TO_JSONB(ARRAY[1,2,3]); -- [1, 2, 3] SELECT ROW_TO_JSON(u) FROM users u LIMIT 1; -- {"id":1,"name":"Alice",...} ``` ▶ Run `ROW_TO_JSON`, `TO_JSON(row)`, and `TO_JSONB(row)` carry over the column names of a table or subquery row, as in PostgreSQL. Two behaviors match PostgreSQL and are worth knowing. `TO_JSONB(row)` returns `jsonb`, which stores object keys sorted by length and then bytewise, so it does not preserve the row’s column *order* — `ROW_TO_JSON` and `TO_JSON` return `json` and do preserve it. And an anonymous `ROW(...)` constructor has no column names to carry over, so it yields positional placeholders: `ROW_TO_JSON(ROW(1,'a'))` returns `{"f1":1,"f2":"a"}`. DB9 Difference: `pretty_bool` has no effect The optional `pretty_bool` argument to `ROW_TO_JSON` is accepted but has **no effect** — output is always compact, for both `true` and `false`. #### JSON\_BUILD\_OBJECT / JSON\_BUILD\_ARRAY / JSON\_OBJECT\_KEYS / JSON\_ARRAY\_ELEMENTS / JSON\_ARRAY\_ELEMENTS\_TEXT [Section titled “JSON\_BUILD\_OBJECT / JSON\_BUILD\_ARRAY / JSON\_OBJECT\_KEYS / JSON\_ARRAY\_ELEMENTS / JSON\_ARRAY\_ELEMENTS\_TEXT”](#json_build_object--json_build_array--json_object_keys--json_array_elements--json_array_elements_text) These are the non-JSONB variants, and are set-returning. DB9 Difference: JSON\_OBJECT\_KEYS does not preserve key order The `JSON` type itself preserves key order on round-trip — `'{"z":1,"a":2,"m":3}'::json::text` returns `{"z":1,"a":2,"m":3}`. However, `JSON_OBJECT_KEYS` normalizes to JSONB internally and therefore returns keys **sorted**, not in their original order: `JSON_OBJECT_KEYS('{"z":1,"a":2,"m":3}'::json)` yields `a, m, z`. Do not rely on these functions to recover insertion order. `JSON_BUILD_OBJECT(...)`, `JSON_BUILD_ARRAY(...)` — Same as JSONB variants but return JSON. `JSON_OBJECT_KEYS(from_json JSON) → SETOF TEXT` — Returns top-level keys. `JSON_ARRAY_ELEMENTS(from_json JSON) → SETOF JSON` — Expands a JSON array. `JSON_ARRAY_ELEMENTS_TEXT(from_json JSON) → SETOF TEXT` — Expands to text. #### JSONB\_SET [Section titled “JSONB\_SET”](#jsonb_set) `JSONB_SET` ``` JSONB_SET(target JSONB, path TEXT[], new_value JSONB [, create_missing BOOLEAN])→ JSONB ``` Returns target with the item at path replaced by new\_value. If create\_missing is true (the default) and a path segment does not exist, it is created. Use the #- operator to delete a path. | Parameter | Type | Required | Default | Description | | ---------------- | --------- | -------- | ------- | ---------------------------------------------------------------- | | `target` | `JSONB` | Yes | — | Source JSONB value to update | | `path` | `TEXT[]` | Yes | — | Path to the element, e.g. '{addr,city}' or '{0}' for array index | | `new_value` | `JSONB` | Yes | — | Replacement value (must be valid JSON) | | `create_missing` | `BOOLEAN` | No | true | If true, creates keys that do not exist along the path | SQL ```sql SELECT JSONB_SET('{"name":"Alice","age":30}'::jsonb, '{age}', '31'); -- {"name": "Alice", "age": 31} SELECT JSONB_SET('{"user":{"name":"Alice"}}'::jsonb, '{user,email}', '"alice@example.com"', true); -- {"user": {"name": "Alice", "email": "alice@example.com"}} -- Update a column in place UPDATE users SET metadata = JSONB_SET(metadata, '{preferences,theme}', '"dark"') WHERE id = 1; ``` #### JSONB\_BUILD\_OBJECT / JSONB\_BUILD\_ARRAY [Section titled “JSONB\_BUILD\_OBJECT / JSONB\_BUILD\_ARRAY”](#jsonb_build_object--jsonb_build_array) `JSONB_BUILD_OBJECT` ``` JSONB_BUILD_OBJECT(key1 TEXT, value1 ANY [, key2, value2, ...])→ JSONB ``` Builds a JSONB object from a list of alternating key/value pairs. Keys must be strings; values are converted to their JSON equivalents automatically. `JSONB_BUILD_ARRAY(val1 ANY, val2 ANY, ...) → JSONB` — Builds a JSONB array from arguments. SQL ```sql SELECT JSONB_BUILD_OBJECT('name', 'Alice', 'age', 30); SELECT JSONB_BUILD_ARRAY(1, 'two', true); -- [1, "two", true] -- {"name": "Alice", "age": 30} -- Build a response object from row data SELECT JSONB_BUILD_OBJECT( 'id', id, 'name', name, 'email', email, 'created_at', created_at ) AS user_json FROM users WHERE id = 1; ``` ▶ Run #### JSONB\_EXTRACT\_PATH / JSONB\_EXTRACT\_PATH\_TEXT [Section titled “JSONB\_EXTRACT\_PATH / JSONB\_EXTRACT\_PATH\_TEXT”](#jsonb_extract_path--jsonb_extract_path_text) `JSONB_EXTRACT_PATH_TEXT` ``` JSONB_EXTRACT_PATH_TEXT(from_json JSONB, VARIADIC path_elem TEXT[])→ TEXT ``` Extracts a nested field as TEXT using a variadic path. Equivalent to the #>> operator. Returns NULL if the path doesn't exist. Use JSONB\_EXTRACT\_PATH (without \_TEXT) to return JSONB instead. `JSONB_EXTRACT_PATH(from_json JSONB, VARIADIC path_elem TEXT[]) → JSONB` — Same path lookup, but returns the element as JSONB. SQL ```sql SELECT JSONB_EXTRACT_PATH_TEXT('{"user":{"name":"Alice","city":"NYC"}}'::jsonb, 'user', 'city'); -- 'NYC' -- Equivalent operator form SELECT '{"user":{"name":"Alice"}}'::jsonb #>> '{user,name}'; -- 'Alice' ``` ▶ Run #### JSONB\_ARRAY\_ELEMENTS / JSONB\_ARRAY\_ELEMENTS\_TEXT [Section titled “JSONB\_ARRAY\_ELEMENTS / JSONB\_ARRAY\_ELEMENTS\_TEXT”](#jsonb_array_elements--jsonb_array_elements_text) `JSONB_ARRAY_ELEMENTS` ``` JSONB_ARRAY_ELEMENTS(from_json JSONB)→ SETOF JSONB ``` Expands a JSON array into a set of JSONB values, one row per element. Use JSONB\_ARRAY\_ELEMENTS\_TEXT to get plain text values instead of JSONB. SQL ```sql SELECT value FROM JSONB_ARRAY_ELEMENTS('[1, 2, 3]'::jsonb); -- 1 -- 2 -- 3 -- Expand an array column for per-element filtering SELECT id, tag FROM products, JSONB_ARRAY_ELEMENTS_TEXT(tags) AS tag WHERE tag = 'electronics'; ``` ▶ Run #### JSONB\_EACH / JSONB\_EACH\_TEXT [Section titled “JSONB\_EACH / JSONB\_EACH\_TEXT”](#jsonb_each--jsonb_each_text) `JSONB_EACH` ``` JSONB_EACH(from_json JSONB)→ SETOF (key TEXT, value JSONB) ``` Expands a JSON object into a set of (key, value) rows, one row per top-level key. Use JSONB\_EACH\_TEXT to get both key and value as TEXT. SQL ```sql SELECT key, value FROM JSONB_EACH('{"a":1,"b":2,"c":3}'::jsonb); -- key | value -- a | 1 -- b | 2 -- c | 3 -- Pivot metadata keys into rows SELECT id, key, value::text FROM users, JSONB_EACH_TEXT(metadata) WHERE key LIKE 'pref_%'; ``` ### JSON Operators [Section titled “JSON Operators”](#json-operators) | Operator | Description | Example | | -------- | ----------------------------- | ------------------------- | | `->` | Get JSON element by key/index | `data->'name'` | | `->>` | Get JSON element as text | `data->>'name'` | | `#>` | Get element by path | `data#>'{addr,city}'` | | `#>>` | Get element by path as text | `data#>>'{addr,city}'` | | `#-` | Delete key/path | `data #- '{addr}'` | | `@>` | Contains | `data @> '{"a":1}'` | | `<@` | Contained by | `'{"a":1}' <@ data` | | `?` | Key exists | `data ? 'email'` | | `?\|` | Any key exists | `data ?\| array['a','b']` | | `?&` | All keys exist | `data ?& array['a','b']` | *** ### Array Functions [Section titled “Array Functions”](#array-functions) #### ARRAY\_LENGTH / ARRAY\_UPPER / ARRAY\_LOWER / CARDINALITY [Section titled “ARRAY\_LENGTH / ARRAY\_UPPER / ARRAY\_LOWER / CARDINALITY”](#array_length--array_upper--array_lower--cardinality) `ARRAY_LENGTH(array, dimension INT) → INT` — Length of the array along the given dimension (1-based). `ARRAY_UPPER(array, dimension INT) → INT` — Upper bound of array dimension. `ARRAY_LOWER(array, dimension INT) → INT` — Lower bound of array dimension (usually 1). `CARDINALITY(array) → INT` — Number of elements in the array. SQL ```sql SELECT ARRAY_LENGTH(ARRAY[1,2,3], 1); -- 3 SELECT ARRAY_UPPER(ARRAY[1,2,3], 1); -- 3 SELECT ARRAY_LOWER(ARRAY[1,2,3], 1); -- 1 SELECT CARDINALITY(ARRAY[1,2,3,4]); -- 4 SELECT CARDINALITY(ARRAY[[1,2],[3,4]]); -- 4 (total elements across all dimensions) ``` ▶ Run #### ARRAY\_POSITION [Section titled “ARRAY\_POSITION”](#array_position) `ARRAY_POSITION(array, element [, subscript INT]) → INT` — Returns the position of the first occurrence of element in array (1-based; NULL if not found). SQL ```sql SELECT ARRAY_POSITION(ARRAY['a','b','c','b'], 'b'); -- 2 SELECT ARRAY_POSITION(ARRAY['a','b','c','b'], 'b', 3); -- 4 (start from position 3) ``` ▶ Run #### ARRAY\_CAT / ARRAY\_APPEND / ARRAY\_PREPEND / ARRAY\_REMOVE [Section titled “ARRAY\_CAT / ARRAY\_APPEND / ARRAY\_PREPEND / ARRAY\_REMOVE”](#array_cat--array_append--array_prepend--array_remove) `ARRAY_CAT(array1, array2) → ARRAY` — Concatenates two arrays. Equivalent to `||` operator. `ARRAY_APPEND(array, element) → ARRAY` — Appends an element to the end. `ARRAY_PREPEND(element, array) → ARRAY` — Prepends an element to the beginning. `ARRAY_REMOVE(array, element) → ARRAY` — Removes all occurrences of element. SQL ```sql SELECT ARRAY_CAT(ARRAY[1,2], ARRAY[3,4]); -- {1,2,3,4} SELECT ARRAY_APPEND(ARRAY[1,2,3], 4); -- {1,2,3,4} SELECT ARRAY_PREPEND(0, ARRAY[1,2,3]); -- {0,1,2,3} SELECT ARRAY_REMOVE(ARRAY[1,2,3,2], 2); -- {1,3} ``` ▶ Run #### ARRAY\_TO\_STRING / STRING\_TO\_ARRAY [Section titled “ARRAY\_TO\_STRING / STRING\_TO\_ARRAY”](#array_to_string--string_to_array) `ARRAY_TO_STRING(array, delimiter TEXT [, null_string TEXT]) → TEXT` — Converts array to a delimited string. `STRING_TO_ARRAY(string TEXT, delimiter TEXT [, null_string TEXT]) → TEXT[]` — Splits a string into a text array. SQL ```sql SELECT ARRAY_TO_STRING(ARRAY[1,2,3], ','); -- '1,2,3' SELECT ARRAY_TO_STRING(ARRAY['a',NULL,'c'], ',', 'X'); -- 'a,X,c' SELECT STRING_TO_ARRAY('a,b,c', ','); -- {a,b,c} ``` ▶ Run #### ARRAY\_LENGTH [Section titled “ARRAY\_LENGTH”](#array_length) `ARRAY_LENGTH` ``` ARRAY_LENGTH(array ANYARRAY, dimension INT)→ INT ``` Returns the length of the requested array dimension. For 1-D arrays, dimension is 1. Returns NULL for an empty array. CARDINALITY() is a simpler alternative that returns the total element count of a 1-D array. SQL ```sql SELECT ARRAY_LENGTH(ARRAY[1, 2, 3, 4, 5], 1); -- 5 SELECT ARRAY_LENGTH(ARRAY[['a','b'],['c','d']], 2); -- 2 (second dimension) SELECT CARDINALITY(ARRAY[1,2,3]); -- 3 ``` ▶ Run #### UNNEST [Section titled “UNNEST”](#unnest) `UNNEST` ``` UNNEST(array ANYARRAY [, array2, ...])→ SETOF element ``` Expands an array into a set of rows, one row per element. When multiple arrays are provided, they are expanded in parallel (zip), padding shorter arrays with NULL out to the length of the longest. SQL ```sql SELECT UNNEST(ARRAY['a', 'b', 'c']); -- a -- b -- c -- Join array column elements as rows SELECT id, tag FROM articles, UNNEST(tags) AS tag WHERE 'postgres' = ANY(tags); ``` `UNNEST()` supports `WITH ORDINALITY`, which adds a 1-based position column: SQL ```sql SELECT * FROM UNNEST(ARRAY['a','b','c']) WITH ORDINALITY; -- a | 1 -- b | 2 -- c | 3 ``` ▶ Run Name the columns with a table alias, and unnest several arrays in parallel: SQL ```sql SELECT v, n FROM UNNEST(ARRAY['x','y']) WITH ORDINALITY AS t(v, n); ``` ▶ Run SQL ```sql SELECT a, b, n FROM UNNEST(ARRAY['x','y'], ARRAY[1,2]) WITH ORDINALITY AS t(a, b, n); ``` ▶ Run DB9 Difference: `WITH ORDINALITY` works only on `UNNEST()` Other set-returning functions reject the clause at parse time: ```plaintext ERROR: syntax error: sql parser error: Expected end of statement, found: WITH at Line: 1, Column (42601) ``` This affects `GENERATE_SERIES`, `JSON_ARRAY_ELEMENTS`, `JSONB_ARRAY_ELEMENTS`, and `REGEXP_SPLIT_TO_TABLE`. Use `ROW_NUMBER()` over the result set when you need positions there: SQL ```sql SELECT val, ROW_NUMBER() OVER () AS pos FROM GENERATE_SERIES(10, 12) AS val; ``` ▶ Run *** ### Regular Expression Functions [Section titled “Regular Expression Functions”](#regular-expression-functions) #### REGEXP\_SPLIT\_TO\_ARRAY [Section titled “REGEXP\_SPLIT\_TO\_ARRAY”](#regexp_split_to_array) `REGEXP_SPLIT_TO_ARRAY(string TEXT, pattern TEXT [, flags TEXT]) → TEXT[]` — Splits string using a regular expression delimiter, returning a text array. SQL ```sql SELECT REGEXP_SPLIT_TO_ARRAY('one two three', '\s+'); -- {one,two,three} ``` ▶ Run #### REGEXP\_REPLACE [Section titled “REGEXP\_REPLACE”](#regexp_replace) `REGEXP_REPLACE` ``` REGEXP_REPLACE(string TEXT, pattern TEXT, replacement TEXT [, flags TEXT])→ TEXT ``` Replaces substring(s) matching a POSIX regular expression. By default replaces only the first match; use the 'g' flag to replace all occurrences. Use \\\1, \\\2 in replacement for back-references. | Parameter | Type | Required | Default | Description | | ------------- | ------ | -------- | ------- | ---------------------------------------------------------------- | | `string` | `TEXT` | Yes | — | Input string | | `pattern` | `TEXT` | Yes | — | POSIX regular expression | | `replacement` | `TEXT` | Yes | — | Replacement string; use \1, \2 for capture group back-references | | `flags` | `TEXT` | No | '' | g = replace all matches, i = case-insensitive | SQL ```sql SELECT REGEXP_REPLACE('Hello World', 'World', 'DB9'); -- 'Hello DB9' SELECT REGEXP_REPLACE('abc123def', '[0-9]+', 'NUM'); -- 'abcNUMdef' SELECT REGEXP_REPLACE(' spaces ', '^\s+|\s+$', '', 'g'); -- 'spaces' SELECT REGEXP_REPLACE('foo bar baz', '\s+', '-', 'g'); -- 'foo-bar-baz' ``` ▶ Run ### Regular Expression Operators [Section titled “Regular Expression Operators”](#regular-expression-operators) | Operator | Description | | -------- | --------------------------------- | | `~` | Matches regex (case-sensitive) | | `~*` | Matches regex (case-insensitive) | | `!~` | Does not match (case-sensitive) | | `!~*` | Does not match (case-insensitive) | *** ### Full-Text Search [Section titled “Full-Text Search”](#full-text-search) #### TO\_TSVECTOR [Section titled “TO\_TSVECTOR”](#to_tsvector) `TO_TSVECTOR([config REGCONFIG,] document TEXT) → TSVECTOR` — Converts text to a tsvector of lexemes for full-text search. The optional config sets the text search configuration (e.g. `'english'`). SQL ```sql SELECT TO_TSVECTOR('english', 'the quick brown fox'); -- 'brown':3 'fox':4 'quick':2 -- Indexed column (GIN index recommended) CREATE INDEX ON articles USING GIN (TO_TSVECTOR('english', body)); ``` ▶ Run #### TO\_TSQUERY / PLAINTO\_TSQUERY / PHRASETO\_TSQUERY / WEBSEARCH\_TO\_TSQUERY [Section titled “TO\_TSQUERY / PLAINTO\_TSQUERY / PHRASETO\_TSQUERY / WEBSEARCH\_TO\_TSQUERY”](#to_tsquery--plainto_tsquery--phraseto_tsquery--websearch_to_tsquery) `TO_TSQUERY([config,] querytext TEXT) → TSQUERY` — Parses a query string with explicit operators (`&`, `|`, `!`, `<->`). `PLAINTO_TSQUERY([config,] querytext TEXT) → TSQUERY` — Converts plain text to a tsquery treating all words as AND-joined terms. `PHRASETO_TSQUERY([config,] querytext TEXT) → TSQUERY` — Creates a phrase query requiring words to appear adjacent. `WEBSEARCH_TO_TSQUERY([config,] querytext TEXT) → TSQUERY` — Converts web-search style query (quoted phrases, `-` exclusions) to a tsquery. SQL ```sql SELECT TO_TSQUERY('english', 'quick & fox'); SELECT PLAINTO_TSQUERY('english', 'quick brown fox'); SELECT PHRASETO_TSQUERY('english', 'quick brown'); -- adjacent words SELECT WEBSEARCH_TO_TSQUERY('english', '"quick brown" -slow'); ``` ▶ Run #### TS\_HEADLINE [Section titled “TS\_HEADLINE”](#ts_headline) `TS_HEADLINE([config REGCONFIG,] document TEXT, query TSQUERY [, options TEXT]) → TEXT` — Highlights matching terms in a document fragment. Options include `StartSel`, `StopSel`, `MaxWords`, `MinWords`, `ShortWord`, `HighlightAll`, `MaxFragments`, `FragmentDelimiter`. SQL ```sql SELECT TS_HEADLINE( 'english', body, PLAINTO_TSQUERY('english', 'database performance'), 'StartSel=, StopSel=, MaxFragments=2' ) AS headline FROM articles WHERE TO_TSVECTOR('english', body) @@ PLAINTO_TSQUERY('english', 'database performance'); ``` ▶ Run *** #### SETWEIGHT [Section titled “SETWEIGHT”](#setweight) `SETWEIGHT` ``` SETWEIGHT(vector TSVECTOR, weight "char")→ TSVECTOR ``` Assigns a weight label to all lexemes in a tsvector. Weights are A (highest), B, C, D (lowest/default). Used to boost certain document fields (e.g. title vs body) when scoring with TS\_RANK. SQL ```sql -- Weight title higher than body SELECT SETWEIGHT(TO_TSVECTOR('english', title), 'A') || SETWEIGHT(TO_TSVECTOR('english', body), 'C') AS document FROM articles; -- Indexed column with weighted fields CREATE INDEX ON articles USING GIN ( SETWEIGHT(TO_TSVECTOR('english', title), 'A') || SETWEIGHT(TO_TSVECTOR('english', body), 'C') ); ``` ▶ Run #### TS\_RANK / TS\_RANK\_CD [Section titled “TS\_RANK / TS\_RANK\_CD”](#ts_rank--ts_rank_cd) `TS_RANK` ``` TS_RANK(vector TSVECTOR, query TSQUERY [, normalization INT])→ FLOAT4 ``` Calculates a relevance score for a tsvector against a tsquery. Higher scores indicate better matches. The normalization bitmask controls whether document length affects the score. | Parameter | Type | Required | Default | Description | | --------------- | ---------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- | | `vector` | `TSVECTOR` | Yes | — | Document tsvector (often a stored/indexed column) | | `query` | `TSQUERY` | Yes | — | Search query | | `normalization` | `INT` | No | 0 | Bitmask: 0=none, 1=1/log(ndoc), 2=1/ndoc, 4=mean harmonic distance, 8=unique words, 16=1/log(unique words), 32=rank/(rank+1) | `TS_RANK_CD(vector, query [, normalization])` — Like TS\_RANK but uses the “cover density” ranking algorithm, which also factors in how close matches are to each other. SQL ```sql SELECT title, TS_RANK( TO_TSVECTOR('english', body), PLAINTO_TSQUERY('english', 'database performance') ) AS rank FROM articles WHERE TO_TSVECTOR('english', body) @@ PLAINTO_TSQUERY('english', 'database performance') ORDER BY rank DESC LIMIT 10; ``` ▶ Run *** ### UUID Functions [Section titled “UUID Functions”](#uuid-functions) `GEN_RANDOM_UUID() → UUID` — Generates a random UUID v4. This is the standard way to generate UUIDs in PostgreSQL 13+. `UUID_GENERATE_V4() → UUID` — Generates a random UUID v4. Built in, so no `CREATE EXTENSION "uuid-ossp"` is required. Prefer `GEN_RANDOM_UUID()`. `UUIDV7() → UUID` — Generates a UUID v7 (time-ordered, monotonically increasing). Useful for primary keys that sort by creation time. SQL ```sql SELECT GEN_RANDOM_UUID(); -- e.g. 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' SELECT UUIDV7(); -- time-ordered UUID v7 -- Common primary key pattern CREATE TABLE uuid_pk_demo ( id UUID PRIMARY KEY DEFAULT UUIDV7(), name TEXT NOT NULL ); ``` ▶ Run *** *** ### Encoding & Hashing [Section titled “Encoding & Hashing”](#encoding--hashing) #### SHA256 [Section titled “SHA256”](#sha256) `SHA256(data BYTEA) → BYTEA` — Returns the SHA-256 digest of binary data as `BYTEA` (32 bytes). Wrap with `ENCODE(..., 'hex')` for a hex string. SQL ```sql SELECT SHA256('hello'::bytea); -- '\x2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824' SELECT ENCODE(SHA256('hello'::bytea), 'hex'); -- '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824' ``` ▶ Run #### DIGEST [Section titled “DIGEST”](#digest) `DIGEST(data TEXT | BYTEA, algorithm TEXT) → BYTEA` — Returns the binary hash of `data` using the named `algorithm`. Supported algorithms: `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`. SQL ```sql SELECT ENCODE(DIGEST('hello', 'sha256'), 'hex'); -- '2cf24dba5fb0a30e...' SELECT ENCODE(DIGEST('hello', 'sha512'), 'hex'); -- '9b71d224bd62f378...' ``` ▶ Run *** #### ENCODE / DECODE [Section titled “ENCODE / DECODE”](#encode--decode) `ENCODE` ``` ENCODE(data BYTEA, format TEXT)→ TEXT ``` Encodes binary data as text. Supported formats: base64, hex, escape. `DECODE` ``` DECODE(string TEXT, format TEXT)→ BYTEA ``` Decodes text-encoded binary data back to BYTEA. Supported formats: base64, hex, escape. SQL ```sql SELECT ENCODE('Hello DB9'::bytea, 'base64'); -- 'SGVsbG8gREI5' SELECT ENCODE('Hello DB9'::bytea, 'hex'); -- '48656c6c6f2044423'... SELECT DECODE('SGVsbG8gREI5', 'base64'); -- '\x48656c6c6f204442...' ``` ▶ Run #### GET\_BYTE / TO\_HEX [Section titled “GET\_BYTE / TO\_HEX”](#get_byte--to_hex) `GET_BYTE(data BYTEA, offset INTEGER) → INTEGER` — Extracts the byte at zero-based `offset`. Errors if the offset is out of range. `TO_HEX(number INTEGER | BIGINT) → TEXT` — Converts a number to its hexadecimal representation. Negative integers are rendered as two’s complement. SQL ```sql SELECT GET_BYTE('\x1234'::bytea, 0); -- 18 SELECT TO_HEX(255); -- 'ff' SELECT TO_HEX(-1); -- 'ffffffff' ``` ▶ Run #### CONVERT\_FROM [Section titled “CONVERT\_FROM”](#convert_from) `CONVERT_FROM(data BYTEA, encoding TEXT) → TEXT` — Converts binary data to text. Only `UTF8` is supported as the source encoding; any other encoding name raises `unrecognized encoding`. SQL ```sql SELECT CONVERT_FROM('\x68656c6c6f'::bytea, 'UTF8'); -- 'hello' ``` ▶ Run #### MD5 [Section titled “MD5”](#md5) `MD5` ``` MD5(string TEXT)→ TEXT ``` Returns the MD5 hash of a string as a lowercase 32-character hex string. Not recommended for security-sensitive hashing. SQL ```sql SELECT MD5('hello'); -- '5d41402abc4b2a76b9719d911017c592' SELECT MD5(RANDOM()::text); -- random 32-char hex string ``` ▶ Run *** ### Vector Functions (pgvector-compatible) [Section titled “Vector Functions (pgvector-compatible)”](#vector-functions-pgvector-compatible) `L2_DISTANCE(a VECTOR, b VECTOR) → FLOAT8` — Euclidean (L2) distance between two vectors. Equivalent to `a <-> b`. `INNER_PRODUCT(a VECTOR, b VECTOR) → FLOAT8` — Dot product. Negative inner product via `a <#> b`. `COSINE_DISTANCE(a VECTOR, b VECTOR) → FLOAT8` — Cosine distance (1 - cosine similarity). Equivalent to `a <=> b`. `VECTOR_DIMS(v VECTOR) → INT` — Returns the number of dimensions. `VECTOR_NORM(v VECTOR) → FLOAT8` — Returns the Euclidean norm (magnitude). SQL ```sql -- Similarity search SELECT id, embedding <-> query_vec AS distance FROM items ORDER BY embedding <-> query_vec LIMIT 10; SELECT VECTOR_DIMS('[1,2,3]'::vector); -- 3 SELECT VECTOR_NORM('[3,4]'::vector); -- 5 ``` *** *** ### Embedding Functions [Section titled “Embedding Functions”](#embedding-functions) `EMBEDDING(text TEXT [, model TEXT]) → VECTOR` — Generates a vector embedding inline in SQL. `EMBED_TEXT(model TEXT, text TEXT) → VECTOR` — Explicit model-specified embedding function. DB9 Difference: three-argument forms are not available The three-argument form `EMBEDDING(text, model, dimensions)` **terminates the connection** (`error: connection closed`) rather than returning an error — do not use it. To control dimensionality, set the session GUC instead: SQL ```sql SET embedding.dimensions = 512; SELECT VECTOR_DIMS(EMBEDDING('hello')); -- 512 ``` `EMBED_TEXT` does accept a third options argument, but only as an **untyped string literal** — casting it explicitly to `jsonb` fails with `function EMBED_TEXT(unknown, unknown, jsonb) does not exist` (`42883`): SQL ```sql SELECT VECTOR_DIMS(EMBED_TEXT('text-embedding-v4', 'hello', '{"dimensions": 512}')); -- 512 SELECT VECTOR_DIMS(EMBED_TEXT('text-embedding-v4', 'hello', '{"dimensions": 512}'::jsonb)); -- 42883 ``` Tracked in db9-server#3222. SQL ```sql -- Requires: CREATE EXTENSION embedding SELECT EMBEDDING('hello world'); -- uses default model -- Semantic search SELECT id, content FROM docs ORDER BY EMBEDDING(content) <=> EMBEDDING('database connection') LIMIT 5; ``` ▶ Run See [Vector Search](/docs/extensions/vector) for end-to-end examples. *** *** ### Document Chunking [Section titled “Document Chunking”](#document-chunking) `CHUNK_TEXT(content TEXT [, max_chars INT, overlap_chars INT, title TEXT])` — Table-valued function that splits text into overlapping chunks for RAG pipelines. Markdown-aware at paragraph, heading, and list boundaries. SQL ```sql SELECT chunk_index, chunk_text, chunk_pos FROM CHUNK_TEXT('Long document...', 500, 50, 'My Doc'); ``` See [CHUNK\_TEXT](/docs/extensions/chunk-text/) for full reference. *** *** ### HTTP Functions (Scalar) [Section titled “HTTP Functions (Scalar)”](#http-functions-scalar) `HTTP_GET(url TEXT) → JSONB` — Sends a GET request. `HTTP_POST(url TEXT, body TEXT, content_type TEXT) → JSONB` — Sends a POST request. `HTTP_PUT(url TEXT, body TEXT, content_type TEXT) → JSONB` — Sends a PUT request. `HTTP_DELETE(url TEXT) → JSONB` — Sends a DELETE request. `HTTP_HEAD(url TEXT) → JSONB` — Sends a HEAD request. `HTTP_PATCH(url TEXT, body TEXT, content_type TEXT) → JSONB` — Sends a PATCH request. `HTTP(method TEXT, url TEXT) → JSONB` — Generic HTTP function. DB9 Difference: HTTP() takes only method and url Only the two-argument form exists. `HTTP(method, url, body, options JSONB)` fails with `function http(unknown, unknown, unknown, jsonb) does not exist` (`42883`). To send a body, use the method-specific functions (`HTTP_POST`, `HTTP_PUT`, `HTTP_PATCH`) instead. All return JSONB with keys: `status` (INT), `content` (TEXT), `content_type` (TEXT), `headers` (JSONB). SQL ```sql SELECT (HTTP_GET('https://api.example.com/data'))->>'content'; SELECT (HTTP_POST( 'https://api.example.com/items', '{"name":"test"}', 'application/json' ))->>'status'; -- e.g. '201' ``` See [http extension](/docs/extensions/http/) for details. *** *** ### Storage Functions [Section titled “Storage Functions”](#storage-functions) `DB9_REFRESH_STORAGE_STATS() → VOID` — Triggers an asynchronous storage scan. Results appear in `_DB9_SYS_STORAGE_STATS` and `_DB9_SYS_TABLE_STORAGE_STATS`. See [Storage Accounting](/docs/platform/storage/). Not available in the current release `DB9_REFRESH_STORAGE_STATS()` requires the background worker subsystem, which is sealed off in the current release. Calling it returns `ERROR: feature "storage_size_scan" is unavailable (PreActivationSeal)`. The `_DB9_SYS_STORAGE_STATS` tables are still populated by other means, so query them directly rather than triggering a manual refresh. `extensions.fs9_storage_stats() → TABLE(total_files BIGINT, total_directories BIGINT, total_logical_bytes BIGINT)` — Returns filesystem storage statistics. Requires `CREATE EXTENSION fs9`. Call with the `extensions.` schema prefix (e.g. `SELECT * FROM extensions.fs9_storage_stats()`) or set `search_path` to include `extensions`. See [fs9 extension](/docs/extensions/fs9/). *** *** ### Sequence Functions [Section titled “Sequence Functions”](#sequence-functions) `NEXTVAL(regclass) → BIGINT` — Advances a sequence and returns the new value. `CURRVAL(regclass) → BIGINT` — Returns the last value from NEXTVAL in the current session. `SETVAL(regclass, value BIGINT [, called BOOLEAN]) → BIGINT` — Sets the current value of a sequence. `LASTVAL() → BIGINT` — Returns the last value returned by NEXTVAL in the current session, for any sequence. `PG_GET_SERIAL_SEQUENCE(table_name TEXT, column_name TEXT) → TEXT` — Returns the name of the sequence associated with a serial column. SQL ```sql SELECT NEXTVAL('my_seq'); SELECT CURRVAL('my_seq'); SELECT SETVAL('my_seq', 100); -- next NEXTVAL returns 101 SELECT SETVAL('my_seq', 100, false); -- next NEXTVAL returns 100 SELECT PG_GET_SERIAL_SEQUENCE('users', 'id'); -- e.g. 'public.users_id_seq' ``` *** *** ### Conditional Expressions [Section titled “Conditional Expressions”](#conditional-expressions) #### NULLIF [Section titled “NULLIF”](#nullif) `NULLIF(value1 ANY, value2 ANY) → same type` — Returns NULL if value1 = value2, otherwise returns value1. The inverse of COALESCE. SQL ```sql SELECT NULLIF(0, 0); -- NULL (avoids division by zero) SELECT 100 / NULLIF(divisor, 0) FROM t; -- safe division ``` #### GREATEST / LEAST [Section titled “GREATEST / LEAST”](#greatest--least) `GREATEST(val1, val2, ...) → same type` — Returns the largest non-NULL value among arguments. `LEAST(val1, val2, ...) → same type` — Returns the smallest non-NULL value among arguments. SQL ```sql SELECT GREATEST(1, 5, 3, 2); -- 5 SELECT LEAST('apple', 'banana', 'cherry'); -- 'apple' ``` ▶ Run #### CASE WHEN [Section titled “CASE WHEN”](#case-when) SQL ```sql -- Searched CASE SELECT CASE WHEN score >= 90 THEN 'A' WHEN score >= 80 THEN 'B' WHEN score >= 70 THEN 'C' ELSE 'F' END AS grade FROM students; -- Simple CASE SELECT CASE status WHEN 'active' THEN 'Active' WHEN 'inactive' THEN 'Inactive' ELSE 'Unknown' END AS label FROM users; ``` *** #### COALESCE [Section titled “COALESCE”](#coalesce) `COALESCE` ``` COALESCE(value1 ANY, value2 ANY [, ...])→ same type ``` Returns the first non-NULL argument, evaluating left-to-right and stopping at the first non-NULL value. All arguments must be of compatible types. The classic null-safe fallback function. SQL ```sql SELECT COALESCE(NULL, NULL, 'fallback'); -- 'fallback' SELECT COALESCE(nickname, first_name, 'Anonymous') FROM users; -- first non-null name -- Provide 0 instead of NULL for aggregates SELECT user_id, COALESCE(SUM(amount), 0) AS total FROM orders GROUP BY user_id; ``` *** ### System / Compatibility Functions [Section titled “System / Compatibility Functions”](#system--compatibility-functions) #### PG\_TYPEOF [Section titled “PG\_TYPEOF”](#pg_typeof) `PG_TYPEOF(any) → REGTYPE` — Returns the data type of its argument. SQL ```sql SELECT PG_TYPEOF(42); -- 'integer' SELECT PG_TYPEOF('hello'); -- 'unknown' SELECT PG_TYPEOF(NOW()); -- 'timestamp with time zone' ``` ▶ Run #### SIZE FUNCTIONS [Section titled “SIZE FUNCTIONS”](#size-functions) `PG_COLUMN_SIZE(any) → INT` — Number of bytes used to store a particular value. SQL ```sql SELECT PG_COLUMN_SIZE(ROW(1, 'hello', NOW())); ``` ▶ Run #### PRIVILEGE FUNCTIONS [Section titled “PRIVILEGE FUNCTIONS”](#privilege-functions) `HAS_TABLE_PRIVILEGE(user TEXT, table TEXT, privilege TEXT) → BOOLEAN` — Tests if user has privilege on table. `HAS_SCHEMA_PRIVILEGE(user TEXT, schema TEXT, privilege TEXT) → BOOLEAN` — Tests schema privilege. `HAS_DATABASE_PRIVILEGE(user TEXT, database TEXT, privilege TEXT) → BOOLEAN` — Tests database privilege. SQL ```sql SELECT HAS_TABLE_PRIVILEGE(CURRENT_USER, 'public.users', 'SELECT'); SELECT HAS_SCHEMA_PRIVILEGE(CURRENT_USER, 'public', 'USAGE'); ``` ▶ Run DB9 Difference: `HAS_DATABASE_PRIVILEGE` only accepts `CREATE` `CREATE` is the only privilege type this function recognizes. `CONNECT`, `TEMP`, and `TEMPORARY` — all valid in PostgreSQL — are rejected: ```plaintext ERROR: unrecognized privilege type: "CONNECT" -- sqlstate 22023 ``` `HAS_TABLE_PRIVILEGE` (`SELECT`, `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, `REFERENCES`, `TRIGGER`) and `HAS_SCHEMA_PRIVILEGE` (`USAGE`, `CREATE`) accept the full standard set. #### DESCRIPTION FUNCTIONS [Section titled “DESCRIPTION FUNCTIONS”](#description-functions) `OBJ_DESCRIPTION(oid OID, catalog_name TEXT) → TEXT` — Returns the comment (description) for a database object. `COL_DESCRIPTION(table_oid OID, column_number INT) → TEXT` — Returns the comment for a table column. SQL ```sql SELECT OBJ_DESCRIPTION('public.users'::regclass, 'pg_class'); SELECT COL_DESCRIPTION('public.users'::regclass, 1); ``` DB9 Difference: description functions return NULL Both functions currently return `NULL` even when a comment exists. `COMMENT ON` succeeds and the underlying catalog row is written — `SELECT description FROM pg_description WHERE objoid = 'public.users'::regclass::oid AND objsubid = 0` returns the comment — but the accessor functions do not read it back. Query `pg_description` directly as a workaround. Tracked in db9-server#3222. #### SYSTEM INFO [Section titled “SYSTEM INFO”](#system-info) `VERSION() → TEXT` — Returns a string describing the server version. `CURRENT_USER → TEXT` — Name of the current effective user. `CURRENT_DATABASE() → NAME` — Name of the current database. `CURRENT_SCHEMA() → NAME` — Current schema search path default. `PG_BACKEND_PID() → INT` — Process ID of the server process for the current session. `FORMAT_TYPE(type_oid OID, typemod INT) → TEXT` — Returns the SQL name of a data type. `PG_ENCODING_TO_CHAR(encoding INT) → TEXT` — Converts an encoding number to its name. SQL ```sql SELECT VERSION(); SELECT CURRENT_USER, CURRENT_DATABASE(), CURRENT_SCHEMA(); SELECT PG_BACKEND_PID(); ``` ▶ Run *** #### GENERATE\_SERIES [Section titled “GENERATE\_SERIES”](#generate_series) `GENERATE_SERIES` ``` GENERATE_SERIES(start, stop [, step])→ SETOF value ``` Generates a series of values from start to stop (inclusive), incrementing by step. Works with integers, numerics, timestamps, and timestamptz. Default step is 1 for numerics or 1 day for timestamps. | Parameter | Type | Required | Default | Description | | --------- | -------------------------------------------- | -------- | ------- | ------------------------------ | | `start` | `INT \| NUMERIC \| TIMESTAMP \| TIMESTAMPTZ` | Yes | — | Series start value (inclusive) | | `stop` | `same as start` | Yes | — | Series end value (inclusive) | | `step` | `INT \| NUMERIC \| INTERVAL` | No | 1 | Increment between values | Usage in DB9 `GENERATE_SERIES` must be used as a set-returning function in the `FROM` clause: `SELECT * FROM generate_series(1, 5)`. Using it directly in the `SELECT` list (e.g. `SELECT generate_series(1, 5)`) fails with a bare `XX000` (`internal error`) that does not mention set-returning functions. `UNNEST`, `JSONB_ARRAY_ELEMENTS`, and `REGEXP_SPLIT_TO_TABLE` do work in the `SELECT` list. SQL ```sql SELECT * FROM GENERATE_SERIES(1, 5); -- 1, 2, 3, 4, 5 SELECT * FROM GENERATE_SERIES(0.0, 1.0, 0.25); -- 0.0, 0.25, 0.50, 0.75, 1.00 -- Generate a date range SELECT * FROM GENERATE_SERIES( '2024-01-01'::date, '2024-01-07'::date, '1 day'::interval ) AS day; ``` ▶ Run Fill time-series gaps by left-joining a generated date range against your own table: SQL ```sql SELECT d.day, COALESCE(COUNT(o.id), 0) AS orders FROM GENERATE_SERIES('2024-01-01'::date, '2024-01-31'::date, '1 day'::interval) AS d(day) LEFT JOIN orders o ON o.created_at::date = d.day GROUP BY d.day ORDER BY d.day; ``` *** ### Background SQL [Section titled “Background SQL”](#background-sql) `PG_BACKGROUND_LAUNCH(sql TEXT) → BIGINT` — Launches a SQL statement asynchronously in a background session. Returns a handle. `PG_BACKGROUND_RESULT(handle BIGINT) → SETOF RECORD` — Waits for and retrieves the result of a background query by handle. SQL ```sql -- Launch a long-running query in the background SELECT PG_BACKGROUND_LAUNCH('VACUUM ANALYZE large_table') AS handle; -- Returns a BIGINT handle, e.g. -467811753460498433 ``` DB9 Difference: launched work cannot be collected `PG_BACKGROUND_RESULT` is not implemented — it fails with `function pg_background_result(integer) does not exist` (`42883`), in both the bare and `extensions.`-qualified forms, so there is currently no way to retrieve the result of a launched statement. Note also that `PG_BACKGROUND_LAUNCH` returns a **large, often negative BIGINT** handle (e.g. `-467811753460498433`), not a small positive integer. Storing it in an `INT` column will overflow. Tracked in db9-server#3222. # Limits & Constraints > Resource limits and constraints across the DB9 engine and extensions. ## Limits & Constraints [Section titled “Limits & Constraints”](#limits--constraints) Resource limits and constraints across the db9 engine and extensions. ### Engine Limits [Section titled “Engine Limits”](#engine-limits) | Resource | Limit | | ------------------------ | ----------------------- | | Statement timeout | 60s (default) | | Idle transaction timeout | 600s / 10 min (default) | Statement timeout can be changed per session The default statement timeout of 60 seconds can be overridden per session with `SET statement_timeout = '120s'`. Long-running operations like index builds or data migrations may need this adjustment. \| View nesting depth | 64 levels | | Pending portals per connection | 32 | | Timestamp precision | Microseconds | Fractional-second precision is applied to columns DB9 timestamps have microsecond precision, matching standard PostgreSQL. A `TIMESTAMP(n)` or `TIMESTAMPTZ(n)` **column** rounds stored values to the declared precision, both on insert and on `ALTER COLUMN ... TYPE`. The typmod is *not* applied in a cast expression (`'...'::timestamp(3)` keeps full microseconds), and `TIME(n)` / `TIMETZ(n)` columns ignore it entirely. \| Encoding | UTF-8 only | ### HTTP Extension Limits [Section titled “HTTP Extension Limits”](#http-extension-limits) | Resource | Limit | | ------------------------- | ------- | | Connect timeout | 1s | | Total timeout | 5s | | Max request body | 256 KiB | | Max response body | 1 MiB | | Max calls per statement | 100 | | Max concurrent per tenant | 20 | ### fs9 Extension Limits [Section titled “fs9 Extension Limits”](#fs9-extension-limits) | Resource | Limit | | ----------------------- | ------------ | | Max file size | 100 MB | | Max glob total size | 100 MB | | Max file traversal | 10,000 files | | Max directory recursion | 10 levels | ### pg\_cron Limits [Section titled “pg\_cron Limits”](#pg_cron-limits) | Resource | Limit | | --------------------- | ----------------------------------- | | Job execution timeout | Default: 5 minutes; max: 30 minutes | ### Unsupported PostgreSQL Features [Section titled “Unsupported PostgreSQL Features”](#unsupported-postgresql-features) The following PostgreSQL features are not supported: * `SMALLINT` / `INT2` type — aliased to `INTEGER` (32-bit) * `REAL` / `FLOAT4` type — aliased to `DOUBLE PRECISION` (64-bit) * `CHAR(n)` fixed-length type — aliased to `VARCHAR(n)` (no padding semantics) * Microsecond timestamp precision (millisecond only) One clarification on a feature often assumed missing: `LISTEN` / `NOTIFY` **is** supported, but only over a direct pgwire connection — a session has to stay open to receive notifications, and notifications are delivered on commit. The stateless HTTP SQL API can send `NOTIFY` but cannot receive notifications; a `LISTEN` issued over it returns a `LISTEN` command tag and then never delivers anything. # Row-Level Security > Row-Level Security (RLS) in DB9 — enable per-row access control with policies, roles, and the browser SDK. Row-Level Security (RLS) restricts which rows a given role can see or modify. When enabled on a table, every query is filtered through one or more **policies** that define access rules — no application-layer filtering required. RLS is the foundation of the [Browser SDK](/docs/sdk-browser/) data access model: the public data API issues queries under a role resolved from the user’s JWT, and RLS policies enforce per-user visibility. ## Enable RLS [Section titled “Enable RLS”](#enable-rls) SQL ```sql -- Enable RLS (default-deny: no policies = no rows visible to non-owners) ALTER TABLE todos ENABLE ROW LEVEL SECURITY; -- Disable RLS (all rows visible to all roles) ALTER TABLE todos DISABLE ROW LEVEL SECURITY; ``` ▶ Run Once enabled, non-superuser and non-owner roles see **zero rows** until you create at least one policy. ### Force RLS on Table Owners [Section titled “Force RLS on Table Owners”](#force-rls-on-table-owners) By default, the table owner bypasses RLS. Use `FORCE` to apply policies to the owner as well: SQL ```sql ALTER TABLE todos FORCE ROW LEVEL SECURITY; -- Remove force ALTER TABLE todos NO FORCE ROW LEVEL SECURITY; ``` ▶ Run Superusers always bypass RLS Superusers always bypass RLS, even with `FORCE ROW LEVEL SECURITY`. The `admin` role is a superuser. Never connect with the `admin` role from browser or untrusted client contexts — use a dedicated non-superuser role with appropriate policies instead. ## Create Policies [Section titled “Create Policies”](#create-policies) A policy defines a boolean expression that filters rows for a specific operation. If the expression evaluates to `true`, the row is accessible. SQL ```sql CREATE POLICY policy_name ON table_name [AS { PERMISSIVE | RESTRICTIVE }] [FOR { SELECT | INSERT | UPDATE | DELETE }] [TO role_name [, ...]] [USING (expression)] [WITH CHECK (expression)]; ``` ### USING vs WITH CHECK [Section titled “USING vs WITH CHECK”](#using-vs-with-check) | Clause | Purpose | Applies to | | ------------ | ------------------------------------------------- | ---------------------- | | `USING` | Filter which existing rows are visible/modifiable | SELECT, UPDATE, DELETE | | `WITH CHECK` | Validate new or modified row values | INSERT, UPDATE | ### Per-Operation Rules [Section titled “Per-Operation Rules”](#per-operation-rules) | Operation | USING | WITH CHECK | Behavior | | --------- | ----------- | ----------- | ------------------------------------------------------------------------------------------------------- | | SELECT | Required | Not allowed | Filters visible rows. | | INSERT | Not allowed | Required | Validates inserted values. | | UPDATE | Required | Optional | USING selects updatable rows; WITH CHECK validates new values. Defaults WITH CHECK to USING if omitted. | | DELETE | Required | Not allowed | Filters deletable rows. | ### Examples [Section titled “Examples”](#examples) **Row ownership — users see only their own data:** SQL ```sql CREATE TABLE todos ( id SERIAL PRIMARY KEY, user_id TEXT NOT NULL, task TEXT, done BOOLEAN DEFAULT false ); ALTER TABLE todos ENABLE ROW LEVEL SECURITY; -- Users can only see their own todos CREATE POLICY user_select ON todos FOR SELECT USING (user_id = current_user); -- Users can only insert todos for themselves CREATE POLICY user_insert ON todos FOR INSERT WITH CHECK (user_id = current_user); -- Users can only update their own todos CREATE POLICY user_update ON todos FOR UPDATE USING (user_id = current_user) WITH CHECK (user_id = current_user); -- Users can only delete their own todos CREATE POLICY user_delete ON todos FOR DELETE USING (user_id = current_user); ``` **Public + private visibility:** SQL ```sql CREATE TABLE posts ( id SERIAL PRIMARY KEY, author TEXT, published BOOLEAN DEFAULT false, content TEXT ); ALTER TABLE posts ENABLE ROW LEVEL SECURITY; -- Anyone can see published posts CREATE POLICY see_published ON posts FOR SELECT USING (published = true); -- Authors can also see their own unpublished drafts CREATE POLICY see_own ON posts FOR SELECT USING (author = current_user); ``` ## Policy Combination Rules [Section titled “Policy Combination Rules”](#policy-combination-rules) ### Permissive Policies (Default) [Section titled “Permissive Policies (Default)”](#permissive-policies-default) Multiple `PERMISSIVE` policies for the same operation are combined with **OR** — if any policy allows the row, it is accessible. SQL ```sql -- Row is visible if published=true OR author=current_user CREATE POLICY see_published ON posts FOR SELECT USING (published = true); CREATE POLICY see_own ON posts FOR SELECT USING (author = current_user); ``` ### Restrictive Policies [Section titled “Restrictive Policies”](#restrictive-policies) `RESTRICTIVE` policies are combined with **AND** on top of the permissive result. Every restrictive policy must pass. SQL ```sql -- Must be published AND visible by permissive policies CREATE POLICY must_be_published ON posts AS RESTRICTIVE FOR SELECT USING (published = true); ``` **Final evaluation:** ```plaintext (permissive_1 OR permissive_2 OR ...) AND restrictive_1 AND restrictive_2 AND ... ``` At least one `PERMISSIVE` policy must match for any row to be accessible, regardless of restrictive policies. ## Alter and Drop Policies [Section titled “Alter and Drop Policies”](#alter-and-drop-policies) SQL ```sql -- Change the USING expression ALTER POLICY see_own ON posts USING (author = current_user OR role = 'admin'); -- Drop a policy DROP POLICY see_own ON posts; -- Drop only if it exists (no error) DROP POLICY IF EXISTS see_own ON posts; ``` ## Bypass Mechanisms [Section titled “Bypass Mechanisms”](#bypass-mechanisms) | Mechanism | Bypasses RLS? | Bypasses FORCE RLS? | | -------------------------- | ------------- | ----------------------- | | Superuser | Always | Always | | Table owner | Yes | No (must obey policies) | | `BYPASSRLS` role attribute | Yes | Yes | ### BYPASSRLS Role Attribute [Section titled “BYPASSRLS Role Attribute”](#bypassrls-role-attribute) Grant a role the ability to bypass all RLS policies: SQL ```sql CREATE ROLE admin_role LOGIN PASSWORD 'pw' BYPASSRLS; -- Grant or revoke later ALTER ROLE admin_role BYPASSRLS; ALTER ROLE admin_role NOBYPASSRLS; ``` ### SECURITY DEFINER Functions [Section titled “SECURITY DEFINER Functions”](#security-definer-functions) Functions declared `SECURITY DEFINER` execute with the identity of the function owner (typically the table owner), bypassing RLS: SQL ```sql CREATE FUNCTION all_posts() RETURNS SETOF posts LANGUAGE SQL SECURITY DEFINER AS $$ SELECT * FROM posts $$; ``` Functions declared `SECURITY INVOKER` (the default) execute with the caller’s identity and obey RLS normally. SECURITY DEFINER bypasses RLS `SECURITY DEFINER` functions run with the function owner’s identity (usually the table owner), which bypasses all RLS policies. Use them deliberately — never expose a `SECURITY DEFINER` function that allows arbitrary row access to untrusted roles. ## Security Barrier [Section titled “Security Barrier”](#security-barrier) RLS predicates are applied as **security barrier** qualifiers. This prevents user-supplied `WHERE` clauses from leaking data through side-channel functions: SQL ```sql -- Even if a malicious function is used in WHERE, it cannot see rows hidden by RLS SELECT * FROM secrets WHERE side_channel_fn(secret_column); -- RLS USING clause filters rows BEFORE the WHERE clause evaluates ``` ## System Catalog [Section titled “System Catalog”](#system-catalog) Inspect RLS configuration through standard catalog views: SQL ```sql -- Check if RLS is enabled on a table SELECT relname, relrowsecurity, relforcerowsecurity FROM pg_class WHERE relname = 'todos'; -- List all policies on a table SELECT policyname, cmd, permissive, roles, qual, with_check FROM pg_policies WHERE tablename = 'todos'; -- Check role attributes SELECT rolname, rolbypassrls FROM pg_roles WHERE rolname = 'app_user'; ``` ▶ Run ## RLS with the Browser SDK [Section titled “RLS with the Browser SDK”](#rls-with-the-browser-sdk) The [Browser SDK](/docs/sdk-browser/) uses RLS to enforce per-user data access. The flow: 1. Configure [BYO JWT authentication](/docs/platform/security-and-auth/#bring-your-own-jwt) on your database. 2. Enable RLS and create policies that reference `current_user` or a custom claim. 3. The browser SDK attaches the user’s JWT to each request. 4. The public data API validates the JWT and issues a connect token with the resolved role. 5. Queries execute under that role — RLS policies filter results automatically. SQL ```sql -- Example: todos visible only to the JWT subject CREATE POLICY user_todos ON todos FOR SELECT USING (user_id = current_user); ``` TypeScript ```typescript // Client-side — SDK handles auth and RLS transparently db9.auth.setSession({ accessToken: userJwt }); const { data } = await db9.from('todos').select('*'); // Only rows where user_id matches the JWT subject are returned ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * [Browser SDK](/docs/sdk-browser/) — Client-side data access with RLS * [Auth & Roles](/docs/sql/auth/) — Role management and grants * [Security & Auth](/docs/platform/security-and-auth/) — Publishable keys and BYO JWT * [Production Checklist](/docs/production-checklist/) — RLS setup guidance # Session Parameters > Session-level configuration with SET/SHOW in DB9. ## Session Parameters [Section titled “Session Parameters”](#session-parameters) Session-level configuration with SET/SHOW. SQL ```sql SET variable = value; -- session-scoped SET LOCAL variable = value; -- transaction-scoped SHOW variable; SHOW ALL; -- list all parameters RESET variable; RESET ALL; -- Functions SELECT current_setting('variable_name'); SELECT set_config('variable_name', 'value', false); ``` `SHOW ALL` is the authoritative list — it reports every parameter this server recognises, with its current value and, where one exists, a description. `SHOW` never errors on a namespaced parameter — even a misspelled one For an unqualified name, `SHOW` rejects what it does not know: SQL ```sql SHOW bogus_control_xyz; -- ERROR: unrecognized configuration parameter "bogus_control_xyz" ``` But any name containing a dot — the `db9.*`, `embedding.*` and `hnsw.*` families below — is treated as a custom namespace, so `SHOW` returns an **empty string** instead of failing: SQL ```sql SHOW db9.max_sort_bytes; -- 134217728 SHOW db9.max_sort_byte; -- (empty) — typo, no error SHOW anything.at.all; -- (empty) — no error ``` A typo therefore reads as “set to nothing” rather than “does not exist”. Use `current_setting('db9.max_sort_byte')`, which raises `unrecognized configuration parameter`, when you need to confirm a namespaced parameter actually exists. ### PostgreSQL-Compatible Parameters [Section titled “PostgreSQL-Compatible Parameters”](#postgresql-compatible-parameters) | Parameter | Default | Mutable | Description | | ------------------------------- | ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `application_name` | `''` | Yes | Application name for the session | | `bytea_output` | `hex` | Yes | Output format for bytea values | | `client_encoding` | `UTF8` | Yes | Client-side character encoding | | `client_min_messages` | `notice` | Yes | Minimum message severity sent to client | | `datestyle` | `ISO, MDY` | No | Date display format | | `default_transaction_isolation` | `read committed` | Yes | Default isolation level for subsequent transactions | | `default_transaction_read_only` | `off` | Yes | Default read-only state for new transactions; writes are rejected with SQLSTATE `25006` — see [Transactions](/docs/sql/transactions/#read-only-transactions) | | `extra_float_digits` | `1` | Yes | Extra precision for floating-point output | | `search_path` | `$user, public` | Yes | Schema search order | | `server_encoding` | `UTF8` | No | Server-side character encoding | | `server_version` | `16.0` | No | Reported PostgreSQL version | | `standard_conforming_strings` | `on` | No | Treat backslashes literally in strings. `SET standard_conforming_strings = off` is rejected — the parser always treats backslashes literally | | `statement_timeout` | `60000ms` | Yes | Query timeout (0 = no limit) | | `timezone` | `UTC` | Yes | Session timezone | | `transaction_isolation` | `read committed` | Yes | Current transaction isolation level | | `lock_timeout` | `0` | Yes | Lock acquisition timeout | | `password_encryption` | `scram-sha-256` | Yes | Password hashing algorithm | | `max_identifier_length` | `63` | No | Maximum identifier length | | `lc_messages` | `C` | Yes | Locale for messages | Other accepted PostgreSQL parameters: `check_function_bodies`, `default_table_access_method`, `default_tablespace`, `default_text_search_config`, `default_transaction_deferrable`, `idle_in_transaction_session_timeout`, `in_hot_standby`, `integer_datetimes`, `intervalstyle`, `lc_monetary`, `lc_numeric`, `lc_time`, `max_index_keys`, `row_security`, `xmloption`. Also readable with `SHOW`, reporting server state rather than a session preference: `is_superuser`, `listen_addresses`, `max_connections` (`1000`), `port` (`5432`), `role`, `server_version_num` (`160000`), `session_authorization`, `transaction_deferrable`, `transaction_read_only`, and `work_mem` (`4MB`). `session_replication_role` is rejected, not honoured `SET session_replication_role = 'replica'` — the usual way to suppress triggers during a bulk load — fails with: ```plaintext ERROR: session_replication_role is not supported; triggers always fire as in "origin" mode ``` Only `origin` is accepted (as a no-op). `ALTER TABLE ... DISABLE TRIGGER` is not available either (`syntax error ... found: DISABLE`), and `pg_trigger.tgenabled` is always `O`. Dropping the trigger for the duration of the load and recreating it afterwards is the only route. ### DB9-Specific Parameters [Section titled “DB9-Specific Parameters”](#db9-specific-parameters) | Parameter | Default | Description | | ---------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `db9.hash_join_work_mem` | `67108864` (64 MB) | Memory limit for the hash join build side (0 = unlimited) | | `db9.dml_table_scan_max_rows` | `10000` | Max rows per auxiliary source for UPDATE FROM / DELETE USING (0 = unlimited) | | `db9.max_sort_bytes` | `134217728` (128 MB) | Memory limit for sort operations | | `db9.prepared_plan_cache_size` | `128` | Prepared statement plan cache size | | `db9.prepared_plan_cache_min_exec` | `5` | Min executions before plan cache promotion | | `db9.retry_max_attempts` | `64` | Max retry attempts for autocommit DML/DDL on write conflict | | `db9.retry_timeout` | `0` | Max wall-time for retries per statement (0 = no limit) | | `db9.use_optimizer` | `on` | Cost-based optimizer (always on; `SET ... = off` is accepted but is a no-op) | | `db9.password_grace_seconds` | `0` | Grace period in seconds during which the old password still works after `ALTER ROLE ... WITH PASSWORD` (0 = immediate replacement) | | `db9.enable_cop_pushdown` | `off` | Enable coprocessor pushdown planning | ### Embedding Parameters [Section titled “Embedding Parameters”](#embedding-parameters) | Parameter | Default | Description | | ----------------------- | ------------------------------------- | ------------------------------------------------------------ | | `embedding.provider` | platform-provided | Embedding provider: `openai` or `bedrock` (session override) | | `embedding.endpoint` | platform-provided | Embedding service endpoint URL (session override) | | `embedding.model` | platform-provided | Model name | | `embedding.api_key` | platform-provided, redacted as `****` | Embedding service API key (session override) | | `embedding.dimensions` | platform-provided | Output vector dimensions | | `embedding.concurrency` | `5` | Max concurrent embedding calls | | `embedding.max_calls` | `100` | Max embedding calls per statement | Every `embedding.*` parameter already has a working value on a new database — the built-in embedding service needs no configuration. Setting them overrides the platform default for the session. Read the live values with `SHOW embedding.provider` / `SHOW embedding.model` rather than assuming they are unset; `embedding.api_key` always reads back as `****`. ### Vector Index Parameters [Section titled “Vector Index Parameters”](#vector-index-parameters) | Parameter | Default | Description | | ---------------- | ------- | ------------------------------------------------- | | `hnsw.ef_search` | `40` | Dynamic candidate list size for HNSW index search | # Transactions & COPY > Transaction control, savepoints, isolation semantics, autocommit behavior, and bulk data import/export with COPY. ## SQL Reference: Transactions [Section titled “SQL Reference: Transactions”](#sql-reference-transactions) Transaction control, savepoints, isolation semantics, and autocommit behavior. ### Transaction Control [Section titled “Transaction Control”](#transaction-control) SQL ```sql BEGIN; -- ... your SQL statements ... COMMIT; -- persist all changes atomically ROLLBACK; -- discard all changes ``` ▶ Run ### Autocommit [Section titled “Autocommit”](#autocommit) Single statements outside an explicit `BEGIN` block run in implicit autocommit mode. The system begins a transaction, executes the statement, and commits (or rolls back on error). Retryable TiKV errors trigger automatic retry with backoff. ### Savepoints [Section titled “Savepoints”](#savepoints) SQL ```sql BEGIN; INSERT INTO users (name) VALUES ('Alice'); SAVEPOINT sp1; INSERT INTO users (name) VALUES ('Bob'); ROLLBACK TO SAVEPOINT sp1; -- Bob's insert is undone RELEASE SAVEPOINT sp1; -- destroys savepoint, merges state COMMIT; -- only Alice is committed ``` ▶ Run Savepoint behavior matches PostgreSQL: duplicate names are allowed (most recent is targeted), `RELEASE` destroys the named savepoint and all later ones, `ROLLBACK TO` re-establishes the savepoint for reuse. ### Isolation Level [Section titled “Isolation Level”](#isolation-level) Transactions are **pessimistic**, and the default isolation level is **READ COMMITTED**. | Level | Behavior | | -------------------------- | -------------------------------------------------------------------------------------------------- | | `READ COMMITTED` (default) | Statement-level snapshots, as in PostgreSQL — each statement sees rows committed before it started | | `READ UNCOMMITTED` | Behaves as `READ COMMITTED`, as in PostgreSQL | | `REPEATABLE READ` | One snapshot held for the transaction’s lifetime | | `SERIALIZABLE` | Not implemented — downgraded to `REPEATABLE READ` | Under `READ COMMITTED`, a statement later in the transaction sees another session’s committed writes: SQL ```sql -- Session A -- Session B BEGIN; -- default: READ COMMITTED SELECT count(*) FROM t; -- 1 INSERT INTO t VALUES (2); -- commits SELECT count(*) FROM t; -- 2 COMMIT; ``` Under `REPEATABLE READ` the same sequence returns `1` both times. DB9 Difference: SERIALIZABLE is downgraded, not rejected DB9 does not implement SERIALIZABLE. TiKV provides snapshot isolation, not PostgreSQL’s serializable snapshot isolation (SSI), so **write skew is not detected**. On the PostgreSQL wire protocol the request is accepted with a warning rather than refused: SQL ```sql BEGIN ISOLATION LEVEL SERIALIZABLE; -- WARNING: TiKV provides snapshot isolation; SERIALIZABLE has been downgraded to REPEATABLE READ SHOW transaction_isolation; -- repeatable read — the level actually in effect ``` `SHOW transaction_isolation` reports the level DB9 applied rather than the one requested, so reading it back confirms the downgrade at runtime. Where you relied on serializability, take explicit row locks with `SELECT ... FOR UPDATE` or add a unique constraint that makes the conflicting write fail. Over the [HTTP SQL API](/docs/api/) the same request is rejected instead: `ERROR: SERIALIZABLE isolation level is not supported. Use REPEATABLE READ or READ COMMITTED instead.` ### Read-Only Transactions [Section titled “Read-Only Transactions”](#read-only-transactions) `BEGIN READ ONLY`, `SET TRANSACTION READ ONLY`, and `SET default_transaction_read_only = on` all enable read-only transaction enforcement. DML and DDL writes are rejected with SQLSTATE `25006`: SQL ```sql BEGIN READ ONLY; INSERT INTO t VALUES (1); -- ERROR: cannot execute INSERT in a read-only transaction -- SQLSTATE: 25006 ROLLBACK; ``` For credential-level write restrictions that apply across sessions, issue a token with a read-only scope (`db9 token create --scope mydb:ro`) — see [CLI reference](/docs/cli/). ### Sequence Behavior [Section titled “Sequence Behavior”](#sequence-behavior) Sequence gaps are expected Sequence advances (`nextval`) are non-transactional — they survive transaction rollbacks, matching PostgreSQL behavior. If a transaction rolls back after calling `nextval`, the sequence value is permanently consumed. Do not rely on sequences for gap-free numbering. ### Failed Transaction State [Section titled “Failed Transaction State”](#failed-transaction-state) When an error occurs in a transaction, it enters a failed state. Only `ROLLBACK`, `COMMIT`, and `END` are accepted. All other statements return an error. This matches PostgreSQL behavior. *** ## SQL Reference: COPY [Section titled “SQL Reference: COPY”](#sql-reference-copy) Bulk data import and export. SQL ```sql -- Import from file (via client) COPY table_name FROM STDIN; -- Import from Parquet (with parquet extension) COPY table_name FROM '/data/file.parquet' FORMAT parquet; ``` Text, CSV, and Parquet formats are supported for `COPY FROM`. # Why DB9 for AI Agents > DB9 gives AI agents a full database toolkit — instant provisioning, built-in embeddings, a queryable file system, HTTP from SQL, branching, and scheduled jobs — all accessible through standard PostgreSQL. AI agents need more than a place to store rows. They need to provision databases on demand, search semantically, ingest files, call APIs, branch safely, and schedule follow-up work — ideally without leaving SQL. DB9 is built for this. Every capability an agent needs ships inside the database server, accessible through the PostgreSQL wire protocol. There is no sidecar, no orchestrator, and no glue service to maintain. ## Who should read this [Section titled “Who should read this”](#who-should-read-this) * **Agent developers** evaluating where to store agent state, context, and artifacts. * **Platform engineers** building multi-agent or multi-tenant systems that need disposable, programmable databases. * **Teams comparing DB9 to Neon, Supabase, or managed Postgres** for AI-heavy workloads. If you already know DB9 is a fit, skip ahead to the [Quick Start](/docs/quickstart/) or the [Agent Workflows](/docs/agent-workflows/overview/) guide. ## The agent-database problem [Section titled “The agent-database problem”](#the-agent-database-problem) Most databases were designed for long-lived applications with human operators. AI agents break that model: | Agents need | Traditional databases offer | | ----------------------------------- | -------------------------------------- | | Create a database in milliseconds | Minutes of provisioning and config | | Embed and search text in one query | Separate embedding service + vector DB | | Read CSV, JSON, or Parquet from SQL | ETL pipeline or external loader | | Call an API from a query | Application-layer HTTP code | | Fork the database to try something | Full backup and restore | | Schedule a cleanup job | External cron or task queue | DB9 closes every gap in that table with compiled-in extensions, not external services. ## What DB9 gives agents [Section titled “What DB9 gives agents”](#what-db9-gives-agents) ### Instant provisioning [Section titled “Instant provisioning”](#instant-provisioning) An agent can create a database in under a second with a single CLI command or SDK call. No signup required No signup is required — anonymous databases work immediately for prototyping. Run `db9 claim` when you’re ready to remove the 5-database limit. Terminal ```bash db9 create --name agent-workspace ``` ▶ Run TypeScript ```typescript import { instantDatabase } from 'get-db9'; const db = await instantDatabase({ name: 'agent-workspace', seed: 'CREATE TABLE context (id SERIAL, key TEXT, value JSONB)', }); // db.connectionString is ready to use ``` ▶ Run The SDK’s `instantDatabase()` checks for an existing database by name and reuses it, or creates one if it doesn’t exist. This makes agent restarts idempotent. ### Built-in embeddings and vector search [Section titled “Built-in embeddings and vector search”](#built-in-embeddings-and-vector-search) DB9 includes a built-in `embedding()` function that calls an embedding provider (OpenAI or AWS Bedrock) and returns a vector — no separate embedding microservice needed. Enable it once per database with `CREATE EXTENSION embedding`, then combine it with pgvector-compatible distance operators to build semantic search in pure SQL: SQL ```sql -- Store a document with its embedding INSERT INTO docs (content, vec) VALUES ('deployment guide', embedding('deployment guide')::vector); -- Semantic search SELECT content FROM docs ORDER BY vec <-> embedding('how do I deploy?')::vector LIMIT 5; ``` ▶ Run Embeddings are generated server-side, cached, and subject to per-tenant concurrency limits (5 concurrent requests by default). Agents don’t need to manage an embedding API client — the database handles it. ### fs9 — query files from SQL [Section titled “fs9 — query files from SQL”](#fs9--query-files-from-sql) Agents produce and consume files: logs, CSVs, JSON exports, Parquet snapshots. DB9’s fs9 extension exposes a queryable file system inside the database: SQL ```sql -- Read a CSV as a table SELECT * FROM extensions.fs9('/data/users.csv'); -- Write a file SELECT fs9_write('/data/output.json', '{"status": "complete"}'); -- Check if a file exists SELECT fs9_exists('/data/users.csv'); ``` ▶ Run fs9 supports CSV, JSON Lines, and Parquet with automatic schema inference. Files can also be managed through the CLI (`db9 fs cp`, `db9 fs sh`) or a FUSE mount. Individual files are limited to 100 MB, with a 128 MB per-operation read budget. ### HTTP from SQL [Section titled “HTTP from SQL”](#http-from-sql) Agents often need to call external services — webhooks, LLM APIs, enrichment endpoints. DB9 lets them do it from SQL: SQL ```sql SELECT status, content FROM http_get( 'https://api.example.com/enrich', '[{"field":"Authorization","value":"Bearer sk-..."}]'::jsonb ); SELECT content FROM http_post( 'https://hooks.slack.com/services/...', '{"text": "Task complete"}', 'application/json' ); ``` Safety boundaries are enforced by default: * HTTPS only (no plaintext HTTP) * Private/loopback IPs are blocked (SSRF protection) * 100 requests per statement, 20 concurrent per tenant * 1 MB max response, 256 KB max request body * 5-second request timeout ### Database branching [Section titled “Database branching”](#database-branching) Agents can fork a database to try a risky operation, then discard the branch if it fails: Terminal ```bash db9 branch create myapp --name experiment # Agent works on the branch... db9 branch delete experiment ``` ▶ Run Branches are full copies of the parent’s schema and data, created asynchronously — poll for `ACTIVE` before connecting. Use cases include preview environments, schema experiments, and rollback points. ### Serverless functions [Section titled “Serverless functions”](#serverless-functions) Agents can deploy JavaScript/TypeScript functions that run with native SQL and filesystem access — useful for data processing pipelines that are too complex to express in a single SQL query. See [Serverless Functions](/docs/functions/). ### Scheduled jobs with pg\_cron [Section titled “Scheduled jobs with pg\_cron”](#scheduled-jobs-with-pg_cron) Agents can schedule recurring work — cache refreshes, log cleanup, periodic API calls — directly in SQL: SQL ```sql SELECT cron.schedule( 'cleanup-old-context', '0 */6 * * *', $$DELETE FROM context WHERE created_at < now() - interval '7 days'$$ ); -- Check job history SELECT * FROM cron.job_run_details ORDER BY runid DESC LIMIT 5; ``` Jobs run inside the database with no external scheduler. The CLI also provides `db9 db cron list`, `db9 db cron create`, `db9 db cron history`, and `db9 db cron status` commands for managing jobs outside SQL. ### One-command agent onboarding [Section titled “One-command agent onboarding”](#one-command-agent-onboarding) DB9 installs itself as a skill for AI coding agents with a single command: Terminal ```bash db9 onboard --agent claude # Claude Code db9 onboard --agent codex # OpenAI Codex db9 onboard --agent opencode # OpenCode db9 onboard --agent agents # Generic .agents directory ``` Skills can be installed at user scope (`~/.claude/skills/db9/`) or project scope (`./.claude/skills/db9/`), and `--dry-run` previews changes before writing anything. Once installed, the agent can use DB9 commands as part of its normal workflow. ## Everything through standard PostgreSQL [Section titled “Everything through standard PostgreSQL”](#everything-through-standard-postgresql) Every capability listed above is accessible through the PostgreSQL wire protocol. This means: * **Any Postgres client works.** `psql`, pgAdmin, DBeaver, language drivers — they all connect to DB9 without adapters. * **ORMs work.** Prisma, Drizzle, TypeORM, Sequelize, Knex, SQLAlchemy, and GORM connect to DB9 as a standard Postgres backend. * **Agents reason in SQL.** LLMs already know SQL. There’s no proprietary query language or SDK to learn. ## When to choose DB9 for agents [Section titled “When to choose DB9 for agents”](#when-to-choose-db9-for-agents) * Your agents need to create, use, and discard databases as part of their workflow. * You want embeddings, file access, HTTP calls, and scheduling in the database layer, not as separate services. * You’re building multi-agent systems where each agent (or task, or user) gets its own isolated database. * You want agents to operate in pure SQL rather than through proprietary APIs. * You need branching for safe experimentation or preview environments. ## When DB9 may not be the best fit [Section titled “When DB9 may not be the best fit”](#when-db9-may-not-be-the-best-fit) * Your agents only need a key-value store or document database — a simpler tool may be enough. * You need extensions that DB9 doesn’t support yet — check the [Extensions](/docs/extensions/) and [SQL limits](/docs/sql/limits/) pages first. * You need a full application platform with auth UI, file storage CDN, and edge functions (consider Supabase). * Your workload requires on-premises or self-hosted deployment. ## Next steps [Section titled “Next steps”](#next-steps) * [Agent Workflows](/docs/agent-workflows/overview/) — the practical guide to building agent systems on DB9 * [Quick Start](/docs/quickstart/) — create your first database and run a query in under a minute * [Overview](/docs/overview/) — understand DB9’s architecture and positioning * [CLI Reference](/docs/cli/) — full command reference including `db9 create`, `db9 onboard`, and `db9 cron` * [TypeScript SDK](/docs/sdk/) — `instantDatabase()` and programmatic database management * [Extensions](/docs/extensions/) — deep dives into fs9, HTTP, embeddings, vector search, and pg\_cron # Frequently Asked Questions > Answers to common questions about DB9 — provisioning, AI agents, extensions, pgvector, branching, framework compatibility, and migration. ## What is DB9? DB9 is a serverless PostgreSQL-compatible database built on TiKV. It speaks the PostgreSQL wire protocol, so any tool that connects to Postgres — psql, Prisma, Drizzle, SQLAlchemy, or a raw driver — connects to DB9 with no adapter needed. What makes DB9 different is what ships inside the server: vector search with built-in embeddings, a queryable file system (fs9), HTTP calls from SQL, scheduled jobs via pg\_cron, and zero-copy branching. These capabilities are compiled into the server, not bolted on through external services. DB9 is designed for AI agents and developers who need instant provisioning. An agent can create a database in under a second using the CLI or TypeScript SDK, store structured data alongside embeddings and files, call external APIs from SQL, and tear the database down when the task is done — all through standard PostgreSQL. Anonymous trial databases work immediately with no account, credit card, or email required. Run db9 claim to remove the 5-database limit when you are ready. ## How is DB9 different from Neon? DB9 and Neon are both serverless PostgreSQL-compatible databases, but they solve different problems. Neon optimizes for infrastructure flexibility — autoscaling between 0.25 and 56 CU, scale-to-zero on idle, and instant copy-on-write branching. DB9 optimizes for instant provisioning, AI agent integration, and built-in application-layer extensions. DB9 creates databases synchronously in under a second; Neon takes seconds for project and endpoint startup. DB9 branches are full data copies (asynchronous, seconds to minutes); Neon branches are copy-on-write and instant regardless of database size. DB9 includes extensions that Neon does not: native embedding() generation, fs9 file system access, HTTP calls from SQL, CHUNK\_TEXT() for RAG, and Parquet import. Neon supports capabilities DB9 does not: PostGIS, pg\_trgm, pgcrypto, ltree, logical replication, read replicas, and full SERIALIZABLE isolation (DB9 downgrades SERIALIZABLE to REPEATABLE READ on pgwire and rejects it over the HTTP SQL API). DB9 does support LISTEN/NOTIFY over a pgwire session. Choose DB9 for agent workflows, multi-tenant provisioning, and SQL-layer application logic. Choose Neon for variable workloads, cost-sensitive staging environments, broad extension support, or efficient branching on large databases. ## How is DB9 different from Supabase? DB9 is a database. Supabase is a full application platform — PostgreSQL database plus authentication, file storage, realtime subscriptions, edge functions, and an auto-generated REST API. They are not direct substitutes. DB9 competes with Supabase's database layer only; everything else in Supabase has no equivalent in DB9. DB9 provisions databases synchronously in under a second, supports anonymous access without signup, and includes application-layer SQL extensions: native embedding() generation, fs9 file system access, HTTP calls from SQL, CHUNK\_TEXT() for RAG pipelines, and Parquet import. Supabase runs standard PostgreSQL with the full extension ecosystem (PostGIS, pg\_trgm, pgcrypto), logical replication, SERIALIZABLE isolation, built-in connection pooling via Supavisor, and CDN-backed file storage. DB9 supports LISTEN/NOTIFY over a pgwire session, but has no change data capture — you emit events yourself with triggers. Choose DB9 when you want a fast, programmable database that agents can create and discard without a surrounding platform. Choose Supabase when you need a complete application backend with auth, realtime, storage, and edge functions provided out of the box. ## Can I use DB9 with Prisma/Drizzle/SQLAlchemy? Yes. DB9 speaks the PostgreSQL wire protocol, so any ORM or driver that supports standard PostgreSQL works without code changes or adapters. Prisma passes 89 of 89 compatibility tests against DB9. Drizzle passes 75 of 75. SQLAlchemy has been tested in both smoke and end-to-end suites. TypeORM: 147 passing, 0 failing, 3 skipped. Sequelize passes 87 of 87. Knex passes 97 of 97. GORM has been tested with smoke and end-to-end coverage. To connect, use the connection string returned by db9 db connect or the TypeScript SDK's instantDatabase() call. Set sslmode=require or the equivalent TLS option for your driver. For Prisma, use the postgresql provider. For Drizzle, use the pg or postgres driver with the standard connection string. For SQLAlchemy, use the postgresql+psycopg2 dialect. DB9 does not require any DB9-specific package — your existing ORM setup works as-is. See the Connect docs for per-ORM connection guides covering Prisma, Drizzle, SQLAlchemy, TypeORM, Sequelize, Knex, and GORM. ## Does DB9 support pgvector? Yes. DB9 includes a pgvector-compatible vector type built into the server. You can store vectors and run similarity searches using the standard pgvector operators (<->, <#>, <=>). HNSW index building is gated off in the current release, so similarity search runs as an exact sequential scan — correct results, but no approximate-index speedup yet. Beyond pgvector compatibility, DB9 also ships a native embedding() function that generates and caches text embeddings server-side without any external embedding API. Enable it once per database with CREATE EXTENSION embedding, then call embedding('your text')::vector in any SQL statement to generate a 1024-dimension vector using the default text-embedding-v4 model. This lets you store, index, and search embeddings entirely in SQL with no application-layer embedding client. DB9 also includes CHUNK\_TEXT() for splitting documents into overlapping chunks before embedding — useful for RAG pipelines. Embeddings are generated server-side, cached to avoid redundant calls, and subject to per-tenant concurrency limits of 5 concurrent requests by default. All vector operations use standard pgvector syntax, so existing pgvector code runs on DB9 without modification. ## How fast does DB9 provision a database? DB9 provisions databases synchronously in under a second. Unlike platforms that create a project, configure endpoints, and wait for compute to start, DB9 creates an isolated TiKV keyspace and returns a ready connection string in a single operation. From the CLI, db9 create --name myapp returns a live database immediately. From the TypeScript SDK, instantDatabase({ name: 'myapp' }) returns a connectionString in one await. No signup is required — anonymous accounts work on first use. The CLI auto-registers an anonymous account and creates the database in one step. Anonymous accounts support up to 5 databases; run db9 claim to remove that limit by linking an identity. This provisioning speed makes database-per-user, database-per-agent-task, and database-per-CI-run patterns practical without orchestration. Each database is a fully isolated keyspace in TiKV — tenants share infrastructure but never data. The same sub-second guarantee applies whether you are creating your first database or your thousandth through the API. ## What are DB9's database limits? DB9 anonymous accounts support up to 5 databases. Run db9 claim to link an identity and remove that limit. For the fs9 file system extension, individual files are limited to 100 MB, with a 128 MB per-operation read budget. For the HTTP extension, limits per SQL statement are 100 requests maximum, 20 concurrent requests per tenant, 1 MB maximum response size, 256 KB maximum request body, and a 5-second per-request timeout. HTTP calls are HTTPS-only and private or loopback IP addresses are blocked for SSRF protection. For the embedding() function, the per-tenant concurrency limit is 5 concurrent requests by default. For database branching, a maximum of 2 concurrent branch creations are allowed per database. DB9 does not currently support table partitioning, logical replication, or advisory locks that are cross-process or global. LISTEN/NOTIFY is supported: notifications are delivered on commit to sessions holding an open pgwire connection, so the stateless HTTP SQL API can send NOTIFY but cannot LISTEN. Transaction isolation is REPEATABLE READ (snapshot isolation via TiKV); SERIALIZABLE is not available, and the two transports differ — the HTTP SQL API rejects it with an error, while a pgwire session downgrades it to REPEATABLE READ with a warning rather than failing. For a full list of SQL constraints, see the Limits and Constraints reference page at /docs/sql/limits/. ## How do I give Claude Code its own database? Run db9 onboard --agent claude from your terminal. This installs a DB9 skill file that teaches Claude Code how to create databases, run SQL, manage files, and use branching — all through the DB9 CLI. The skill is placed at \~/.claude/skills/db9/ by default (user scope, available across all projects). To scope it to one project only, add --scope project. To install at both levels, use --scope both. Preview what will be installed without writing anything: db9 onboard --agent claude --dry-run. After onboarding, Claude Code can call db9 create, db9 db sql, db9 fs cp, db9 branch create, and other CLI commands as part of its normal workflow. Anonymous accounts work immediately — no API key setup required for Claude Code to start using DB9. The same onboard command supports other agents: codex for OpenAI Codex, opencode for OpenCode, and agents for a generic .agents directory. Each agent gets the same skill file so it can provision, query, and manage databases through natural-language instructions backed by DB9's CLI. ## Does DB9 support branching? Yes. DB9 supports database branching through the CLI and TypeScript SDK. To create a branch: db9 db branch create myapp --name preview. Branches are full data copies of the parent database, created asynchronously. State progresses from CLONING to ACTIVE as the copy completes. Each branch is a fully independent database with its own isolated TiKV keyspace — branches share history with the parent but diverge completely on write. A maximum of 2 concurrent branch creations are allowed per database. Branches are useful for preview environments, schema experiments, rollback points, and safe testing of destructive operations. Delete a branch with db9 branch delete when you no longer need it. Important difference from Neon: Neon branches use copy-on-write storage and are instant regardless of database size. DB9 copies all data, so branch creation takes seconds to minutes and each branch consumes its own storage. DB9 branching works well for agent workflows and CI with small-to-medium databases. ## What is fs9? fs9 is DB9's built-in queryable file system extension. It lets you read, write, and query files directly from SQL without ETL pipelines or external loaders. Enable it with CREATE EXTENSION fs9. Once enabled, SELECT \* FROM extensions.fs9('/data/users.csv') reads a CSV file as a SQL table with automatic schema inference. fs9 supports CSV, JSON Lines, and Parquet formats. Write files from SQL with fs9\_write(), check existence with fs9\_exists(), and import Parquet files with read\_parquet() or COPY FORMAT parquet. Files are accessible through three interfaces: SQL queries via the fs9 extension functions, CLI commands (db9 fs cp, db9 fs sh, db9 fs ls), and a FUSE mount via db9 fs mount that exposes the DB9 file system as a local directory. Individual files are limited to 100 MB with a 128 MB per-operation read budget. fs9 is especially useful for AI agents that produce and consume files — logs, CSVs, JSON exports, Parquet snapshots — keeping them co-located with the SQL data they relate to.