Skip to content

Commit af6b4a3

Browse files
committed
Change hypertable foreign key handling
Don't copy foreign key constraints to the individual chunks and instead modify the lookup query to propagate to individual chunks to mimic how postgres does this for partitioned tables. This patch also removes the requirement for foreign key columns to be segmentby columns.
1 parent fe533e2 commit af6b4a3

20 files changed

+251
-173
lines changed

.unreleased/pr_7134

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implements: #7134 Refactor foreign key handling for compressed hypertables

sql/updates/latest-dev.sql

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,22 @@ SELECT pg_catalog.pg_extension_config_dump(pg_get_serial_sequence('_timescaledb_
6565

6666
GRANT SELECT ON _timescaledb_catalog.chunk_column_stats TO PUBLIC;
6767
GRANT SELECT ON _timescaledb_catalog.chunk_column_stats_id_seq TO PUBLIC;
68+
69+
-- Remove foreign key constraints from compressed chunks
70+
DO $$
71+
DECLARE
72+
conrelid regclass;
73+
conname name;
74+
BEGIN
75+
FOR conrelid, conname IN
76+
SELECT
77+
con.conrelid::regclass,
78+
con.conname
79+
FROM _timescaledb_catalog.chunk ch
80+
JOIN pg_constraint con ON con.conrelid = format('%I.%I',schema_name,table_name)::regclass AND con.contype='f'
81+
WHERE NOT ch.dropped AND EXISTS(SELECT FROM _timescaledb_catalog.chunk ch2 WHERE NOT ch2.dropped AND ch2.compressed_chunk_id=ch.id)
82+
LOOP
83+
EXECUTE format('ALTER TABLE %s DROP CONSTRAINT %I', conrelid, conname);
84+
END LOOP;
85+
END $$;
86+

sql/updates/reverse-dev.sql

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,20 @@ DROP FUNCTION IF EXISTS @[email protected]_column_stats(REGCLASS, NAME, BOOLEAN
55
ALTER EXTENSION timescaledb DROP TABLE _timescaledb_catalog.chunk_column_stats;
66
ALTER EXTENSION timescaledb DROP SEQUENCE _timescaledb_catalog.chunk_column_stats_id_seq;
77
DROP TABLE IF EXISTS _timescaledb_catalog.chunk_column_stats;
8+
9+
-- Add foreign key constraints back to compressed chunks
10+
DO $$
11+
DECLARE
12+
chunkrelid regclass;
13+
conname name;
14+
conoid oid;
15+
BEGIN
16+
FOR chunkrelid, conname, conoid IN
17+
SELECT format('%I.%I',ch.schema_name,ch.table_name)::regclass, con.conname, con.oid
18+
FROM _timescaledb_catalog.hypertable ht
19+
JOIN pg_constraint con ON con.contype = 'f' AND con.conrelid=format('%I.%I',ht.schema_name,ht.table_name)::regclass
20+
JOIN _timescaledb_catalog.chunk ch on ch.hypertable_id=ht.compressed_hypertable_id and not ch.dropped
21+
LOOP
22+
EXECUTE format('ALTER TABLE %s ADD CONSTRAINT %I %s', chunkrelid, conname, pg_get_constraintdef(conoid));
23+
END LOOP;
24+
END $$;

src/chunk_constraint.c

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ ts_chunk_constraint_scan_by_dimension_slice_id(int32 dimension_slice_id, ChunkCo
729729
}
730730

731731
static bool
732-
chunk_constraint_need_on_chunk(const char chunk_relkind, Form_pg_constraint conform)
732+
chunk_constraint_need_on_chunk(Form_pg_constraint conform)
733733
{
734734
if (conform->contype == CONSTRAINT_CHECK)
735735
{
@@ -753,26 +753,9 @@ chunk_constraint_need_on_chunk(const char chunk_relkind, Form_pg_constraint conf
753753
if (conform->contype == CONSTRAINT_FOREIGN && OidIsValid(conform->conparentid))
754754
return false;
755755

756-
/* Foreign tables do not support non-check constraints, so skip them */
757-
if (chunk_relkind == RELKIND_FOREIGN_TABLE)
758-
return false;
759-
760756
return true;
761757
}
762758

763-
static bool
764-
chunk_constraint_is_check(const char chunk_relkind, Form_pg_constraint conform)
765-
{
766-
if (conform->contype == CONSTRAINT_CHECK)
767-
{
768-
/*
769-
* check constraints supported on foreign tables (like OSM chunks)
770-
*/
771-
return true;
772-
}
773-
return false;
774-
}
775-
776759
int
777760
ts_chunk_constraints_add_dimension_constraints(ChunkConstraints *ccs, int32 chunk_id,
778761
const Hypercube *cube)
@@ -799,7 +782,7 @@ chunk_constraint_add(HeapTuple constraint_tuple, void *arg)
799782
ConstraintContext *cc = arg;
800783
Form_pg_constraint constraint = (Form_pg_constraint) GETSTRUCT(constraint_tuple);
801784

802-
if (chunk_constraint_need_on_chunk(cc->chunk_relkind, constraint))
785+
if (cc->chunk_relkind != RELKIND_FOREIGN_TABLE && chunk_constraint_need_on_chunk(constraint))
803786
{
804787
ts_chunk_constraints_add(cc->ccs, cc->chunk_id, 0, NULL, NameStr(constraint->conname));
805788
return CONSTR_PROCESSED;
@@ -828,7 +811,7 @@ chunk_constraint_add_check(HeapTuple constraint_tuple, void *arg)
828811
ConstraintContext *cc = arg;
829812
Form_pg_constraint constraint = (Form_pg_constraint) GETSTRUCT(constraint_tuple);
830813

831-
if (chunk_constraint_is_check(cc->chunk_relkind, constraint))
814+
if (constraint->contype == CONSTRAINT_CHECK)
832815
{
833816
ts_chunk_constraints_add(cc->ccs,
834817
cc->chunk_id,
@@ -867,7 +850,7 @@ ts_chunk_constraint_create_on_chunk(const Hypertable *ht, const Chunk *chunk, Oi
867850

868851
con = (Form_pg_constraint) GETSTRUCT(tuple);
869852

870-
if (chunk_constraint_need_on_chunk(chunk->relkind, con))
853+
if (chunk->relkind != RELKIND_FOREIGN_TABLE && chunk_constraint_need_on_chunk(con))
871854
{
872855
ChunkConstraint *cc = ts_chunk_constraints_add(chunk->constraints,
873856
chunk->fd.id,

src/planner/planner.c

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,49 @@ preprocess_query(Node *node, PreprocessQueryContext *context)
395395
* src/backend/utils/adt/ri_triggers.c
396396
*/
397397

398+
/*
399+
* RI_FKey_cascade_del
400+
*
401+
* DELETE FROM [ONLY] <fktable> WHERE $1 = fkatt1 [AND ...]
402+
*/
403+
if (query->commandType == CMD_DELETE && list_length(query->rtable) == 1 &&
404+
context->root->glob->boundParams && query->jointree->quals &&
405+
IsA(query->jointree->quals, OpExpr))
406+
{
407+
RangeTblEntry *rte = linitial_node(RangeTblEntry, query->rtable);
408+
if (!rte->inh && rte->rtekind == RTE_RELATION)
409+
{
410+
Hypertable *ht =
411+
ts_hypertable_cache_get_entry(hcache, rte->relid, CACHE_FLAG_MISSING_OK);
412+
if (ht)
413+
{
414+
rte->inh = true;
415+
}
416+
}
417+
}
418+
419+
/*
420+
* RI_FKey_cascade_upd
421+
*
422+
* UPDATE [ONLY] <fktable> SET fkatt1 = $1 [, ...]
423+
* WHERE $n = fkatt1 [AND ...]
424+
*/
425+
if (query->commandType == CMD_UPDATE && list_length(query->rtable) == 1 &&
426+
context->root->glob->boundParams && query->jointree->quals &&
427+
IsA(query->jointree->quals, OpExpr))
428+
{
429+
RangeTblEntry *rte = linitial_node(RangeTblEntry, query->rtable);
430+
if (!rte->inh && rte->rtekind == RTE_RELATION)
431+
{
432+
Hypertable *ht =
433+
ts_hypertable_cache_get_entry(hcache, rte->relid, CACHE_FLAG_MISSING_OK);
434+
if (ht)
435+
{
436+
rte->inh = true;
437+
}
438+
}
439+
}
440+
398441
/*
399442
* RI_FKey_check
400443
*

src/process_utility.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,18 @@ check_chunk_alter_table_operation_allowed(Oid relid, AlterTableStmt *stmt)
153153
}
154154
break;
155155
}
156+
case AT_DropConstraint:
157+
{
158+
/* if this is an OSM chunk, block the operation */
159+
Chunk *chunk = ts_chunk_get_by_relid(relid, false /* fail_if_not_found */);
160+
if (chunk && chunk->fd.osm_chunk)
161+
{
162+
ereport(ERROR,
163+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
164+
errmsg("operation not supported on OSM chunk tables")));
165+
}
166+
break;
167+
}
156168
default:
157169
/* disable by default */
158170
all_allowed = false;

test/expected/rowsecurity-14.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4794,7 +4794,7 @@ ALTER TABLE r2 FORCE ROW LEVEL SECURITY;
47944794
UPDATE r1 SET a = a+5;
47954795
ERROR: new row for relation "_hyper_28_117_chunk" violates check constraint "constraint_117"
47964796
DETAIL: Failing row contains (15).
4797-
CONTEXT: SQL statement "UPDATE ONLY "_timescaledb_internal"."_hyper_28_117_chunk" SET "a" = $1 WHERE $2 OPERATOR(pg_catalog.=) "a""
4797+
CONTEXT: SQL statement "UPDATE ONLY "regress_rls_schema"."r2" SET "a" = $1 WHERE $2 OPERATOR(pg_catalog.=) "a""
47984798
-- Remove FORCE from r2
47994799
ALTER TABLE r2 NO FORCE ROW LEVEL SECURITY;
48004800
-- As owner, we now bypass RLS

test/expected/rowsecurity-15.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4796,7 +4796,7 @@ ALTER TABLE r2 FORCE ROW LEVEL SECURITY;
47964796
UPDATE r1 SET a = a+5;
47974797
ERROR: new row for relation "_hyper_28_117_chunk" violates check constraint "constraint_117"
47984798
DETAIL: Failing row contains (15).
4799-
CONTEXT: SQL statement "UPDATE ONLY "_timescaledb_internal"."_hyper_28_117_chunk" SET "a" = $1 WHERE $2 OPERATOR(pg_catalog.=) "a""
4799+
CONTEXT: SQL statement "UPDATE ONLY "regress_rls_schema"."r2" SET "a" = $1 WHERE $2 OPERATOR(pg_catalog.=) "a""
48004800
-- Remove FORCE from r2
48014801
ALTER TABLE r2 NO FORCE ROW LEVEL SECURITY;
48024802
-- As owner, we now bypass RLS

test/expected/rowsecurity-16.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4796,7 +4796,7 @@ ALTER TABLE r2 FORCE ROW LEVEL SECURITY;
47964796
UPDATE r1 SET a = a+5;
47974797
ERROR: new row for relation "_hyper_28_117_chunk" violates check constraint "constraint_117"
47984798
DETAIL: Failing row contains (15).
4799-
CONTEXT: SQL statement "UPDATE ONLY "_timescaledb_internal"."_hyper_28_117_chunk" SET "a" = $1 WHERE $2 OPERATOR(pg_catalog.=) "a""
4799+
CONTEXT: SQL statement "UPDATE ONLY "regress_rls_schema"."r2" SET "a" = $1 WHERE $2 OPERATOR(pg_catalog.=) "a""
48004800
-- Remove FORCE from r2
48014801
ALTER TABLE r2 NO FORCE ROW LEVEL SECURITY;
48024802
-- As owner, we now bypass RLS

tsl/src/compression/compression_storage.c

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@
5252
static void set_toast_tuple_target_on_chunk(Oid compressed_table_id);
5353
static void set_statistics_on_compressed_chunk(Oid compressed_table_id);
5454
static void create_compressed_chunk_indexes(Chunk *chunk, CompressionSettings *settings);
55-
static void clone_constraints_to_chunk(Oid ht_reloid, const Chunk *compressed_chunk);
56-
static List *get_fk_constraints(Oid reloid);
5755

5856
int32
5957
compression_hypertable_create(Hypertable *ht, Oid owner, Oid tablespace_oid)
@@ -149,8 +147,6 @@ compression_chunk_create(Chunk *src_chunk, Chunk *chunk, List *column_defs, Oid
149147

150148
create_compressed_chunk_indexes(chunk, settings);
151149

152-
clone_constraints_to_chunk(src_chunk->hypertable_relid, chunk);
153-
154150
return chunk->table_id;
155151
}
156152

@@ -346,54 +342,3 @@ create_compressed_chunk_indexes(Chunk *chunk, CompressionSettings *settings)
346342

347343
ReleaseSysCache(index_tuple);
348344
}
349-
350-
static void
351-
clone_constraints_to_chunk(Oid ht_reloid, const Chunk *compressed_chunk)
352-
{
353-
CatalogSecurityContext sec_ctx;
354-
List *constraint_list = get_fk_constraints(ht_reloid);
355-
356-
ListCell *lc;
357-
ts_catalog_database_info_become_owner(ts_catalog_database_info_get(), &sec_ctx);
358-
foreach (lc, constraint_list)
359-
{
360-
Oid conoid = lfirst_oid(lc);
361-
CatalogInternalCall2(DDL_CONSTRAINT_CLONE,
362-
Int32GetDatum(conoid),
363-
Int32GetDatum(compressed_chunk->table_id));
364-
}
365-
ts_catalog_restore_user(&sec_ctx);
366-
}
367-
368-
static List *
369-
get_fk_constraints(Oid reloid)
370-
{
371-
SysScanDesc scan;
372-
ScanKeyData scankey;
373-
HeapTuple tuple;
374-
List *conlist = NIL;
375-
376-
Relation pg_constr = table_open(ConstraintRelationId, AccessShareLock);
377-
378-
ScanKeyInit(&scankey,
379-
Anum_pg_constraint_conrelid,
380-
BTEqualStrategyNumber,
381-
F_OIDEQ,
382-
ObjectIdGetDatum(reloid));
383-
384-
scan = systable_beginscan(pg_constr, ConstraintRelidTypidNameIndexId, true, NULL, 1, &scankey);
385-
while (HeapTupleIsValid(tuple = systable_getnext(scan)))
386-
{
387-
Form_pg_constraint form = (Form_pg_constraint) GETSTRUCT(tuple);
388-
389-
if (form->contype == CONSTRAINT_FOREIGN)
390-
{
391-
conlist = lappend_oid(conlist, form->oid);
392-
}
393-
}
394-
395-
systable_endscan(scan);
396-
table_close(pg_constr, AccessShareLock);
397-
398-
return conlist;
399-
}

tsl/src/compression/create.c

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -512,14 +512,13 @@ add_time_to_order_by_if_not_included(OrderBySettings obs, ArrayType *segmentby,
512512
/* returns list of constraints that need to be cloned on the compressed hypertable
513513
* This is limited to foreign key constraints now
514514
*/
515-
static List *
515+
static void
516516
validate_existing_constraints(Hypertable *ht, CompressionSettings *settings)
517517
{
518518
Relation pg_constr;
519519
SysScanDesc scan;
520520
ScanKeyData scankey;
521521
HeapTuple tuple;
522-
List *conlist = NIL;
523522

524523
ArrayType *arr;
525524

@@ -537,11 +536,17 @@ validate_existing_constraints(Hypertable *ht, CompressionSettings *settings)
537536
Form_pg_constraint form = (Form_pg_constraint) GETSTRUCT(tuple);
538537

539538
/*
540-
* We check primary, unique, and exclusion constraints. Move foreign
541-
* key constraints over to compression table ignore triggers
539+
* We check primary, unique, and exclusion constraints.
542540
*/
543-
if (form->contype == CONSTRAINT_CHECK || form->contype == CONSTRAINT_TRIGGER)
541+
if (form->contype == CONSTRAINT_CHECK || form->contype == CONSTRAINT_TRIGGER
542+
#if PG17_GE
543+
|| form->contype == CONSTRAINT_NOTNULL
544+
/* CONSTRAINT_NOTNULL introduced in PG17, see b0e96f311985 */
545+
#endif
546+
)
547+
{
544548
continue;
549+
}
545550
else if (form->contype == CONSTRAINT_EXCLUSION)
546551
{
547552
ereport(ERROR,
@@ -573,53 +578,23 @@ validate_existing_constraints(Hypertable *ht, CompressionSettings *settings)
573578

574579
arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */
575580
numkeys = ts_array_length(arr);
576-
if (ARR_NDIM(arr) != 1 || numkeys < 0 || ARR_HASNULL(arr) ||
577-
ARR_ELEMTYPE(arr) != INT2OID)
578-
elog(ERROR, "conkey is not a 1-D smallint array");
579581
attnums = (int16 *) ARR_DATA_PTR(arr);
580582
for (j = 0; j < numkeys; j++)
581583
{
582584
const char *attname = get_attname(settings->fd.relid, attnums[j], false);
583585

584-
if (form->contype == CONSTRAINT_FOREIGN)
585-
{
586-
/* is this a segment-by column */
587-
if (!ts_array_is_member(settings->fd.segmentby, attname))
588-
ereport(ERROR,
589-
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
590-
errmsg("column \"%s\" must be used for segmenting", attname),
591-
errdetail("The foreign key constraint \"%s\" cannot be"
592-
" enforced with the given compression configuration.",
593-
NameStr(form->conname))));
594-
}
595-
#if PG17_GE
596-
else if (form->contype == CONSTRAINT_NOTNULL)
597-
{
598-
/* CONSTRAINT_NOTNULL introduced in PG17, see b0e96f311985 */
599-
continue;
600-
}
601-
#endif
602586
/* is colno a segment-by or order_by column */
603-
else if (!form->conindid && !ts_array_is_member(settings->fd.segmentby, attname) &&
604-
!ts_array_is_member(settings->fd.orderby, attname))
587+
if (!form->conindid && !ts_array_is_member(settings->fd.segmentby, attname) &&
588+
!ts_array_is_member(settings->fd.orderby, attname))
605589
ereport(WARNING,
606590
(errmsg("column \"%s\" should be used for segmenting or ordering",
607591
attname)));
608592
}
609-
610-
if (form->contype == CONSTRAINT_FOREIGN)
611-
{
612-
Name conname = palloc0(NAMEDATALEN);
613-
namestrcpy(conname, NameStr(form->conname));
614-
conlist = lappend(conlist, conname);
615-
}
616593
}
617594
}
618595

619596
systable_endscan(scan);
620597
table_close(pg_constr, AccessShareLock);
621-
622-
return conlist;
623598
}
624599

625600
/*

tsl/test/expected/compression.out

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,10 +1144,9 @@ SELECT create_hypertable('table1','col1', chunk_time_interval => 10);
11441144
(22,public,table1,t)
11451145
(1 row)
11461146

1147-
-- Trying to list an incomplete set of fields of the compound key (should fail with a nice message)
1147+
-- Trying to list an incomplete set of fields of the compound key
11481148
ALTER TABLE table1 SET (timescaledb.compress, timescaledb.compress_segmentby = 'col1');
11491149
NOTICE: default order by for hypertable "table1" is set to ""
1150-
ERROR: column "col2" must be used for segmenting
11511150
-- Listing all fields of the compound key should succeed:
11521151
ALTER TABLE table1 SET (timescaledb.compress, timescaledb.compress_segmentby = 'col1,col2');
11531152
NOTICE: default order by for hypertable "table1" is set to ""

0 commit comments

Comments
 (0)