diff --git a/CHANGELOG.md b/CHANGELOG.md index bb4188a..194d561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,18 @@ which was true until that script existed. setting to the other's storage. - `default_version` moves from `1.0-dev` to `1.0-alpha`, so `SELECT extversion FROM pg_extension` now agrees with `VERSION`. +- `CREATE INDEX` decodes only the columns the index needs (#413). The index + build received an `IndexInfo` carrying the key columns and the expression and + predicate trees, and discarded it, so a one-column index on a wide table read + every column. Both readers are now projected: the one a serial build opens for + itself, and the shared scan a parallel build arrives with, which comes through + the table-access-method scan interface and has nowhere to carry a projection. + The parallel branch is not a corner case. With every parallel setting left at + its default, a 1.5 million row table of incompressible text, 459 MB on disk, + is built in parallel, so that is the branch a table of consequential size + takes. On 300,000 rows of 20 columns on PostgreSQL 18, a one-column index + drops from 568 ms to 73 ms with workers allowed, against 563 ms for the same + index on a heap table. ### Upgrading diff --git a/src/columnar.h b/src/columnar.h index 6741e6c..7aeada7 100644 --- a/src/columnar.h +++ b/src/columnar.h @@ -451,6 +451,10 @@ extern void PgColumnarCheckFreeSpaceNoOverlap(uint64 storageId); * ------------------------------------------------------------------------- */ extern uint64 PgColumnarNextStorageId(void); extern void PgColumnarInsertNativeStorageRow(const NativeStorageMetadata *s); + +/* projection: needed attnos (pull_varattnos form) -> the reader's 0-based set */ +extern Bitmapset *PgColumnarProjectionFromAttnos(Bitmapset *needed, int natts, + int *nProjected); extern void PgColumnarSetSortedExtent(uint64 storageId, int64 firstGroup, int64 lastGroup); extern void PgColumnarCheckNativeFormatVersion(uint64 storageId, const char *relName); @@ -610,6 +614,18 @@ extern int64 PgColumnarWriteParquetFile(Relation rel, Snapshot snapshot, int nRestrictGroups); extern void PgColumnarParquetCheckExportable(Relation rel); +/* + * Column projection on an already-opened reader (#413). The table-AM scan + * interface has nowhere to carry a projection, so a reader obtained through it + * reads every column; a caller that knows better narrows it here, before the + * first read. PgColumnarReadProjectedCount reports what the reader WILL decode, + * read off colWanted, so a caller reporting a projection cannot report one it + * failed to apply. + */ +extern void PgColumnarReadSetProjection(PgColumnarReadState *readState, + Bitmapset *projectedColumns); +extern int PgColumnarReadProjectedCount(PgColumnarReadState *readState); + /* * Parallel scan (gap 23): point the read state at a shared atomic that hands out * stripe indices, so several workers scanning the same relation each claim diff --git a/src/columnar_customscan.c b/src/columnar_customscan.c index 6572757..5c57ef1 100644 --- a/src/columnar_customscan.c +++ b/src/columnar_customscan.c @@ -169,17 +169,23 @@ static const CustomExecMethods pgcolumnar_exec_methods = { * carry a ctid system Var). This is the projection pushed into the reader * (spec 9). */ -static Bitmapset * -pgcolumnar_projected_columns(CustomScan *cscan, int natts, int *nProjected) +/* + * PgColumnarProjectionFromAttnos + * Turn a set of needed attribute numbers, in pull_varattnos' offset form, + * into the reader's 0-based projection. Returns NULL for "read every + * column", which is what a system column, a whole-row Var, or an empty set + * all mean. + * + * Shared because index_build_range_scan needs the same computation over a + * different source (#413): its columns come from IndexInfo rather than from + * a plan. The escapes are the interesting part and are worth having once. + */ +Bitmapset * +PgColumnarProjectionFromAttnos(Bitmapset *needed, int natts, int *nProjected) { - Bitmapset *needed = NULL; Bitmapset *projected = NULL; - Index scanrelid = cscan->scan.scanrelid; int attno; - pull_varattnos((Node *) cscan->scan.plan.targetlist, scanrelid, &needed); - pull_varattnos((Node *) cscan->scan.plan.qual, scanrelid, &needed); - /* a system column or whole-row Var forces reading every column */ for (attno = FirstLowInvalidHeapAttributeNumber + 1; attno <= 0; attno++) { @@ -210,6 +216,19 @@ pgcolumnar_projected_columns(CustomScan *cscan, int natts, int *nProjected) return projected; } +static Bitmapset * +pgcolumnar_projected_columns(CustomScan *cscan, int natts, int *nProjected) +{ + Bitmapset *needed = NULL; + Index scanrelid = cscan->scan.scanrelid; + + pull_varattnos((Node *) cscan->scan.plan.targetlist, scanrelid, &needed); + pull_varattnos((Node *) cscan->scan.plan.qual, scanrelid, &needed); + + return PgColumnarProjectionFromAttnos(needed, natts, nProjected); +} + + /* * pgcolumnar_commute_strategy * The btree comparison strategy for "value op column" given the strategy diff --git a/src/columnar_reader.c b/src/columnar_reader.c index 4a1ebe2..9684b65 100644 --- a/src/columnar_reader.c +++ b/src/columnar_reader.c @@ -1553,6 +1553,78 @@ PgColumnarReadSetParallelCounter(PgColumnarReadState *readState, readState->parallelCounter = counter; } +/* + * PgColumnarReadSetProjection + * Narrow an already-opened reader to a set of columns (issue #413). + * + * The table-AM scan interface has nowhere to put a projection, so a reader + * opened through pgcolumnar_scan_begin reads every column. A caller that + * does know which columns it needs -- an index build knows, from IndexInfo + * -- can say so here instead. + * + * Only legal before the first read. colWanted drives what the group loader + * decodes, and a group already loaded under a wider projection would be + * reused under a narrower one, so changing it mid-scan would silently + * return unset values rather than fail. The caller is expected to do this + * immediately after obtaining the reader; the assertion states the rule and + * the early return keeps a release build honest. + * + * A NULL set means "all columns", matching PgColumnarBeginRead. + */ +void +PgColumnarReadSetProjection(PgColumnarReadState *readState, + Bitmapset *projectedColumns) +{ + int pc; + MemoryContext old; + + Assert(!readState->started); + if (readState->started) + return; + + /* + * Copy into the read state's own context, not the caller's. Nothing reads + * this field after the setter today, so this is consistency rather than a + * fixed bug -- PgColumnarBeginRead builds the same field in readContext and + * PgColumnarReadRestrictToGroups says so in its own comment. A field owned by + * two different contexts depending on which function set it is a trap for + * whoever reads it next. + */ + old = MemoryContextSwitchTo(readState->readContext); + bms_free(readState->projectedColumns); + readState->projectedColumns = bms_copy(projectedColumns); + MemoryContextSwitchTo(old); + readState->allColumnsWanted = (projectedColumns == NULL || + !pgcolumnar_enable_column_projection); + for (pc = 0; pc < readState->natts; pc++) + readState->colWanted[pc] = readState->allColumnsWanted || + bms_is_member(pc, projectedColumns); +} + +/* + * PgColumnarReadProjectedCount + * How many columns this reader will actually decode. + * + * Read from colWanted, the field the group loader consults, so a caller + * reporting a projection reports what the reader WILL DO rather than what + * the caller computed and may have failed to apply. That distinction is + * the whole point: a projection computed and then dropped on the floor is + * exactly the bug this accessor exists to make visible (#413). + */ +int +PgColumnarReadProjectedCount(PgColumnarReadState *readState) +{ + int pc; + int n = 0; + + for (pc = 0; pc < readState->natts; pc++) + { + if (readState->colWanted[pc]) + n++; + } + return n; +} + /* * PgColumnarReadRestrictToGroups * Restrict this scan to the given row group numbers (issue #149). Groups diff --git a/src/columnar_tableam.c b/src/columnar_tableam.c index b497d69..5163516 100644 --- a/src/columnar_tableam.c +++ b/src/columnar_tableam.c @@ -29,6 +29,7 @@ #include "executor/tuptable.h" #include "miscadmin.h" #include "nodes/pathnodes.h" +#include "optimizer/optimizer.h" #include "optimizer/pathnode.h" #include "optimizer/plancat.h" #include "port/atomics.h" @@ -1431,6 +1432,8 @@ pgcolumnar_index_build_range_scan(Relation table_rel, Relation index_rel, { PgColumnarReadState *readState; bool ownReadState; + Bitmapset *projected; + int nProjected = 0; EState *estate; ExprContext *econtext; ExprState *predicate; @@ -1470,6 +1473,50 @@ pgcolumnar_index_build_range_scan(Relation table_rel, Relation index_rel, * snapshot. The reader advances the command id internally for * read-your-writes. */ + /* + * Project. The columns this build needs are all in our own arguments and we + * were throwing them away, so a one-column index on a wide table decoded + * every column (#413). + * + * Three sources, and missing any of them reads unset slot values: + * ii_IndexAttrNumbers the key columns + * ii_Expressions an expression index references more + * ii_Predicate a partial index evaluates against more + * + * PgColumnarProjectionFromAttnos returns NULL for "every column", which + * covers a whole-row or system-column reference, and is what the custom scan + * already does with the same escapes. + * + * Computed before the branch because BOTH readers need it. Every + * participant in a parallel build computes the same set from the same + * IndexInfo, so they agree without having to communicate. + */ + { + Bitmapset *needed = NULL; + int i; + + for (i = 0; i < index_info->ii_NumIndexAttrs; i++) + { + AttrNumber attno = index_info->ii_IndexAttrNumbers[i]; + + /* 0 marks an expression column; Vars come from ii_Expressions */ + if (attno != 0) + needed = bms_add_member(needed, + attno - FirstLowInvalidHeapAttributeNumber); + } + pull_varattnos((Node *) index_info->ii_Expressions, 1, &needed); + pull_varattnos((Node *) index_info->ii_Predicate, 1, &needed); + + /* + * nProjected is a required out-parameter, not the number we report. + * The DEBUG1 line below reads the count off the reader instead; see + * the comment there for why. + */ + projected = PgColumnarProjectionFromAttnos(needed, + RelationGetDescr(table_rel)->natts, + &nProjected); + } + if (scan != NULL) { /* @@ -1479,6 +1526,18 @@ pgcolumnar_index_build_range_scan(Relation table_rel, Relation index_rel, */ readState = pgcolumnar_scan_read_state((PgColumnarScanDesc) scan, RelationGetDescr(table_rel)); + + /* + * This reader was opened through the table-AM scan interface, which has + * nowhere to carry a projection, so it would decode every column. We + * know better here, so narrow it before the first read. + * + * This branch is not hypothetical and it is not the rare case: with + * parallel maintenance workers available, EVERY participant including + * the leader arrives here, and the serial branch below never runs. Fix + * only the serial branch and a parallel build stays unprojected. + */ + PgColumnarReadSetProjection(readState, projected); ownReadState = false; } else @@ -1490,10 +1549,32 @@ pgcolumnar_index_build_range_scan(Relation table_rel, Relation index_rel, else snapshot = GetTransactionSnapshot(); - readState = PgColumnarBeginRead(table_rel, snapshot, NULL, NULL, 0, NULL); + readState = PgColumnarBeginRead(table_rel, snapshot, NULL, + projected, 0, NULL); ownReadState = true; } + /* + * Say which branch ran and how wide the reader it produced will actually + * read, so a test can assert the projection NARROWED rather than infer it + * from a stopwatch. A fix that silently did nothing would pass every + * correctness check, and a wall-clock check on a quiet machine, which is + * the failure mode this projection is being added to avoid. + * + * The count comes from the READER, not from nProjected. Reporting what we + * computed would keep printing "1 of 20" if the parallel branch stopped + * applying it, and the assertion guarding that branch would pass while the + * build read every column. Reporting what the reader will decode cannot. + * + * DEBUG1, so it costs nothing at the default log level. + */ + elog(DEBUG1, + "columnar: %s index build on \"%s\" projecting %d of %d columns", + scan != NULL ? "parallel" : "serial", + RelationGetRelationName(index_rel), + PgColumnarReadProjectedCount(readState), + RelationGetDescr(table_rel)->natts); + while (true) { CHECK_FOR_INTERRUPTS(); diff --git a/test/native_index_projection.sh b/test/native_index_projection.sh new file mode 100755 index 0000000..919fa6e --- /dev/null +++ b/test/native_index_projection.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# +# CREATE INDEX projects only the columns the index needs (issue #413). +# +# pgcolumnar_index_build_range_scan opened its reader with no projection, so building a +# one-column index on a wide table decoded every column. It never had to: the callback +# receives IndexInfo, which carries ii_IndexAttrNumbers and the expression and predicate +# trees. The information was in its own arguments and was thrown away. +# +# Measured on 300,000 rows of 20 columns, index on the key alone, non-assert PG18: +# +# before columnar 1,403 ms heap 149 ms 9.4x slower than heap +# after columnar 87 ms heap 144 ms 1.65x faster +# +# What is asserted, in the order the risk sits: +# +# 1. the projection NARROWED, read from the build's own DEBUG1 line. A fix that +# silently did nothing passes every correctness check below, and a wall-clock check +# on a quiet machine, so this is the assertion with teeth. +# 2. expression and partial indexes project their EXTRA columns. Getting those wrong +# evaluates against unset slot values, which is a wrong answer and not a slow one. +# 3. a forced index scan and a forced seq scan agree over the FULL ordered result. +# Point lookups can be satisfied by an index that is wrong for keys nobody asked for. +# 4. the PARALLEL build projects as well. Its reader arrives through the table-AM scan +# interface, which carries no projection, and it is the branch core takes by default +# for any table of consequential size. See the section below for the measurements. +# +# amcheck runs where the build has contrib and skips VISIBLY where it does not. Source +# builds have no contrib, and a silent skip is the defect this file exists to prevent. +# +# Usage: test/native_index_projection.sh [PG_CONFIG] +# Written fresh for pgColumnar. +set -uo pipefail +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +pgc_setup "${1:-/usr/local/pg17/bin/pg_config}" + +ROWS=${PGC_IDXPROJ_ROWS:-300000} + +# lib.sh's q() emits psql's output for every statement, so "SET ...; SELECT ..." in one +# call returns "SET" as well as the value. Keep the session but read only the result. +qset() { # $1 = SET clause(s), $2 = query + env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres \ + -d "$PGC_DB" -At -q -c "$1" -c "$2" 2>/dev/null | tail -1 +} + + +# 20 columns: an int key, an int secondary, 18 wide text. +cols="k int, k2 int" +vals="g, g*2" +for i in $(seq 1 18); do cols="$cols, c$i text"; vals="$vals, repeat('x',80)||g"; done +psql_run "CREATE TABLE w ($cols) USING pgcolumnar;" +psql_run "INSERT INTO w SELECT $vals FROM generate_series(1,$ROWS) g;" +check "fixture rows" "$(q 'SELECT count(*) FROM w')" "$ROWS" +check "fixture is wide" \ + "$(q "SELECT count(*) FROM pg_attribute WHERE attrelid='w'::regclass AND attnum>0")" "20" + +# The projection each build chose, from its own DEBUG1 line. This is the check a +# do-nothing fix would fail and the correctness checks below would not. +proj() { # $1 index name, $2 CREATE INDEX statement -> "n of m" + env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" \ + -c "SET client_min_messages=debug1;" -c "$2" 2>&1 \ + | grep -oE "index build on \"$1\" projecting [0-9]+ of [0-9]+" \ + | grep -oE '[0-9]+ of [0-9]+' | head -1 +} + +check "plain index projects one column of twenty" \ + "$(proj w_k 'CREATE INDEX w_k ON w (k)')" "1 of 20" +check "two-column index projects two" \ + "$(proj w_k12 'CREATE INDEX w_k12 ON w (k, k2)')" "2 of 20" +check "an index on a late column projects one, not everything before it" \ + "$(proj w_c18 'CREATE INDEX w_c18 ON w (c18)')" "1 of 20" +check "an expression index projects its expression's columns" \ + "$(proj w_expr 'CREATE INDEX w_expr ON w ((k + k2))')" "2 of 20" +check "a partial index projects the predicate's columns too" \ + "$(proj w_part 'CREATE INDEX w_part ON w (k) WHERE k2 > 100')" "2 of 20" +check "an expression over a text column projects that column" \ + "$(proj w_len 'CREATE INDEX w_len ON w ((length(c1)))')" "1 of 20" + +# Correctness. An index built from under-projected data indexes unset slot values, so +# each of these must find what a sequential scan finds. +check "plain index finds the row" \ + "$(qset 'SET enable_seqscan=off' 'SELECT k2 FROM w WHERE k = 12345')" "24690" +check "expression index finds the row" \ + "$(qset 'SET enable_seqscan=off' 'SELECT count(*) FROM w WHERE (k + k2) = 30000')" "1" +check "partial index finds the row inside its predicate" \ + "$(qset 'SET enable_seqscan=off' 'SELECT count(*) FROM w WHERE k = 50000 AND k2 > 100')" "1" + +# The oracle, depending on nothing but PostgreSQL. Compare the FULL ordered result of a +# forced index scan against a forced seq scan. +# +# Both sides must be a real md5. A down cluster, an errored query or a predicate that +# matches nothing all yield "", and `check "" ""` compares nothing with nothing and +# prints PASS (#418). This oracle is the strongest assertion in the file, so it is the +# worst one to have silently comparing two empty strings. +# +# Local guard on purpose. #418 proposes `check_num` in `test/lib.sh` and +# @ChronicallyJD owns it; this file should adopt that helper when it lands and drop +# the check below. +agree() { # $1 label, $2 predicate, $3 selected expression + local viaix viaseq + viaix=$(qset "SET enable_seqscan=off; SET enable_bitmapscan=off" \ + "SELECT md5(string_agg(t::text, ',' ORDER BY t)) + FROM (SELECT $3 AS t FROM w WHERE $2) s") + viaseq=$(qset "SET enable_indexscan=off; SET enable_bitmapscan=off; SET enable_indexonlyscan=off" \ + "SELECT md5(string_agg(t::text, ',' ORDER BY t)) + FROM (SELECT $3 AS t FROM w WHERE $2) s") + if ! grep -qE '^[0-9a-f]{32}$' <<<"$viaix" || ! grep -qE '^[0-9a-f]{32}$' <<<"$viaseq"; then + check "$1 (both sides must be a real result, not empty)" \ + "index=[$viaix] seq=[$viaseq]" "two md5 hashes" + return + fi + check "$1" "$viaix" "$viaseq" +} +agree "plain index agrees with a sequential scan" "k BETWEEN 1000 AND 9999" "k" +agree "late-column index agrees" "c18 > repeat('x',80)||'99000'" "k" +agree "expression index agrees" "(k + k2) BETWEEN 300 AND 30000" "k" +agree "partial index agrees on its subset" "k2 > 100 AND k < 5000" "k" +agree "text expression index agrees" "length(c1) = 83" "k" + +# The PARALLEL build, which is the default path for any table of consequential size and +# was the one left unprojected. +# +# index_build_range_scan gets its reader two ways. A serial build opens its own, and +# every participant of a parallel build (leader included) arrives with the shared +# TableScanDesc, whose reader came through the table-AM scan interface with nowhere to +# carry a projection. Projecting only the serial branch reads every column exactly when +# it costs most. +# +# This is not a tuning corner. Measured on this fixture at 1.5M rows of incompressible +# text, 459 MB on disk, with every parallel GUC at its default, core chose workers and +# the build took the parallel branch. Forcing it here only keeps the assertion cheap. +# +# Serial-branch-only projection scores 568 ms against heap's 563 ms on the parallel arm, +# and 71 ms on the serial arm: the wall clock alone reads as "fixed" if you measure the +# serial arm, which is why the branch is named in the DEBUG1 line and asserted here. +PAR="SET max_parallel_maintenance_workers=4; SET min_parallel_table_scan_size='0'; + SET maintenance_work_mem='256MB';" + +branch() { # $1 index name, $2 SET clauses, $3 CREATE INDEX -> " of " + env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" \ + -c "SET client_min_messages=debug1; $2" -c "$3" 2>&1 \ + | grep -oE "(parallel|serial) index build on \"$1\" projecting [0-9]+ of [0-9]+" \ + | sed -E 's/ index build on .* projecting / /' | head -1 +} + +# Assert the PREMISE first. If core declines to go parallel here, the next check would +# pass by reading the serial branch and prove nothing about the one under test. +check "forcing parallel maintenance workers does reach the parallel branch" \ + "$(branch w_par "$PAR" 'CREATE INDEX w_par ON w (k)' | cut -d' ' -f1)" "parallel" +check "a parallel build projects one column of twenty, not all twenty" \ + "$(branch w_par2 "$PAR" 'CREATE INDEX w_par2 ON w (k2)')" "parallel 1 of 20" +check "a parallel expression build projects its expression's columns" \ + "$(branch w_pare "$PAR" 'CREATE INDEX w_pare ON w ((k + k2))')" "parallel 2 of 20" +check "a parallel partial build projects the predicate's columns too" \ + "$(branch w_parp "$PAR" 'CREATE INDEX w_parp ON w (k) WHERE k2 > 100')" "parallel 2 of 20" +# +# On c17, NOT on k2. A serially built index on k2 would sit beside the parallel-built +# w_par2 covering the same column, and the planner is free to pick either, so the two +# oracles below would silently validate the serial build. This check only has to show +# the serial branch is still reachable, so it goes on a column no oracle reads. +check "the serial branch is still reached when workers are refused" \ + "$(branch w_ser 'SET max_parallel_maintenance_workers=0;' \ + 'CREATE INDEX w_ser ON w (c17)' | cut -d' ' -f1)" "serial" + +# Every participant reads through one shared reader, so an under-projected or +# double-counted parallel build shows up as wrong or duplicated index entries. +# +# Assert the premise first. "The index scan agrees with the seq scan" proves nothing +# about a parallel build unless the scan actually uses the parallel-built index, and +# nothing in the query text guarantees that: it names a column, not an index. +# qset ends in `tail -1`, which would keep only the last plan line, so read the whole +# EXPLAIN directly. +usesix() { # $1 = query -> the index name the plan scans + env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" \ + -At -q -c "SET enable_seqscan=off; SET enable_bitmapscan=off;" \ + -c "EXPLAIN (COSTS OFF) $1" 2>/dev/null \ + | grep -oE 'using [a-z0-9_]+' | head -1 | cut -d' ' -f2 +} +check "the oracle below really reads the parallel-built index" \ + "$(usesix 'SELECT k2 FROM w WHERE k2 BETWEEN 2000 AND 19998')" "w_par2" +agree "parallel-built index agrees with a sequential scan" "k2 BETWEEN 2000 AND 19998" "k2" +check "parallel-built index returns each row once" \ + "$(qset 'SET enable_seqscan=off; SET enable_bitmapscan=off' \ + 'SELECT count(*) FROM w WHERE k2 BETWEEN 2 AND 2000')" "1000" + +# amcheck where available. The skip must be visible: a check that reports nothing is +# indistinguishable from a check that passes, which is what this file is about. +if psql_run "CREATE EXTENSION IF NOT EXISTS amcheck;" >/dev/null 2>&1 && + [ "$(q "SELECT count(*) FROM pg_proc WHERE proname='bt_index_check'")" != "0" ]; then + for ix in w_k w_k12 w_c18 w_expr w_part w_len w_par w_par2 w_pare w_parp w_ser; do + out=$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres \ + -d "$PGC_DB" -c "SELECT bt_index_check('$ix'::regclass)" 2>&1) + check "bt_index_check($ix)" \ + "$(grep -qE 'ERROR' <<<"$out" && echo bad || echo ok)" "ok" + done +else + echo "SKIP amcheck is not installed on this build; the seq-scan oracle above still ran" +fi + +pgc_summary diff --git a/test/run_all_versions.sh b/test/run_all_versions.sh index 3bb2cce..a647f6d 100755 --- a/test/run_all_versions.sh +++ b/test/run_all_versions.sh @@ -208,7 +208,7 @@ SRCDIR="${PGC_RUN_SRCDIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" SUITES=(harness_selftest docs_style smoke phase2 phase3 phase4 phase5 phase6 audit concurrency unique_conc \ differential recovery replication native_backend_crash fuzz fuzz_parquet fuzz_arrow hardening concurrent_diff parallel sorted_projection \ arrow_export parquet_export read_stream corruption \ - generated_columns temporal arrow_import index_only projections arrow_nested parquet_import parquet_nested arrow_nested_import parquet_nested_import native_writer native_roundtrip native_encoding native_fastdecode native_zonemap write_minmax_fastpath write_fsst_compressed fsst_margin encode_invariants encode_effort native_skip pushdown_report native_agg native_agg_deletes native_agg_addcolumn native_groupagg ungrouped_vector_agg parallel_vector_agg native_bloom bloom_setting bloom_lazy native_vecskip native_index native_fetch_position native_dml alter_column_type native_ios native_projection native_cluster pg19_vacuum_options native_repack native_compact native_recluster recluster_extent native_vacuum_race native_sort_by sort_status native_reclaim native_ownership drop_cleanup pg_dump_roundtrip native_reclaim_cycles native_reclaim_frag native_reclaim_reconcile native_gap native_format native_truncate native_rewrite native_rewrite_conc rewrite_group_scan native_parquet_schema native_read_parquet native_parquet_fdw native_parquet_pushdown native_parquet_hardening server_file_privilege native_parquet_stack native_parquet_units native_parquet_flba native_parquet_codecs native_parquet_projection native_parquet_multifile native_parquet_streaming native_parquet_partition native_cancel cancel_decode wal_envelope decode_interrupts import_exclusion import_deferred parallel_copy parallel_export_parquet fk_referencing row_triggers native_lazy_slot native_ctas native_fetch_cache native_fetch_interrupt analyze_stats analyze_reltuples native_fetch_projection column_projection isolation) + generated_columns temporal arrow_import index_only projections arrow_nested parquet_import parquet_nested arrow_nested_import parquet_nested_import native_writer native_roundtrip native_encoding native_fastdecode native_zonemap write_minmax_fastpath write_fsst_compressed fsst_margin encode_invariants encode_effort native_skip pushdown_report native_agg native_agg_deletes native_agg_addcolumn native_groupagg ungrouped_vector_agg parallel_vector_agg native_bloom bloom_setting bloom_lazy native_vecskip native_index native_index_projection native_fetch_position native_dml alter_column_type native_ios native_projection native_cluster pg19_vacuum_options native_repack native_compact native_recluster recluster_extent native_vacuum_race native_sort_by sort_status native_reclaim native_ownership drop_cleanup pg_dump_roundtrip native_reclaim_cycles native_reclaim_frag native_reclaim_reconcile native_gap native_format native_truncate native_rewrite native_rewrite_conc rewrite_group_scan native_parquet_schema native_read_parquet native_parquet_fdw native_parquet_pushdown native_parquet_hardening server_file_privilege native_parquet_stack native_parquet_units native_parquet_flba native_parquet_codecs native_parquet_projection native_parquet_multifile native_parquet_streaming native_parquet_partition native_cancel cancel_decode wal_envelope decode_interrupts import_exclusion import_deferred parallel_copy parallel_export_parquet fk_referencing row_triggers native_lazy_slot native_ctas native_fetch_cache native_fetch_interrupt analyze_stats analyze_reltuples native_fetch_projection column_projection isolation) # Default matrix: one assert-enabled pg_config per major, 15 through 19. DEFAULT_CONFIGS=(