diff --git a/CHANGELOG.md b/CHANGELOG.md index d9b5c68..f74bcb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ - Upgraded PostGIS to 3.6.4, pgBackRest to 2.59.3, system_stats to 4.0, supautils to 3.4.4, and pgvector to 0.8.7 in standard images - Upgraded Spock to 5.0.13 in the spock 5 images +- Added PostgreSQL 19 (rc2) postgres, minimal and standard images for spock 5. + They omit pg_tokenizer and vchord_bm25, which have no PG 19 build yet; the + tests covering those extensions are skipped on PG 19 until they ship. They + also omit pg_stat_monitor until a release that builds on PostgreSQL 19 RC2. + No coldfront or spock 6 images for PG 19 yet. - Upgraded lolor to 1.2.3 in minimal images - Upgraded system_stats to 4.1 in standard images diff --git a/examples/compose/distributed/docker-compose.yaml b/examples/compose/distributed/docker-compose.yaml index ce2acfe..447894a 100644 --- a/examples/compose/distributed/docker-compose.yaml +++ b/examples/compose/distributed/docker-compose.yaml @@ -113,7 +113,19 @@ configs: #!/usr/bin/env bash set -Eeo pipefail - EXTENSIONS=("pg_stat_statements" "pgaudit" "snowflake" "spock" "postgis-3" "pg_stat_monitor" "pg_tokenizer" "pg_cron" "vectorize" "pgedge_vectorizer") + WANTED=("pg_stat_statements" "pgaudit" "snowflake" "spock" "postgis-3" "pg_stat_monitor" "pg_tokenizer" "pg_cron" "vectorize" "pgedge_vectorizer") + + # Preload only the libraries this image ships: preloading a missing one + # stops the postmaster. Not every extension has a build for every major. + PKGLIBDIR=$$(pg_config --pkglibdir) + EXTENSIONS=() + for EXT in "$${WANTED[@]}"; do + if [[ -f "$$PKGLIBDIR/$$EXT.so" ]]; then + EXTENSIONS+=("$$EXT") + else + echo "Skipping $$EXT: not available in this image" + fi + done PGCONF="$$PGDATA/postgresql.conf" @@ -175,6 +187,11 @@ configs: echo "Initializing extensions: $${EXTENSIONS[*]}" for EXT in "$${EXTENSIONS[@]}"; do + AVAILABLE=$$(psql -v ON_ERROR_STOP=1 --username "$$POSTGRES_USER" --dbname "$$POSTGRES_DB" -tAc "SELECT 1 FROM pg_available_extensions WHERE name = '$$EXT';") + if [[ "$$AVAILABLE" != "1" ]]; then + echo "Skipping extension $$EXT: not available in this image" + continue + fi echo "Creating extension: $$EXT" psql -v ON_ERROR_STOP=1 --username "$$POSTGRES_USER" --dbname "$$POSTGRES_DB" -c "CREATE EXTENSION IF NOT EXISTS \"$$EXT\";" done diff --git a/examples/compose/enterprise/docker-compose.yaml b/examples/compose/enterprise/docker-compose.yaml index 6ece451..1e2d258 100644 --- a/examples/compose/enterprise/docker-compose.yaml +++ b/examples/compose/enterprise/docker-compose.yaml @@ -33,7 +33,19 @@ configs: #!/usr/bin/env bash set -Eeo pipefail - EXTENSIONS=("pg_stat_statements" "pgaudit" "snowflake" "spock" "postgis-3" "pg_stat_monitor" "pg_tokenizer" "pg_cron" "vectorize" "pgedge_vectorizer") + WANTED=("pg_stat_statements" "pgaudit" "snowflake" "spock" "postgis-3" "pg_stat_monitor" "pg_tokenizer" "pg_cron" "vectorize" "pgedge_vectorizer") + + # Preload only the libraries this image ships: preloading a missing one + # stops the postmaster. Not every extension has a build for every major. + PKGLIBDIR=$$(pg_config --pkglibdir) + EXTENSIONS=() + for EXT in "$${WANTED[@]}"; do + if [[ -f "$$PKGLIBDIR/$$EXT.so" ]]; then + EXTENSIONS+=("$$EXT") + else + echo "Skipping $$EXT: not available in this image" + fi + done PGCONF="$$PGDATA/postgresql.conf" @@ -95,6 +107,11 @@ configs: echo "Initializing extensions: $${EXTENSIONS[*]}" for EXT in "$${EXTENSIONS[@]}"; do + AVAILABLE=$$(psql -v ON_ERROR_STOP=1 --username "$$POSTGRES_USER" --dbname "$$POSTGRES_DB" -tAc "SELECT 1 FROM pg_available_extensions WHERE name = '$$EXT';") + if [[ "$$AVAILABLE" != "1" ]]; then + echo "Skipping extension $$EXT: not available in this image" + continue + fi echo "Creating extension: $$EXT" psql -v ON_ERROR_STOP=1 --username "$$POSTGRES_USER" --dbname "$$POSTGRES_DB" -c "CREATE EXTENSION IF NOT EXISTS \"$$EXT\";" done \ No newline at end of file diff --git a/packagelists/amd64/pg19-rc2-postgres.txt b/packagelists/amd64/pg19-rc2-postgres.txt new file mode 100644 index 0000000..c9176bf --- /dev/null +++ b/packagelists/amd64/pg19-rc2-postgres.txt @@ -0,0 +1,2 @@ +pgedge-postgresql19-19-rc2_1.el9 +pgedge-postgresql19-server-19-rc2_1.el9 diff --git a/packagelists/amd64/pg19-rc2-spock5.0.14-minimal.txt b/packagelists/amd64/pg19-rc2-spock5.0.14-minimal.txt new file mode 100644 index 0000000..627fb32 --- /dev/null +++ b/packagelists/amd64/pg19-rc2-spock5.0.14-minimal.txt @@ -0,0 +1,3 @@ +pgedge-spock50_19-5.0.14-1.el9 +pgedge-snowflake_19-2.6.1-1.el9 +pgedge-lolor_19-1.2.3-1.el9 diff --git a/packagelists/amd64/pg19-rc2-spock5.0.14-standard.txt b/packagelists/amd64/pg19-rc2-spock5.0.14-standard.txt new file mode 100644 index 0000000..da3ad37 --- /dev/null +++ b/packagelists/amd64/pg19-rc2-spock5.0.14-standard.txt @@ -0,0 +1,12 @@ +pgedge-pgaudit_19-19-rc1_1.el9 +pgedge-postgis36_19-3.6.4-2.el9 +pgedge-pgvector_19-0.8.7-1.el9 +pgedge-pgbackrest-2.60.0-1.el9 +pgedge-python3-psycopg2-2.9.12-1.el9 +pgedge-pgmq_19-1.13.1-1.el9 +pgedge-pg_cron_19-1.6.8-2.el9 +pgedge-pg-vectorize_19-0.27.1-1.el9 +pgedge-vectorizer_19-1.1-1.el9 +pgedge-system_stats_19-4.1-1.el9 +pgedge-supautils_19-4.0.0-1.el9 +pgedge-safesession_19-1.1-1.el9 diff --git a/packagelists/arm64/pg19-rc2-postgres.txt b/packagelists/arm64/pg19-rc2-postgres.txt new file mode 100644 index 0000000..c9176bf --- /dev/null +++ b/packagelists/arm64/pg19-rc2-postgres.txt @@ -0,0 +1,2 @@ +pgedge-postgresql19-19-rc2_1.el9 +pgedge-postgresql19-server-19-rc2_1.el9 diff --git a/packagelists/arm64/pg19-rc2-spock5.0.14-minimal.txt b/packagelists/arm64/pg19-rc2-spock5.0.14-minimal.txt new file mode 100644 index 0000000..627fb32 --- /dev/null +++ b/packagelists/arm64/pg19-rc2-spock5.0.14-minimal.txt @@ -0,0 +1,3 @@ +pgedge-spock50_19-5.0.14-1.el9 +pgedge-snowflake_19-2.6.1-1.el9 +pgedge-lolor_19-1.2.3-1.el9 diff --git a/packagelists/arm64/pg19-rc2-spock5.0.14-standard.txt b/packagelists/arm64/pg19-rc2-spock5.0.14-standard.txt new file mode 100644 index 0000000..da3ad37 --- /dev/null +++ b/packagelists/arm64/pg19-rc2-spock5.0.14-standard.txt @@ -0,0 +1,12 @@ +pgedge-pgaudit_19-19-rc1_1.el9 +pgedge-postgis36_19-3.6.4-2.el9 +pgedge-pgvector_19-0.8.7-1.el9 +pgedge-pgbackrest-2.60.0-1.el9 +pgedge-python3-psycopg2-2.9.12-1.el9 +pgedge-pgmq_19-1.13.1-1.el9 +pgedge-pg_cron_19-1.6.8-2.el9 +pgedge-pg-vectorize_19-0.27.1-1.el9 +pgedge-vectorizer_19-1.1-1.el9 +pgedge-system_stats_19-4.1-1.el9 +pgedge-supautils_19-4.0.0-1.el9 +pgedge-safesession_19-1.1-1.el9 diff --git a/scripts/build_pgedge_images.py b/scripts/build_pgedge_images.py index 2c952b7..65dc7f5 100755 --- a/scripts/build_pgedge_images.py +++ b/scripts/build_pgedge_images.py @@ -3,6 +3,7 @@ import json import logging import os +import re import subprocess from dataclasses import dataclass @@ -94,7 +95,9 @@ class PgEdgeImage: @property def postgres_major(self) -> str: - return self.postgres_version.split(".")[0] + # Leading digits, so a pre-release version like "19-rc1" still yields + # the major the packages and paths use. + return re.match(r"\d+", self.postgres_version).group() @property def spock_major(self) -> str: @@ -272,6 +275,10 @@ def make_flavor_images( postgres_version="18.6", spock_version="", epoch=1, flavor="postgres", is_latest_for_pg_major=True, ), + PgEdgeImage( + postgres_version="19-rc2", spock_version="", epoch=1, flavor="postgres", + is_latest_for_pg_major=True, package_release_channel="staging", + ), # pg16 images *make_flavor_images( postgres_version="16.15", @@ -296,6 +303,15 @@ def make_flavor_images( is_latest_for_pg_major=True, is_latest_for_spock_major=True, ), + # pg19 images. No coldfront or spock 6 line yet: neither has a PG 19 build. + *make_flavor_images( + postgres_version="19-rc2", + spock_version="5.0.14", + epochs={"minimal": 1, "standard": 1}, + channels={"minimal": "staging", "standard": "staging"}, + is_latest_for_pg_major=True, + is_latest_for_spock_major=True, + ), # pg16 spock60 images *make_flavor_images( postgres_version="16.15", diff --git a/tests/main.go b/tests/main.go index 3182c5b..a135935 100644 --- a/tests/main.go +++ b/tests/main.go @@ -7,6 +7,8 @@ import ( "fmt" "log" "os" + "sort" + "strconv" "strings" "time" @@ -30,6 +32,10 @@ type Test struct { MinimalOnly bool // Only run on minimal-or-later (needs the pgEdge extensions) StandardOnly bool // Only run on standard-or-later flavors (standard, coldfront) ColdfrontOnly bool // Only run on the coldfront flavor + + // TEMPORARY: see hasTokenizerBM25. Remove this field, every use of it, and + // the tests it guards once PG 19 has pg_tokenizer and vchord_bm25 builds. + TokenizerBM25Only bool } // includesMinimal reports whether a flavor ships the pgEdge extensions minimal @@ -63,6 +69,11 @@ type DefaultEntrypointRunner struct { func main() { image, flavor := parseFlags() + // TEMPORARY: see hasTokenizerBM25. + if major := pgMajorFromImage(image); major >= 19 { + hasTokenizerBM25 = false + } + // Derive the expected spock major version from the image tag (e.g. // "...-spock6.0.0-beta1-standard" -> "6"). Used to assert that the image // actually ships the spock version its tag advertises. @@ -113,6 +124,39 @@ func spockMajorFromImage(image string) string { return major.String() } +// TEMPORARY: pg_tokenizer and vchord_bm25 have no PG 19 build, so the PG 19 +// packagelists omit them. Tests that need either are skipped there, and +// pg_tokenizer is left out of shared_preload_libraries. Delete this, the +// Test.TokenizerBM25Only field and the tests it guards once both ship for +// PG 19; the suite is then the same on every major again. +var hasTokenizerBM25 = true + +// pgMajorFromImage extracts the PostgreSQL major from an image reference's tag, +// e.g. "...:18.6-spock5.0.13-standard-1" yields 18 and "...:19-spock5-minimal" +// yields 19. It returns 0 when the reference has no tag or the tag does not +// start with a number. Only a colon after the last "/" starts a tag, so the +// port in an untagged "127.0.0.1:5000/pgedge-postgres" is not read as a major. +func pgMajorFromImage(image string) int { + tag := "" + if idx := strings.LastIndex(image, ":"); idx > strings.LastIndex(image, "/") { + tag = image[idx+1:] + } + + var digits strings.Builder + for _, c := range tag { + if c < '0' || c > '9' { + break + } + digits.WriteRune(c) + } + + major, err := strconv.Atoi(digits.String()) + if err != nil { + return 0 + } + return major +} + func parseFlags() (string, string) { image := flag.String("image", "", "Docker image to test (required)") flavor := flag.String("flavor", "", "Image flavor: postgres, minimal, standard or coldfront (required)") @@ -131,6 +175,21 @@ func parseFlags() (string, string) { log.Fatalf("Invalid flavor '%s'. Must be 'postgres', 'minimal', 'standard' or 'coldfront'", *flavor) } + // The PostgreSQL major decides which extensions the image is expected to + // ship, and that in turn decides shared_preload_libraries. Getting it wrong + // does not merely skip an assertion: preloading a module the image does not + // have stops the postmaster, so a reference we cannot read a major from is + // refused here rather than misconfiguring the container. A digest-only + // reference is the common case -- "@sha256:..." puts hex where the tag + // belongs. CI retags by digest before calling this, so it passes a tag. + if strings.Contains(*image, "@") { + log.Fatalf("Image %q is a digest reference; pass a tagged one, e.g. %s:19-rc2-spock5.0.13-standard-1", + *image, strings.SplitN(*image, "@", 2)[0]) + } + if pgMajorFromImage(*image) == 0 { + log.Fatalf("Cannot read a PostgreSQL major from image %q. The tag has to start with it, e.g. 18.6-... or 19-rc2-...", *image) + } + return *image, *flavor } @@ -222,6 +281,27 @@ func printPhaseHeader(title string) { fmt.Println() } +// skippedSuites describes the tests withheld on this image and why. A gate that +// silently skips is a gate nobody notices has outlived its cause, so the counts +// are printed on every run. +func skippedSuites(flavor string) []string { + if !includesStandard(flavor) { + return nil + } + + var out []string + if !hasTokenizerBM25 { + n := 0 + for _, t := range getExtensionCustomScriptsTests() { + if t.TokenizerBM25Only { + n++ + } + } + out = append(out, fmt.Sprintf("%d skipped: image ships no pg_tokenizer/vchord_bm25 (see hasTokenizerBM25)", n)) + } + return out +} + func printSummary(errorCount int, flavor, spockMajor string) { tests := buildTestSuite(spockMajor) extensionTests := 0 @@ -235,6 +315,9 @@ func printSummary(errorCount int, flavor, spockMajor string) { if t.ColdfrontOnly && flavor != "coldfront" { continue } + if t.TokenizerBM25Only && !hasTokenizerBM25 { + continue + } extensionTests++ } @@ -249,6 +332,9 @@ func printSummary(errorCount int, flavor, spockMajor string) { fmt.Println() fmt.Println("Test Summary") fmt.Printf(" Tests Executed: %d\n", testsRun) + for _, line := range skippedSuites(flavor) { + fmt.Printf(" %s\n", line) + } fmt.Printf(" Errors: %d\n", errorCount) if errorCount == 0 { fmt.Printf(" Status: ✅ ALL TESTS PASSED\n") @@ -535,7 +621,12 @@ func (r *TestRunner) Start() error { sharedLibs = "spock,snowflake" } if includesStandard(r.flavor) { - sharedLibs = "spock,snowflake,pgaudit,supautils,pg_cron,pg_tokenizer" + sharedLibs = "spock,snowflake,pgaudit,supautils,pg_cron" + // TEMPORARY: see hasTokenizerBM25. Preloading an extension the image + // does not ship stops the postmaster from starting at all. + if hasTokenizerBM25 { + sharedLibs += ",pg_tokenizer" + } } // pg_duckdb and coldfront install hooks at postmaster start. The image's own // entrypoint already passes them, but the -c built below is appended after it @@ -784,6 +875,9 @@ func (r *TestRunner) RunTests(tests []Test) int { if test.ColdfrontOnly && r.flavor != "coldfront" { continue } + if test.TokenizerBM25Only && !hasTokenizerBM25 { + continue + } fmt.Printf(" %-55s ", test.Name) @@ -1183,6 +1277,22 @@ func expectFailureContaining(want string) func(exitCode int, output string) erro // extension's after-create.sql granting the database's own owner exactly // the access it documents, in whichever schema the extension actually // landed in, not a hardcoded one. +// privilegedExtensions is the supautils.privileged_extensions list the gate +// tests set. TEMPORARY: see hasTokenizerBM25 -- the two entries are dropped on +// a major that has no build for them, so supautils does not gate an extension +// that cannot be installed. +func privilegedExtensions() string { + extensions := []string{ + "address_standardizer", "address_standardizer_data_us", "pg_cron", + "postgis", "postgis_tiger_geocoder", "postgis_topology", + } + if hasTokenizerBM25 { + extensions = append(extensions, "pg_tokenizer", "vchord_bm25") + } + sort.Strings(extensions) + return strings.Join(extensions, ", ") +} + func getExtensionCustomScriptsTests() []Test { return []Test{ { @@ -1214,7 +1324,7 @@ func getExtensionCustomScriptsTests() []Test { { Name: "configure supautils.privileged_extensions", StandardOnly: true, - Cmd: "psql -U postgres -d testdb -t -A -c \"ALTER SYSTEM SET supautils.privileged_extensions = 'address_standardizer, address_standardizer_data_us, pg_cron, pg_tokenizer, vchord_bm25, postgis, postgis_tiger_geocoder, postgis_topology';\"", + Cmd: "psql -U postgres -d testdb -t -A -c \"ALTER SYSTEM SET supautils.privileged_extensions = '" + privilegedExtensions() + "';\"", ExpectedOutput: expectSuccess, }, { @@ -1380,20 +1490,22 @@ func getExtensionCustomScriptsTests() []Test { ExpectedOutput: expectFailureContaining("permission denied for table job"), }, { - Name: "gate installs pg_tokenizer", - StandardOnly: true, - Cmd: "psql -U gate_test_role -d testdb -t -A -c \"CREATE EXTENSION pg_tokenizer;\"", - ExpectedOutput: expectSuccess, + Name: "gate installs pg_tokenizer", + StandardOnly: true, + Cmd: "psql -U gate_test_role -d testdb -t -A -c \"CREATE EXTENSION pg_tokenizer;\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { // Real query against a real table in tokenizer_catalog, the // same reasoning as address_standardizer_data_us above: a // table grant with no matching schema USAGE fails here even // though has_table_privilege would report true. - Name: "pg_tokenizer after-create.sql granted access to tokenizer_catalog", - StandardOnly: true, - Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT count(*) FROM tokenizer_catalog.tokenizer;\"", - ExpectedOutput: expectSuccess, + Name: "pg_tokenizer after-create.sql granted access to tokenizer_catalog", + StandardOnly: true, + Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT count(*) FROM tokenizer_catalog.tokenizer;\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { // The configuration functions run as the caller and write the @@ -1403,60 +1515,68 @@ func getExtensionCustomScriptsTests() []Test { // the owner INSERT, UPDATE, and DELETE on those tables. The // tokenizer is left in place for the third-party role's // tokenize() case below, and dropped after it. - Name: "pg_tokenizer after-create.sql lets the owner create a text analyzer", - StandardOnly: true, - Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_text_analyzer('probe_analyzer', $$ pre_tokenizer = 'unicode_segmentation' $$);\"", - ExpectedOutput: expectSuccess, + Name: "pg_tokenizer after-create.sql lets the owner create a text analyzer", + StandardOnly: true, + Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_text_analyzer('probe_analyzer', $$ pre_tokenizer = 'unicode_segmentation' $$);\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { - Name: "pg_tokenizer after-create.sql lets the owner create a tokenizer", - StandardOnly: true, - Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_tokenizer('probe_tokenizer', $$ model = 'bert_base_uncased' $$);\"", - ExpectedOutput: expectSuccess, + Name: "pg_tokenizer after-create.sql lets the owner create a tokenizer", + StandardOnly: true, + Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_tokenizer('probe_tokenizer', $$ model = 'bert_base_uncased' $$);\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { - Name: "pg_tokenizer after-create.sql lets the owner create a stopword list", - StandardOnly: true, - Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_stopwords('probe_stopwords', E'it\\nis\\na');\"", - ExpectedOutput: expectSuccess, + Name: "pg_tokenizer after-create.sql lets the owner create a stopword list", + StandardOnly: true, + Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_stopwords('probe_stopwords', E'it\\nis\\na');\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { - Name: "pg_tokenizer after-create.sql lets the owner create a synonym list", - StandardOnly: true, - Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_synonym('probe_synonyms', 'pgsql postgres postgresql');\"", - ExpectedOutput: expectSuccess, + Name: "pg_tokenizer after-create.sql lets the owner create a synonym list", + StandardOnly: true, + Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_synonym('probe_synonyms', 'pgsql postgres postgresql');\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { // A superuser adding a table to tokenizer_catalog after // install, as an extension upgrade would: the default // privileges after-create.sql sets give the owner write // access to it without the script being run again. - Name: "pg_tokenizer after-create.sql default privileges cover a table added later", - StandardOnly: true, - Cmd: "psql -U postgres -d testdb -t -A -c \"CREATE TABLE tokenizer_catalog.probe_added_later(id int);\"", - ExpectedOutput: expectSuccess, + Name: "pg_tokenizer after-create.sql default privileges cover a table added later", + StandardOnly: true, + Cmd: "psql -U postgres -d testdb -t -A -c \"CREATE TABLE tokenizer_catalog.probe_added_later(id int);\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { - Name: "pg_tokenizer after-create.sql lets the owner write a table added later", - StandardOnly: true, - Cmd: "psql -U gate_test_role -d testdb -t -A -c \"INSERT INTO tokenizer_catalog.probe_added_later VALUES (1); UPDATE tokenizer_catalog.probe_added_later SET id = 2; DELETE FROM tokenizer_catalog.probe_added_later;\"", - ExpectedOutput: expectSuccess, + Name: "pg_tokenizer after-create.sql lets the owner write a table added later", + StandardOnly: true, + Cmd: "psql -U gate_test_role -d testdb -t -A -c \"INSERT INTO tokenizer_catalog.probe_added_later VALUES (1); UPDATE tokenizer_catalog.probe_added_later SET id = 2; DELETE FROM tokenizer_catalog.probe_added_later;\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { - Name: "gate installs vchord_bm25", - StandardOnly: true, - Cmd: "psql -U gate_test_role -d testdb -t -A -c \"CREATE EXTENSION vchord_bm25;\"", - ExpectedOutput: expectSuccess, + Name: "gate installs vchord_bm25", + StandardOnly: true, + Cmd: "psql -U gate_test_role -d testdb -t -A -c \"CREATE EXTENSION vchord_bm25;\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { // bm25_catalog holds only the bm25vector type and its support // functions, no tables: declaring a column of that type is // the real thing the database's owner needs USAGE on the // schema for. - Name: "vchord_bm25 after-create.sql granted access to bm25_catalog", - StandardOnly: true, - Cmd: "psql -U gate_test_role -d testdb -t -A -c \"CREATE TABLE bm25_probe(id int, v bm25_catalog.bm25vector);\"", - ExpectedOutput: expectSuccess, + Name: "vchord_bm25 after-create.sql granted access to bm25_catalog", + StandardOnly: true, + Cmd: "psql -U gate_test_role -d testdb -t -A -c \"CREATE TABLE bm25_probe(id int, v bm25_catalog.bm25vector);\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { // The earlier, unrelated common extension test already @@ -1616,18 +1736,20 @@ func getExtensionCustomScriptsTests() []Test { ExpectedOutput: expectSuccess, }, { - Name: "third-party role reaches vchord_bm25/pg_tokenizer via PUBLIC", - StandardOnly: true, - Cmd: "psql -U reporting_role -d testdb -t -A -c \"SELECT pg_typeof('{1:1}'::bm25_catalog.bm25vector); SELECT count(*) FROM tokenizer_catalog.tokenizer;\"", - ExpectedOutput: expectSuccess, + Name: "third-party role reaches vchord_bm25/pg_tokenizer via PUBLIC", + StandardOnly: true, + Cmd: "psql -U reporting_role -d testdb -t -A -c \"SELECT pg_typeof('{1:1}'::bm25_catalog.bm25vector); SELECT count(*) FROM tokenizer_catalog.tokenizer;\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { // The write side stays pg_database_owner-only regardless // of the PUBLIC read grant above. - Name: "third-party role still refused a tokenizer_catalog write", - StandardOnly: true, - Cmd: "psql -U reporting_role -d testdb -t -A -c \"INSERT INTO tokenizer_catalog.tokenizer(name) VALUES ('probe');\"", - ExpectedOutput: expectFailureContaining("permission denied for table tokenizer"), + Name: "third-party role still refused a tokenizer_catalog write", + StandardOnly: true, + Cmd: "psql -U reporting_role -d testdb -t -A -c \"INSERT INTO tokenizer_catalog.tokenizer(name) VALUES ('probe');\"", + ExpectedOutput: expectFailureContaining("permission denied for table tokenizer"), + TokenizerBM25Only: true, }, { // Schema USAGE makes every function in tokenizer_catalog @@ -1638,19 +1760,21 @@ func getExtensionCustomScriptsTests() []Test { // them SECURITY DEFINER, but still real work running before // any table-ACL check fires. Confirmed cleanly refused at // the function call itself now, not at some later step. - Name: "third-party role refused the tokenizer_catalog config-management functions", - StandardOnly: true, - Cmd: "psql -U reporting_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_tokenizer('probe', 'x');\"", - ExpectedOutput: expectFailureContaining("permission denied for function create_tokenizer"), + Name: "third-party role refused the tokenizer_catalog config-management functions", + StandardOnly: true, + Cmd: "psql -U reporting_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_tokenizer('probe', 'x');\"", + ExpectedOutput: expectFailureContaining("permission denied for function create_tokenizer"), + TokenizerBM25Only: true, }, { // A model function parses its configuration and loads a model // before it writes anything, so refusing it at EXECUTE, not at // the table write, is what keeps that work from running. - Name: "third-party role refused the tokenizer_catalog model functions", - StandardOnly: true, - Cmd: "psql -U reporting_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_huggingface_model('probe', 'x');\"", - ExpectedOutput: expectFailureContaining("permission denied for function create_huggingface_model"), + Name: "third-party role refused the tokenizer_catalog model functions", + StandardOnly: true, + Cmd: "psql -U reporting_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_huggingface_model('probe', 'x');\"", + ExpectedOutput: expectFailureContaining("permission denied for function create_huggingface_model"), + TokenizerBM25Only: true, }, { // The three functions the read-only use case actually @@ -1658,19 +1782,21 @@ func getExtensionCustomScriptsTests() []Test { // apply_text_analyzer() to process text against an // existing configuration, list_preload_models() to see // what's available. - Name: "third-party role keeps the read-only tokenizer_catalog functions", - StandardOnly: true, - Cmd: "psql -U reporting_role -d testdb -t -A -c \"SELECT tokenizer_catalog.list_preload_models();\"", - ExpectedOutput: expectSuccess, + Name: "third-party role keeps the read-only tokenizer_catalog functions", + StandardOnly: true, + Cmd: "psql -U reporting_role -d testdb -t -A -c \"SELECT tokenizer_catalog.list_preload_models();\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { // Against the tokenizer and text analyzer the owner created // above: the configuration the owner manages is usable by // every role, not only readable. - Name: "third-party role uses the owner's tokenizer and text analyzer", - StandardOnly: true, - Cmd: "psql -U reporting_role -d testdb -t -A -c \"SELECT tokenizer_catalog.tokenize('PostgreSQL is great', 'probe_tokenizer'); SELECT tokenizer_catalog.apply_text_analyzer('PostgreSQL is great', 'probe_analyzer');\"", - ExpectedOutput: expectSuccess, + Name: "third-party role uses the owner's tokenizer and text analyzer", + StandardOnly: true, + Cmd: "psql -U reporting_role -d testdb -t -A -c \"SELECT tokenizer_catalog.tokenize('PostgreSQL is great', 'probe_tokenizer'); SELECT tokenizer_catalog.apply_text_analyzer('PostgreSQL is great', 'probe_analyzer');\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { // pg_cron is deliberately not part of this fix: cron.job is @@ -1685,10 +1811,11 @@ func getExtensionCustomScriptsTests() []Test { { // Dropped only now, after the third-party role's tokenize() // case above has used them. - Name: "pg_tokenizer after-create.sql lets the owner drop what it created", - StandardOnly: true, - Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT tokenizer_catalog.drop_synonym('probe_synonyms'); SELECT tokenizer_catalog.drop_stopwords('probe_stopwords'); SELECT tokenizer_catalog.drop_tokenizer('probe_tokenizer'); SELECT tokenizer_catalog.drop_text_analyzer('probe_analyzer');\"", - ExpectedOutput: expectSuccess, + Name: "pg_tokenizer after-create.sql lets the owner drop what it created", + StandardOnly: true, + Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT tokenizer_catalog.drop_synonym('probe_synonyms'); SELECT tokenizer_catalog.drop_stopwords('probe_stopwords'); SELECT tokenizer_catalog.drop_tokenizer('probe_tokenizer'); SELECT tokenizer_catalog.drop_text_analyzer('probe_analyzer');\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { // The reason every script grants to pg_database_owner rather @@ -1702,16 +1829,18 @@ func getExtensionCustomScriptsTests() []Test { ExpectedOutput: expectSuccess, }, { - Name: "new owner inherits the grants after-create.sql gave pg_database_owner", - StandardOnly: true, - Cmd: "psql -U new_owner_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_tokenizer('probe_reassigned', $$ model = 'bert_base_uncased' $$); SELECT tokenizer_catalog.drop_tokenizer('probe_reassigned');\"", - ExpectedOutput: expectSuccess, + Name: "new owner inherits the grants after-create.sql gave pg_database_owner", + StandardOnly: true, + Cmd: "psql -U new_owner_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_tokenizer('probe_reassigned', $$ model = 'bert_base_uncased' $$); SELECT tokenizer_catalog.drop_tokenizer('probe_reassigned');\"", + ExpectedOutput: expectSuccess, + TokenizerBM25Only: true, }, { - Name: "former owner loses the grants after-create.sql gave pg_database_owner", - StandardOnly: true, - Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_tokenizer('probe_reassigned', $$ model = 'bert_base_uncased' $$);\"", - ExpectedOutput: expectFailureContaining("permission denied for function create_tokenizer"), + Name: "former owner loses the grants after-create.sql gave pg_database_owner", + StandardOnly: true, + Cmd: "psql -U gate_test_role -d testdb -t -A -c \"SELECT tokenizer_catalog.create_tokenizer('probe_reassigned', $$ model = 'bert_base_uncased' $$);\"", + ExpectedOutput: expectFailureContaining("permission denied for function create_tokenizer"), + TokenizerBM25Only: true, }, } }