Skip to content

Commit d26ec76

Browse files
replace pgdump_all with --no-owner and --no-privielges
1 parent dc23d2f commit d26ec76

2 files changed

Lines changed: 112 additions & 54 deletions

File tree

flow/connectors/postgres/pgdump_schema.go

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,9 @@ import (
1212
"github.com/PeerDB-io/peerdb/flow/generated/protos"
1313
)
1414

15-
// RunPgDumpSchema first migrates roles via pg_dumpall --roles-only, then streams
16-
// a schema-only pg_dump from source directly into psql on the destination,
17-
// piping stdout into stdin without intermediate files.
15+
// RunPgDumpSchema streams a schema-only pg_dump from source directly into psql
16+
// on the destination, piping stdout into stdin without intermediate files.
1817
func RunPgDumpSchema(ctx context.Context, srcConfig *protos.PostgresConfig, dstConfig *protos.PostgresConfig) error {
19-
// Step 1: migrate roles from source to destination
20-
if err := pipeCommand(ctx, srcConfig, dstConfig, "pg_dumpall", buildPgDumpAllArgs(srcConfig)); err != nil {
21-
return fmt.Errorf("pg_dumpall roles migration failed: %w", err)
22-
}
23-
24-
// Step 2: migrate schema from source to destination
2518
if err := pipeCommand(ctx, srcConfig, dstConfig, "pg_dump", buildPgDumpArgs(srcConfig)); err != nil {
2619
return fmt.Errorf("pg_dump schema migration failed: %w", err)
2720
}
@@ -88,23 +81,6 @@ func pipeCommand(
8881
return nil
8982
}
9083

91-
func buildPgDumpAllArgs(config *protos.PostgresConfig) []string {
92-
port := config.Port
93-
if port == 0 {
94-
port = 5432
95-
}
96-
97-
args := []string{
98-
"--roles-only",
99-
"-h", config.Host,
100-
"-p", strconv.FormatUint(uint64(port), 10),
101-
}
102-
if config.User != "" {
103-
args = append(args, "-U", config.User)
104-
}
105-
return args
106-
}
107-
10884
func buildPgDumpArgs(config *protos.PostgresConfig) []string {
10985
port := config.Port
11086
if port == 0 {
@@ -113,6 +89,8 @@ func buildPgDumpArgs(config *protos.PostgresConfig) []string {
11389

11490
args := []string{
11591
"--schema-only",
92+
"--no-owner",
93+
"--no-privileges",
11694
"-h", config.Host,
11795
"-p", strconv.FormatUint(uint64(port), 10),
11896
"-d", config.Database,

flow/e2e/pg_schema_dump_test.go

Lines changed: 108 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"github.com/jackc/pgx/v5"
99
"github.com/stretchr/testify/require"
1010

11-
connpostgres "github.com/PeerDB-io/peerdb/flow/connectors/postgres"
1211
"github.com/PeerDB-io/peerdb/flow/generated/protos"
1312
"github.com/PeerDB-io/peerdb/flow/internal"
1413
)
@@ -283,54 +282,135 @@ func (s PeerFlowE2ETestSuitePG) Test_PG_Schema_Dump_And_CDC() {
283282
RequireEnvCanceled(s.t, env)
284283
}
285284

286-
// Test_PG_Schema_Dump_Role_Migration verifies that pg_dumpall --roles-only
287-
// propagates a role from a source PG cluster to a separate destination PG
288-
// cluster. Roles are global objects per cluster, so this requires two clusters
289-
// (peerdb-postgres + peerdb-postgres2).
290-
func (s PeerFlowE2ETestSuitePG) Test_PG_Schema_Dump_Role_Migration() {
291-
srcCfg := internal.GetAncillaryPostgresConfigFromEnv()
285+
// Test_PG_Schema_Dump_No_Owner_No_Privileges verifies that the schema dump does
286+
// not emit owner or grant statements that reference roles. We create a role on
287+
// the source, give it ownership and grants on a table, then dump into a
288+
// secondary cluster where that role does NOT exist. With --no-owner and
289+
// --no-privileges the dump must succeed; without them it would fail on
290+
// ALTER TABLE ... OWNER TO <missing_role> / GRANT ... TO <missing_role>.
291+
//
292+
// Also verifies initial load + CDC into the dumped table on the destination
293+
// cluster, so we know the table is usable end-to-end.
294+
func (s PeerFlowE2ETestSuitePG) Test_PG_Schema_Dump_No_Owner_No_Privileges() {
292295
dstCfg := internal.GetSecondaryPostgresConfigFromEnv()
293296

294-
roleName := "peerdb_test_role_" + s.suffix
295-
296-
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
297-
defer cancel()
297+
srcSchema := "e2e_test_" + s.suffix
298+
roleName := "peerdb_owner_role_" + s.suffix
299+
tableName := "owned_tbl"
300+
qualified := fmt.Sprintf("%s.%s", srcSchema, tableName)
298301

302+
// destination connection (for assertions + role-absence sanity)
299303
dstConnStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s",
300304
dstCfg.Host, dstCfg.Port, dstCfg.User, dstCfg.Password, dstCfg.Database)
301-
dstConn, err := pgx.Connect(ctx, dstConnStr)
305+
dstConn, err := pgx.Connect(s.t.Context(), dstConnStr)
302306
require.NoError(s.t, err, "failed to connect to secondary postgres on %s:%d (is the postgres2 tilt resource running?)", dstCfg.Host, dstCfg.Port)
303-
defer dstConn.Close(ctx)
307+
s.t.Cleanup(func() { dstConn.Close(s.t.Context()) })
304308

305-
// sanity: role must not pre-exist on destination
306-
var preExists bool
307-
require.NoError(s.t, dstConn.QueryRow(ctx,
308-
"SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname=$1)", roleName).Scan(&preExists))
309-
require.False(s.t, preExists, "role %s unexpectedly exists on destination before test", roleName)
309+
// sanity: role must not exist on destination
310+
var roleExistsOnDst bool
311+
require.NoError(s.t, dstConn.QueryRow(s.t.Context(),
312+
"SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname=$1)", roleName).Scan(&roleExistsOnDst))
313+
require.False(s.t, roleExistsOnDst, "role %s unexpectedly exists on destination", roleName)
310314

311-
// create role on source
315+
// create role + owned/granted table on source with seed rows
312316
_, err = s.Conn().Exec(s.t.Context(), fmt.Sprintf("CREATE ROLE %s LOGIN PASSWORD 'pw'", roleName))
313317
require.NoError(s.t, err)
318+
_, err = s.Conn().Exec(s.t.Context(), fmt.Sprintf(
319+
"CREATE TABLE %s (id SERIAL PRIMARY KEY, val TEXT)", qualified))
320+
require.NoError(s.t, err)
321+
_, err = s.Conn().Exec(s.t.Context(), fmt.Sprintf("ALTER TABLE %s OWNER TO %s", qualified, roleName))
322+
require.NoError(s.t, err)
323+
_, err = s.Conn().Exec(s.t.Context(), fmt.Sprintf("GRANT SELECT, INSERT ON %s TO %s", qualified, roleName))
324+
require.NoError(s.t, err)
325+
326+
for i := 1; i <= 5; i++ {
327+
_, err = s.Conn().Exec(s.t.Context(),
328+
fmt.Sprintf("INSERT INTO %s (val) VALUES ($1)", qualified),
329+
fmt.Sprintf("snap_%d", i))
330+
require.NoError(s.t, err)
331+
}
332+
314333
s.t.Cleanup(func() {
315334
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 30*time.Second)
316335
defer cleanupCancel()
336+
_, _ = s.Conn().Exec(cleanupCtx, fmt.Sprintf("DROP TABLE IF EXISTS %s", qualified))
317337
_, _ = s.Conn().Exec(cleanupCtx, "DROP ROLE IF EXISTS "+roleName)
318338

319339
dropConn, err := pgx.Connect(cleanupCtx, dstConnStr)
320340
if err != nil {
321-
s.t.Logf("failed to connect to destination for role cleanup: %v", err)
341+
s.t.Logf("failed to connect to destination for cleanup: %v", err)
322342
return
323343
}
324344
defer dropConn.Close(cleanupCtx)
325-
if _, err := dropConn.Exec(cleanupCtx, "DROP ROLE IF EXISTS "+roleName); err != nil {
326-
s.t.Logf("failed to drop destination role %s: %v", roleName, err)
327-
}
345+
_, _ = dropConn.Exec(cleanupCtx, fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", srcSchema))
328346
})
329347

330-
require.NoError(s.t, connpostgres.RunPgDumpSchema(ctx, srcCfg, dstCfg))
348+
// register destination peer pointing at postgres2
349+
dstPeerName := "pgdump_noowner_dst_" + s.suffix
350+
CreatePeer(s.t, &protos.Peer{
351+
Name: dstPeerName,
352+
Type: protos.DBType_POSTGRES,
353+
Config: &protos.Peer_PostgresConfig{PostgresConfig: dstCfg},
354+
})
331355

332-
var postExists bool
333-
require.NoError(s.t, dstConn.QueryRow(ctx,
334-
"SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname=$1)", roleName).Scan(&postExists))
335-
require.True(s.t, postExists, "role %s should have been migrated to destination cluster", roleName)
356+
config := &protos.FlowConnectionConfigs{
357+
FlowJobName: s.attachSuffix("test_pgdump_noowner"),
358+
DestinationName: dstPeerName,
359+
TableMappings: []*protos.TableMapping{{
360+
SourceTableIdentifier: qualified,
361+
DestinationTableIdentifier: qualified,
362+
}},
363+
SourceName: GeneratePostgresPeer(s.t).Name,
364+
MaxBatchSize: 100,
365+
DoInitialSnapshot: true,
366+
System: protos.TypeSystem_PG,
367+
Env: map[string]string{
368+
"PEERDB_PG_AUTOMATED_SCHEMA_DUMP": "true",
369+
},
370+
}
371+
372+
tc := NewTemporalClient(s.t)
373+
env := ExecutePeerflow(s.t, tc, config)
374+
SetupCDCFlowStatusQuery(s.t, env, config)
375+
376+
// initial load: pg_dump must succeed despite missing owner/grantee role,
377+
// then snapshot copies the 5 seed rows.
378+
EnvWaitFor(s.t, env, 3*time.Minute, "initial load owned_tbl", func() bool {
379+
var count int64
380+
err := dstConn.QueryRow(s.t.Context(),
381+
fmt.Sprintf("SELECT COUNT(*) FROM %s", qualified)).Scan(&count)
382+
return err == nil && count == 5
383+
})
384+
385+
// CDC: insert more rows on source and wait for them on dst
386+
for i := 6; i <= 10; i++ {
387+
_, err = s.Conn().Exec(s.t.Context(),
388+
fmt.Sprintf("INSERT INTO %s (val) VALUES ($1)", qualified),
389+
fmt.Sprintf("cdc_%d", i))
390+
EnvNoError(s.t, env, err)
391+
}
392+
393+
EnvWaitFor(s.t, env, 3*time.Minute, "cdc owned_tbl", func() bool {
394+
var count int64
395+
err := dstConn.QueryRow(s.t.Context(),
396+
fmt.Sprintf("SELECT COUNT(*) FROM %s", qualified)).Scan(&count)
397+
return err == nil && count == 10
398+
})
399+
400+
// owner on dst should be the connecting user, not the (missing) source role
401+
var dstOwner string
402+
require.NoError(s.t, dstConn.QueryRow(s.t.Context(),
403+
"SELECT tableowner FROM pg_tables WHERE schemaname=$1 AND tablename=$2",
404+
srcSchema, tableName).Scan(&dstOwner))
405+
require.NotEqual(s.t, roleName, dstOwner, "destination table should not be owned by the source-only role")
406+
407+
// no grants should reference the missing role on dst
408+
var grantCount int
409+
require.NoError(s.t, dstConn.QueryRow(s.t.Context(),
410+
"SELECT COUNT(*) FROM information_schema.table_privileges WHERE table_schema=$1 AND table_name=$2 AND grantee=$3",
411+
srcSchema, tableName, roleName).Scan(&grantCount))
412+
require.Zero(s.t, grantCount, "no privileges should be granted to the source-only role on destination")
413+
414+
env.Cancel(s.t.Context())
415+
RequireEnvCanceled(s.t, env)
336416
}

0 commit comments

Comments
 (0)