Repository navigation
Quote comma-containing storage option values so PAX tables restore - #127
talmacschen-arch wants to merge 2 commits into
Conversation
GetTableStorage builds a table's WITH clause by joining pg_class.reloptions
with ", ". A single reloption is stored as a "name=value" element, and some
PAX storage options (minmax_columns, cluster_columns) hold a comma-separated
column list as their value. Joining the array verbatim makes the commas
inside such a value indistinguishable from the separator between options, so
the restored DDL
... USING pax WITH (minmax_columns=a,b, cluster_columns=a,b, ...)
fails with `ERROR: unrecognized parameter "b"` and the table is never
created. Its data load, ALTER ... OWNER, indexes and ANALYZE then all cascade
into "relation does not exist", so the table is lost on restore.
Single-quote the value of any reloption whose value contains a comma so the
list survives as one value. Options without a comma are emitted unchanged, so
backup output for existing tables is unaffected. This mirrors the existing
single-quoting in buildStorageOptionsString for storage servers.
Add an integration test that backs up a PAX table with multi-column
minmax_columns/cluster_columns and asserts the emitted WITH clause restores
verbatim.
Cluster validationVerified on a live Apache Cloudberry 2.1.0 / SynxDB4 (4.5.4-3) cluster with the patched A PAX table with multi-column list options was created and populated: CREATE TABLE public.pmm(a int, b int, c int)
USING pax WITH (minmax_columns='a,b', cluster_columns='a,b') DISTRIBUTED BY (a);
INSERT INTO public.pmm SELECT i, i%10, i%3 FROM generate_series(1,1000) i;
Kernel side ( Without this fix, |
liang8283
left a comment
There was a problem hiding this comment.
Thanks for tracking this down — the root cause analysis is spot on, and the fix works for regular tables. I verified it end to end on a Cloudberry cluster with a full gpbackup → gprestore round trip, building both main and this branch:
| Object | main |
this PR |
|---|---|---|
PAX table, minmax_columns='a,b', cluster_columns='a,b', ... |
❌ unrecognized parameter "b", table + data lost |
✅ all rows restored |
PAX table, bloomfilter_columns='a,b', minmax_columns='b,c' |
❌ | ✅ |
| PAX partitioned table (parent + leaf partition) | ❌ | ✅ |
PAX table, minmax_columns='A,B c' (mixed-case / space in names) |
❌ | ✅ |
PAX table, minmax_columns='a' (single column) |
✅ | ✅ unchanged output |
PAX materialized view, minmax_columns='a,b' |
❌ | ❌ still fails |
The new integration test runs (not skipped) and passes on that cluster, and go test ./backup/... passes.
A few things I'd like to see addressed before merge:
1. Materialized views are not covered (blocking)
GetMaterializedViews builds its WITH clause the same way, in backup/queries_relations.go (both version6Query and atLeast7Query):
coalesce(' WITH (' || array_to_string(c.reloptions, ', ') || ')', '') AS options,So a PAX materialized view still produces a broken WITH clause:
CREATE MATERIALIZED VIEW public.pax_mv USING pax WITH (minmax_columns=a,b) AS ...
ERROR: unrecognized parameter "b" (SQLSTATE 22023)
followed by ALTER MATERIALIZED VIEW ... OWNER failing with relation does not exist. Could the reloption formatting be pulled into one shared SQL snippet (e.g. a Go helper that returns the expression for a given column) and used in all three places, plus a matching integration test for the matview case?
2. Quoting only on , leaves closely related cases broken (suggestion)
Any value that is not a plain lowercase identifier/number has the same problem, even without a comma. Both of these fail on main and on this branch:
minmax_columns='A' -> WITH (minmax_columns=A) -> ERROR: invalid column name 'a' (identifier gets downcased)
minmax_columns='x y' -> WITH (minmax_columns=x y) -> ERROR: syntax error at or near "y"
These aren't regressions from this PR, but since it's the same root cause it seems worth fixing together. Something like quoting whenever the value isn't a simple token keeps the output for existing tables byte-for-byte unchanged:
array_to_string(ARRAY(
SELECT CASE WHEN substr(opt, strpos(opt, '=') + 1) ~ '^[a-z0-9_.]+$'
THEN opt
ELSE split_part(opt, '=', 1) || '=' || quote_literal(substr(opt, strpos(opt, '=') + 1))
END
FROM unnest(reloptions) WITH ORDINALITY AS u(opt, n)
ORDER BY n), ', ')I tried this on the same cluster against PAX (minmax_columns='a,b', cluster_columns='A', bloomfilter_columns='x y'), AO column (compresstype=zlib, compresslevel=1, blocksize=32768, checksum=true), heap (fillfactor=70, autovacuum_enabled=false) and a PAX matview. The generated WITH clauses all re-create cleanly, and the recreated relations' reloptions are identical to the originals. Unquoted options (compresstype=zstd, compresslevel=5, checksum=true, ...) are emitted exactly as before.
The initial fix single-quoted only option values containing a comma. A value
that is not a plain token breaks the round trip even without a comma, verified
on a live Cloudberry 2.1.0 cluster:
- an uppercase/quoted identifier is silently downcased
(minmax_columns=A targets column "a");
- a value containing a space is a syntax error (bloomfilter_columns=x y).
Quote whenever the value is not a simple token (not matching ^[a-z0-9_.]+$),
which also covers the comma case. Plain-token values (compresstype=zstd,
compresslevel=5, fillfactor=70, checksum=true, ...) are emitted unchanged, so
backup output for existing heap/AO tables stays byte-for-byte identical. WITH
ORDINALITY preserves the original option order.
Add an integration test for the uppercase/spaced cases asserting the emitted
WITH clause restores verbatim.
Note: PAX materialized views (GetMaterializedViews in queries_relations.go)
build their WITH clause the same way and have the same defect; that path is
left for a follow-up PR to keep this change focused on base tables.
|
Thanks for the thorough review and the cross-cluster verification! Point 2 (non-token values) — fixed in this PR. I reproduced both cases on a live Cloudberry 2.1.0 (SynxDB4 4.5.4-3) cluster:
The quoting condition is now "quote unless the value is a simple token", i.e. it quotes whenever Point 1 (materialized views) — deferred to a follow-up PR. I confirmed the matview defect is real ( |
|
Follow-up on point 1 (materialized views): the materialized-view path is now fixed in a dedicated PR — #130 ("Quote non-token storage option values for materialized views"). It applies the same quoting to For completeness, incremental materialized views (IVM) are handled in #131, which builds on the same This PR (#127) stays scoped to base tables as discussed. |
Problem
GetTableStoragebuilds a table'sWITH (...)clause by joiningpg_class.reloptionswith,:A reloption is stored as a single
name=valuearray element. The PAX storage optionsminmax_columns/cluster_columnshold a comma-separated column list as their value, so the value itself contains commas. Joining the array verbatim makes those embedded commas indistinguishable from the,separator between options, and the restored DDLfails:
The table is then never created, so its data load,
ALTER ... OWNER, indexes andANALYZEall cascade intorelation does not exist— the table and its data are lost on restore. Observed on Apache Cloudberry 2.1.0 restoring PAX tables with multi-columnminmax_columns.The PAX kernel stores these as a single string reloption and splits the value on commas itself (
add_string_reloption+SplitDirectoriesString(value, ',')), so the value must reach the server single-quoted as one string.Fix
Single-quote the value of any reloption whose value contains a comma. Options without a comma are emitted unchanged, so backup output for existing tables is unaffected. This mirrors the single-quoting already done in
buildStorageOptionsStringfor storage servers.Testing
Added an integration test in the
GetTableStorageOptionsblock that creates a PAX table with multi-columnminmax_columns/cluster_columnsand asserts the emittedWITHclause (a) single-quotes the list values and (b) restores verbatim via a realCREATE TABLE. Gated to Cloudberry, where PAX is available.