diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9b7e75d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,245 @@ +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 + +# 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: + # 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 + steps: + - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - 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) + 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 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 }} + 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 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/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/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/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/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 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/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 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" diff --git a/sql/000_install.sql b/sql/000_install.sql index e4e968a..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; @@ -20,7 +22,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 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