Repository navigation
Quote comma-containing storage option values so PAX tables restore - #127
talmacschen-arch wants to merge 1 commit into
Conversation
Cluster validationVerified on a live Apache Cloudberry 2.1.0 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.
|
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 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. |
woblerr
left a comment
There was a problem hiding this comment.
- GPDB 5 compatibility
WITH ORDINALITY is not supported by GPDB 5, but version validation still accepts GPDB 5.1+. The query has no version guard.
Two options:
- Keep a compatible query for GPDB 5.
- Drop GPDB 5 support and update validation, tests, and docs.
@tuhaihe What do you think? Do we still need code-level compatibility with GPDB 5?
- The updated regex still misses numeric column names
The updated regex handles uppercase names and spaces from the earlier review, but leaves 001 unquoted. This turns minmax_columns='001' into minmax_columns=001, which SQL parses as 1, changing the target column. Could string-valued options always be quoted?
BEGIN;
CREATE TABLE src ("001" int, "1" int)
USING pax WITH (minmax_columns='001')
DISTRIBUTED BY ("001");
-- WITH clause emitted by this PR:
CREATE TABLE dst ("001" int, "1" int)
USING pax WITH (minmax_columns=001)
DISTRIBUTED BY ("001");
SELECT relname, reloptions
FROM pg_class
WHERE oid IN ('src'::regclass, 'dst'::regclass);
-- Expected: src has minmax_columns=001, dst has minmax_columns=1.
ROLLBACK;The parser conversion is confirmed; the PAX example still needs a live cluster check.
A backup of a PAX table carrying a comma-separated storage option list
restored with, e.g.:
) USING pax WITH (minmax_columns=v1,v2) ...
ERROR: unrecognized parameter "v2"
pg_class.reloptions is an array of "name=value" strings, and a value is
free to contain a comma: PAX's minmax_columns, bloomfilter_columns and
cluster_columns all hold comma-separated column lists. Flattening the
array with array_to_string(reloptions, ', ') loses the element
boundaries, so one option comes back out as several and the restore
fails on the fragments. The backup succeeds, so the damage only surfaces
on restore. The same flattening also silently downcases an upper-case
value (minmax_columns=A targets column "a"), turns a value with a space
into a syntax error, and re-reads a numeric value as an integer
(minmax_columns=001 becomes column "1").
Flatten reloptions the way the server itself prints them, in
get_reloptions() (ruleutils.c): split each element on its first '=',
quote the name as an identifier, and leave the value bare only when it is
already an identifier that needs no quoting (quote_ident(value) = value);
otherwise emit a quoted literal. A value we quote is a value the server
would have quoted, so the output matches pg_dump byte for byte and the
round trip is exact. This also quotes numbers and reserved words
(fillfactor='70', compresstype='none', checksum='true'), which the
parser accepts either way.
The rule lives in one helper, relOptionsClause(), applied to the three
places that flatten a reloptions array into a WITH (...) clause:
GetTableStorage for tables, and both the GPDB6 and GPDB7+/Cloudberry
variants of GetAllViews, so a PAX materialized view carrying the same
options restores too.
generate_series over the array subscripts keeps the options in catalog
order; it replaces unnest(...) WITH ORDINALITY, which GPDB 5
(PostgreSQL 8.3) does not support, and LATERAL is avoided for the same
reason, so the query needs no version guard. NULLIF(..., '') preserves
the NULL-when-empty behaviour the callers depend on.
The integration expectations that hardcoded the unquoted forms are
updated to the server-faithful ones. New integration specs cover a value
with a comma, a non-token value (upper-case / space), a numeric value,
and a PAX materialized view; each asserts the quoted output and restores
the emitted WITH clause verbatim, so a failure to quote would fail the
spec. Verified on a live Apache Cloudberry 2.1.0 cluster: all pass, and
the round-tripped tables' reloptions match the originals.
77cf51b to
0ab48ed
Compare
|
Reworked to address the review. Instead of the comma-only regex, this now mirrors the server's own
The output now matches pg_dump byte for byte, which does change existing backups: scalar numbers and reserved words are quoted ( Verified end to end on a live Apache Cloudberry 2.1.0 cluster: the GetTableStorage/GetAllViews specs (comma, non-token, numeric, PAX matview, view/matview options) and the create-statement specs all pass, and round-tripped tables' reloptions match the originals. This also folds in #130, which I'll close as covered here. |
woblerr
left a comment
There was a problem hiding this comment.
LGTM
Both issues are resolved.
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.