From 057a9f5926b650bb8c92d5cb0c756ba758a17f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lemuel=20Roberto=20Bonif=C3=A1cio?= Date: Sun, 30 Aug 2026 20:38:24 -0300 Subject: [PATCH 1/5] Bump the schema version to 0.0.1 The first release needs a version that is not the 0.0.0 placeholder the install script has seeded since the version table existed. The seeded row stays the version's only home: the release tooling reads it from here rather than keeping a copy that could drift. --- sql/000_install.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/000_install.sql b/sql/000_install.sql index e4e968a..9addb65 100644 --- a/sql/000_install.sql +++ b/sql/000_install.sql @@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS cel.schema_version ( ); INSERT INTO cel.schema_version (version) -VALUES ('0.0.0') +VALUES ('0.0.1') ON CONFLICT (version) DO NOTHING; -- The installed schema version. IMMUTABLE is deliberately wrong for From 99a0e400ce23f0b81a667ad674fa5b8d55d0231b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lemuel=20Roberto=20Bonif=C3=A1cio?= Date: Sun, 30 Aug 2026 20:38:24 -0300 Subject: [PATCH 2/5] Add a manual release workflow and artifact build Consumers currently need the repository and knowledge of the sql/ file order to install. A tagged release with downloadable artifacts removes both: scripts/build-release.sh concatenates the numbered scripts into an all-in bundle, a core-only file and one file per extension library, with checksums, reading the version from the row 000_install.sql seeds. Concatenation is safe because every script opens and closes its own transactions. The workflow is workflow_dispatch only and guarded to main on emfga/cel4postgres: publishing is a deliberate act, never a side effect of a push, and forks carrying this file must not release. Before publishing it boots the same pinned postgres image compose uses with the all-in and core artifacts as the initdb script and asserts cel.version() answers, so a bundle that fails to install can never reach a release. The tag is created by the release itself, so re-running for an already published version fails rather than republishing. --- .github/workflows/release.yml | 92 +++++++++++++++++++++++++++++++++++ .gitignore | 1 + scripts/build-release.sh | 60 +++++++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100755 scripts/build-release.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7b4bf33 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,92 @@ +name: Release + +# Manual only. Publishing is a deliberate act: someone chooses the +# moment, from main, on the canonical repository -- never a fork, +# and never as a side effect of a push. +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + release: + # Guarded twice over: workflow_dispatch lets a caller pick any + # ref, and forks carry this file too. Anything but main on + # emfga/cel4postgres skips rather than publishes. + if: >- + github.repository == 'emfga/cel4postgres' && + github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + # gh release create pushes the tag and uploads the assets. + contents: write + + steps: + - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Build artifacts + id: build + run: | + ./scripts/build-release.sh + version=$(sed -n \ + "s/^VALUES ('\([0-9][0-9.]*\)')$/\1/p" \ + sql/000_install.sql) + echo "version=$version" >>"$GITHUB_OUTPUT" + + # A published artifact must install, not merely concatenate. + # Boot the same pinned image compose uses with the artifact as + # the initdb script and assert cel.version() answers. 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. + - name: Smoke-test artifacts + env: + VERSION: ${{ steps.build.outputs.version }} + run: | + 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 + if [ "$got" != "$VERSION" ]; then + echo "expected $VERSION, got '$got'" >&2 + exit 1 + fi + done + + # The tag does not exist beforehand; gh creates it at the + # released commit, and re-running on an already-released + # version fails here instead of republishing. + - name: Publish release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.build.outputs.version }} + run: | + notes="Install with: psql -v ON_ERROR_STOP=1" + notes="$notes -f cel4postgres--$VERSION.sql." + notes="$notes Verify downloads against SHA256SUMS." + gh release create "v$VERSION" dist/* \ + --target "$GITHUB_SHA" \ + --title "cel4postgres $VERSION" \ + --notes "$notes" diff --git a/.gitignore b/.gitignore index 3e44385..65f97d2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ # CI-only corpus checkout location .cel-expr/ +dist/ diff --git a/scripts/build-release.sh b/scripts/build-release.sh new file mode 100755 index 0000000..d744a30 --- /dev/null +++ b/scripts/build-release.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env sh +# Build the release artifacts into dist/. +# +# The version has one home: the row 000_install.sql seeds into +# cel.schema_version. This script reads it from there rather than +# keeping a copy that could drift. +# +# 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: +# +# psql -v ON_ERROR_STOP=1 -f cel4postgres--.sql + +set -eu + +cd "$(dirname "$0")/.." + +version=$(sed -n "s/^VALUES ('\([0-9][0-9.]*\)')$/\1/p" \ + sql/000_install.sql) +case $version in + *.*.*) ;; + *) + echo "could not read the version from sql/000_install.sql" >&2 + exit 1 + ;; +esac + +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. +bundle() { + out=$1 + shift + for f in "$@"; do + printf -- '-- ---- %s ----\n\n' "$f" + cat "$f" + printf '\n' + done >"dist/$out" +} + +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 +# shellcheck disable=SC2086 +bundle "cel4postgres-core--$version.sql" $core +for f in $exts; do + name=$(basename "$f" .sql | sed 's/^[0-9]*_//') + bundle "cel4postgres-$name--$version.sql" "$f" +done + +(cd dist && sha256sum -- *.sql >SHA256SUMS) + +echo "version $version" +ls -l dist From b3726b23b00f1ce74ff2acbb1894605a66aa637f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lemuel=20Roberto=20Bonif=C3=A1cio?= Date: Sun, 30 Aug 2026 20:55:17 -0300 Subject: [PATCH 3/5] Validate releases through pg_tle before publishing pg_tle is the recommended install path on RDS and Aurora, so a release that only proved the psql channel could ship artifacts that fail through CREATE EXTENSION. The release workflow now wraps the exact dist/ files into pgtle.install_extension at test time and installs them as a real extension before anything publishes: a matrix covers the all-in bundle, the core alone, and the core with each extension library, and each variant must evaluate an expression that needs its environment -- env rows exist even in a core-only install, so only a working call is evidence the extension's items are there. The wrapper strips top-level BEGIN/COMMIT because a pg_tle script runs inside CREATE EXTENSION's own transaction, and refuses input containing its dollar-quote tag rather than emit a script that breaks somewhere deep in pg_tle. The image builds aws/pg_tle, pinned like every other reference, on the same postgres image compose pins, extracted from compose.yaml so the two cannot drift. Nothing built here is published; the channel is validated, the wrapped form is not (yet) an artifact. --- .github/workflows/release.yml | 221 ++++++++++++++++++++++++++++------ docker/pg_tle.Dockerfile | 20 +++ scripts/pgtle-wrap.sh | 39 ++++++ 3 files changed, 246 insertions(+), 34 deletions(-) create mode 100644 docker/pg_tle.Dockerfile create mode 100755 scripts/pgtle-wrap.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7b4bf33..9b7e75d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,20 +9,27 @@ on: permissions: contents: read +# Guarded twice over on every job: workflow_dispatch lets a caller +# pick any ref, and forks carry this file too. Anything but main on +# emfga/cel4postgres skips rather than publishes. +# +# The artifact build is deterministic and instant, so each job +# rebuilds dist/ from the checkout instead of passing files +# between jobs -- fewer moving parts, and nothing to pin. + jobs: - release: - # Guarded twice over: workflow_dispatch lets a caller pick any - # ref, and forks carry this file too. Anything but main on - # emfga/cel4postgres skips rather than publishes. + # 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. + smoke: if: >- github.repository == 'emfga/cel4postgres' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 15 - permissions: - # gh release create pushes the tag and uploads the assets. - contents: write - steps: - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: @@ -30,31 +37,18 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Build artifacts - id: build + - name: Build and install artifacts run: | ./scripts/build-release.sh version=$(sed -n \ "s/^VALUES ('\([0-9][0-9.]*\)')$/\1/p" \ sql/000_install.sql) - echo "version=$version" >>"$GITHUB_OUTPUT" - - # A published artifact must install, not merely concatenate. - # Boot the same pinned image compose uses with the artifact as - # the initdb script and assert cel.version() answers. 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. - - name: Smoke-test artifacts - env: - VERSION: ${{ steps.build.outputs.version }} - run: | 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" + "cel4postgres--$version.sql" \ + "cel4postgres-core--$version.sql" do echo "== $artifact" docker run -d --rm --name relcheck \ @@ -69,24 +63,183 @@ jobs: sleep 2 done docker rm -f relcheck >/dev/null - if [ "$got" != "$VERSION" ]; then - echo "expected $VERSION, got '$got'" >&2 + if [ "$got" != "$version" ]; then + echo "expected $version, got '$got'" >&2 exit 1 fi done - # The tag does not exist beforehand; gh creates it at the - # released commit, and re-running on an already-released - # version fails here instead of republishing. - - name: Publish release + # The flagship channel: the same dist/ files, wrapped by + # scripts/pgtle-wrap.sh into pgtle.install_extension, installed + # with CREATE EXTENSION cel4postgres, and proven by evaluating + # an expression that needs the variant's environment. Env rows + # exist even in the core-only install, so only an evaluation is + # evidence that an extension's items are really there. + pgtle: + if: >- + github.repository == 'emfga/cel4postgres' && + github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - variant: all + parts: all + env: standard + expr: 1 + 2 + want: '{"v": 3, "@t": "int"}' + - variant: core + parts: core + env: standard + expr: 1 + 2 + want: '{"v": 3, "@t": "int"}' + - variant: ext_comprehensions + parts: core ext_comprehensions + env: standard,two_var_comprehensions + expr: '[1, 2, 3].transformList(i, v, v * 2)' + want: >- + {"v": [{"v": 2, "@t": "int"}, {"v": 4, "@t": + "int"}, {"v": 6, "@t": "int"}], "@t": "list"} + - variant: ext_optionals + parts: core ext_optionals + env: standard,optionals + expr: optional.of(1).hasValue() + want: '{"v": true, "@t": "bool"}' + - variant: ext_strings + parts: core ext_strings + env: standard,strings + expr: '"a,b".split(",")' + want: >- + {"v": [{"v": "a", "@t": "string"}, {"v": "b", + "@t": "string"}], "@t": "list"} + - variant: ext_math + parts: core ext_math + env: standard,math + expr: math.least(1, 2) + want: '{"v": 1, "@t": "int"}' + - variant: ext_lists + parts: core ext_lists + env: standard,lists + expr: '[3, 1, 2].sort()' + want: >- + {"v": [{"v": 1, "@t": "int"}, {"v": 2, "@t": + "int"}, {"v": 3, "@t": "int"}], "@t": "list"} + - variant: ext_encoders + parts: core ext_encoders + env: standard,encoders + expr: base64.encode(b"hi") + want: '{"v": "aGk=", "@t": "string"}' + - variant: ext_bindings + parts: core ext_bindings + env: standard,bindings + expr: cel.bind(x, 1, x + x) + want: '{"v": 2, "@t": "int"}' + - variant: ext_network + parts: core ext_network + env: standard,network + expr: isIP("127.0.0.1") + want: '{"v": true, "@t": "bool"}' + steps: + - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Build pg_tle image + run: | + base=$(sed -n 's/^ *image: \(postgres:[^ ]*\)$/\1/p' \ + compose.yaml) + docker build -f docker/pg_tle.Dockerfile \ + --build-arg BASE_IMAGE="$base" -t pgtle . + + - name: Install through pg_tle and evaluate + env: + PARTS: ${{ matrix.parts }} + CEL_ENV: ${{ matrix.env }} + EXPR: ${{ matrix.expr }} + WANT: ${{ matrix.want }} + run: | + ./scripts/build-release.sh + version=$(sed -n \ + "s/^VALUES ('\([0-9][0-9.]*\)')$/\1/p" \ + sql/000_install.sql) + files= + for p in $PARTS; do + if [ "$p" = all ]; then + files="dist/cel4postgres--$version.sql" + else + files="$files dist/cel4postgres-$p--$version.sql" + fi + done + # shellcheck disable=SC2086 + ./scripts/pgtle-wrap.sh cel4postgres "$version" \ + $files >wrapped.sql + docker run -d --rm --name tle \ + -e POSTGRES_DB=cel -e POSTGRES_USER=cel \ + -e POSTGRES_PASSWORD=pw \ + -v "$PWD/wrapped.sql:/wrapped.sql:ro" \ + pgtle -c shared_preload_libraries=pg_tle >/dev/null + for _ in $(seq 60); do + docker exec tle pg_isready -U cel -d cel -q \ + 2>/dev/null && break + sleep 2 + done + docker exec tle psql -U cel -d cel -v ON_ERROR_STOP=1 \ + -c 'CREATE EXTENSION pg_tle;' \ + -f /wrapped.sql \ + -c 'CREATE EXTENSION cel4postgres;' + got=$(docker exec tle psql -U cel -d cel -tAc \ + "SELECT extversion FROM pg_extension + WHERE extname = 'cel4postgres'") + if [ "$got" != "$version" ]; then + echo "pg_extension says '$got', not $version" >&2 + exit 1 + fi + got=$(docker exec tle psql -U cel -d cel -tAc " + SELECT cel.evaluate(\$e\$$EXPR\$e\$, '{}', + '$CEL_ENV')") + docker rm -f tle >/dev/null + if [ "$got" != "$WANT" ]; then + echo "evaluate returned: $got" >&2 + echo "expected: $WANT" >&2 + exit 1 + fi + + # The tag does not exist beforehand; gh creates it at the + # released commit, and re-running on an already-released + # version fails here instead of republishing. + publish: + if: >- + github.repository == 'emfga/cel4postgres' && + github.ref == 'refs/heads/main' + needs: [smoke, pgtle] + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + # gh release create pushes the tag and uploads the assets. + contents: write + steps: + - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Build and publish env: GH_TOKEN: ${{ github.token }} - VERSION: ${{ steps.build.outputs.version }} run: | + ./scripts/build-release.sh + version=$(sed -n \ + "s/^VALUES ('\([0-9][0-9.]*\)')$/\1/p" \ + sql/000_install.sql) notes="Install with: psql -v ON_ERROR_STOP=1" - notes="$notes -f cel4postgres--$VERSION.sql." + notes="$notes -f cel4postgres--$version.sql." notes="$notes Verify downloads against SHA256SUMS." - gh release create "v$VERSION" dist/* \ + gh release create "v$version" dist/* \ --target "$GITHUB_SHA" \ - --title "cel4postgres $VERSION" \ + --title "cel4postgres $version" \ --notes "$notes" diff --git a/docker/pg_tle.Dockerfile b/docker/pg_tle.Dockerfile new file mode 100644 index 0000000..646ebd2 --- /dev/null +++ b/docker/pg_tle.Dockerfile @@ -0,0 +1,20 @@ +# The pinned postgres image plus pg_tle, for validating that the +# release artifacts install through the flagship channel +# (CREATE EXTENSION via pgtle.install_extension). Validation-only: +# nothing built here is published. +# +# BASE_IMAGE must match compose.yaml's pinned image; the workflow +# extracts it from there so the two cannot drift. +ARG BASE_IMAGE=postgres:18-alpine +FROM ${BASE_IMAGE} + +# Pinned like every other reference: a bump is a deliberate change. +ARG PG_TLE_VERSION=v1.5.2 + +RUN apk add --no-cache --virtual .build \ + build-base git flex bison openssl-dev krb5-dev \ + && git clone --depth 1 --branch "${PG_TLE_VERSION}" \ + https://github.com/aws/pg_tle.git /tmp/pg_tle \ + && make -C /tmp/pg_tle install with_llvm=no \ + && rm -rf /tmp/pg_tle \ + && apk del .build diff --git a/scripts/pgtle-wrap.sh b/scripts/pgtle-wrap.sh new file mode 100755 index 0000000..7bf0125 --- /dev/null +++ b/scripts/pgtle-wrap.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env sh +# Wrap release artifacts into a pg_tle registration script, so the +# flagship channel (CREATE EXTENSION via pg_tle) can be validated +# against the exact files a release publishes. Validation-only: +# the output is generated where needed, never published. +# +# pgtle-wrap.sh ... > 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. + +set -eu + +name=$1 +version=$2 +shift 2 + +tag='_cel_pgtle_' +tmp=$(mktemp) +trap 'rm -f "$tmp"' EXIT + +cat -- "$@" | grep -v -x -e 'BEGIN;' -e 'COMMIT;' >"$tmp" + +# The bundle becomes one dollar-quoted literal; the quoting breaks +# if its tag ever appears in the content, so refuse instead of +# emitting a script that fails somewhere deep in pg_tle. +if grep -qF "\$$tag\$" "$tmp"; then + echo "dollar-quote tag \$$tag\$ appears in the input" >&2 + exit 1 +fi + +printf "SELECT pgtle.install_extension(\n" +printf " '%s',\n '%s',\n 'CEL for PostgreSQL',\n" \ + "$name" "$version" +printf '$%s$\n' "$tag" +cat "$tmp" +printf '$%s$\n);\n' "$tag" From c27f023543abc54aca614bbbc75616d960acde31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lemuel=20Roberto=20Bonif=C3=A1cio?= Date: Sun, 30 Aug 2026 21:09:29 -0300 Subject: [PATCH 4/5] Make code comments stand alone on a fresh checkout Comments across the tree cited planning notes that were never committed -- numbered documents, rulings, phase labels -- so a reader without that context met references that resolve to nothing. Each one now either states its fact directly, names an in-tree source (CLAUDE.md, docs/CONFORMANCE.md, a pinned cel-go location), or is gone. A few were not just unreachable but wrong: the install header named a script that does not exist, two files pointed at a sql/ext/ directory that was never created, the optionals header promised as future work what part two of the same file already implements, and the conformance package comment predated the corpus runner. Those are corrected against the tree, and the full bundle was reinstalled afterwards to prove the SQL still loads. --- conformance/envs.go | 2 +- conformance/format_test.go | 14 +++++++------- conformance/infra_test.go | 11 ++++------- conformance/report.go | 2 +- conformance/run.go | 6 +++--- internal/codec/codec.go | 12 ++++++------ internal/codec/compare.go | 4 ++-- internal/codec/type.go | 8 ++++---- internal/corpus/corpus_test.go | 8 +++----- internal/oracle/options.go | 2 +- internal/oracle/shape.go | 6 +++--- sql/000_install.sql | 4 +++- sql/010_registry.sql | 4 ++-- sql/020_values.sql | 9 ++++----- sql/030_parse.sql | 6 +++--- sql/040_check.sql | 10 +++++----- sql/050_eval.sql | 12 ++++++------ sql/060_stdlib.sql | 8 ++++---- sql/070_wkt.sql | 17 ++++++++--------- sql/100_ext_comprehensions.sql | 4 ++-- sql/110_ext_optionals.sql | 7 +++---- sql/120_ext_strings.sql | 4 ++-- sql/140_ext_lists.sql | 5 ++--- 23 files changed, 79 insertions(+), 86 deletions(-) diff --git a/conformance/envs.go b/conformance/envs.go index bf5aeef..bb60492 100644 --- a/conformance/envs.go +++ b/conformance/envs.go @@ -7,7 +7,7 @@ package conformance // default environment quietly gained an extension is not a passing // file. // -// macros2 was measured (Phase 0): every one of its 46 cases uses the +// macros2 was measured: every one of its 46 cases uses the // two-var comprehension macros and none uses optional syntax, so the // whole file takes two_var_comprehensions and nothing else. var fileEnvs = map[string]string{ diff --git a/conformance/format_test.go b/conformance/format_test.go index 00a503a..9e91a66 100644 --- a/conformance/format_test.go +++ b/conformance/format_test.go @@ -13,13 +13,13 @@ import ( // string(double) is implemented by cel._double_text, which must match // Go's %g exactly, because that is what cel-go's string(double) emits -// (common/types/double.go:141, pinned v0.32.0). The workspace ruling -// on I2 committed this test: it fuzzes that claim on every CI run -// rather than trusting a handful of probes -- and its very first run -// proved bare float8::text insufficient (Go switches to scientific -// notation at e+06, Postgres at e+15), which is why the function -// exists. A mismatch here reopens the formatting question with a -// concrete value in hand. +// (common/types/double.go:141, pinned v0.32.0). This test fuzzes +// that claim on every CI run rather than trusting a handful of +// probes -- and its very first run proved bare float8::text +// insufficient (Go switches to scientific notation at e+06, +// Postgres at e+15), which is why the function exists. A mismatch +// here reopens the formatting question with a concrete value in +// hand. // // Non-finite values are excluded by design: they never reach the // Postgres formatter (the evaluator emits +Inf/-Inf/NaN itself from diff --git a/conformance/infra_test.go b/conformance/infra_test.go index fae38b2..f1556d3 100644 --- a/conformance/infra_test.go +++ b/conformance/infra_test.go @@ -1,10 +1,7 @@ -// Package conformance holds the suite that measures cel4postgres -// against the cel-spec conformance corpus. -// -// Nothing here reads a .textproto yet. These are the infrastructure -// tests: they fail loudly when the database is missing or the schema -// was never installed, so that a later red conformance run is never -// ambiguous about which of the two went wrong. +// The infrastructure tests: they fail loudly when the database is +// missing or the schema was never installed, so that a red +// conformance run is never ambiguous about which of the two went +// wrong. package conformance import ( diff --git a/conformance/report.go b/conformance/report.go index 1763f85..ce3f513 100644 --- a/conformance/report.go +++ b/conformance/report.go @@ -88,7 +88,7 @@ type CaseFailure struct { // Divergence is a case the two implementations judge differently. // Since cel4postgres follows the corpus wherever the two disagree -// (the corpus-first ruling), these are almost always cases cel-go +// (docs/CONFORMANCE.md), these are almost always cases cel-go // itself does not satisfy -- which is exactly what a reader comparing // the two needs told. type Divergence struct { diff --git a/conformance/run.go b/conformance/run.go index b2381ff..5d363f3 100644 --- a/conformance/run.go +++ b/conformance/run.go @@ -234,8 +234,8 @@ func stageErrors(envelope any) ([]any, bool) { return errs, ok } -// checkOptions builds the options argument of cel.check (decision 7): -// the case's container and its type_env ident declarations. +// checkOptions builds the options argument of cel.check: the case's +// container and its type_env ident declarations. func checkOptions(tc *test.SimpleTest) ([]byte, error) { options := map[string]any{} if tc.GetContainer() != "" { @@ -271,7 +271,7 @@ func activationJSON(tc *test.SimpleTest) ([]byte, error) { } // compareResult applies the case's result matcher. A missing matcher -// defaults to value: bool true (measured, workspace doc 01). +// defaults to value: bool true (measured against cel-go v0.32.0). func compareResult(got any, rawResult []byte, tc *test.SimpleTest) error { compare := func(want any) error { if codec.Equal(want, got) { diff --git a/internal/codec/codec.go b/internal/codec/codec.go index fc8b989..72c807c 100644 --- a/internal/codec/codec.go +++ b/internal/codec/codec.go @@ -1,7 +1,7 @@ // Package codec converts between the conformance corpus's protobuf // value/type messages and the tagged-jsonb representation cel4postgres -// uses (workspace docs 02 and 03: tag key "@t", maps as entry arrays, -// timestamps as {s, n, tz}, non-finite doubles as strings). +// uses: tag key "@t", maps as entry arrays, timestamps as {s, n, tz}, +// non-finite doubles as strings. // // Numbers are carried as json.Number end to end so int64/uint64 // payloads never pass through a float64. @@ -54,8 +54,8 @@ func doublePayload(v float64) any { } // FromExprValue converts a corpus binding or expected result. Errors -// and unknowns are values in this representation (workspace doc 02), -// so all three ExprValue kinds map to a tag. +// and unknowns are values in this representation, so all three +// ExprValue kinds map to a tag. func FromExprValue(v *expr.ExprValue) (Tagged, error) { switch kind := v.GetKind().(type) { case *expr.ExprValue_Value: @@ -136,8 +136,8 @@ func FromValue(v *expr.Value) (Tagged, error) { } // fromWellKnown converts the well-known-type messages that are in -// scope without a descriptor pool (decision 3). Anything else is a -// descriptor-dependent case the skip list already names. +// scope without a descriptor pool (CLAUDE.md, Scope). Anything else +// is a descriptor-dependent case the skip list already names. func fromWellKnown(message any) (Tagged, error) { switch m := message.(type) { case *timestamppb.Timestamp: diff --git a/internal/codec/compare.go b/internal/codec/compare.go index 1923bb0..25e4ae1 100644 --- a/internal/codec/compare.go +++ b/internal/codec/compare.go @@ -7,8 +7,8 @@ import ( ) // Equal compares two tagged values structurally, with the two -// relaxations the conformance comparison requires (workspace doc 08): -// map entries are order-agnostic, and NaN matches NaN (the spec's +// relaxations the conformance comparison requires: map entries are +// order-agnostic, and NaN matches NaN (the spec's // rule; cel-go's own harness lacks it, ours has it). Kinds must match // exactly -- an int result never equals a uint or double expectation, // because result identity is what the tag exists to carry. diff --git a/internal/codec/type.go b/internal/codec/type.go index bed36b3..a22ceba 100644 --- a/internal/codec/type.go +++ b/internal/codec/type.go @@ -6,8 +6,8 @@ import ( expr "cel.dev/expr" ) -// TypeJSON is the type encoding of workspace doc 03, used in checked -// ASTs, registry rows and declarations. +// TypeJSON is the type encoding used in checked ASTs, registry rows +// and declarations. type TypeJSON = map[string]any func kindOnly(kind string) TypeJSON { @@ -24,8 +24,8 @@ var primitiveKinds = map[expr.Type_PrimitiveType]string{ } // wellKnownMessages normalizes well-known message names exactly as -// cel-go's checkedWellKnowns does at declaration time (workspace doc -// 03; cel-go common/types.go:834). +// cel-go's checkedWellKnowns does at declaration time (cel-go +// v0.32.0, common/types.go:834). var wellKnownMessages = map[string]TypeJSON{ "google.protobuf.Timestamp": kindOnly("timestamp"), "google.protobuf.Duration": kindOnly("duration"), diff --git a/internal/corpus/corpus_test.go b/internal/corpus/corpus_test.go index 7c7dce6..afb776b 100644 --- a/internal/corpus/corpus_test.go +++ b/internal/corpus/corpus_test.go @@ -11,8 +11,8 @@ import ( // The corpus is external input: these tests pin the facts the suite // depends on -- the checkout is findable, every file parses, and the -// file census matches what the conformance target (workspace doc 01) -// was measured against. A count change here means the corpus moved +// file census matches what the conformance target was measured +// against. A count change here means the corpus moved // and the target needs re-measuring, not that this test is wrong. func TestEveryFileParses(t *testing.T) { @@ -21,9 +21,7 @@ func TestEveryFileParses(t *testing.T) { t.Fatal(err) } - // 25 included + 6 excluded. Workspace doc 01 said "26 of 32"; - // the corpus at the pinned checkout has 31 -- recorded in the - // workspace ISSUES register when this was measured. + // 25 included + 6 excluded at the pinned checkout. if len(files) != 31 { t.Errorf("corpus has %d files, the target was measured on 31", len(files)) diff --git a/internal/oracle/options.go b/internal/oracle/options.go index 7e62493..55e1143 100644 --- a/internal/oracle/options.go +++ b/internal/oracle/options.go @@ -12,7 +12,7 @@ import ( // build the equivalent reference environment. "standard" is the base // cel.NewEnv (standard library and macros) plus identifier-escape // syntax, which cel-go's conformance run enables for the whole corpus -// and our standard env includes (workspace doc 01). +// and our standard env includes. var envOptions = map[string][]cel.EnvOption{ // Cross-type numeric comparisons and error-on-bad-presence-test // are cel-go options, but the conformance corpus requires both diff --git a/internal/oracle/shape.go b/internal/oracle/shape.go index d1cfbbf..80c373d 100644 --- a/internal/oracle/shape.go +++ b/internal/oracle/shape.go @@ -12,9 +12,9 @@ import ( "cel.dev/cel-go/common/types/ref" ) -// ParseShape parses an expression with cel-go and returns its AST as -// the node shape cel4postgres emits (workspace doc 03), without ids -// or source offsets. The parse unit tests diff cel.parse output +// ParseShape parses an expression with cel-go and returns its AST +// as the node shape cel4postgres emits, without ids or source +// offsets. The parse unit tests diff cel.parse output // against this, so the two parsers are compared structurally rather // than by transcription. func ParseShape( diff --git a/sql/000_install.sql b/sql/000_install.sql index 9addb65..a656fab 100644 --- a/sql/000_install.sql +++ b/sql/000_install.sql @@ -4,7 +4,9 @@ -- 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/install.sql +-- Run with: psql -v ON_ERROR_STOP=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; diff --git a/sql/010_registry.sql b/sql/010_registry.sql index c8232a1..7394daa 100644 --- a/sql/010_registry.sql +++ b/sql/010_registry.sql @@ -169,8 +169,8 @@ ON CONFLICT (name) DO NOTHING; -- Extension environments. The rows exist from day one so an env -- union like 'standard,strings' resolves before the extension's own --- install script has seeded any items into them; the scripts under --- sql/ext/ fill them in later phases. optionals owns the +-- install script has seeded any items into them; the sql/1xx +-- extension scripts fill them in. optionals owns the -- optional-syntax parse flag. INSERT INTO cel.env (name, flags) VALUES ('strings', '{}'), diff --git a/sql/020_values.sql b/sql/020_values.sql index 18ee6ce..7e7e56a 100644 --- a/sql/020_values.sql +++ b/sql/020_values.sql @@ -132,10 +132,9 @@ COMMIT; BEGIN; --- Tagged-value primitives. The kind tag carries type identity --- (workspace doc 02); these helpers are the single place equality, --- ordering and payload access are defined, so every impl and the --- evaluator agree on them. +-- 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. CREATE OR REPLACE FUNCTION cel._err(msg text, id bigint DEFAULT NULL) RETURNS jsonb @@ -327,7 +326,7 @@ BEGIN RETURN true; ELSE -- Opaque and future kinds: structural payload identity unless - -- a registered equality overrides it (extension phases). + -- a registered equality overrides it. RETURN a - '@t' = b - '@t'; END CASE; END; diff --git a/sql/030_parse.sql b/sql/030_parse.sql index 4721492..e6e0ab5 100644 --- a/sql/030_parse.sql +++ b/sql/030_parse.sql @@ -1,8 +1,8 @@ -- cel4postgres -- lexer, parser, macro engine. -- --- Hand-written lexer + precedence-climbing parser (workspace decision: --- cel-go itself carries a Pratt parser with these semantics). Errors --- travel as OUT parameters, never exceptions: cel.parse is labelled +-- Hand-written lexer + precedence-climbing parser (cel-go itself +-- carries a Pratt parser with these semantics). Errors travel as +-- OUT parameters, never exceptions: cel.parse is labelled -- PARALLEL SAFE, and a BEGIN/EXCEPTION block's subtransaction would -- break that promise inside a parallel worker. -- diff --git a/sql/040_check.sql b/sql/040_check.sql index 5e0d927..0cafdc1 100644 --- a/sql/040_check.sql +++ b/sql/040_check.sql @@ -10,8 +10,9 @@ -- nullability, and declaration-ordered overload resolution with -- result-type widening. -- --- Types are the doc-03 json encoding; the substitution mapping is a --- jsonb object keyed by the parameter type's canonical jsonb text. +-- Types are the registry's json type encoding; the substitution +-- mapping is a jsonb object keyed by the parameter type's canonical +-- jsonb text. -- Errors fail fast: conformance asserts on check-failure existence, -- never on collecting several. @@ -429,8 +430,7 @@ BEGIN -- Joining null with a nullable type keeps the nullable type: -- the corpus's legacy_nullable_types section fixes this and -- cel-go v0.32.0 skips those cases as known-wrong (its - -- mostGeneral would answer null) -- see the workspace - -- measurements log for the adjudication. + -- mostGeneral would answer null). IF prev ->> 'kind' = 'null' AND cel._ck_nullable(cur) AND cur ->> 'kind' <> 'null' THEN t := cur; @@ -1248,7 +1248,7 @@ END; $$; -- Checks a parse envelope under an environment. options carries the --- per-case container and extra ident declarations (decision 7). +-- per-case container and extra ident declarations. -- Returns the annotated envelope, or {"errors": [...]}. CREATE OR REPLACE FUNCTION cel.check(ast jsonb, env text, options jsonb) RETURNS jsonb diff --git a/sql/050_eval.sql b/sql/050_eval.sql index 407ded8..4146557 100644 --- a/sql/050_eval.sql +++ b/sql/050_eval.sql @@ -155,8 +155,7 @@ $$; -- Candidate variable names for a dotted name under a container: -- container "a.b" tries a.b.name, a.name, name -- cel-go's namespace --- resolution order (measured: container/shadowing runs in --- measurements.md). +-- resolution order, measured against v0.32.0. CREATE OR REPLACE FUNCTION cel._name_candidates( name text, absolute boolean, ctr text ) @@ -621,9 +620,10 @@ BEGIN END IF; v := v -> 'v' -> 'v'; END IF; - -- Key type restriction and duplicate rejection are dynamic - -- (corpus-first rulings: forbidden double/null keys and - -- normalized duplicates both error at construction). + -- Key type restriction and duplicate rejection are dynamic: + -- forbidden double/null keys and normalized duplicates both + -- error at construction (the corpus's rule -- see + -- docs/CONFORMANCE.md on following it over cel-go). IF key ->> '@t' NOT IN ('bool', 'int', 'uint', 'string') THEN RETURN cel._err(format( 'unsupported map key type: %s', key ->> '@t'), nid); @@ -698,7 +698,7 @@ BEGIN END; $$; --- The comprehension fold (workspace doc 06). The loop-termination +-- The comprehension fold. The loop-termination -- rule is load-bearing: only a genuine bool false stops iteration -- -- an error or unknown condition keeps folding, which is how exists -- recovers from early errors when a later element is true. diff --git a/sql/060_stdlib.sql b/sql/060_stdlib.sql index 1349c9d..85d89f4 100644 --- a/sql/060_stdlib.sql +++ b/sql/060_stdlib.sql @@ -7,7 +7,7 @@ -- would -- no privileged path (CLAUDE.md, the four registries). -- -- Semantics are cel-go v0.32.0's, encoded from measured runs and the --- pinned source (workspace measurements.md): checked int64/uint64 +-- pinned source: checked int64/uint64 -- arithmetic with overflow sentinels, IEEE-754 double arithmetic -- with the three non-finite sentinels, Go-style truncated division -- and remainder. @@ -500,7 +500,7 @@ AS $$ $$; -- Indexing. List indices accept int plus losslessly-coercible --- double/uint (cel-go list index semantics, workspace doc 06). +-- double/uint (cel-go list index semantics). CREATE OR REPLACE FUNCTION cel._f_index_list(args jsonb[]) RETURNS jsonb @@ -1126,8 +1126,8 @@ AS $$ E'\n', '')); $$; --- String tests. matches() is Postgres ~ for this milestone --- (decision 9: all corpus patterns measured to agree with RE2). +-- String tests. matches() is Postgres ~ (all corpus patterns +-- measured to agree with RE2). CREATE OR REPLACE FUNCTION cel._f_contains(args jsonb[]) RETURNS jsonb diff --git a/sql/070_wkt.sql b/sql/070_wkt.sql index 0009c10..5bf51c7 100644 --- a/sql/070_wkt.sql +++ b/sql/070_wkt.sql @@ -1,13 +1,12 @@ -- Well-known types: timestamps, durations, the wrapper types, --- Struct/Value/ListValue, Any and NullValue (workspace doc 07, --- decision 3). Everything registers through the same four tables the --- standard library uses; nothing here is a core patch. +-- Struct/Value/ListValue, Any and NullValue. Everything registers +-- through the same four tables the standard library uses; nothing +-- here is a core patch. -- -- Timestamp values are {"s": epoch seconds, "n": nanos 0..1e9-1, --- "tz": fixed offset minutes}; durations are total nanoseconds --- (doc 02). Semantics are cel-go v0.32.0's (common/types/ --- timestamp.go, duration.go, overflow.go), confirmed by conformance --- runs -- see the workspace measurements log. +-- "tz": fixed offset minutes}; durations are total nanoseconds. +-- Semantics are cel-go v0.32.0's (common/types/timestamp.go, +-- duration.go, overflow.go), confirmed by conformance runs. BEGIN; @@ -502,7 +501,7 @@ AS $$ SELECT cel._ts_get(args, 'milliseconds') $$; -- Duration getters are truncated totals, except getMilliseconds, -- which is the sub-second component: the corpus and cel-java agree --- against cel-go v0.32.0 here (workspace ISSUES R2 adjudication). +-- against cel-go v0.32.0 here. CREATE OR REPLACE FUNCTION cel._f_dur_hours(args jsonb[]) RETURNS jsonb LANGUAGE sql IMMUTABLE PARALLEL SAFE SET search_path = cel, pg_temp @@ -659,7 +658,7 @@ AS $$ $$; -- Any needs a descriptor pool to pack; its row exists so the name --- resolves (workspace doc 07). +-- resolves. CREATE OR REPLACE FUNCTION cel._wkt_any(fields jsonb) RETURNS jsonb LANGUAGE sql IMMUTABLE PARALLEL SAFE SET search_path = cel, pg_temp diff --git a/sql/100_ext_comprehensions.sql b/sql/100_ext_comprehensions.sql index 443095b..15f193d 100644 --- a/sql/100_ext_comprehensions.sql +++ b/sql/100_ext_comprehensions.sql @@ -7,8 +7,8 @@ -- 'two_var_comprehensions' env; nothing ships in 'standard'. -- -- Extension scripts live at the top of sql/ with a 1xx prefix --- because initdb runs only the directory's top level; the doc-07 --- sql/ext/ subdirectory would silently not install. +-- because initdb runs only the directory's top level; a +-- subdirectory would silently not install. BEGIN; diff --git a/sql/110_ext_optionals.sql b/sql/110_ext_optionals.sql index ef4eafc..422167a 100644 --- a/sql/110_ext_optionals.sql +++ b/sql/110_ext_optionals.sql @@ -1,10 +1,9 @@ -- The optionals extension, part one: the optional_type opaque type -- and the optional.of / optional.ofNonZeroValue / optional.none / -- value / hasValue functions (cel-go cel/library.go optionals, pinned --- v0.32.0). The optional-syntax sugar (x.?f, [?x], {?k: v}), or / --- orValue, and the optMap/optFlatMap macros arrive with the Phase 6 --- extension work; type_deduction's optional sections need only the --- declaration surface here. +-- v0.32.0). Part two, further down, adds the optional-syntax sugar +-- (x.?f, [?x], {?k: v}), or / orValue, and the optMap/optFlatMap +-- macros. -- -- An optional value is the opaque -- {"@t": "opaque", "type": "optional_type", "v": diff --git a/sql/120_ext_strings.sql b/sql/120_ext_strings.sql index 0ffc07f..dafb054 100644 --- a/sql/120_ext_strings.sql +++ b/sql/120_ext_strings.sql @@ -8,7 +8,7 @@ -- are character-based under UTF8, which lines up. One deliberate -- divergence from cel-go: indexOf/lastIndexOf with an out-of-range -- offset error instead of returning -1 -- the corpus and cel-java --- agree against cel-go v0.32.0 there (workspace ISSUES R2). +-- agree against cel-go v0.32.0 there. BEGIN; @@ -41,7 +41,7 @@ $$; -- Shared scan for indexOf / lastIndexOf. The empty-substring case -- returns the clamped offset before the bounds check (matching both -- cel-go and cel-java); a non-empty search with an out-of-range --- offset errors (corpus + cel-java adjudication, ISSUES R2). +-- offset errors (corpus + cel-java adjudication). CREATE OR REPLACE FUNCTION cel._str_index( s text, sub text, off numeric, backwards boolean ) diff --git a/sql/140_ext_lists.sql b/sql/140_ext_lists.sql index 3036091..ecfb4a8 100644 --- a/sql/140_ext_lists.sql +++ b/sql/140_ext_lists.sql @@ -127,9 +127,8 @@ BEGIN END IF; idx := idx || i; END LOOP; - -- Insertion sort on indices (stable, adequate for test-sized - -- lists; the no-performance-work rule was struck but conformance - -- sizes do not warrant more). + -- Insertion sort on indices: stable, and adequate for + -- conformance-sized lists. FOR i IN 2 .. n LOOP j := i; WHILE j > 1 LOOP From 018513d70f92866a6016673d8403e6878b0c0a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lemuel=20Roberto=20Bonif=C3=A1cio?= Date: Sun, 30 Aug 2026 21:18:56 -0300 Subject: [PATCH 5/5] Add an installation guide for AWS and self-hosted The README's install section assumed a checkout and knowledge of the sql/ file order, and said nothing about the release artifacts or managed PostgreSQL. The guide documents both channels -- plain SQL anywhere, and pg_tle on RDS/Aurora for real extension semantics -- plus checksum verification, the grants an application role needs, and the current upgrade story. Every command in it was run before being written down: the pg_tle path is the one CI validates per release, and the grants snippet was exercised with a fresh non-superuser role (evaluation works, registry writes are denied). --- README.md | 6 +- docs/INSTALL.md | 151 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 docs/INSTALL.md diff --git a/README.md b/README.md index 6b304d4..33e4170 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,11 @@ done ``` It needs a role that may create the `cel` schema. It does not need -superuser. +superuser. [docs/INSTALL.md](docs/INSTALL.md) is the full guide: +release artifacts and their checksums, self-hosted and AWS +RDS/Aurora instructions — including installing as a real extension +via [pg_tle](https://github.com/aws/pg_tle) — and the grants an +application role needs. ## Scope diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 0000000..9d30e13 --- /dev/null +++ b/docs/INSTALL.md @@ -0,0 +1,151 @@ +# Installing cel4postgres + +cel4postgres installs by running SQL against a database you can +already connect to. There is nothing to compile, no server package, +no filesystem access and no restart — which is what lets it install +on managed PostgreSQL (AWS RDS, Aurora) exactly as on a self-hosted +server. It is developed and tested against PostgreSQL 18. + +Two install channels exist: + +- **Plain SQL** — `psql -f` of a release artifact. Works anywhere, + needs only a role that may create the `cel` schema. +- **pg_tle** — on platforms that offer [pg_tle][pg_tle] (RDS, + Aurora), the same artifact registers as a real extension: + `CREATE EXTENSION cel4postgres`, version visible in `\dx`, clean + `DROP EXTENSION`. + +## Getting the files + +Download from the [releases page][releases]: + +| File | Contents | +|---|---| +| `cel4postgres--.sql` | everything: core + all extensions | +| `cel4postgres-core--.sql` | core only (`cel.evaluate`, standard library, well-known types) | +| `cel4postgres-ext_--.sql` | one extension library | +| `SHA256SUMS` | checksums for all of the above | + +Verify downloads before running them: + +```bash +sha256sum -c SHA256SUMS --ignore-missing +``` + +Extension libraries install into the same schema and are visible +only under their own environment name (`strings`, `math`, `lists`, +`encoders`, `bindings`, `optionals`, `two_var_comprehensions`, +`network`) — installing all of them changes nothing for callers +that use the `standard` environment. Extension files require the +core to be installed first. + +From a checkout instead, `./scripts/build-release.sh` produces the +same files under `dist/`. + +## Self-hosted PostgreSQL + +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" \ + -f cel4postgres--.sql +``` + +Then verify: + +```sql +SELECT cel.version(); +SELECT cel.evaluate('1 + 2', '{}', 'standard'); +-- {"v": 3, "@t": "int"} +``` + +To uninstall: `DROP SCHEMA cel CASCADE;` + +## AWS RDS and Aurora + +### Plain SQL (no prerequisites) + +The self-hosted instructions above work unchanged: connect as the +master user (or any role with `CREATE` on the database) and run the +artifact. No parameter group changes, no reboot. + +### As an extension, via pg_tle + +[pg_tle][pg_tle] (Trusted Language Extensions) is AWS's mechanism +for installing extensions without filesystem access. It gets you +real extension semantics: the version shows in `\dx`, and +`DROP EXTENSION cel4postgres` removes everything cleanly. This +path is validated in CI against pg_tle v1.5.2 for every release. + +One-time instance setup (this part needs a reboot; see the +[AWS documentation][aws-tle]): + +1. In the instance's DB parameter group, add `pg_tle` to + `shared_preload_libraries`, and reboot. +2. As the master user: + + ```sql + CREATE EXTENSION pg_tle; + GRANT pgtle_admin TO ; + ``` + +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`): + +```bash +./scripts/pgtle-wrap.sh cel4postgres \ + cel4postgres--.sql > cel4postgres.pgtle.sql + +psql -v ON_ERROR_STOP=1 "$DATABASE_URL" \ + -f cel4postgres.pgtle.sql \ + -c 'CREATE EXTENSION cel4postgres;' +``` + +To install the core plus a subset of extensions, pass the core +file followed by the chosen extension files to `pgtle-wrap.sh` +instead of the all-in bundle. + +To uninstall: + +```sql +DROP EXTENSION cel4postgres; +SELECT pgtle.uninstall_extension('cel4postgres'); +``` + +## Access control + +The installing role owns everything and is the only one that can +write the registry tables — which is the security boundary: whoever +can write `cel.overload` decides what the evaluator dispatches to. +Application roles get evaluation and read-only registry access. +For each application role: + +```sql +GRANT USAGE ON SCHEMA cel TO ; +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA cel TO ; +GRANT SELECT ON cel.type, cel.overload, cel.macro, + cel.env, cel.env_item, cel.schema_version + TO ; +``` + +All three grants are required: evaluation runs `SECURITY INVOKER`, +so the calling role itself executes the internal functions and +reads the registry tables. The role still cannot register anything +— registry writes stay owner-only. + +## Versions and upgrading + +`SELECT cel.version()` reports the installed version, recorded in +`cel.schema_version` at install time. + +There is no in-place upgrade path yet: moving to a new version +means uninstalling and installing the new artifact. Note that this +also removes anything you registered in the registry tables +yourself — re-register after reinstalling. + +[releases]: https://github.com/emfga/cel4postgres/releases +[pg_tle]: https://github.com/aws/pg_tle +[aws-tle]: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL_trusted_language_extension.html