Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 42 additions & 15 deletions .github/workflows/cloudberry-backup-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@
# and runs Cloudberry-backup tests against a demo Cloudberry cluster.
#
# Workflow Overview:
# 1. Build Cloudberry from source and upload the installation as an artifact.
# 2. For each test target (unit, integration, end_to_end), restore the
# Cloudberry installation, create a demo cluster, and run tests.
# 1. For each Cloudberry branch (REL_2_STABLE, main), build Cloudberry
# from source and upload the installation as an artifact.
# 2. For each Cloudberry branch and test target (unit, integration,
# end_to_end, ...), restore the matching Cloudberry installation,
# create a demo cluster, and run tests.
#
# Notes:
# - Each test job runs in an isolated environment and creates its own demo cluster.
# - Artifacts are used to avoid rebuilding Cloudberry for every test target.
# - Artifact names carry the Cloudberry branch so the two builds never mix.
# --------------------------------------------------------------------

name: cloudberry-backup-ci
Expand All @@ -53,15 +56,16 @@ permissions:

env:
CLOUDBERRY_REPO: apache/cloudberry
# Currently targeting REL_2_STABLE (2.x) branch only.
# TODO: Switch back to "main" once Cloudberry 3.x / main branch stabilizes with PG16 support.
CLOUDBERRY_REF: REL_2_STABLE
# The Cloudberry branches to test against are set by the
# `cloudberry_ref` matrix of each job:
# - REL_2_STABLE: Cloudberry 2.x (PG14 kernel), the current stable line.
# - main: the future Cloudberry 3.x (PG16 kernel).
CLOUDBERRY_DIR: cloudberry
CLOUDBERRY_BACKUP_DIR: cloudberry-backup

jobs:
build_cloudberry:
name: Build Cloudberry From Source
name: Build Cloudberry From Source (${{ matrix.cloudberry_ref }})
runs-on: ubuntu-22.04
timeout-minutes: 180
container:
Expand All @@ -73,6 +77,11 @@ jobs:
-v /usr/local:/host_usr_local
-v /opt:/host_opt

strategy:
fail-fast: false
matrix:
cloudberry_ref: [REL_2_STABLE, main]

steps:
- name: Free Disk Space
run: |
Expand All @@ -99,7 +108,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: ${{ env.CLOUDBERRY_REPO }}
ref: ${{ env.CLOUDBERRY_REF }}
ref: ${{ matrix.cloudberry_ref }}
fetch-depth: 1
submodules: true
path: ${{ env.CLOUDBERRY_DIR }}
Expand Down Expand Up @@ -159,22 +168,27 @@ jobs:
- name: Upload Cloudberry Installation
uses: actions/upload-artifact@v4
with:
name: cloudberry-db-install
name: cloudberry-db-install-${{ matrix.cloudberry_ref }}
path: cloudberry-db.tgz
if-no-files-found: error
retention-days: 7

- name: Upload Cloudberry Source
uses: actions/upload-artifact@v4
with:
name: cloudberry-source
name: cloudberry-source-${{ matrix.cloudberry_ref }}
path: cloudberry-src.tgz
if-no-files-found: error
retention-days: 7

test_cloudberry_backup:
name: Cloudberry-backup Tests (${{ matrix.test_target }})
name: Cloudberry-backup Tests (${{ matrix.cloudberry_ref }}, ${{ matrix.test_target }})
needs: [build_cloudberry]
# `needs` on a matrix job fails as soon as any one of its legs fails, so
# run anyway and let each leg fail on its own if the Cloudberry build it
# downloads is missing. That keeps a broken main build from hiding the
# REL_2_STABLE results.
if: ${{ !cancelled() }}
runs-on: ubuntu-22.04
timeout-minutes: 180
container:
Expand All @@ -189,6 +203,7 @@ jobs:
strategy:
fail-fast: false
matrix:
cloudberry_ref: [REL_2_STABLE, main]
test_target: [smoke, unit, integration, end_to_end, s3_plugin_e2e, regression, scale, package_install]

steps:
Expand Down Expand Up @@ -222,12 +237,12 @@ jobs:
- name: Download Cloudberry Installation
uses: actions/download-artifact@v4
with:
name: cloudberry-db-install
name: cloudberry-db-install-${{ matrix.cloudberry_ref }}

- name: Download Cloudberry Source
uses: actions/download-artifact@v4
with:
name: cloudberry-source
name: cloudberry-source-${{ matrix.cloudberry_ref }}

- name: Restore Cloudberry Installation
run: |
Expand Down Expand Up @@ -281,6 +296,7 @@ jobs:
CLOUDBERRY_BACKUP_SRC: ${{ github.workspace }}/${{ env.CLOUDBERRY_BACKUP_DIR }}
CLOUDBERRY_SRC: ${{ github.workspace }}/${{ env.CLOUDBERRY_DIR }}
TEST_TARGET: ${{ matrix.test_target }}
CLOUDBERRY_REF: ${{ matrix.cloudberry_ref }}
run: |
set -euo pipefail
TEST_LOG_ROOT="${GITHUB_WORKSPACE}/test-logs/${TEST_TARGET}"
Expand Down Expand Up @@ -434,7 +450,17 @@ jobs:
;;
regression)
pushd ${CLOUDBERRY_SRC}/src/test/regress
./pg_regress --dbname=regression --host=localhost --port=7000 --init-file=init_file --schedule=./minimal_schedule || true
# On Cloudberry main (PG16 kernel) the shared test tables
# (char_tbl, int2_tbl, ...) are created by test_setup, which
# minimal_schedule does not list; without it most tests fail
# and the regression database ends up nearly empty. Run it
# first wherever it exists; on REL_2_STABLE it is harmless.
regress_schedule=./minimal_schedule
if [ -f sql/test_setup.sql ]; then
regress_schedule=/tmp/regression_minimal_schedule
{ echo "test: test_setup"; cat ./minimal_schedule; } > "${regress_schedule}"
fi
./pg_regress --dbname=regression --host=localhost --port=7000 --init-file=init_file --schedule="${regress_schedule}" || true
cat regression.diffs 2>/dev/null || true
popd

Expand Down Expand Up @@ -569,6 +595,7 @@ jobs:

{
echo "## Cloudberry-backup Test Summary"
echo "- Cloudberry: ${CLOUDBERRY_REF}"
echo "- Target: ${TEST_TARGET}"
if [ ${status} -eq 0 ]; then
echo "- Result: PASS"
Expand All @@ -584,7 +611,7 @@ jobs:
if: failure()
uses: actions/upload-artifact@v4
with:
name: cloudberry-backup-logs-${{ matrix.test_target }}
name: cloudberry-backup-logs-${{ matrix.cloudberry_ref }}-${{ matrix.test_target }}
path: test-logs/${{ matrix.test_target }}
if-no-files-found: warn
retention-days: 7
6 changes: 4 additions & 2 deletions backup/queries_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,8 @@ func GetForeignDataWrappers(connectionPool *dbconn.DBConn) []ForeignDataWrapper
SELECT pg_catalog.quote_ident(option_name) || ' ' || pg_catalog.quote_literal(option_value)
FROM pg_options_to_table(fdwoptions) ORDER BY option_name), ', ') AS options
FROM pg_foreign_data_wrapper
WHERE oid >= %d AND %s`, FIRST_NORMAL_OBJECT_ID, ExtensionFilterClause(""))
WHERE oid >= %d AND %s
ORDER BY oid`, FIRST_NORMAL_OBJECT_ID, ExtensionFilterClause(""))

err := connectionPool.Select(&results, query)
gplog.FatalOnError(err)
Expand Down Expand Up @@ -889,7 +890,8 @@ func GetForeignServers(connectionPool *dbconn.DBConn) []ForeignServer {
FROM pg_options_to_table(fs.srvoptions) ORDER BY option_name), ', ') AS options
FROM pg_foreign_server fs
LEFT JOIN pg_foreign_data_wrapper fdw ON fdw.oid = srvfdw
WHERE fs.oid >= %d AND %s`, FIRST_NORMAL_OBJECT_ID, ExtensionFilterClause("fs"))
WHERE fs.oid >= %d AND %s
ORDER BY fs.oid`, FIRST_NORMAL_OBJECT_ID, ExtensionFilterClause("fs"))

err := connectionPool.Select(&results, query)
gplog.FatalOnError(err)
Expand Down
10 changes: 8 additions & 2 deletions end_to_end/end_to_end_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2158,8 +2158,14 @@ LANGUAGE plpgsql NO SQL;`)
Expect(contents).To(ContainSubstring("CREATE VIEW public.key_dependent_view AS \nSELECT\n\tNULL::integer AS key,\n\tNULL::character varying(20) COLLATE pg_catalog.\"C\" AS data;"))
Expect(contents).To(ContainSubstring("CREATE VIEW public.key_dependent_view_no_cols AS \nSELECT;"))
Expect(contents).To(ContainSubstring("ALTER TABLE ONLY public.view_base_table ADD CONSTRAINT view_base_table_pkey PRIMARY KEY (key);"))
Expect(contents).To(ContainSubstring("CREATE OR REPLACE VIEW public.key_dependent_view AS SELECT view_base_table.key,\n (view_base_table.data COLLATE \"C\") AS data\n FROM public.view_base_table\n GROUP BY view_base_table.key;"))
Expect(contents).To(ContainSubstring("CREATE OR REPLACE VIEW public.key_dependent_view_no_cols AS SELECT\n FROM public.view_base_table\n GROUP BY view_base_table.key\n HAVING (length((view_base_table.data)::text) > 0);"))
if testutils.IsCBDB3OrLater(backupConn) {
// PG16 no longer qualifies columns of a single-table query.
Expect(contents).To(ContainSubstring("CREATE OR REPLACE VIEW public.key_dependent_view AS SELECT key,\n (data COLLATE \"C\") AS data\n FROM public.view_base_table\n GROUP BY key;"))
Expect(contents).To(ContainSubstring("CREATE OR REPLACE VIEW public.key_dependent_view_no_cols AS SELECT\n FROM public.view_base_table\n GROUP BY key\n HAVING (length((data)::text) > 0);"))
} else {
Expect(contents).To(ContainSubstring("CREATE OR REPLACE VIEW public.key_dependent_view AS SELECT view_base_table.key,\n (view_base_table.data COLLATE \"C\") AS data\n FROM public.view_base_table\n GROUP BY view_base_table.key;"))
Expect(contents).To(ContainSubstring("CREATE OR REPLACE VIEW public.key_dependent_view_no_cols AS SELECT\n FROM public.view_base_table\n GROUP BY view_base_table.key\n HAVING (length((view_base_table.data)::text) > 0);"))
}

gprestore(gprestorePath, restoreHelperPath, timestamp,
"--redirect-db", "restoredb",
Expand Down
6 changes: 4 additions & 2 deletions integration/metadata_globals_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ var _ = Describe("backup integration create statement tests", func() {
defer testhelper.AssertQueryRuns(connectionPool, `DROP ROLE testuser`)
})
It("grants a role without ADMIN OPTION", func() {
testutils.GrantAdminToGrantorIfRequired(connectionPool, "usergroup", "testrole")
numRoleMembers := len(backup.GetRoleMembers(connectionPool))
expectedRoleMember := backup.RoleMember{Role: "usergroup", Member: "testuser", Grantor: "testrole", IsAdmin: false}
backup.PrintRoleMembershipStatements(backupfile, tocfile, []backup.RoleMember{expectedRoleMember})
Expand All @@ -425,14 +426,15 @@ var _ = Describe("backup integration create statement tests", func() {
resultRoleMembers := backup.GetRoleMembers(connectionPool)
Expect(resultRoleMembers).To(HaveLen(numRoleMembers + 1))
for _, roleMember := range resultRoleMembers {
if roleMember.Role == "usergroup" {
if roleMember.Role == "usergroup" && roleMember.Member == "testuser" {
structmatcher.ExpectStructsToMatch(&expectedRoleMember, &roleMember)
return
}
}
Fail("Role 'testuser' is not a member of role 'usergroup'")
})
It("grants a role WITH ADMIN OPTION", func() {
testutils.GrantAdminToGrantorIfRequired(connectionPool, "usergroup", "testrole")
numRoleMembers := len(backup.GetRoleMembers(connectionPool))
expectedRoleMember := backup.RoleMember{Role: "usergroup", Member: "testuser", Grantor: "testrole", IsAdmin: true}
backup.PrintRoleMembershipStatements(backupfile, tocfile, []backup.RoleMember{expectedRoleMember})
Expand All @@ -442,7 +444,7 @@ var _ = Describe("backup integration create statement tests", func() {
resultRoleMembers := backup.GetRoleMembers(connectionPool)
Expect(resultRoleMembers).To(HaveLen(numRoleMembers + 1))
for _, roleMember := range resultRoleMembers {
if roleMember.Role == "usergroup" {
if roleMember.Role == "usergroup" && roleMember.Member == "testuser" {
structmatcher.ExpectStructsToMatch(&expectedRoleMember, &roleMember)
return
}
Expand Down
26 changes: 22 additions & 4 deletions integration/metadata_globals_queries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,17 @@ CREATEEXTTABLE (protocol='gphdfs', type='writable')`
})
})
Describe("GetRoleMembers", func() {
/*
* Since PG16 (Cloudberry 3.x) a superuser that grants a membership
* without GRANTED BY is recorded as the bootstrap superuser rather
* than as itself.
*/
implicitGrantor := func(currentRole string) string {
if testutils.IsCBDB3OrLater(connectionPool) {
return testutils.GetUserByID(connectionPool, 10)
}
return currentRole
}
BeforeEach(func() {
testhelper.AssertQueryRuns(connectionPool, `CREATE ROLE usergroup`)
testhelper.AssertQueryRuns(connectionPool, `CREATE ROLE testuser`)
Expand All @@ -516,7 +527,7 @@ CREATEEXTTABLE (protocol='gphdfs', type='writable')`
})
It("returns a role without ADMIN OPTION", func() {
testhelper.AssertQueryRuns(connectionPool, "GRANT usergroup TO testuser")
expectedRoleMember := backup.RoleMember{Role: "usergroup", Member: "testuser", Grantor: "testrole", IsAdmin: false}
expectedRoleMember := backup.RoleMember{Role: "usergroup", Member: "testuser", Grantor: implicitGrantor("testrole"), IsAdmin: false}

roleMembers := backup.GetRoleMembers(connectionPool)

Expand All @@ -529,13 +540,14 @@ CREATEEXTTABLE (protocol='gphdfs', type='writable')`
Fail("Role 'testuser' is not a member of role 'usergroup'")
})
It("returns a role WITH ADMIN OPTION", func() {
testutils.GrantAdminToGrantorIfRequired(connectionPool, "usergroup", "testrole")
testhelper.AssertQueryRuns(connectionPool, "GRANT usergroup TO testuser WITH ADMIN OPTION GRANTED BY testrole")
expectedRoleMember := backup.RoleMember{Role: "usergroup", Member: "testuser", Grantor: "testrole", IsAdmin: true}

roleMembers := backup.GetRoleMembers(connectionPool)

for _, roleMember := range roleMembers {
if roleMember.Role == "usergroup" {
if roleMember.Role == "usergroup" && roleMember.Member == "testuser" {
structmatcher.ExpectStructsToMatch(&expectedRoleMember, &roleMember)
return
}
Expand All @@ -552,7 +564,7 @@ CREATEEXTTABLE (protocol='gphdfs', type='writable')`
testhelper.AssertQueryRuns(connectionPool, `CREATE ROLE "1testuser"`)
defer testhelper.AssertQueryRuns(connectionPool, `DROP ROLE "1testuser"`)
testhelper.AssertQueryRuns(connectionPool, `GRANT "1usergroup" TO "1testuser"`)
expectedRoleMember := backup.RoleMember{Role: `"1usergroup"`, Member: `"1testuser"`, Grantor: `"1testrole"`, IsAdmin: false}
expectedRoleMember := backup.RoleMember{Role: `"1usergroup"`, Member: `"1testuser"`, Grantor: implicitGrantor(`"1testrole"`), IsAdmin: false}

roleMembers := backup.GetRoleMembers(connectionPool)

Expand All @@ -565,6 +577,12 @@ CREATEEXTTABLE (protocol='gphdfs', type='writable')`
Fail(`Role "1testuser" is not a member of role "1usergroup"`)
})
It("handles dropped granter", func() {
if testutils.IsCBDB3OrLater(connectionPool) {
// Since PG16 a role that granted a membership cannot be
// dropped while that membership exists, so a membership
// with a dropped grantor can no longer be produced.
Skip("Test not applicable to Cloudberry 3.x and later")
}
testhelper.AssertQueryRuns(connectionPool, `CREATE ROLE testdropgranter_role`)
defer testhelper.AssertQueryRuns(connectionPool, `DROP ROLE testdropgranter_role`)
testhelper.AssertQueryRuns(connectionPool, `CREATE ROLE testdropgranter_member`)
Expand All @@ -583,7 +601,7 @@ CREATEEXTTABLE (protocol='gphdfs', type='writable')`
testhelper.AssertQueryRuns(connectionPool, "CREATE CAST (oid AS text) WITH FUNCTION pg_catalog.text(oid) AS IMPLICIT;")
defer testhelper.AssertQueryRuns(connectionPool, "DROP FUNCTION pg_catalog.text(oid) CASCADE;")
testhelper.AssertQueryRuns(connectionPool, "GRANT usergroup TO testuser")
expectedRoleMember := backup.RoleMember{Role: "usergroup", Member: "testuser", Grantor: "testrole", IsAdmin: false}
expectedRoleMember := backup.RoleMember{Role: "usergroup", Member: "testuser", Grantor: implicitGrantor("testrole"), IsAdmin: false}

roleMembers := backup.GetRoleMembers(connectionPool)

Expand Down
7 changes: 6 additions & 1 deletion integration/postdata_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,11 +402,16 @@ AS $$ BEGIN RAISE EXCEPTION 'exception'; END; $$;`)
DROP TABLE public.view_base_table CASCADE;
`)

viewDef := " SELECT view_base_table.key,\n (view_base_table.data COLLATE \"C\") AS data\n FROM public.view_base_table\n GROUP BY view_base_table.key;"
if testutils.IsCBDB3OrLater(connectionPool) {
// PG16 no longer qualifies columns of a single-table query.
viewDef = " SELECT key,\n (data COLLATE \"C\") AS data\n FROM public.view_base_table\n GROUP BY key;"
}
view1 := backup.View{
Schema: "public",
Name: "key_dependent_view",
Definition: sql.NullString{
String: " SELECT view_base_table.key,\n (view_base_table.data COLLATE \"C\") AS data\n FROM public.view_base_table\n GROUP BY view_base_table.key;",
String: viewDef,
Valid: true,
},
ColumnDefs: []backup.ColumnDefinition{
Expand Down
20 changes: 20 additions & 0 deletions testutils/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,26 @@ func SkipIfBefore7(connectionPool *dbconn.DBConn) {
}
}

/*
* Cloudberry 3.x is built on the PostgreSQL 16 kernel. Compare the major
* version directly: a development build reports a version such as
* "3.0.0-devel", which sorts before "3.0.0" under semver and so fails
* Version.AtLeast("3").
*/
func IsCBDB3OrLater(connectionPool *dbconn.DBConn) bool {
return connectionPool.Version.IsCBDB() && connectionPool.Version.SemVer.Major >= 3
}

/*
* Since PG16 (Cloudberry 3.x) the role named in GRANTED BY must hold ADMIN
* OPTION on the granted role, even when that role is a superuser.
*/
func GrantAdminToGrantorIfRequired(connectionPool *dbconn.DBConn, role string, grantor string) {
if IsCBDB3OrLater(connectionPool) {
testhelper.AssertQueryRuns(connectionPool, fmt.Sprintf("GRANT %s TO %s WITH ADMIN OPTION", role, grantor))
}
}

func InitializeTestTOC(buffer io.Writer, which string) (*toc.TOC, *utils.FileWithByteCount) {
tocfile := &toc.TOC{}
tocfile.InitializeMetadataEntryMap()
Expand Down
Loading