Busca en todas las páginas de la documentación
Run SQL files and parameterized checks in CI, deploy pipelines, and repeatable admin tasks with psql non-interactive mode.
psql "$DATABASE_URL" \
-v ON_ERROR_STOP=1 \
-f migrations/001_init.sql
psql "$DATABASE_URL" -c "SELECT count(*) FROM schema_migrations;"When to reach for this: Migration runners, smoke tests after deploy, seed scripts, and DBA playbooks checked into git.
#!/usr/bin/env bash
set -euo pipefail
export PGAPPNAME=ci-schema-check
psql "$DATABASE_URL" \
--single-transaction \
-v ON_ERROR_STOP=1 \
-v tenant_id=42 \
-f scripts/verify_rls.sql \
-o /tmp/verify.out-- scripts/verify_rls.sql
\set ON_ERROR_STOP on
SELECT current_setting('app.tenant_id', true) AS tenant;
SELECT count(*) AS orphan_orders
FROM orders o
WHERE o.tenant_id <> :'tenant_id'::bigint;What this demonstrates:
-v ON_ERROR_STOP=1 aborts the script on first SQL error (essential in CI)--single-transaction wraps the file in one BEGIN/COMMIT:tenant_id substitutes psql variables (use quoted :'name' for literal safety)PGAPPNAME shows up in pg_stat_activity.application_name| Flag | Purpose |
|---|---|
-f file.sql | Execute file |
\i file.sql | Same, from inside psql |
-a | Echo all input to stdout |
-q | Quiet banners |
-t | Tuples only (no headers) |
-A | Unaligned output |
-- Set in shell: psql -v start_date=2026-01-01
SELECT :'start_date'::date;
-- Prompt if unset
\prompt 'Enter tenant id' tenant_id:name is SQL identifier substitution - dangerous for user input.:'name' is string literal substitution - still escape-aware, not a full prepared statement.# .github/workflows/db-smoke.yml (excerpt)
- name: Schema smoke test
run: |
psql "${{ secrets.STAGING_DATABASE_URL }}" \
-v ON_ERROR_STOP=1 \
-f ci/assert_indexes.sql-- ci/assert_indexes.sql
SELECT 1/(count(*) > 0)::int AS idx_orders_created_exists
FROM pg_indexes
WHERE tablename = 'orders' AND indexname = 'idx_orders_created_at';0 success1 fatal error (with ON_ERROR_STOP)2 connection failure3 script error in -f file-v ON_ERROR_STOP=1 in automation.:'user_input' in dynamic SQL from operators. Fix: Validate format; use app-layer prepared statements for user data.\i relative paths - Depends on cwd when psql started. Fix: Absolute paths or cd in wrapper script.\copy needs client-side path. Fix: Mount fixtures or use INSERT seeds.--single-transaction when all statements are transaction-safe.| Alternative | Use When | Don't Use When |
|---|---|---|
| Flyway/Liquibase | Versioned migration history | One-off DBA script |
pg_prove (pgTAP) | Test-heavy SQL suites | Quick smoke query |
| Application migration ORM | App team owns schema | DBA-governed SQL-only policy |
psql excels at ops scripts and CI checks. Application code should use parameterized queries for user-facing paths.
Environment variable DATABASE_URL or PGPASSWORD in CI secret store - never commit credentials.
Yes - \i, \set, \connect work in script files. They are client-side, not sent to server.
Likely no ON_ERROR_STOP or checks ran against wrong database URL. Add \conninfo at top of scripts.
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.