From bea2613455bea01aea9530c74c605800ed99d624 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Erik=20Nordstr=C3=B6m?= <erik@timescale.com>
Date: Tue, 6 Jul 2021 09:26:53 +0200
Subject: [PATCH] Add constraints when copying chunks across data nodes

Chunk constraints are now added after a chunk has been copied from one
data node to another. The constraints are added when the chunk is made
visible on the destination node, i.e., after data has been copied and
the chunk's metadata is created.

As an alternative, the constraints could be added when the chunk table
is first created, but before the metadata for the chunk is added. This
would have the benefit of validating each copied (inserted) row
against the constraints during the data copy phase. However, this
would also necessitate decoupling the step of creating the constraint
metadata from the creation of the actual constraints since the other
chunk metadata that is referenced does not yet exist. Such decoupling
would require validating that the metadata actually matches the
constraints of the table when the metadata is later created.

One downside of adding the constraints after data copying is that it
necessitates validating all the chunk's rows against the constraints
after insertion as opposed to during insertion. If this turns out to
be a performance issue, validation could be initially deferred. This
is left as a future optimization.
---
 src/chunk.c                     |  1 +
 tsl/src/chunk_api.c             | 10 +++++
 tsl/test/expected/chunk_api.out | 74 +++++++++++++++++++++++++++++----
 tsl/test/sql/chunk_api.sql      | 33 +++++++++++++++
 4 files changed, 109 insertions(+), 9 deletions(-)

diff --git a/src/chunk.c b/src/chunk.c
index 8a40c65f2..170f98a6a 100644
--- a/src/chunk.c
+++ b/src/chunk.c
@@ -1293,6 +1293,7 @@ chunk_create_from_hypercube_and_table_after_lock(const Hypertable *ht, Hypercube
 	chunk_add_constraints(chunk);
 	chunk_insert_into_metadata_after_lock(chunk);
 	chunk_add_inheritance(chunk, ht);
+	chunk_create_table_constraints(chunk);
 
 	return chunk;
 }
diff --git a/tsl/src/chunk_api.c b/tsl/src/chunk_api.c
index d39e8d49c..fe0976428 100644
--- a/tsl/src/chunk_api.c
+++ b/tsl/src/chunk_api.c
@@ -331,6 +331,16 @@ get_hypercube_from_slices(Jsonb *slices, const Hypertable *ht)
 
 	return hc;
 }
+
+/*
+ * Create a chunk and its metadata.
+ *
+ * This function will create a chunk, either from an existing table or by
+ * creating a new table. If chunk_table_relid is InvalidOid, the chunk table
+ * will be created, otherwise the table referenced by the relid will be
+ * used. The chunk will be associated with the hypertable given by
+ * hypertable_relid.
+ */
 Datum
 chunk_create(PG_FUNCTION_ARGS)
 {
diff --git a/tsl/test/expected/chunk_api.out b/tsl/test/expected/chunk_api.out
index 71c3fb8de..38937a4cb 100644
--- a/tsl/test/expected/chunk_api.out
+++ b/tsl/test/expected/chunk_api.out
@@ -796,6 +796,20 @@ ORDER BY chunk_name DESC
 LIMIT 1 \gset
 SELECT slices AS "SLICES"
 FROM _timescaledb_internal.show_chunk(:'CHUNK_SCHEMA'||'.'||:'CHUNK_NAME') \gset
+-- Save the constraints info in a table for later comparison
+CREATE TABLE original_chunk_constraints AS
+SELECT "Constraint", "Type", "Columns", "Index"::text, "Expr", "Deferrable", "Deferred", "Validated"
+FROM test.show_constraints(format('%I.%I', :'CHUNK_SCHEMA', :'CHUNK_NAME')::regclass);
+-- Save contraints metadata
+CREATE TABLE original_chunk_constraints_metadata AS
+SELECT
+    chunk_id,
+    dimension_slice_id,
+    constraint_name,
+    hypertable_constraint_name
+FROM _timescaledb_catalog.chunk_constraint con
+INNER JOIN _timescaledb_catalog.chunk ch ON (con.chunk_id = ch.id)
+WHERE ch.schema_name = :'CHUNK_SCHEMA' AND ch.table_name = :'CHUNK_NAME';
 DROP TABLE :CHUNK_SCHEMA.:CHUNK_NAME;
 SELECT attach_tablespace('tablespace1', 'chunkapi');
  attach_tablespace 
@@ -830,12 +844,51 @@ SELECT _timescaledb_internal.create_chunk('chunkapi', :'SLICES', :'CHUNK_SCHEMA'
  (11,10,_timescaledb_internal,_hyper_10_10_chunk,r,"{""time"": [1514419200000000, 1515024000000000]}",t)
 (1 row)
 
-SELECT * FROM test.show_constraints(format('%I.%I', :'CHUNK_SCHEMA', :'CHUNK_NAME')::regclass);
-     Constraint      | Type | Columns | Index |              Expr              | Deferrable | Deferred | Validated 
----------------------+------+---------+-------+--------------------------------+------------+----------+-----------
- chunkapi_temp_check | c    | {temp}  | -     | (temp > (0)::double precision) | f          | f        | t
-(1 row)
+-- Compare original and new constraints
+SELECT * FROM original_chunk_constraints;
+        Constraint         | Type | Columns  |                   Index                    |                                                                      Expr                                                                      | Deferrable | Deferred | Validated 
+---------------------------+------+----------+--------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------+------------+----------+-----------
+ 10_1_chunkapi_device_fkey | f    | {device} | devices_pkey                               |                                                                                                                                                | f          | f        | t
+ 10_2_chunkapi_pkey        | p    | {time}   | _timescaledb_internal."10_2_chunkapi_pkey" |                                                                                                                                                | f          | f        | t
+ chunkapi_temp_check       | c    | {temp}   | -                                          | (temp > (0)::double precision)                                                                                                                 | f          | f        | t
+ constraint_15             | c    | {time}   | -                                          | (("time" >= 'Wed Dec 27 16:00:00 2017 PST'::timestamp with time zone) AND ("time" < 'Wed Jan 03 16:00:00 2018 PST'::timestamp with time zone)) | f          | f        | t
+(4 rows)
 
+SELECT * FROM test.show_constraints(format('%I.%I', :'CHUNK_SCHEMA', :'CHUNK_NAME')::regclass);
+        Constraint         | Type | Columns  |                   Index                    |                                                                      Expr                                                                      | Deferrable | Deferred | Validated 
+---------------------------+------+----------+--------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------+------------+----------+-----------
+ 11_3_chunkapi_device_fkey | f    | {device} | devices_pkey                               |                                                                                                                                                | f          | f        | t
+ 11_4_chunkapi_pkey        | p    | {time}   | _timescaledb_internal."11_4_chunkapi_pkey" |                                                                                                                                                | f          | f        | t
+ chunkapi_temp_check       | c    | {temp}   | -                                          | (temp > (0)::double precision)                                                                                                                 | f          | f        | t
+ constraint_16             | c    | {time}   | -                                          | (("time" >= 'Wed Dec 27 16:00:00 2017 PST'::timestamp with time zone) AND ("time" < 'Wed Jan 03 16:00:00 2018 PST'::timestamp with time zone)) | f          | f        | t
+(4 rows)
+
+-- Compare original and new chunk constraints metadata
+SELECT * FROM original_chunk_constraints_metadata;
+ chunk_id | dimension_slice_id |      constraint_name      | hypertable_constraint_name 
+----------+--------------------+---------------------------+----------------------------
+       10 |                 15 | constraint_15             | 
+       10 |                    | 10_1_chunkapi_device_fkey | chunkapi_device_fkey
+       10 |                    | 10_2_chunkapi_pkey        | chunkapi_pkey
+(3 rows)
+
+SELECT
+    chunk_id,
+    dimension_slice_id,
+    constraint_name,
+    hypertable_constraint_name
+FROM _timescaledb_catalog.chunk_constraint con
+INNER JOIN _timescaledb_catalog.chunk ch ON (con.chunk_id = ch.id)
+WHERE ch.schema_name = :'CHUNK_SCHEMA' AND ch.table_name = :'CHUNK_NAME';
+ chunk_id | dimension_slice_id |      constraint_name      | hypertable_constraint_name 
+----------+--------------------+---------------------------+----------------------------
+       11 |                 16 | constraint_16             | 
+       11 |                    | 11_3_chunkapi_device_fkey | chunkapi_device_fkey
+       11 |                    | 11_4_chunkapi_pkey        | chunkapi_pkey
+(3 rows)
+
+DROP TABLE original_chunk_constraints;
+DROP TABLE original_chunk_constraints_metadata;
 -- The chunk should inherit the hypertable
 SELECT relname
 FROM pg_catalog.pg_inherits, pg_class 
@@ -926,10 +979,13 @@ SELECT * FROM chunkapi ORDER BY 1,2,3;
 -- are specific to the chunk.  Currently, foreign key, unique, and
 -- primary key constraints are not inherited or auto-created.
 SELECT * FROM test.show_constraints(format('%I.%I', :'CHUNK_SCHEMA', :'CHUNK_NAME')::regclass);
-     Constraint      | Type | Columns | Index |              Expr              | Deferrable | Deferred | Validated 
----------------------+------+---------+-------+--------------------------------+------------+----------+-----------
- chunkapi_temp_check | c    | {temp}  | -     | (temp > (0)::double precision) | f          | f        | t
-(1 row)
+        Constraint         | Type | Columns  |                   Index                    |                                                                      Expr                                                                      | Deferrable | Deferred | Validated 
+---------------------------+------+----------+--------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------+------------+----------+-----------
+ 13_7_chunkapi_device_fkey | f    | {device} | devices_pkey                               |                                                                                                                                                | f          | f        | t
+ 13_8_chunkapi_pkey        | p    | {time}   | _timescaledb_internal."13_8_chunkapi_pkey" |                                                                                                                                                | f          | f        | t
+ chunkapi_temp_check       | c    | {temp}   | -                                          | (temp > (0)::double precision)                                                                                                                 | f          | f        | t
+ constraint_18             | c    | {time}   | -                                          | (("time" >= 'Wed Dec 27 16:00:00 2017 PST'::timestamp with time zone) AND ("time" < 'Wed Jan 03 16:00:00 2018 PST'::timestamp with time zone)) | f          | f        | t
+(4 rows)
 
 DROP TABLE chunkapi;
 \c :TEST_DBNAME :ROLE_SUPERUSER
diff --git a/tsl/test/sql/chunk_api.sql b/tsl/test/sql/chunk_api.sql
index a7f24eddf..51334420a 100644
--- a/tsl/test/sql/chunk_api.sql
+++ b/tsl/test/sql/chunk_api.sql
@@ -404,6 +404,23 @@ LIMIT 1 \gset
 SELECT slices AS "SLICES"
 FROM _timescaledb_internal.show_chunk(:'CHUNK_SCHEMA'||'.'||:'CHUNK_NAME') \gset
 
+-- Save the constraints info in a table for later comparison
+CREATE TABLE original_chunk_constraints AS
+SELECT "Constraint", "Type", "Columns", "Index"::text, "Expr", "Deferrable", "Deferred", "Validated"
+FROM test.show_constraints(format('%I.%I', :'CHUNK_SCHEMA', :'CHUNK_NAME')::regclass);
+
+-- Save contraints metadata
+CREATE TABLE original_chunk_constraints_metadata AS
+SELECT
+    chunk_id,
+    dimension_slice_id,
+    constraint_name,
+    hypertable_constraint_name
+FROM _timescaledb_catalog.chunk_constraint con
+INNER JOIN _timescaledb_catalog.chunk ch ON (con.chunk_id = ch.id)
+WHERE ch.schema_name = :'CHUNK_SCHEMA' AND ch.table_name = :'CHUNK_NAME';
+
+
 DROP TABLE :CHUNK_SCHEMA.:CHUNK_NAME;
 
 SELECT attach_tablespace('tablespace1', 'chunkapi');
@@ -418,8 +435,24 @@ SELECT tablespace FROM pg_tables WHERE tablename = :'CHUNK_NAME';
 SELECT _timescaledb_internal.create_chunk('chunkapi', :'SLICES', :'CHUNK_SCHEMA', :'CHUNK_NAME',
 	   format('%I.%I', :'CHUNK_SCHEMA', :'CHUNK_NAME')::regclass);
 
+-- Compare original and new constraints
+SELECT * FROM original_chunk_constraints;
 SELECT * FROM test.show_constraints(format('%I.%I', :'CHUNK_SCHEMA', :'CHUNK_NAME')::regclass);
 
+-- Compare original and new chunk constraints metadata
+SELECT * FROM original_chunk_constraints_metadata;
+SELECT
+    chunk_id,
+    dimension_slice_id,
+    constraint_name,
+    hypertable_constraint_name
+FROM _timescaledb_catalog.chunk_constraint con
+INNER JOIN _timescaledb_catalog.chunk ch ON (con.chunk_id = ch.id)
+WHERE ch.schema_name = :'CHUNK_SCHEMA' AND ch.table_name = :'CHUNK_NAME';
+
+DROP TABLE original_chunk_constraints;
+DROP TABLE original_chunk_constraints_metadata;
+
 -- The chunk should inherit the hypertable
 SELECT relname
 FROM pg_catalog.pg_inherits, pg_class