Files
continuum-backend/scripts/dev-env.sh
T
octoturge 0e6a4aaa93 Record that this client cannot run behind pgCat 1.2.0, and stop lying about it
A real pooled database became available, so the deployment assumption in
src/db/index.ts - "points at pgCat/PgBouncer, so keep prepared statements
disabled" - was finally testable. It does not hold.

Through pgCat 1.2.0 an unparameterised `select 1` succeeds, and so does
`select count(*) from organisations`, but any query carrying a bind
parameter hangs forever. That is true with prepare:false, with
prepare:true, with fetch_types:false, and inside an explicit
transaction. The same queries straight at Postgres all pass, and psql's
\bind through the same pool returns its row, so the fault is specific to
how postgres-js frames those messages rather than to extended protocol.
Every Drizzle query binds parameters, so no subset of this app works
there. drizzle-kit hung at commit for the same reason - it builds its
own client and drizzle.config.ts cannot pass prepare:false - having
executed all 14 statements of 0000, which then rolled back.

Transaction-mode pooling itself was confirmed, not assumed: in one
client session consecutive statements hopped between server backends
while statements inside BEGIN..COMMIT stayed pinned to one.

So the comment now records what was measured, names PgBouncer as the
pooler this configuration actually suits, and says migrations go
straight at Postgres. dev-env.sh's note gets the same correction.

Two real bugs found on the way there:

- e2e-authz.sh and e2e-edge.sh hardcoded `psql -h localhost -U continuum
  -d continuum` while both read DATABASE_URL for the backend under test.
  Point DATABASE_URL elsewhere and the suite seeds fixtures into one
  database while exercising another - and its own failure message says
  "no database at $DATABASE_URL", naming a URL nothing ever tried. Both
  now use `psql "$DATABASE_URL"`.

- drizzle.config.ts fell back to port 6432, where nothing listens here,
  so a bare db:migrate failed confusingly. It now matches dev-env.sh's
  DEV_DATABASE_URL.

bun test 128 pass / 0 fail; e2e-authz.sh PASS with 68 checks against
local Postgres after the psql_q change. e2e-edge.sh was not re-run
because it drives the real printers, and its change is the same line.
2026-10-20 09:39:39 +00:00

239 lines
8.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Brings the local dependency stack (Valkey + Postgres/pgvector) up and down,
# and applies the Drizzle schema to it.
#
# Two backends behind one interface, chosen automatically:
#
# docker docker-compose.yml - Valkey 8 + pgvector/pgvector:pg17. What CI
# and a normal dev machine should use; closest to production.
# native apt packages started directly - postgresql-16 + pgvector + Valkey
# 7.2 from Ubuntu's own repositories.
#
# The native path exists because Docker provably cannot run in some sandboxes,
# including the one this was written in: dockerd needs CAP_NET_ADMIN to build
# its bridge, and even with `--bridge=none --iptables=false
# --storage-driver=vfs` it cannot unpack an image, because registering a layer
# needs user namespaces. Both are host-level grants, so there is nothing a
# script can do about it. Rather than leave the database untestable there, this
# falls back to running the same two services from apt. Set CONTINUUM_DEV_BACKEND
# to force one.
#
# Usage:
# scripts/dev-env.sh up start the services, wait until they answer
# scripts/dev-env.sh down stop them (keeps data)
# scripts/dev-env.sh reset stop them AND delete all data
# scripts/dev-env.sh status what is running, and which backend
# scripts/dev-env.sh psql open a shell on the database
# scripts/dev-env.sh schema apply the committed migrations (drizzle-kit migrate)
# scripts/dev-env.sh push reshape the db from src/db/schema, no migration
# scripts/dev-env.sh env print the DATABASE_URL/VALKEY_URL to use
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
PG_VERSION=16
PG_PORT=5432
VALKEY_PORT=6379
# Straight at Postgres, not pgCat: .env.example points at the pooler for
# production, and neither backend here runs one. Keep it that way even where a
# pooler is reachable - parameterised queries from postgres-js hang through
# pgCat 1.2.0, and drizzle-kit cannot migrate through it. See src/db/index.ts.
DEV_DATABASE_URL="postgres://continuum:continuum@localhost:${PG_PORT}/continuum"
DEV_VALKEY_URL="redis://localhost:${VALKEY_PORT}"
NATIVE_STATE="${XDG_RUNTIME_DIR:-/tmp}/continuum-dev-env"
# --- backend selection -----------------------------------------------------
# Docker is used when its daemon is already answering. Deliberately does not
# try to start dockerd: on a machine where it works there is a service manager
# doing that, and on a machine where it does not, starting it just produces a
# daemon that cannot unpack an image (see the header). Force either backend
# with CONTINUUM_DEV_BACKEND=docker|native.
docker_available() {
command -v docker >/dev/null 2>&1 || return 1
sudo -n docker info >/dev/null 2>&1
}
backend() {
if [ -n "${CONTINUUM_DEV_BACKEND:-}" ]; then
echo "$CONTINUUM_DEV_BACKEND"
elif docker_available; then
echo docker
else
echo native
fi
}
# --- waiting ---------------------------------------------------------------
# Poll for readiness rather than sleeping a fixed amount: Postgres accepts
# connections before it will serve them, and a failure should report why.
wait_for_postgres() {
for _ in $(seq 1 60); do
if pg_isready -h localhost -p "$PG_PORT" -U continuum -d continuum >/dev/null 2>&1; then
return 0
fi
sleep 0.5
done
echo "postgres did not become ready on port $PG_PORT" >&2
return 1
}
wait_for_valkey() {
for _ in $(seq 1 40); do
if valkey-cli -p "$VALKEY_PORT" ping 2>/dev/null | grep -q PONG; then
return 0
fi
sleep 0.5
done
echo "valkey did not answer on port $VALKEY_PORT" >&2
return 1
}
# --- native backend --------------------------------------------------------
native_up() {
mkdir -p "$NATIVE_STATE"
if ! pg_isready -h localhost -p "$PG_PORT" >/dev/null 2>&1; then
echo "==> starting postgresql-$PG_VERSION"
# pg_ctlcluster rather than systemctl: there is no service manager here.
sudo pg_ctlcluster "$PG_VERSION" main start
fi
wait_for_postgres_socket
# Idempotent: both of these are no-ops on a second run.
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='continuum'" | grep -q 1; then
echo "==> creating role 'continuum'"
sudo -u postgres psql -qc "CREATE ROLE continuum LOGIN PASSWORD 'continuum' SUPERUSER" >/dev/null
fi
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='continuum'" | grep -q 1; then
echo "==> creating database 'continuum'"
sudo -u postgres createdb -O continuum continuum
fi
# frame_embeddings needs it, and it is why the compose file uses the
# pgvector image rather than plain postgres.
sudo -u postgres psql -d continuum -qc "CREATE EXTENSION IF NOT EXISTS vector" >/dev/null
if ! valkey-cli -p "$VALKEY_PORT" ping >/dev/null 2>&1; then
echo "==> starting valkey"
valkey-server --port "$VALKEY_PORT" --daemonize yes \
--dir "$NATIVE_STATE" --appendonly yes \
--logfile "$NATIVE_STATE/valkey.log"
fi
wait_for_valkey
}
# Postgres has to be up before the role exists, so this waits on the server
# rather than on a successful `continuum` login.
wait_for_postgres_socket() {
for _ in $(seq 1 60); do
if sudo -u postgres pg_isready -q 2>/dev/null; then return 0; fi
sleep 0.5
done
echo "postgres did not start" >&2
return 1
}
native_down() {
if valkey-cli -p "$VALKEY_PORT" ping >/dev/null 2>&1; then
echo "==> stopping valkey"
valkey-cli -p "$VALKEY_PORT" shutdown nosave 2>/dev/null || true
fi
if sudo -u postgres pg_isready -q 2>/dev/null; then
echo "==> stopping postgresql-$PG_VERSION"
sudo pg_ctlcluster "$PG_VERSION" main stop || true
fi
}
native_reset() {
native_down
echo "==> dropping the continuum database and valkey's data"
sudo pg_ctlcluster "$PG_VERSION" main start
wait_for_postgres_socket
sudo -u postgres dropdb --if-exists continuum
sudo pg_ctlcluster "$PG_VERSION" main stop || true
rm -rf "$NATIVE_STATE"
}
native_status() {
echo "backend: native (apt packages, no container runtime)"
if sudo -u postgres pg_isready -q 2>/dev/null; then
echo " postgres: up on :$PG_PORT ($(sudo -u postgres psql -tAc 'SELECT version()' | cut -d, -f1))"
else
echo " postgres: down"
fi
if valkey-cli -p "$VALKEY_PORT" ping >/dev/null 2>&1; then
echo " valkey: up on :$VALKEY_PORT"
else
echo " valkey: down"
fi
}
# --- docker backend --------------------------------------------------------
docker_up() {
sudo docker compose up -d
echo "==> waiting for containers to report healthy"
for _ in $(seq 1 60); do
local unhealthy
unhealthy="$(sudo docker compose ps --format '{{.Service}} {{.Health}}' \
| awk '$2 != "healthy" { print $1 }' || true)"
[ -z "$unhealthy" ] && return 0
sleep 1
done
echo "timed out: $(sudo docker compose ps --format '{{.Service}} {{.Health}}')" >&2
return 1
}
# --- commands --------------------------------------------------------------
cmd_up() {
case "$(backend)" in
docker) echo "==> backend: docker compose"; docker_up ;;
native) echo "==> backend: native (docker is unavailable here - see this script's header)"; native_up ;;
esac
echo
cmd_env
echo
echo "Next: scripts/dev-env.sh schema"
}
cmd_down() { case "$(backend)" in docker) sudo docker compose down ;; native) native_down ;; esac; }
cmd_reset() { case "$(backend)" in docker) sudo docker compose down -v ;; native) native_reset ;; esac; }
cmd_status() { case "$(backend)" in docker) echo "backend: docker"; sudo docker compose ps ;; native) native_status ;; esac; }
cmd_psql() { PGPASSWORD=continuum psql -h localhost -p "$PG_PORT" -U continuum -d continuum "$@"; }
cmd_env() { echo "DATABASE_URL=$DEV_DATABASE_URL"; echo "VALKEY_URL=$DEV_VALKEY_URL"; }
# Applies the committed migrations in drizzle/, the same way a deploy would.
# This is the default because it is the path that has to work in production -
# testing `push` locally and `migrate` in production tests two different things.
cmd_schema() {
echo "==> applying committed migrations to $DEV_DATABASE_URL"
DATABASE_URL="$DEV_DATABASE_URL" bunx drizzle-kit migrate
}
# Diffs the live database against src/db/schema and reshapes it in place, with
# no migration file. Faster while iterating on the schema, and destructive: it
# will drop a column to make the database match. Local only - and whatever it
# does is not recorded anywhere, so run `bun run db:generate` before committing
# a schema change made this way.
cmd_push() {
echo "==> pushing src/db/schema straight onto $DEV_DATABASE_URL"
DATABASE_URL="$DEV_DATABASE_URL" bunx drizzle-kit push --force
}
case "${1:-}" in
up) cmd_up ;;
down) cmd_down ;;
reset) cmd_reset ;;
status) cmd_status ;;
psql) shift; cmd_psql "$@" ;;
schema) cmd_schema ;;
push) cmd_push ;;
env) cmd_env ;;
*) awk 'NR>1 && /^#/ { sub(/^# ?/, ""); print; next } NR>1 { exit }' "${BASH_SOURCE[0]}"; exit 1 ;;
esac