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
24 changes: 24 additions & 0 deletions src/bin/pgcopydb/indexes.c
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,30 @@ copydb_create_constraints(CopyDataSpec *specs, PGSQL *dst, SourceTable *table)
break;
}

/*
* Idempotency guard: if this constraint already has a done_time in the
* summary, a previous pass created it, so skip it rather than running
* ALTER TABLE ADD CONSTRAINT again (which fails with "index ... is
* already associated with a constraint"). Look up into a throwaway
* spec so the command just prepared into indexSpecs is preserved.
*/
CopyIndexSpec doneCheck = { .sourceIndex = index };

if (!summary_lookup_constraint(sourceDB, &doneCheck))
{
/* errors have already been logged */
success = false;
break;
}

if (doneCheck.summary.doneTime > 0)
{
log_debug("Skipping constraint %s: already created (done at %lld)",
index->constraintName,
(long long) doneCheck.summary.doneTime);
continue;
}

if (!summary_add_constraint(sourceDB, &indexSpecs))
{
/* errors have already been logged */
Expand Down
43 changes: 14 additions & 29 deletions src/bin/pgcopydb/summary.c
Original file line number Diff line number Diff line change
Expand Up @@ -1424,7 +1424,7 @@ summary_add_constraint(DatabaseCatalog *catalog, CopyIndexSpec *indexSpecs)
}

char *sql =
"insert or replace into summary(pid, conoid, start_time_epoch, command)"
"insert or ignore into summary(pid, conoid, start_time_epoch, command)"
"values($1, $2, $3, $4)";

if (!semaphore_lock(&(catalog->sema)))
Expand Down Expand Up @@ -1780,46 +1780,32 @@ summary_table_count_indexes_left(DatabaseCatalog *catalog,

/*
* When asked to create an index for a constraint and the index is neither
* a UNIQUE nor a PRIMARY KEY index, then we can't use the ALTER TABLE ...
* ADD CONSTRAINT ... USING INDEX ... command, because this only works with
* UNIQUE and PRIMARY KEY indexes.
* a UNIQUE nor a PRIMARY KEY index (such as an EXCLUSION constraint), then
* we skip creating the index directly and instead create it as part of the
* "plain" ALTER TABLE ... ADD CONSTRAINT ... command during the constraint
* phase.
*
* This means that we have to skip creating the index first, and will only
* then create it during the constraint phase, as part of the "plain" ALTER
* TABLE ... ADD CONSTRAINT ... command.
*
* So when counting the indexes that are left to be created before we can
* install the constraints, we should also skip counting these.
* Even though we skip running CREATE INDEX for such indexes, each one is
* still queued and processed by a worker, which calls
* copydb_mark_index_as_done before checking whether all indexes are done.
* We must therefore count these indexes here so that countIndexesLeft
* reaches zero exactly once — when the last worker finishes its index —
* rather than once per non-skipped index and again for each skipped index.
*/
char *sql =
"with idx(indexoid) as"
" ("
" select i.oid as indexoid "
" from s_table t join s_index i on i.tableoid = t.oid"
" where tableoid = $1 "
" ), "
" skipidx(indexoid) as "
" ("
" select i.oid as indexoid "
" from s_table t "
" join s_index i on i.tableoid = t.oid "
" join s_constraint c on c.indexoid = i.oid "
" where not i.isprimary and not i.isunique"
" and tableoid = $2 "
" ),"
" indexlist(indexoid) as"
" ( "
" select indexoid from idx "
" except "
" select indexoid from skipidx "
" ) "
" select count(l.indexoid) "
" from indexlist l "
" select count(i.indexoid) "
" from idx i "
" where not exists "
" ( "
" select 1 "
" from summary s "
" where s.indexoid = l.indexoid "
" where s.indexoid = i.indexoid "
" and s.pid > 0 and s.done_time_epoch > 0"
" ) ";

Expand All @@ -1843,7 +1829,6 @@ summary_table_count_indexes_left(DatabaseCatalog *catalog,

/* bind our parameters now */
BindParam params[] = {
{ BIND_PARAMETER_TYPE_INT64, "tableoid", table->oid, NULL },
{ BIND_PARAMETER_TYPE_INT64, "tableoid", table->oid, NULL }
};

Expand Down
9 changes: 9 additions & 0 deletions tests/unit/expected/17-exclusion-with-pkey.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-[ RECORD 1 ]-----------------------------
conname | excl_with_pkey_pkey
contype | p
condef | PRIMARY KEY (id)
-[ RECORD 2 ]-----------------------------
conname | excl_with_pkey_excl
contype | x
condef | EXCLUDE USING btree (val WITH =)

24 changes: 24 additions & 0 deletions tests/unit/expected/6-exclusion-constraint-index-jobs.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE DATABASE
CREATE COLLATION
-[ RECORD 1 ]-----------------------------
conname | excl_with_pkey_pkey
contype | p
condef | PRIMARY KEY (id)
-[ RECORD 2 ]-----------------------------
conname | excl_with_pkey_excl
contype | x
condef | EXCLUDE USING btree (val WITH =)

DROP DATABASE
CREATE DATABASE
CREATE COLLATION
-[ RECORD 1 ]-----------------------------
conname | excl_with_pkey_pkey
contype | p
condef | PRIMARY KEY (id)
-[ RECORD 2 ]-----------------------------
conname | excl_with_pkey_excl
contype | x
condef | EXCLUDE USING btree (val WITH =)

DROP DATABASE
58 changes: 58 additions & 0 deletions tests/unit/script/6-exclusion-constraint-index-jobs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#! /bin/bash

set -x
set -e

# This script expects the following environment variables to be set:
#
# - PGCOPYDB_SOURCE_PGURI
# - PGCOPYDB_TARGET_PGURI
#
# Regression test for the double constraint creation bug.
#
# Tables with both a PRIMARY KEY and an EXCLUSION constraint triggered
# "ERROR: index is already associated with a constraint" with --index-jobs 1.
# The bug was that EXCLUSION constraint indexes were excluded from the
# "indexes remaining" count, causing copydb_create_constraints to be called
# twice by the same worker when it processed the skipped EXCLUSION index.

TARGET_BASE="${PGCOPYDB_TARGET_PGURI%/*}"

for jobs in 1 4
do
TMPDB="pgcopydb_excl_ij${jobs}"
TARGET_URI="${TARGET_BASE}/${TMPDB}"
TMPDIR="/tmp/pgcopydb-excl-ij${jobs}"

psql -q -d "${PGCOPYDB_TARGET_PGURI}" -c "CREATE DATABASE ${TMPDB}"

# The source schema contains a table using a custom collation.
# Pre-create it on the target so that --skip-collations works correctly.
psql -q -d "${TARGET_URI}" -c "
CREATE COLLATION IF NOT EXISTS mycol
(locale = 'fr-FR-x-icu', provider = 'icu');
"

pgcopydb clone \
--source "${PGCOPYDB_SOURCE_PGURI}" \
--target "${TARGET_URI}" \
--index-jobs "${jobs}" \
--table-jobs 2 \
--dir "${TMPDIR}" \
--not-consistent \
--skip-collations \
--fail-fast > /dev/null

# Verify both constraints survived the copy, using the same SQL as the
# SQL test suite. The SQL test covers a standard copy; this script
# additionally exercises --index-jobs 1 and --index-jobs 4 separately
# to catch the double-constraint-creation bug.
psql -d "${TARGET_URI}" \
--no-psqlrc \
--expanded \
--file ./sql/17-exclusion-with-pkey.sql

psql -q -d "${PGCOPYDB_TARGET_PGURI}" -c "DROP DATABASE ${TMPDB}"

rm -rf "${TMPDIR}"
done
11 changes: 11 additions & 0 deletions tests/unit/setup/17-exclusion-with-pkey.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
DROP TABLE IF EXISTS excl_with_pkey;

CREATE TABLE excl_with_pkey (
id integer NOT NULL,
val integer NOT NULL
);

ALTER TABLE excl_with_pkey ADD CONSTRAINT excl_with_pkey_pkey PRIMARY KEY (id);
ALTER TABLE excl_with_pkey ADD CONSTRAINT excl_with_pkey_excl EXCLUDE USING btree (val WITH =);

INSERT INTO excl_with_pkey VALUES (1, 10), (2, 20), (3, 30);
1 change: 1 addition & 0 deletions tests/unit/setup/setup.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
\ir 14-uuid.sql
\ir 15-attgenerated.sql
\ir 16-sequences.sql
\ir 17-exclusion-with-pkey.sql
7 changes: 7 additions & 0 deletions tests/unit/sql/17-exclusion-with-pkey.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
select c.conname, c.contype, pg_get_constraintdef(c.oid) as condef
from pg_constraint c
join pg_class r on r.oid = c.conrelid
join pg_namespace n on n.oid = r.relnamespace
where n.nspname = 'public' and r.relname = 'excl_with_pkey'
and c.contype != 'n' -- PostgreSQL 18 adds NOT NULL constraints to pg_constraint
order by c.contype, c.conname;
Loading