This guide walks you through spinning up the Northwind PostgreSQL database with Docker and using it to test every major oda feature end-to-end.
Northwind is a classic sample database originally created by Microsoft. It models a fictional food trading company with 14 tables covering customers, orders, products, employees, suppliers, shippers, and more — rich enough to write interesting queries across multiple joined tables.
Tables: categories, customers, employees, employee_territories, order_details, orders, products, region, shippers, suppliers, territories, us_states, customer_customer_demo, customer_demographics
Run the setup script from the project root. It downloads northwind.sql (if not already present) and starts a PostgreSQL container with the database pre-loaded:
bash scripts/setup-northwind.shThe script will print connection details and confirm the container is healthy before exiting. The database is exposed on port 5433 (to avoid clashing with the test container on port 5432).
To stop the container later:
docker compose -f docker-compose-northwind.yml down # stop, keep data
docker compose -f docker-compose-northwind.yml down -v # stop, wipe dataIf you haven't run oda init yet:
uv run oda inituv run oda connections addEnter the following when prompted:
| Field | Value |
|---|---|
| Name | northwind |
| Dialect | postgresql |
| Host | localhost |
| Port | 5433 |
| Database | northwind |
| User | postgres |
| Password | postgres |
Verify the connection is live:
uv run oda connections test northwinduv run oda connect northwindThis sets northwind as the active connection and writes .opencode/rules/data-agent.md for OpenCode.
List all schemas:
uv run oda schemasList all tables:
uv run oda tablesInspect a specific table:
uv run oda describe orders
uv run oda describe order_details
uv run oda describe customersPreview sample rows:
uv run oda sample orders
uv run oda sample products --n 10Profile a table (null counts, distinct values, min/max):
uv run oda profile orders
uv run oda profile productsuv run oda docs generateThis writes a markdown file per table under docs/data-catalog/. Add --enrich to include column statistics:
uv run oda docs generate --enrichCheck doc freshness at any time:
uv run oda docs statusuv run oda query "SELECT company_name, country FROM customers ORDER BY country LIMIT 10"
uv run oda query "SELECT product_name, unit_price, units_in_stock FROM products ORDER BY unit_price DESC LIMIT 10"
uv run oda query "SELECT first_name, last_name, title FROM employees ORDER BY last_name"uv run oda query "SELECT country, COUNT(*) AS customer_count FROM customers GROUP BY country ORDER BY customer_count DESC"
uv run oda query "SELECT category_id, COUNT(*) AS product_count, AVG(unit_price) AS avg_price FROM products GROUP BY category_id ORDER BY category_id"uv run oda query "
SELECT o.order_id, c.company_name, o.order_date, o.freight
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
ORDER BY o.order_date DESC
LIMIT 10
"uv run oda query "
SELECT p.product_name, c.category_name, p.unit_price
FROM products p
JOIN categories c ON p.category_id = c.category_id
ORDER BY c.category_name, p.product_name
LIMIT 20
"uv run oda query "
SELECT
c.company_name,
COUNT(DISTINCT o.order_id) AS order_count,
ROUND(SUM(od.unit_price * od.quantity * (1 - od.discount))::numeric, 2) AS total_revenue
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
JOIN order_details od ON o.order_id = od.order_id
GROUP BY c.company_name
ORDER BY total_revenue DESC
LIMIT 10
"uv run oda query "
SELECT e.first_name, e.last_name, t.territory_description, r.region_description
FROM employees e
JOIN employee_territories et ON e.employee_id = et.employee_id
JOIN territories t ON et.territory_id = t.territory_id
JOIN region r ON t.region_id = r.region_id
ORDER BY e.last_name
"# JSON output
uv run oda query "SELECT * FROM shippers" --format json
# CSV output
uv run oda query "SELECT product_name, unit_price FROM products ORDER BY unit_price DESC LIMIT 20" --format csvoda hard-blocks all write operations. These should all fail with a SafetyError:
# Write operations are blocked
uv run oda query "INSERT INTO customers (customer_id, company_name) VALUES ('TEST', 'Test Co')"
uv run oda query "UPDATE products SET unit_price = 0 WHERE product_id = 1"
uv run oda query "DELETE FROM orders WHERE order_id = 10248"
uv run oda query "DROP TABLE customers"After running a few queries, inspect the history:
uv run oda history list
uv run oda history list --n 5
uv run oda history search "customers"
uv run oda history statsRecord a data quirk about the Northwind schema for future sessions:
uv run oda memory add \
--title "Order revenue calculation" \
--category data_quality \
--content "Use unit_price * quantity * (1 - discount) from order_details for accurate revenue. The discount column is a float between 0 and 1."Verify it was saved:
uv run oda memory list
uv run oda memory search "revenue"With the connection active and docs generated, open OpenCode from the project root:
opencodeThen ask questions in plain English:
Which country has the most customers?
Show me the top 5 products by total revenue across all orders.
Which employees have the most orders assigned to them?
List all customers from Germany with their total order count.
OpenCode reads docs/data-catalog/ for schema context, constructs the SQL, and calls oda query to execute it. If a query returns zero rows, the diagnostic output is fed back to OpenCode automatically so it can self-correct and retry.
The screenshot below shows OpenCode answering "Show me the top 5 products by total revenue across all orders." — it reads the schema catalog, constructs the correct three-table join with discount calculation, executes it via uv run oda query, and presents the ranked results:
# Stop the container, keep the data volume
docker compose -f docker-compose-northwind.yml down
# Stop and wipe all data (forces fresh reload of northwind.sql on next start)
docker compose -f docker-compose-northwind.yml down -v
# Remove the downloaded SQL file
rm docker/northwind/northwind.sql