Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ jobs:
# than reaching the suite as a mysterious test failure.
- run: docker compose up -d --wait

# Instant, and it refuses a sql/ script that opens or closes a
# transaction -- the property the plain release artifacts
# depend on.
- name: Build release artifacts
run: ./scripts/build-release.sh

- name: Check formatting
run: |
unformatted=$(gofmt -l .)
Expand Down
69 changes: 45 additions & 24 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ permissions:
# between jobs -- fewer moving parts, and nothing to pin.

jobs:
# The plain-scripts channel: the artifact is the initdb script
# for the same pinned image compose uses, and readiness is
# cel.version() answering. The all-in bundle and the core-only
# file are the two shapes that could break independently; each
# extension file is a verbatim copy of a sql/ script CI already
# installs.
# The plain-scripts channel, both shapes of the all-in bundle and
# the core-only file. A plain artifact must run inside a
# transaction it does not own: installed between BEGIN and
# ROLLBACK it has to leave no cel schema behind -- one stray
# COMMIT; would keep it -- and installed with --single-transaction
# it has to answer cel.version(). A -tx artifact installs with a
# bare psql -f. Each extension file is a verbatim copy of a sql/
# script CI already installs.
smoke:
if: >-
github.repository == 'emfga/cel4postgres' &&
Expand All @@ -45,29 +47,48 @@ jobs:
sql/000_install.sql)
image=$(sed -n 's/^ *image: \(postgres:[^ ]*\)$/\1/p' \
compose.yaml)
init=/docker-entrypoint-initdb.d/install.sql
for artifact in \
"cel4postgres--$version.sql" \
"cel4postgres-core--$version.sql"
do
echo "== $artifact"
docker run -d --rm --name relcheck \
-e POSTGRES_DB=cel -e POSTGRES_USER=cel \
-e POSTGRES_PASSWORD=pw \
-v "$PWD/dist/$artifact:$init:ro" \
"$image" >/dev/null
got=
for _ in $(seq 60); do
got=$(docker exec relcheck psql -U cel -d cel -tAc \
'SELECT cel.version()' 2>/dev/null) && break
sleep 2
done
docker rm -f relcheck >/dev/null
docker run -d --rm --name relcheck \
-e POSTGRES_DB=cel -e POSTGRES_USER=cel \
-e POSTGRES_PASSWORD=pw \
-v "$PWD/dist:/dist:ro" \
"$image" >/dev/null
# -h 127.0.0.1: over the socket, pg_isready can reach
# initdb's temporary server; only the final one listens
# on TCP.
for _ in $(seq 60); do
docker exec relcheck pg_isready -h 127.0.0.1 \
-U cel -d cel -q 2>/dev/null && break
sleep 2
done
sql() {
docker exec -e PGOPTIONS='-c client_min_messages=warning' \
relcheck psql -h 127.0.0.1 -U cel -d cel \
-v ON_ERROR_STOP=1 -q "$@"
}
check() {
got=$(sql -tAc 'SELECT cel.version()')
if [ "$got" != "$version" ]; then
echo "expected $version, got '$got'" >&2
exit 1
fi
sql -c 'DROP SCHEMA cel CASCADE'
}
for name in cel4postgres cel4postgres-core; do
plain="/dist/$name--$version.sql"
echo "== $plain"
sql -c 'BEGIN' -f "$plain" -c 'ROLLBACK'
left=$(sql -tAc "SELECT to_regnamespace('cel')")
if [ -n "$left" ]; then
echo "$plain committed on its own" >&2
exit 1
fi
sql --single-transaction -f "$plain"
check
echo "== /dist/$name-tx--$version.sql"
sql -f "/dist/$name-tx--$version.sql"
check
done
docker rm -f relcheck >/dev/null

# The flagship channel: the same dist/ files, wrapped by
# scripts/pgtle-wrap.sh into pgtle.install_extension, installed
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,13 @@ numeric prefix. Nothing about them is specific to the compose setup:

```bash
for f in sql/*.sql; do
psql -v ON_ERROR_STOP=1 -f "$f" "$YOUR_DATABASE_URL"
psql -v ON_ERROR_STOP=1 -1 -f "$f" "$YOUR_DATABASE_URL"
done
```

The scripts open no transaction of their own; `-1` makes each file
all or nothing.

It needs a role that may create the `cel` schema. It does not need
superuser. [docs/INSTALL.md](docs/INSTALL.md) is the full guide:
release artifacts and their checksums, self-hosted and AWS
Expand Down
30 changes: 27 additions & 3 deletions docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@ Download from the [releases page][releases]:
| `cel4postgres--<version>.sql` | everything: core + all extensions |
| `cel4postgres-core--<version>.sql` | core only (`cel.evaluate`, standard library, well-known types) |
| `cel4postgres-ext_<name>--<version>.sql` | one extension library |
| `cel4postgres…-tx--<version>.sql` | the same file, between one `BEGIN;` and one `COMMIT;` |
| `SHA256SUMS` | checksums for all of the above |

The plain files contain no transaction control, so they run inside
a transaction someone else opens: psql's `--single-transaction`, a
migration tool's, or pg_tle's `CREATE EXTENSION`. Each `-tx` twin
opens and commits exactly one transaction of its own, for a bare
`psql -f` that should still be all or nothing.

Verify downloads before running them:

```bash
Expand All @@ -48,10 +55,14 @@ Run the artifact as a role that may create the `cel` schema —
the database owner is enough; superuser is not needed:

```bash
psql -v ON_ERROR_STOP=1 "$DATABASE_URL" \
psql -v ON_ERROR_STOP=1 --single-transaction "$DATABASE_URL" \
-f cel4postgres--<version>.sql
```

or, equivalently, `psql -v ON_ERROR_STOP=1 -f
cel4postgres-tx--<version>.sql`. Either way a failure leaves the
database as it was.

Then verify:

```sql
Expand Down Expand Up @@ -92,8 +103,9 @@ One-time instance setup (this part needs a reboot; see the

Then register and install cel4postgres. The artifact is wrapped
into a `pgtle.install_extension` call by a script from this
repository (it strips top-level transaction statements, which are
not allowed inside `CREATE EXTENSION`):
repository. Wrap the plain files, not the `-tx` ones: transaction
control is not allowed inside `CREATE EXTENSION`, and the script
refuses a file that contains it.

```bash
./scripts/pgtle-wrap.sh cel4postgres <version> \
Expand All @@ -115,6 +127,18 @@ DROP EXTENSION cel4postgres;
SELECT pgtle.uninstall_extension('cel4postgres');
```

## Inside a migration tool

Migration tools (Flyway, Liquibase, Kysely, Alembic, Rails, …)
usually run each migration inside a transaction they own. Embed the
plain `cel4postgres--<version>.sql` as the body of one migration and
execute it as a single multi-statement string, without bind
parameters — the file is many statements with `$$`-quoted function
bodies, which PostgreSQL accepts only over the simple query
protocol. The whole install then commits or rolls back with the
migration. Do not embed a `-tx` file: its `COMMIT;` would end the
tool's transaction halfway through.

## Access control

The installing role owns everything and is the only one that can
Expand Down
40 changes: 31 additions & 9 deletions scripts/build-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@
#
# Three granularities, per the distribution decision: an all-in
# bundle, a core-only file (000-070), and one file per extension
# library (100-170). Plain concatenation is safe because every
# sql/ script opens and closes its own transactions. Install with
# ON_ERROR_STOP so a failure stops between them:
# library (100-170).
#
# psql -v ON_ERROR_STOP=1 -f cel4postgres--<version>.sql
# Each comes in two shapes. The plain file carries no transaction
# control, so it runs inside whatever transaction the caller already
# holds -- a migration tool's, pg_tle's CREATE EXTENSION, or psql's
# --single-transaction:
#
# psql -v ON_ERROR_STOP=1 -1 -f cel4postgres--<version>.sql
#
# The -tx file is the same bundle between exactly one BEGIN; and one
# COMMIT;, for a plain `psql -f` that should still be all or nothing.

set -eu

Expand All @@ -27,31 +33,47 @@ case $version in
;;
esac

# The sources carry no transaction control; that is what lets the
# plain artifacts run inside a caller's transaction. A BEGIN; or
# COMMIT; line reappearing in sql/ would silently commit that
# transaction halfway, so refuse to build rather than ship it.
if grep -n -x -e 'BEGIN;' -e 'COMMIT;' sql/*.sql; then
echo "sql/ must not open or close transactions" >&2
exit 1
fi

rm -rf dist
mkdir -p dist

# Concatenate the named files, each behind a banner naming its
# source, so an error line in a bundle is traceable to a script.
# Writes <name>--<version>.sql and its <name>-tx--<version>.sql
# twin.
bundle() {
out=$1
name=$1
shift
for f in "$@"; do
printf -- '-- ---- %s ----\n\n' "$f"
cat "$f"
printf '\n'
done >"dist/$out"
done >"dist/$name--$version.sql"
{
printf 'BEGIN;\n\n'
cat "dist/$name--$version.sql"
printf 'COMMIT;\n'
} >"dist/$name-tx--$version.sql"
}

core=$(ls sql/0[0-9][0-9]_*.sql)
exts=$(ls sql/1[0-9][0-9]_*.sql)

# shellcheck disable=SC2086
bundle "cel4postgres--$version.sql" $core $exts
bundle cel4postgres $core $exts
# shellcheck disable=SC2086
bundle "cel4postgres-core--$version.sql" $core
bundle cel4postgres-core $core
for f in $exts; do
name=$(basename "$f" .sql | sed 's/^[0-9]*_//')
bundle "cel4postgres-$name--$version.sql" "$f"
bundle "cel4postgres-$name" "$f"
done

(cd dist && sha256sum -- *.sql >SHA256SUMS)
Expand Down
15 changes: 10 additions & 5 deletions scripts/pgtle-wrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
#
# pgtle-wrap.sh <extname> <version> <artifact.sql>... > out.sql
#
# The artifacts run unchanged except for one transformation: their
# top-level BEGIN;/COMMIT; lines are dropped, because a pg_tle
# script executes inside CREATE EXTENSION's own transaction, where
# transaction control is not allowed.
# The artifacts run unchanged. Pass the plain ones, never a -tx
# twin: a pg_tle script executes inside CREATE EXTENSION's own
# transaction, where transaction control is not allowed.

set -eu

Expand All @@ -21,7 +20,13 @@ tag='_cel_pgtle_'
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT

cat -- "$@" | grep -v -x -e 'BEGIN;' -e 'COMMIT;' >"$tmp"
cat -- "$@" >"$tmp"

if grep -q -x -e 'BEGIN;' -e 'COMMIT;' "$tmp"; then
echo "input controls transactions; pass the plain artifacts," \
"not the -tx ones" >&2
exit 1
fi

# The bundle becomes one dollar-quoted literal; the quoting breaks
# if its tag ever appears in the content, so refuse instead of
Expand Down
12 changes: 6 additions & 6 deletions sql/000_install.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
-- the cel schema. Nothing here requires superuser, a filesystem, or
-- a server restart: see CLAUDE.md, "Installation and privileges".
--
-- Run with: psql -v ON_ERROR_STOP=1 -f sql/000_install.sql
-- Run with: psql -v ON_ERROR_STOP=1 -1 -f sql/000_install.sql
-- followed by the other sql/ scripts in numbered order (or use a
-- release artifact, which bundles them already ordered).

BEGIN;
--
-- No script opens a transaction of its own. The caller decides:
-- psql -1 (--single-transaction) per file, a migration tool's
-- transaction around a whole bundle, or pg_tle's CREATE EXTENSION.

CREATE SCHEMA IF NOT EXISTS cel;

Expand All @@ -22,7 +24,7 @@ CREATE TABLE IF NOT EXISTS cel.schema_version (
);

INSERT INTO cel.schema_version (version)
VALUES ('0.0.1')
VALUES ('0.0.2')
ON CONFLICT (version) DO NOTHING;

-- The installed schema version. IMMUTABLE is deliberately wrong for
Expand All @@ -39,5 +41,3 @@ AS $$
ORDER BY applied_at DESC, version DESC
LIMIT 1;
$$;

COMMIT;
4 changes: 0 additions & 4 deletions sql/010_registry.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
-- core could reach anything the registry cannot describe, the
-- registry would stop being the extension mechanism.

BEGIN;

-- Custom and well-known types: name resolution, construction,
-- equality and conversion hooks. Every row visible in an env also
-- implies an identifier of type type(T) under the type's name.
Expand Down Expand Up @@ -182,5 +180,3 @@ INSERT INTO cel.env (name, flags) VALUES
('optionals', '{"optional_syntax": true}'),
('network', '{}')
ON CONFLICT (name) DO NOTHING;

COMMIT;
12 changes: 0 additions & 12 deletions sql/020_values.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
-- Run after 000_install.sql. Everything here is a pure function over
-- tagged jsonb values or their scalar payloads; nothing reads a table.

BEGIN;

-- Renders a finite double the way CEL's string(double) must: cel-go
-- delegates to Go's %g (common/types/double.go:141, pinned v0.32.0).
-- Postgres's own float8 output is close but measurably different in
Expand Down Expand Up @@ -128,10 +126,6 @@ BEGIN
END;
$$;

COMMIT;

BEGIN;

-- Tagged-value primitives. The kind tag carries type identity;
-- these helpers are the single place equality, ordering and payload
-- access are defined, so every impl and the evaluator agree on them.
Expand Down Expand Up @@ -465,10 +459,6 @@ BEGIN
END;
$$;

COMMIT;

BEGIN;

-- Exact float8 -> numeric. The built-in cast goes through the
-- shortest decimal text, which identifies the double uniquely but is
-- NOT its exact binary value (36028797018963968::float8::numeric
Expand Down Expand Up @@ -506,5 +496,3 @@ BEGIN
* ('1e' || e)::numeric;
END;
$$;

COMMIT;
Loading
Loading