6.0 KiB
| title | status | type | priority | created_at | updated_at | blocked_by | ||
|---|---|---|---|---|---|---|---|---|
| Create infra/schema.sql — idempotent D1 schema for quotes and quote_tags | completed | task | high | 2026-03-10T23:32:10Z | 2026-03-10T23:32:18Z |
|
Production schema is applied once after tofu apply using:
wrangler d1 execute quotesdb --file infra/schema.sql --remote
For local dev, the native main() calls repo.run_migrations() (rusqlite execute_batch).
For tests, test setup calls NativeRepository::run_migrations() directly.
The D1 run_migrations() method in D1Repository (wasm32 path, ticket 00aff0) may still call
CREATE TABLE IF NOT EXISTS defensively on startup — but the canonical provisioning path for
production is the wrangler CLI command above, not a startup handler.
infra/schema.sql is the single source of truth for the SQL that wrangler applies. The Rust
constants in db/migrations.rs (ticket 00aff0) contain the same SQL in split form suitable for
the D1 prepare().run() API and rusqlite execute_batch.
1. Create infra/schema.sql
-- quotesdb D1 schema
-- =============================================================================
-- Apply to production D1:
-- wrangler d1 execute quotesdb --file infra/schema.sql --remote
--
-- Apply locally (wrangler dev):
-- wrangler d1 execute quotesdb --local --file infra/schema.sql
--
-- For native dev/test builds, NativeRepository::run_migrations() applies
-- equivalent SQL automatically via rusqlite on startup.
-- =============================================================================
-- Stores individual quotes. auth_code is the 4-word passphrase for edit/delete.
CREATE TABLE IF NOT EXISTS quotes (
id TEXT PRIMARY KEY, -- NanoID (~21 chars)
text TEXT NOT NULL,
author TEXT NOT NULL,
source TEXT, -- optional: book, speech, etc.
date TEXT, -- optional: ISO date YYYY-MM-DD
auth_code TEXT NOT NULL, -- 4-word passphrase, stored plaintext
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Join table linking quotes to zero or more tags. Cascades on quote deletion.
CREATE TABLE IF NOT EXISTS quote_tags (
quote_id TEXT NOT NULL REFERENCES quotes(id) ON DELETE CASCADE,
tag TEXT NOT NULL,
PRIMARY KEY (quote_id, tag)
);
2. Incremental migration convention (document in infra/README.md)
For future schema changes, create numbered migration files:
infra/migrations/
001_initial.sql -- (retroactive, same content as schema.sql)
002_add_index.sql -- future: e.g. CREATE INDEX IF NOT EXISTS ...
Apply individually:
wrangler d1 execute quotesdb --file infra/migrations/002_add_index.sql --remote
No automated migration tracking is needed at this project's scale.
3. Full deployment workflow to document in infra/README.md
# Step 1 — provision infrastructure (creates Worker, D1 database, Pages project)
cd infra/
tofu apply
# Step 2 — apply initial schema to D1 (run once after first apply)
cd ..
wrangler d1 execute quotesdb --file infra/schema.sql --remote
# Re-running step 2 is safe (CREATE TABLE IF NOT EXISTS).
4. Local dev workflow
# Start local API (applies schema automatically via NativeRepository::run_migrations())
cargo run
# or with a specific DB file:
DATABASE_URL=./dev.sqlite cargo run
No manual wrangler step needed for local dev.
null_resource local-exec (rejected): Provisioners are an OpenTofu anti-pattern. They don't
re-run unless the resource is tainted, aren't tracked in state, are OS-dependent (requires
wrangler installed on the CI runner at apply time), and hard to test. Breaking tofu apply
idempotency is not worth the single-command convenience.
API startup migration for D1 (rejected): Cloudflare Workers spin up per-request via V8
isolates. Calling DDL (CREATE TABLE IF NOT EXISTS) on every request is wasteful and fragile.
The native main() calls run_migrations() at startup because it runs as a real server, but
the Workers handler does NOT. The D1 provisioning path must be a separate step.
# Smoke-test the schema against a local SQLite file
sqlite3 /tmp/test_quotesdb.sqlite < infra/schema.sql
sqlite3 /tmp/test_quotesdb.sqlite ".tables"
# Should output: quote_tags quotes
# Clean up
rm /tmp/test_quotesdb.sqlite
- Resolves dependency: TRIAGE 5c0c64 (D1 migrations strategy — wrangler step chosen)
- Resolves dependency: TRIAGE 580e66 (DB migration strategy for Workers — same decision)
- Blocked by: d0da0b (D1 resource — need database name confirmed as "quotesdb")
- Informs: 75489a (migration workflow docs — documents the wrangler command from this ticket)
- Informs: 00aff0 (DB abstraction — migrations.rs constants mirror this file's SQL)
- Sub-project: quotesdb/infra
`feat(quotesdb): add infra/schema.sql with idempotent D1 schema for quotes and quote_tags`