From 4dcbe6114dda6d4a59224033be03552d8338a4b9 Mon Sep 17 00:00:00 2001 From: Matvey Arye Date: Wed, 16 Aug 2017 14:47:32 -0400 Subject: [PATCH] Add support for hypertable constraints This PR add support for primary-key, foreign-key, unique, and exclusion constraints. Previously supported are CHECK and NOT NULL constraints. Now, foreign key constraints where a hypertable references a plain table is support (while vice versa, with a plain table references a hypertable, is still not). --- Makefile | 1 - scripts/test_updates.sh | 5 + sql/chunk.sql | 13 +- sql/chunk_constraint.sql | 177 ++++++++++++ sql/ddl_api.sql | 7 +- sql/ddl_internal.sql | 95 ++++--- sql/ddl_triggers.sql | 60 ++-- sql/functions_load_order.txt | 1 + sql/schema_info.sql | 11 + sql/setup_main.sql | 11 +- sql/tables.sql | 11 +- sql/updates/post-0.4.2--0.5.0-dev.sql | 13 + sql/updates/pre-0.4.2--0.5.0-dev.sql | 25 ++ sql/util_internal_table_ddl.sql | 30 -- src/cache_invalidate.c | 15 +- src/catalog.c | 47 +++- src/catalog.h | 26 +- src/chunk_constraint.c | 20 +- src/compat.c | 18 ++ src/ddl_utils.c | 71 ----- src/ddl_utils.h | 9 - src/extension.c | 11 + src/process_utility.c | 190 +++++++++++++ src/utils.c | 11 + test/expected/alternate_users.out | 8 +- test/expected/constraint.out | 378 ++++++++++++++++++++++++++ test/expected/create_chunks.out | 124 +++++++++ test/expected/ddl.out | 8 +- test/expected/ddl_single.out | 8 +- test/expected/insert_single.out | 2 +- test/expected/pg_dump.out | 16 ++ test/expected/upsert.out | 4 +- test/sql/constraint.sql | 301 ++++++++++++++++++++ test/sql/include/ddl_ops_1.sql | 4 +- test/sql/updates/setup.sql | 17 +- test/sql/updates/test-0.1.1.sql | 7 + 36 files changed, 1523 insertions(+), 232 deletions(-) create mode 100644 sql/chunk_constraint.sql delete mode 100644 src/ddl_utils.c delete mode 100644 src/ddl_utils.h create mode 100644 test/expected/constraint.out create mode 100644 test/sql/constraint.sql diff --git a/Makefile b/Makefile index 7844b776b..9fba3993b 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,6 @@ SRCS = \ src/dimension_slice.c \ src/dimension_vector.c \ src/hypercube.c \ - src/ddl_utils.c \ src/chunk_constraint.c \ src/partitioning.c \ src/planner_utils.c \ diff --git a/scripts/test_updates.sh b/scripts/test_updates.sh index 03d102099..5737b5d9c 100755 --- a/scripts/test_updates.sh +++ b/scripts/test_updates.sh @@ -63,6 +63,11 @@ docker rm -f timescaledb-clean docker run -d --name timescaledb-clean -v ${BASE_DIR}:/src -v ${CLEAN_VOLUME}:/var/lib/postgresql/data ${UPDATE_TO_IMAGE}:${UPDATE_TO_TAG} wait_for_pg timescaledb-clean +#same restart of sequences needed on update container in case the update script modified sequences +docker rm -f timescaledb-updated +docker run -d --name timescaledb-updated -v ${BASE_DIR}:/src -v ${UPDATE_VOLUME}:/var/lib/postgresql/data ${UPDATE_TO_IMAGE}:${UPDATE_TO_TAG} +wait_for_pg timescaledb-updated + echo "Testing" docker exec timescaledb-updated /bin/bash \ -c "psql -X -v ECHO=ALL -h localhost -U postgres -d single -f /src/test/sql/updates/test-0.1.1.sql" > ${PGTEST_TMPDIR}/updated.out diff --git a/sql/chunk.sql b/sql/chunk.sql index e6eb87f27..7e35d20e0 100644 --- a/sql/chunk.sql +++ b/sql/chunk.sql @@ -12,6 +12,7 @@ CREATE OR REPLACE FUNCTION _timescaledb_internal.dimension_calculate_default_ran OUT range_end BIGINT) AS '$libdir/timescaledb', 'dimension_calculate_closed_range_default' LANGUAGE C STABLE; + -- Trigger for when chunk rows are changed. -- On Insert: create chunk table, add indexes, add triggers. -- On Delete: drop table @@ -21,6 +22,7 @@ $BODY$ DECLARE kind pg_class.relkind%type; hypertable_row _timescaledb_catalog.hypertable; + main_table_oid OID; BEGIN IF TG_OP = 'INSERT' THEN PERFORM _timescaledb_internal.chunk_create_table(NEW.id); @@ -28,14 +30,21 @@ BEGIN PERFORM _timescaledb_internal.create_chunk_index_row(NEW.schema_name, NEW.table_name, hi.main_schema_name, hi.main_index_name, hi.definition) FROM _timescaledb_catalog.hypertable_index hi - WHERE hi.hypertable_id = NEW.hypertable_id; + WHERE hi.hypertable_id = NEW.hypertable_id + ORDER BY format('%I.%I',main_schema_name, main_index_name)::regclass; SELECT * INTO STRICT hypertable_row FROM _timescaledb_catalog.hypertable WHERE id = NEW.hypertable_id; + main_table_oid := format('%I.%I', hypertable_row.schema_name, hypertable_row.table_name)::regclass; + + PERFORM _timescaledb_internal.create_chunk_constraint(NEW.id, oid) + FROM pg_constraint + WHERE conrelid = main_table_oid + AND _timescaledb_internal.need_chunk_constraint(NEW.hypertable_id, oid); PERFORM _timescaledb_internal.create_chunk_trigger(NEW.id, tgname, _timescaledb_internal.get_general_trigger_definition(oid)) FROM pg_trigger - WHERE tgrelid = format('%I.%I', hypertable_row.schema_name, hypertable_row.table_name)::regclass + WHERE tgrelid = main_table_oid AND _timescaledb_internal.need_chunk_trigger(NEW.hypertable_id, oid); RETURN NEW; diff --git a/sql/chunk_constraint.sql b/sql/chunk_constraint.sql new file mode 100644 index 000000000..14f0dfacb --- /dev/null +++ b/sql/chunk_constraint.sql @@ -0,0 +1,177 @@ +-- Creates a constraint on a chunk. +CREATE OR REPLACE FUNCTION _timescaledb_internal.create_chunk_constraint( + chunk_id INTEGER, + constraint_oid OID +) + RETURNS VOID LANGUAGE PLPGSQL AS +$BODY$ +DECLARE + sql_code TEXT; + constraint_row pg_constraint; + chunk_row _timescaledb_catalog.chunk; +BEGIN + SELECT * INTO STRICT constraint_row FROM pg_constraint WHERE OID = constraint_oid; + SELECT * INTO STRICT chunk_row FROM _timescaledb_catalog.chunk WHERE id = chunk_id; + + INSERT INTO _timescaledb_catalog.chunk_constraint (hypertable_constraint_name, chunk_id, constraint_name) + SELECT constraint_row.conname, chunk_row.id, format('%s_%s_%s', chunk_id, nextval('_timescaledb_catalog.chunk_constraint_name'), constraint_row.conname); +END +$BODY$; + +-- Drop a constraint on a chunk +-- static +CREATE OR REPLACE FUNCTION _timescaledb_internal.drop_chunk_constraint( + chunk_id INTEGER, + constraint_name NAME +) + RETURNS VOID LANGUAGE PLPGSQL AS +$BODY$ +DECLARE + chunk_row _timescaledb_catalog.chunk; +BEGIN + DELETE FROM _timescaledb_catalog.chunk_constraint c + WHERE c.hypertable_constraint_name = drop_chunk_constraint.constraint_name + AND c.chunk_id = drop_chunk_constraint.chunk_id; +END +$BODY$; + +-- do I need to add a hypertable constraint to the chunks?; +CREATE OR REPLACE FUNCTION _timescaledb_internal.need_chunk_constraint( + hypertable_id INTEGER, + constraint_oid OID +) + RETURNS BOOLEAN LANGUAGE PLPGSQL VOLATILE AS +$BODY$ +DECLARE + constraint_row record; +BEGIN + SELECT * INTO STRICT constraint_row FROM pg_constraint WHERE OID = constraint_oid; + + IF constraint_row.contype IN ('c') THEN + -- check and not null constraints handled by regular inheritance (from docs): + -- All check constraints and not-null constraints on a parent table are automatically inherited by its children, + -- unless explicitly specified otherwise with NO INHERIT clauses. Other types of constraints + -- (unique, primary key, and foreign key constraints) are not inherited." + + IF constraint_row.connoinherit THEN + RAISE 'NO INHERIT option not supported on hypertables: %', constraint_row.conname + USING ERRCODE = 'IO101'; + END IF; + + RETURN FALSE; + END IF; + RETURN TRUE; +END +$BODY$; + +-- Creates a constraint on all chunks for a hypertable. +CREATE OR REPLACE FUNCTION _timescaledb_internal.add_constraint( + hypertable_id INTEGER, + constraint_oid OID +) + RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS +$BODY$ +DECLARE + constraint_row pg_constraint; + hypertable_row _timescaledb_catalog.hypertable; +BEGIN + IF _timescaledb_internal.need_chunk_constraint(hypertable_id, constraint_oid) THEN + SELECT * INTO STRICT constraint_row FROM pg_constraint WHERE OID = constraint_oid; + + --check the validity of an index if a constrain uses an index + --note: foreign-key constraints are excluded because they point to indexes on the foreign table /not/ the hypertable + IF constraint_row.conindid <> 0 AND constraint_row.contype != 'f' THEN + SELECT * INTO STRICT hypertable_row FROM _timescaledb_catalog.hypertable WHERE id = hypertable_id; + PERFORM _timescaledb_internal.check_index(constraint_row.conindid, hypertable_row); + END IF; + + PERFORM _timescaledb_internal.create_chunk_constraint(c.id, constraint_oid) + FROM _timescaledb_catalog.chunk c + WHERE c.hypertable_id = add_constraint.hypertable_id; + END IF; +END +$BODY$; + +CREATE OR REPLACE FUNCTION _timescaledb_internal.add_constraint_by_name( + hypertable_id INTEGER, + constraint_name name +) + RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS +$BODY$ +DECLARE + constraint_oid OID; +BEGIN + SELECT oid INTO STRICT constraint_oid FROM pg_constraint WHERE conname = constraint_name + AND conrelid = _timescaledb_internal.main_table_from_hypertable(hypertable_id); + + PERFORM _timescaledb_internal.add_constraint(hypertable_id, constraint_oid); +END +$BODY$; + + +-- Drops constraint on all chunks for a hypertable. +CREATE OR REPLACE FUNCTION _timescaledb_internal.drop_constraint( + hypertable_id INTEGER, + constraint_name NAME +) + RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS +$BODY$ +BEGIN + PERFORM _timescaledb_internal.drop_chunk_constraint(c.id, constraint_name) + FROM _timescaledb_catalog.chunk c + WHERE c.hypertable_id = drop_constraint.hypertable_id; +END +$BODY$; + +CREATE OR REPLACE FUNCTION _timescaledb_internal.on_change_chunk_constraint() + RETURNS TRIGGER LANGUAGE PLPGSQL AS +$BODY$ +DECLARE + chunk_row _timescaledb_catalog.chunk; + constraint_oid OID; + ds_row _timescaledb_catalog.dimension_slice; + sql_code TEXT; +BEGIN + IF TG_OP = 'INSERT' THEN + SELECT * INTO STRICT chunk_row FROM _timescaledb_catalog.chunk c WHERE c.id = NEW.chunk_id; + IF NEW.dimension_slice_id IS NOT NULL THEN + SELECT * INTO STRICT ds_row FROM _timescaledb_catalog.dimension_slice ds WHERE ds.id = NEW.dimension_slice_id; + EXECUTE format( + $$ + ALTER TABLE %1$I.%2$I + ADD CONSTRAINT %3$s CHECK(%4$s) + $$, + chunk_row.schema_name, chunk_row.table_name, + NEW.constraint_name, + _timescaledb_internal.dimension_slice_get_constraint_sql(ds_row.id) + ); + ELSIF NEW.hypertable_constraint_name IS NOT NULL THEN + SELECT con.oid INTO STRICT constraint_oid + FROM _timescaledb_catalog.chunk c + INNER JOIN _timescaledb_catalog.hypertable h ON (h.id = c.hypertable_id) + INNER JOIN pg_constraint con ON (con.conrelid = format('%I.%I',h.schema_name, h.table_name)::regclass + AND con.conname = NEW.hypertable_constraint_name) + WHERE c.id = NEW.chunk_id; + + sql_code := format($$ ALTER TABLE %I.%I ADD CONSTRAINT %I %s $$, + chunk_row.schema_name, chunk_row.table_name, NEW.constraint_name, pg_get_constraintdef(constraint_oid) + ); + EXECUTE sql_code; + ELSE + RAISE 'Unknown constraint type'; + END IF; + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + SELECT * INTO chunk_row FROM _timescaledb_catalog.chunk c WHERE c.id = OLD.chunk_id; + IF FOUND THEN + EXECUTE format($$ ALTER TABLE %I.%I DROP CONSTRAINT %I $$, + chunk_row.schema_name, chunk_row.table_name, OLD.constraint_name + ); + END IF; + RETURN OLD; + END IF; + + PERFORM _timescaledb_internal.on_trigger_error(TG_OP, TG_TABLE_SCHEMA, TG_TABLE_NAME); +END +$BODY$; + diff --git a/sql/ddl_api.sql b/sql/ddl_api.sql index ce6c41b2f..073df750b 100644 --- a/sql/ddl_api.sql +++ b/sql/ddl_api.sql @@ -138,6 +138,10 @@ BEGIN USING ERRCODE = 'IO101'; END; + PERFORM _timescaledb_internal.add_constraint(hypertable_row.id, oid) + FROM pg_constraint + WHERE conrelid = main_table; + PERFORM 1 FROM pg_index, LATERAL _timescaledb_internal.add_index( @@ -146,7 +150,8 @@ BEGIN (SELECT relname FROM pg_class WHERE oid = indexrelid::regclass), _timescaledb_internal.get_general_index_definition(indexrelid, indrelid, hypertable_row) ) - WHERE indrelid = main_table; + WHERE indrelid = main_table AND _timescaledb_internal.need_chunk_index(hypertable_row.id, pg_index.indexrelid) + ORDER BY pg_index.indexrelid; PERFORM 1 FROM pg_trigger, diff --git a/sql/ddl_internal.sql b/sql/ddl_internal.sql index 4c617676e..f3b2a2528 100644 --- a/sql/ddl_internal.sql +++ b/sql/ddl_internal.sql @@ -161,6 +161,24 @@ BEGIN END $BODY$; +-- do I need to add a hypertable index to the chunks?; +CREATE OR REPLACE FUNCTION _timescaledb_internal.need_chunk_index( + hypertable_id INTEGER, + index_oid OID +) + RETURNS BOOLEAN LANGUAGE PLPGSQL VOLATILE AS +$BODY$ +DECLARE + associated_constraint BOOLEAN; +BEGIN + --do not add an index with associated constraints + SELECT count(*) > 0 INTO STRICT associated_constraint FROM pg_constraint WHERE conindid = index_oid; + + RETURN NOT associated_constraint; +END +$BODY$; + + -- Add an index to a hypertable CREATE OR REPLACE FUNCTION _timescaledb_internal.add_index( hypertable_id INTEGER, @@ -174,26 +192,21 @@ INSERT INTO _timescaledb_catalog.hypertable_index (hypertable_id, main_schema_na VALUES (hypertable_id, main_schema_name, main_index_name, definition); $BODY$; --- Add a trigger to a hypertable +CREATE OR REPLACE FUNCTION _timescaledb_internal.trigger_is_row_trigger(tgtype int2) RETURNS BOOLEAN + AS '$libdir/timescaledb', 'trigger_is_row_trigger' LANGUAGE C IMMUTABLE STRICT; + +-- do I need to add a hypertable trigger to the chunks? CREATE OR REPLACE FUNCTION _timescaledb_internal.need_chunk_trigger( hypertable_id INTEGER, trigger_oid OID ) - RETURNS BOOLEAN LANGUAGE PLPGSQL VOLATILE AS + RETURNS BOOLEAN LANGUAGE SQL STABLE AS $BODY$ -DECLARE - trigger_row record; -BEGIN - SELECT * INTO STRICT trigger_row FROM pg_trigger WHERE OID = trigger_oid; - - IF (trigger_row.tgtype & (1 << 0) != 0) THEN - -- row trigger - RETURN TRUE; - END IF; - RETURN FALSE; -END +-- row trigger and not an internal trigger used for constraints +SELECT _timescaledb_internal.trigger_is_row_trigger(t.tgtype) AND NOT t.tgisinternal FROM pg_trigger t WHERE OID = trigger_oid; $BODY$; + -- Add a trigger to a hypertable CREATE OR REPLACE FUNCTION _timescaledb_internal.add_trigger( hypertable_id INTEGER, @@ -273,6 +286,37 @@ BEGIN END $BODY$; +--Makes sure the index is valid for a hypertable. +CREATE OR REPLACE FUNCTION _timescaledb_internal.check_index(index_oid REGCLASS, hypertable_row _timescaledb_catalog.hypertable) +RETURNS VOID LANGUAGE plpgsql STABLE AS +$BODY$ +DECLARE + index_row RECORD; + missing_column TEXT; +BEGIN + SELECT * INTO STRICT index_row FROM pg_index WHERE indexrelid = index_oid; + IF index_row.indisunique OR index_row.indisexclusion THEN + -- unique/exclusion index must contain time and all partition dimension columns. + + -- get any partitioning columns that are not included in the index. + SELECT d.column_name INTO missing_column + FROM _timescaledb_catalog.dimension d + WHERE d.hypertable_id = hypertable_row.id AND + d.column_name NOT IN ( + SELECT attname + FROM pg_attribute + WHERE attrelid = index_row.indrelid AND + attnum = ANY(index_row.indkey) + ); + + IF missing_column IS NOT NULL THEN + RAISE EXCEPTION 'Cannot create a unique index without the column: % (used in partitioning)', missing_column + USING ERRCODE = 'IO103'; + END IF; + END IF; +END +$BODY$; + -- Create the "general definition" of an index. The general definition -- is the corresponding create index command with the placeholders /*TABLE_NAME*/ -- and /*INDEX_NAME*/ @@ -288,8 +332,6 @@ DECLARE def TEXT; index_name TEXT; c INTEGER; - index_row RECORD; - missing_column TEXT; BEGIN -- Get index definition def := pg_get_indexdef(index_oid); @@ -298,28 +340,7 @@ BEGIN RAISE EXCEPTION 'Cannot process index with no definition: %', index_oid::TEXT; END IF; - SELECT * INTO STRICT index_row FROM pg_index WHERE indexrelid = index_oid; - - IF index_row.indisunique THEN - -- unique index must contain time and all partition dimension columns. - - -- get any partitioning columns that are not included in the index. - SELECT d.column_name INTO missing_column - FROM _timescaledb_catalog.dimension d - WHERE d.hypertable_id = hypertable_row.id AND - d.column_name NOT IN ( - SELECT attname - FROM pg_attribute - WHERE attrelid = table_oid AND - attnum = ANY(index_row.indkey) - ); - - IF missing_column IS NOT NULL THEN - RAISE EXCEPTION 'Cannot create a unique index without the column: % (used in partitioning)', missing_column - USING ERRCODE = 'IO103'; - END IF; - END IF; - + PERFORM _timescaledb_internal.check_index(index_oid, hypertable_row); SELECT count(*) INTO c FROM regexp_matches(def, 'ON '||table_oid::TEXT || ' USING', 'g'); diff --git a/sql/ddl_triggers.sql b/sql/ddl_triggers.sql index 34ecbe56f..d663f1af4 100644 --- a/sql/ddl_triggers.sql +++ b/sql/ddl_triggers.sql @@ -9,14 +9,6 @@ */ -CREATE OR REPLACE FUNCTION _timescaledb_internal.ddl_is_change_owner(pg_ddl_command) - RETURNS bool IMMUTABLE STRICT - AS '$libdir/timescaledb' LANGUAGE C; - -CREATE OR REPLACE FUNCTION _timescaledb_internal.ddl_change_owner_to(pg_ddl_command) - RETURNS name IMMUTABLE STRICT - AS '$libdir/timescaledb' LANGUAGE C; - -- Handles ddl create index commands on hypertables CREATE OR REPLACE FUNCTION _timescaledb_internal.ddl_process_create_index() RETURNS event_trigger LANGUAGE plpgsql @@ -49,7 +41,7 @@ BEGIN hypertable_row.schema_name, (SELECT relname FROM pg_class WHERE oid = info.objid::regclass), def - ); + ) WHERE _timescaledb_internal.need_chunk_index(hypertable_row.id, info.objid); END LOOP; END $BODY$; @@ -135,6 +127,9 @@ BEGIN END $BODY$; + + + -- Handles ddl drop index commands on hypertables CREATE OR REPLACE FUNCTION _timescaledb_internal.ddl_process_drop_index() RETURNS event_trigger LANGUAGE plpgsql @@ -180,42 +175,29 @@ BEGIN END $BODY$; - CREATE OR REPLACE FUNCTION _timescaledb_internal.ddl_process_alter_table() - RETURNS event_trigger LANGUAGE plpgsql + CREATE OR REPLACE FUNCTION _timescaledb_internal.ddl_change_owner(main_table OID, new_table_owner NAME) + RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $BODY$ DECLARE - info record; - new_table_owner TEXT; - chunk_row _timescaledb_catalog.chunk; hypertable_row _timescaledb_catalog.hypertable; + chunk_row _timescaledb_catalog.chunk; BEGIN - --NOTE: pg_event_trigger_ddl_commands prevents this SECURITY DEFINER function from being called outside trigger. - FOR info IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP - IF NOT _timescaledb_internal.is_main_table(info.objid) THEN - RETURN; - END IF; - - IF _timescaledb_internal.ddl_is_change_owner(info.command) THEN - --if change owner then change owners on all chunks - new_table_owner := _timescaledb_internal.ddl_change_owner_to(info.command); - hypertable_row := _timescaledb_internal.hypertable_from_main_table(info.objid); - - FOR chunk_row IN - SELECT * - FROM _timescaledb_catalog.chunk - WHERE hypertable_id = hypertable_row.id - LOOP - EXECUTE format( - $$ - ALTER TABLE %1$I.%2$I OWNER TO %3$I - $$, - chunk_row.schema_name, chunk_row.table_name, - new_table_owner - ); - END LOOP; - END IF; + hypertable_row := _timescaledb_internal.hypertable_from_main_table(main_table); + FOR chunk_row IN + SELECT * + FROM _timescaledb_catalog.chunk + WHERE hypertable_id = hypertable_row.id + LOOP + EXECUTE format( + $$ + ALTER TABLE %1$I.%2$I OWNER TO %3$I + $$, + chunk_row.schema_name, chunk_row.table_name, + new_table_owner + ); END LOOP; END $BODY$; + diff --git a/sql/functions_load_order.txt b/sql/functions_load_order.txt index c6931561b..f92e2bd10 100644 --- a/sql/functions_load_order.txt +++ b/sql/functions_load_order.txt @@ -4,6 +4,7 @@ sql/util_time.sql sql/util_internal_table_ddl.sql sql/chunk.sql sql/chunk_trigger.sql +sql/chunk_constraint.sql sql/hypertable_triggers.sql sql/hypertable_index_triggers.sql sql/partitioning.sql diff --git a/sql/schema_info.sql b/sql/schema_info.sql index e1602d1ff..d7d85788f 100644 --- a/sql/schema_info.sql +++ b/sql/schema_info.sql @@ -42,6 +42,17 @@ $BODY$ WHERE c.OID = table_oid; $BODY$; +CREATE OR REPLACE FUNCTION _timescaledb_internal.main_table_from_hypertable( + hypertable_id int +) + RETURNS regclass LANGUAGE SQL STABLE AS +$BODY$ + SELECT format('%I.%I',h.schema_name, h.table_name)::regclass + FROM _timescaledb_catalog.hypertable h + WHERE id = hypertable_id; +$BODY$; + + -- Get the name of the time column for a chunk. -- -- schema_name, table_name - name of the schema and table for the table represented by the crn. diff --git a/sql/setup_main.sql b/sql/setup_main.sql index d5d99e821..bfdb1333f 100644 --- a/sql/setup_main.sql +++ b/sql/setup_main.sql @@ -20,6 +20,12 @@ BEGIN AFTER UPDATE OR DELETE OR INSERT ON _timescaledb_catalog.chunk FOR EACH ROW EXECUTE PROCEDURE _timescaledb_internal.on_change_chunk(); + DROP TRIGGER IF EXISTS trigger_main_on_change_chunk_constraint + ON _timescaledb_catalog.chunk_constraint; + CREATE TRIGGER trigger_main_on_change_chunk_constraint + AFTER UPDATE OR DELETE OR INSERT ON _timescaledb_catalog.chunk_constraint + FOR EACH ROW EXECUTE PROCEDURE _timescaledb_internal.on_change_chunk_constraint(); + -- no DELETE: it would be a no-op DROP TRIGGER IF EXISTS trigger_1_main_on_change_hypertable ON _timescaledb_catalog.hypertable; @@ -67,10 +73,6 @@ BEGIN ON sql_drop EXECUTE PROCEDURE _timescaledb_internal.ddl_process_drop_trigger(); - CREATE EVENT TRIGGER ddl_alter_table ON ddl_command_end - WHEN tag IN ('alter table') - EXECUTE PROCEDURE _timescaledb_internal.ddl_process_alter_table(); - CREATE EVENT TRIGGER ddl_check_drop_command ON sql_drop EXECUTE PROCEDURE _timescaledb_internal.ddl_process_drop_table(); @@ -81,7 +83,6 @@ BEGIN ALTER EXTENSION timescaledb ADD EVENT TRIGGER ddl_drop_index; ALTER EXTENSION timescaledb ADD EVENT TRIGGER ddl_create_trigger; ALTER EXTENSION timescaledb ADD EVENT TRIGGER ddl_drop_trigger; - ALTER EXTENSION timescaledb ADD EVENT TRIGGER ddl_alter_table; ALTER EXTENSION timescaledb ADD EVENT TRIGGER ddl_check_drop_command; END IF; diff --git a/sql/tables.sql b/sql/tables.sql index c2456cdd9..dd77421ff 100644 --- a/sql/tables.sql +++ b/sql/tables.sql @@ -119,10 +119,17 @@ SELECT pg_catalog.pg_extension_config_dump(pg_get_serial_sequence('_timescaledb_ -- on the chunk's data table. CREATE TABLE _timescaledb_catalog.chunk_constraint ( chunk_id INTEGER NOT NULL REFERENCES _timescaledb_catalog.chunk(id) ON DELETE CASCADE, - dimension_slice_id INTEGER NOT NULL REFERENCES _timescaledb_catalog.dimension_slice(id) ON DELETE CASCADE, - PRIMARY KEY(chunk_id, dimension_slice_id) + dimension_slice_id INTEGER NULL REFERENCES _timescaledb_catalog.dimension_slice(id) ON DELETE CASCADE, + constraint_name NAME NOT NULL, + hypertable_constraint_name NAME NULL, + UNIQUE(chunk_id, constraint_name) ); SELECT pg_catalog.pg_extension_config_dump('_timescaledb_catalog.chunk_constraint', ''); +CREATE INDEX ON _timescaledb_catalog.chunk_constraint(chunk_id, dimension_slice_id); + +CREATE SEQUENCE _timescaledb_catalog.chunk_constraint_name; +SELECT pg_catalog.pg_extension_config_dump('_timescaledb_catalog.chunk_constraint_name', ''); + -- Represents an index on the hypertable CREATE TABLE IF NOT EXISTS _timescaledb_catalog.hypertable_index ( diff --git a/sql/updates/post-0.4.2--0.5.0-dev.sql b/sql/updates/post-0.4.2--0.5.0-dev.sql index 45a28325c..e76ae98b7 100644 --- a/sql/updates/post-0.4.2--0.5.0-dev.sql +++ b/sql/updates/post-0.4.2--0.5.0-dev.sql @@ -17,3 +17,16 @@ DROP FUNCTION IF EXISTS _timescaledb_internal.chunk_calculate_new_ranges(INTEGER DROP FUNCTION IF EXISTS _timescaledb_internal.chunk_id_get_by_dimensions(INTEGER[], BIGINT[]) CASCADE; DROP FUNCTION IF EXISTS _timescaledb_internal.chunk_get_dimensions_constraint_sql(INTEGER[], BIGINT[]) CASCADE; DROP FUNCTION IF EXISTS _timescaledb_internal.chunk_get_dimension_constraint_sql(INTEGER, BIGINT) CASCADE; +CREATE TRIGGER trigger_main_on_change_chunk_constraint +AFTER UPDATE OR DELETE OR INSERT ON _timescaledb_catalog.chunk_constraint +FOR EACH ROW EXECUTE PROCEDURE _timescaledb_internal.on_change_chunk_constraint(); + +SELECT _timescaledb_internal.add_constraint(h.id, c.oid) +FROM _timescaledb_catalog.hypertable h +INNER JOIN pg_constraint c ON (c.conrelid = format('%I.%I', h.schema_name, h.table_name)::regclass); + +DELETE FROM _timescaledb_catalog.hypertable_index hi +WHERE EXISTS ( + SELECT 1 FROM pg_constraint WHERE conindid = format('%I.%I', hi.main_schema_name, hi.main_index_name)::regclass +); + diff --git a/sql/updates/pre-0.4.2--0.5.0-dev.sql b/sql/updates/pre-0.4.2--0.5.0-dev.sql index e69de29bb..daba6ec7b 100644 --- a/sql/updates/pre-0.4.2--0.5.0-dev.sql +++ b/sql/updates/pre-0.4.2--0.5.0-dev.sql @@ -0,0 +1,25 @@ +DROP FUNCTION _timescaledb_internal.ddl_is_change_owner(pg_ddl_command); +DROP FUNCTION _timescaledb_internal.ddl_change_owner_to(pg_ddl_command); + +DROP FUNCTION _timescaledb_internal.chunk_add_constraints(integer); +DROP FUNCTION _timescaledb_internal.ddl_process_alter_table() CASCADE; + +CREATE INDEX ON _timescaledb_catalog.chunk_constraint(chunk_id, dimension_slice_id) WHERE dimension_slice_id IS NOT NULL; + +ALTER TABLE _timescaledb_catalog.chunk_constraint +DROP CONSTRAINT chunk_constraint_pkey, +ADD COLUMN constraint_name NAME; + +UPDATE _timescaledb_catalog.chunk_constraint +SET constraint_name = format('constraint_%s', dimension_slice_id); + +ALTER TABLE _timescaledb_catalog.chunk_constraint +ALTER COLUMN constraint_name SET NOT NULL, +ALTER COLUMN dimension_slice_id DROP NOT NULL; + +ALTER TABLE _timescaledb_catalog.chunk_constraint +ADD COLUMN hypertable_constraint_name NAME NULL, +ADD CONSTRAINT chunk_constraint_pkey PRIMARY KEY (chunk_id, constraint_name); + +CREATE SEQUENCE _timescaledb_catalog.chunk_constraint_name; +SELECT pg_catalog.pg_extension_config_dump('_timescaledb_catalog.chunk_constraint_name', ''); diff --git a/sql/util_internal_table_ddl.sql b/sql/util_internal_table_ddl.sql index c99276298..737c0b17e 100644 --- a/sql/util_internal_table_ddl.sql +++ b/sql/util_internal_table_ddl.sql @@ -73,8 +73,6 @@ BEGIN chunk_row.schema_name, chunk_row.table_name, table_owner ); - - PERFORM _timescaledb_internal.chunk_add_constraints(chunk_id); END $BODY$; @@ -115,34 +113,6 @@ BEGIN END $BODY$; -CREATE OR REPLACE FUNCTION _timescaledb_internal.chunk_add_constraints( - chunk_id INTEGER -) - RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS -$BODY$ -DECLARE - constraint_row record; -BEGIN - FOR constraint_row IN - SELECT c.schema_name, c.table_name, ds.id as dimension_slice_id - FROM _timescaledb_catalog.chunk c - INNER JOIN _timescaledb_catalog.chunk_constraint cc ON (cc.chunk_id = c.id) - INNER JOIN _timescaledb_catalog.dimension_slice ds ON (ds.id = cc.dimension_slice_id) - WHERE c.id = chunk_add_constraints.chunk_id - LOOP - EXECUTE format( - $$ - ALTER TABLE %1$I.%2$I - ADD CONSTRAINT constraint_%3$s CHECK(%4$s) - $$, - constraint_row.schema_name, constraint_row.table_name, - constraint_row.dimension_slice_id, - _timescaledb_internal.dimension_slice_get_constraint_sql(constraint_row.dimension_slice_id) - ); - END LOOP; -END -$BODY$; - -- Outputs the create_hypertable command to recreate the given hypertable. -- -- This is currently used internally for our single hypertable backup tool diff --git a/src/cache_invalidate.c b/src/cache_invalidate.c index 37df848aa..42ad190d6 100644 --- a/src/cache_invalidate.c +++ b/src/cache_invalidate.c @@ -79,9 +79,20 @@ invalidate_relcache_trigger(PG_FUNCTION_ARGS) if (!CALLED_AS_TRIGGER(fcinfo)) elog(ERROR, "not called by trigger manager"); - /* arg 0 = relid of the proxy table */ + /* arg 0 = name of the proxy table */ proxy_oid = catalog_get_cache_proxy_id_by_name(catalog, trigdata->tg_trigger->tgargs[0]); - CacheInvalidateRelcacheByRelid(proxy_oid); + if (proxy_oid != 0) + { + CacheInvalidateRelcacheByRelid(proxy_oid); + } + else + { + /* + * This can happen during upgrade scripts when the catalog is + * unavailable + */ + CacheInvalidateRelcacheByRelid(get_relname_relid(trigdata->tg_trigger->tgargs[0], get_namespace_oid(CACHE_SCHEMA_NAME, false))); + } /* tuple to return to executor */ if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) diff --git a/src/catalog.c b/src/catalog.c index b176648b2..8bb4ade43 100644 --- a/src/catalog.c +++ b/src/catalog.c @@ -10,6 +10,7 @@ #include #include #include +#include #include "catalog.h" #include "extension.h" @@ -60,7 +61,7 @@ static const TableIndexDef catalog_table_index_definitions[_MAX_CATALOG_TABLES] [CHUNK_CONSTRAINT] = { .length = _MAX_CHUNK_CONSTRAINT_INDEX, .names = (char *[]) { - [CHUNK_CONSTRAINT_CHUNK_ID_DIMENSION_SLICE_ID_IDX] = "chunk_constraint_pkey", + [CHUNK_CONSTRAINT_CHUNK_ID_DIMENSION_SLICE_ID_IDX] = "chunk_constraint_chunk_id_dimension_slice_id_idx", } } }; @@ -73,6 +74,27 @@ static const char *catalog_table_serial_id_names[_MAX_CATALOG_TABLES] = { [CHUNK_CONSTRAINT] = NULL, }; +typedef struct InternalFunctionDef +{ + char *name; + size_t args; +} InternalFunctionDef; + +const static InternalFunctionDef internal_function_definitions[_MAX_INTERNAL_FUNCTIONS] = { + [DDL_CHANGE_OWNER] = { + .name = "ddl_change_owner", + .args = 2, + }, + [DDL_ADD_CONSTRAINT] = { + .name = "add_constraint_by_name", + .args = 2, + }, + [DDL_DROP_CONSTRAINT] = { + .name = "drop_constraint", + .args = 2, + } +}; + /* Names for proxy tables used for cache invalidation. Must match names in * sql/cache.sql */ static const char *cache_proxy_table_names[_MAX_CACHE_TYPES] = { @@ -126,7 +148,7 @@ catalog_get(void) if (MyDatabaseId == catalog.database_id) return &catalog; - if (!extension_is_loaded()) + if (!extension_is_loaded() || !IsTransactionState()) return &catalog; memset(&catalog, 0, sizeof(Catalog)); @@ -187,6 +209,21 @@ catalog_get(void) catalog.caches[i].inval_proxy_id = get_relname_relid(cache_proxy_table_names[i], catalog.cache_schema_id); + catalog.internal_schema_id = get_namespace_oid(INTERNAL_SCHEMA_NAME, false); + for (i = 0; i < _MAX_INTERNAL_FUNCTIONS; i++) + { + InternalFunctionDef def = internal_function_definitions[i]; + FuncCandidateList funclist = + FuncnameGetCandidates(list_make2(makeString(INTERNAL_SCHEMA_NAME), makeString(def.name)), + def.args, NULL, false, false, false); + + if (funclist == NULL || funclist->next) + elog(ERROR, "Oid lookup failed for the function %s with %lu args", def.name, def.args); + + catalog.functions[i].function_id = funclist->oid; + } + + return &catalog; } @@ -208,6 +245,12 @@ catalog_get_cache_proxy_id(Catalog *catalog, CacheType type) return catalog->caches[type].inval_proxy_id; } +Oid +catalog_get_internal_function_id(Catalog *catalog, InternalFunction func) +{ + return catalog->functions[func].function_id; +} + Oid catalog_get_cache_proxy_id_by_name(Catalog *catalog, const char *relname) { diff --git a/src/catalog.h b/src/catalog.h index 3c32f2ea4..ba2891830 100644 --- a/src/catalog.h +++ b/src/catalog.h @@ -30,8 +30,17 @@ enum CatalogTable _MAX_CATALOG_TABLES, }; +typedef enum InternalFunction +{ + DDL_CHANGE_OWNER = 0, + DDL_ADD_CONSTRAINT, + DDL_DROP_CONSTRAINT, + _MAX_INTERNAL_FUNCTIONS, +} InternalFunction; + #define CATALOG_SCHEMA_NAME "_timescaledb_catalog" #define CACHE_SCHEMA_NAME "_timescaledb_cache" +#define INTERNAL_SCHEMA_NAME "_timescaledb_internal" #define EXTENSION_NAME "timescaledb" /****************************** @@ -259,6 +268,8 @@ enum Anum_chunk_constraint { Anum_chunk_constraint_chunk_id = 1, Anum_chunk_constraint_dimension_slice_id, + Anum_chunk_constraint_constraint_name, + Anum_chunk_constraint_hypertable_constraint_name, _Anum_chunk_constraint_max, }; @@ -269,6 +280,8 @@ typedef struct FormData_chunk_constraint { int32 chunk_id; int32 dimension_slice_id; + NameData constraint_name; + NameData hypertable_constraint_name; } FormData_chunk_constraint; typedef FormData_chunk_constraint *Form_chunk_constraint; @@ -281,9 +294,9 @@ enum enum Anum_chunk_constraint_chunk_id_dimension_slice_id_idx { - Anum_chunk_constraint_chunk_id_dimension_id_idx_chunk_id = 1, - Anum_chunk_constraint_chunk_id_dimension_id_idx_dimension_slice_id, - _Anum_chunk_constraint_chunk_id_dimension_id_idx_max, + Anum_chunk_constraint_chunk_id_dimension_slice_id_idx_chunk_id = 1, + Anum_chunk_constraint_chunk_id_dimension_slice_id_idx_dimension_slice_id, + _Anum_chunk_constraint_chunk_id_dimension_slice_id_idx_max, }; @@ -323,6 +336,11 @@ typedef struct Catalog Oid inval_proxy_id; } caches[_MAX_CACHE_TYPES]; Oid owner_uid; + Oid internal_schema_id; + struct + { + Oid function_id; + } functions[_MAX_INTERNAL_FUNCTIONS]; } Catalog; @@ -339,6 +357,8 @@ void catalog_reset(void); Oid catalog_get_cache_proxy_id(Catalog *catalog, CacheType type); Oid catalog_get_cache_proxy_id_by_name(Catalog *catalog, const char *relname); +Oid catalog_get_internal_function_id(Catalog *catalog, InternalFunction func); + const char *catalog_get_cache_proxy_name(CacheType type); bool catalog_become_owner(Catalog *catalog, CatalogSecurityContext *sec_ctx); diff --git a/src/chunk_constraint.c b/src/chunk_constraint.c index 29fab93ca..df428e1e9 100644 --- a/src/chunk_constraint.c +++ b/src/chunk_constraint.c @@ -18,13 +18,19 @@ chunk_constraint_fill(ChunkConstraint *cc, HeapTuple tuple) return cc; } +static bool +chunk_constraint_for_dimension_slice(TupleInfo *ti, void *data) +{ + return !heap_attisnull(ti->tuple, Anum_chunk_constraint_dimension_slice_id); +} + static bool chunk_constraint_tuple_found(TupleInfo *ti, void *data) { Chunk *chunk = data; chunk_constraint_fill(&chunk->constraints[chunk->num_constraints++], ti->tuple); - + if (chunk->capacity == chunk->num_constraints) return false; @@ -50,6 +56,7 @@ chunk_constraint_scan_by_chunk_id(Chunk *chunk) .nkeys = 1, .scankey = scankey, .data = chunk, + .filter = chunk_constraint_for_dimension_slice, .tuple_found = chunk_constraint_tuple_found, .lockmode = AccessShareLock, .scandirection = ForwardScanDirection, @@ -57,7 +64,7 @@ chunk_constraint_scan_by_chunk_id(Chunk *chunk) chunk->num_constraints = 0; - ScanKeyInit(&scankey[0], Anum_chunk_constraint_chunk_id_dimension_id_idx_chunk_id, + ScanKeyInit(&scankey[0], Anum_chunk_constraint_chunk_id_dimension_slice_id_idx_chunk_id, BTEqualStrategyNumber, F_INT4EQ, Int32GetDatum(chunk->fd.id)); num_found = scanner_scan(&scanCtx); @@ -132,12 +139,13 @@ chunk_constraint_scan_by_dimension_slice_id(DimensionSlice *slice, ChunkScanCtx .nkeys = 1, .scankey = scankey, .data = &data, + .filter = chunk_constraint_for_dimension_slice, .tuple_found = chunk_constraint_dimension_id_tuple_found, .lockmode = AccessShareLock, .scandirection = ForwardScanDirection, }; - ScanKeyInit(&scankey[0], Anum_chunk_constraint_chunk_id_dimension_id_idx_dimension_slice_id, + ScanKeyInit(&scankey[0], Anum_chunk_constraint_chunk_id_dimension_slice_id_idx_dimension_slice_id, BTEqualStrategyNumber, F_INT4EQ, Int32GetDatum(slice->fd.id)); num_found = scanner_scan(&scanCtx); @@ -151,10 +159,16 @@ chunk_constraint_insert_relation(Relation rel, ChunkConstraint *constraint) TupleDesc desc = RelationGetDescr(rel); Datum values[Natts_chunk_constraint]; bool nulls[Natts_chunk_constraint] = {false}; + NameData constraint_name; + + sprintf(constraint_name.data, "constraint_%d", constraint->fd.dimension_slice_id); memset(values, 0, sizeof(values)); values[Anum_chunk_constraint_chunk_id - 1] = constraint->fd.chunk_id; values[Anum_chunk_constraint_dimension_slice_id - 1] = constraint->fd.dimension_slice_id; + values[Anum_chunk_constraint_constraint_name - 1] = NameGetDatum(&constraint_name); + + nulls[Anum_chunk_constraint_hypertable_constraint_name - 1] = true; catalog_insert_values(rel, desc, values, nulls); } diff --git a/src/compat.c b/src/compat.c index 9509e0c93..d6fa2faa5 100644 --- a/src/compat.c +++ b/src/compat.c @@ -20,3 +20,21 @@ insert_main_table_trigger_after(PG_FUNCTION_ARGS) elog(ERROR, "Deprecated trigger function should not be invoked"); PG_RETURN_NULL(); } + +PG_FUNCTION_INFO_V1(ddl_is_change_owner); + +Datum +ddl_is_change_owner(PG_FUNCTION_ARGS) +{ + elog(ERROR, "Deprecated function should not be invoked"); + PG_RETURN_NULL(); +} + +PG_FUNCTION_INFO_V1(ddl_change_owner_to); + +Datum +ddl_change_owner_to(PG_FUNCTION_ARGS) +{ + elog(ERROR, "Deprecated function should not be invoked"); + PG_RETURN_NULL(); +} diff --git a/src/ddl_utils.c b/src/ddl_utils.c deleted file mode 100644 index 6c8064e58..000000000 --- a/src/ddl_utils.c +++ /dev/null @@ -1,71 +0,0 @@ -#include -#include - -#include "ddl_utils.h" - -enum ddl_cmd_type -{ - DDL_CHANGE_OWNER, DDL_OTHER -}; - -static enum ddl_cmd_type -ddl_alter_table_subcmd(CollectedCommand *cmd) -{ - ListCell *cell; - - if (cmd->type == SCT_AlterTable) - { - foreach(cell, cmd->d.alterTable.subcmds) - { - CollectedATSubcmd *sub = lfirst(cell); - AlterTableCmd *subcmd = (AlterTableCmd *) sub->parsetree; - - Assert(IsA(subcmd, AlterTableCmd)); - - switch (subcmd->subtype) - { - case AT_ChangeOwner: - return DDL_CHANGE_OWNER; - default: - break; - } - } - } - return DDL_OTHER; -} - - -Datum -ddl_change_owner_to(PG_FUNCTION_ARGS) -{ - CollectedATSubcmd *sub; - AlterTableCmd *altersub; - RoleSpec *role; - Name user = palloc0(NAMEDATALEN); - CollectedCommand *cmd = (CollectedCommand *) PG_GETARG_POINTER(0); - - Assert(cmd->type == SCT_AlterTable); - Assert(list_length(cmd->d.alterTable.subcmds) == 1); - sub = linitial(cmd->d.alterTable.subcmds); - - Assert(IsA(sub->parsetree, AlterTableCmd)); - altersub = (AlterTableCmd *) sub->parsetree; - - Assert(IsA(altersub->newowner, RoleSpec)); - role = (RoleSpec *) altersub->newowner; - - memcpy(user->data, role->rolename, NAMEDATALEN); - - PG_RETURN_NAME(user); -} - -Datum -ddl_is_change_owner(PG_FUNCTION_ARGS) -{ - bool ret = - DDL_CHANGE_OWNER == ddl_alter_table_subcmd( - (CollectedCommand *) PG_GETARG_POINTER(0) - ); - - PG_RETURN_BOOL(ret); -} diff --git a/src/ddl_utils.h b/src/ddl_utils.h deleted file mode 100644 index 032fd53d0..000000000 --- a/src/ddl_utils.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef TIMESCALEDB_DDL_UTILS_H -#define TIMESCALEDB_DDL_UTILS_H -#include -#include - -PG_FUNCTION_INFO_V1(ddl_is_change_owner); -PG_FUNCTION_INFO_V1(ddl_change_owner_to); - -#endif /* TIMESCALEDB_DDL_UTILS_H */ diff --git a/src/extension.c b/src/extension.c index 78c368e11..2f9d1043d 100644 --- a/src/extension.c +++ b/src/extension.c @@ -180,6 +180,17 @@ extension_is_loaded(void) extension_update_state(); } + if (creating_extension && OidIsValid(get_extension_oid(EXTENSION_NAME, true)) && get_extension_oid(EXTENSION_NAME, true) == CurrentExtensionObject) + { + /* turn off extension during upgrade scripts */ + + /* + * This is necessary so that, for example, the catalog does not go + * looking for things that aren't yet there. + */ + return false; + } + switch (extstate) { case EXTENSION_STATE_CREATED: diff --git a/src/process_utility.c b/src/process_utility.c index c36118477..a560fc523 100644 --- a/src/process_utility.c +++ b/src/process_utility.c @@ -4,11 +4,14 @@ #include #include #include +#include #include #include #include +#include #include #include +#include #include #include "utils.h" @@ -308,6 +311,185 @@ process_reindex(Node *parsetree) return false; } +static void +process_altertable_change_owner(Hypertable *ht, AlterTableCmd *cmd, Oid relid) +{ + RoleSpec *role; + + Assert(IsA(cmd->newowner, RoleSpec)); + role = (RoleSpec *) cmd->newowner; + OidFunctionCall2(catalog_get_internal_function_id(catalog_get(), DDL_CHANGE_OWNER), + ObjectIdGetDatum(relid), CStringGetDatum(role->rolename)); +} + +static void +process_altertable_add_constraint(Hypertable *ht, const char* constraint_name) +{ + Assert(constraint_name != NULL); + OidFunctionCall2(catalog_get_internal_function_id(catalog_get(), DDL_ADD_CONSTRAINT), + Int32GetDatum(ht->fd.id), CStringGetDatum(constraint_name)); +} + + +static void +process_altertable_drop_constraint(Hypertable *ht, AlterTableCmd *cmd, Oid relid) +{ + char *constraint_name = NULL; + + constraint_name = cmd->name; + Assert(constraint_name != NULL); + OidFunctionCall2(catalog_get_internal_function_id(catalog_get(), DDL_DROP_CONSTRAINT), + Int32GetDatum(ht->fd.id), CStringGetDatum(constraint_name)); +} + +/* foreign-key constraints to hypertables are not allowed */ +static void +verify_constraint(Constraint *stmt) +{ + if (stmt->contype == CONSTR_FOREIGN) + { + RangeVar *primary_table = stmt->pktable; + Oid primary_oid = RangeVarGetRelid(primary_table, NoLock, true); + + if (OidIsValid(primary_oid)) + { + Cache *hcache = hypertable_cache_pin(); + Hypertable *ht = hypertable_cache_get_entry(hcache, primary_oid); + + if (NULL != ht) + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Foreign keys to hypertables are not supported."))); + } + cache_release(hcache); + } + } +} + +static void +verify_constraint_list(List *constraint_list) +{ + ListCell *lc; + + foreach(lc, constraint_list) + { + Constraint *constraint = (Constraint *) lfirst(lc); + + verify_constraint(constraint); + } +} + +/* process all create table commands to make sure their constraints are kosher */ +static void +process_create_table(Node *parsetree) +{ + CreateStmt *stmt = (CreateStmt *) parsetree; + ListCell *lc; + + verify_constraint_list(stmt->constraints); + foreach(lc, stmt->tableElts) + { + ColumnDef *column_def = (ColumnDef *) lfirst(lc); + + verify_constraint_list(column_def->constraints); + } +} + +/* process all regular-table alter commands to make sure they aren't adding + * foreign-key constraints to hypertables */ +static void +process_altertable_plain_table(Node *parsetree) +{ + AlterTableStmt *stmt = (AlterTableStmt *) parsetree; + ListCell *lc; + + foreach(lc, stmt->cmds) + { + AlterTableCmd *cmd = (AlterTableCmd *) lfirst(lc); + + switch (cmd->subtype) + { + case AT_AddConstraint: + case AT_AddConstraintRecurse: + { + Constraint *constraint = (Constraint *) cmd->def; + + Assert(IsA(cmd->def, Constraint)); + + verify_constraint(constraint); + } + default: + break; + } + } +} + +static void +process_altertable(Node *parsetree) +{ + AlterTableStmt *stmt = (AlterTableStmt *) parsetree; + Oid relid = AlterTableLookupRelation(stmt, NoLock); + Cache *hcache = NULL; + ListCell *lc; + Hypertable *ht; + + if (!OidIsValid(relid)) + return; + + hcache = hypertable_cache_pin(); + + ht = hypertable_cache_get_entry(hcache, relid); + if (NULL == ht) + { + cache_release(hcache); + process_altertable_plain_table(parsetree); + return; + } + + foreach(lc, stmt->cmds) + { + AlterTableCmd *cmd = (AlterTableCmd *) lfirst(lc); + + switch (cmd->subtype) + { + case AT_ChangeOwner: + process_altertable_change_owner(ht, cmd, relid); + break; + case AT_AddIndex: + { + Assert(IsA(cmd->def, IndexStmt)); + IndexStmt *stmt = (IndexStmt *) cmd->def; + + Assert(stmt->isconstraint); + process_altertable_add_constraint(ht, stmt->idxname); + } + /* + * AddConstraint sometimes transformed to AddIndex if Index is + * involved. different path than CREATE INDEX. + */ + case AT_AddConstraint: + case AT_AddConstraintRecurse: + { + Assert(IsA(cmd->def, Constraint)); + Constraint *stmt = (Constraint *) cmd->def; + + process_altertable_add_constraint(ht, stmt->conname); + } + + break; + case AT_DropConstraint: + case AT_DropConstraintRecurse: + process_altertable_drop_constraint(ht, cmd, relid); + break; + default: + break; + } + } + + cache_release(hcache); +} + /* Hook-intercept for ProcessUtility. */ static void @@ -329,12 +511,20 @@ timescaledb_ProcessUtility(Node *parsetree, case T_TruncateStmt: process_truncate(parsetree); break; + case T_AlterTableStmt: + /* Process main table first to error out if not a valid alter */ + prev_ProcessUtility(parsetree, query_string, context, params, dest, completion_tag); + process_altertable(parsetree); + return; case T_AlterObjectSchemaStmt: process_alterobjectschema(parsetree); break; case T_RenameStmt: process_rename(parsetree); break; + case T_CreateStmt: + process_create_table(parsetree); + break; case T_CopyStmt: if (process_copy(parsetree, query_string, completion_tag)) return; diff --git a/src/utils.c b/src/utils.c index 2b6a86443..c0182fe5a 100644 --- a/src/utils.c +++ b/src/utils.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -373,3 +374,13 @@ date_bucket(PG_FUNCTION_ARGS) bucketed = DirectFunctionCall2(timestamp_bucket, PG_GETARG_DATUM(0), converted_ts); return DirectFunctionCall1(timestamp_date, bucketed); } + +PG_FUNCTION_INFO_V1(trigger_is_row_trigger); + +Datum +trigger_is_row_trigger(PG_FUNCTION_ARGS) +{ + int16 tgtype = PG_GETARG_INT16(0); + + PG_RETURN_BOOL(TRIGGER_FOR_ROW(tgtype)); +} diff --git a/test/expected/alternate_users.out b/test/expected/alternate_users.out index 9ca5744f2..a6418829c 100644 --- a/test/expected/alternate_users.out +++ b/test/expected/alternate_users.out @@ -133,7 +133,7 @@ SELECT * FROM _timescaledb_catalog.hypertable; 4 | customSchema | Hypertable_1 | _timescaledb_internal | _hyper_4 | 1 (4 rows) -SELECT * FROM _timescaledb_catalog.hypertable_index; +SELECT * FROM _timescaledb_catalog.hypertable_index ORDER BY format('%I.%I', main_schema_name, main_index_name)::regclass; hypertable_id | main_schema_name | main_index_name | definition ---------------+------------------+------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------- 1 | public | one_Partition_device_id_timeCustom_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree (device_id, "timeCustom" DESC NULLS LAST) WHERE (device_id IS NOT NULL) @@ -144,9 +144,9 @@ SELECT * FROM _timescaledb_catalog.hypertable_index; 1 | public | one_Partition_timeCustom_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("timeCustom" DESC) 2 | public | 1dim_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) 3 | public | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") + 4 | customSchema | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") 3 | public | Hypertable_1_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) 3 | public | Hypertable_1_Device_id_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("Device_id", "time" DESC) - 4 | customSchema | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") 4 | customSchema | Hypertable_1_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) (12 rows) @@ -161,7 +161,7 @@ INSERT INTO "customSchema"."Hypertable_1"(time, "Device_id", temp_c, humidity, s VALUES(1257894000000000000, 'dev1', 30, 70, 1, 2, 3, 100); INSERT INTO "customSchema"."Hypertable_1"(time, "Device_id", temp_c, humidity, sensor_1, sensor_2, sensor_3, sensor_4) VALUES(1257894000000000001, 'dev1', 30, 70, 1, 2, 3, 100); -SELECT * FROM _timescaledb_catalog.hypertable_index; +SELECT * FROM _timescaledb_catalog.hypertable_index ORDER BY format('%I.%I', main_schema_name, main_index_name)::regclass; hypertable_id | main_schema_name | main_index_name | definition ---------------+------------------+------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------- 1 | public | one_Partition_device_id_timeCustom_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree (device_id, "timeCustom" DESC NULLS LAST) WHERE (device_id IS NOT NULL) @@ -172,9 +172,9 @@ SELECT * FROM _timescaledb_catalog.hypertable_index; 1 | public | one_Partition_timeCustom_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("timeCustom" DESC) 2 | public | 1dim_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) 3 | public | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") + 4 | customSchema | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") 3 | public | Hypertable_1_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) 3 | public | Hypertable_1_Device_id_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("Device_id", "time" DESC) - 4 | customSchema | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") 4 | customSchema | Hypertable_1_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) 3 | public | Hypertable_1_time_temp_c_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", temp_c) 3 | public | ind_humidity | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", humidity) diff --git a/test/expected/constraint.out b/test/expected/constraint.out new file mode 100644 index 000000000..b5df6c34c --- /dev/null +++ b/test/expected/constraint.out @@ -0,0 +1,378 @@ +\o /dev/null +\ir include/create_single_db.sql +SET client_min_messages = WARNING; +DROP DATABASE IF EXISTS single; +SET client_min_messages = NOTICE; +CREATE DATABASE single; +\c single +CREATE EXTENSION IF NOT EXISTS timescaledb; +SET timescaledb.disable_optimizations = :DISABLE_OPTIMIZATIONS; +\o +CREATE TABLE hyper ( + time BIGINT NOT NULL, + device_id TEXT NOT NULL, + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10) +); +SELECT * FROM create_hypertable('hyper', 'time', chunk_time_interval => 10); + create_hypertable +------------------- + +(1 row) + +--check and not-null constraints are inherited through regular inheritance. +\set ON_ERROR_STOP 0 +INSERT INTO hyper(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 9); +ERROR: new row for relation "_hyper_1_1_chunk" violates check constraint "hyper_sensor_1_check" +INSERT INTO hyper(time, device_id,sensor_1) VALUES +(1257987700000000000, NULL, 11); +ERROR: null value in column "device_id" violates not-null constraint +\set ON_ERROR_STOP 1 +INSERT INTO hyper(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +INSERT INTO hyper(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +----------------------- UNIQUE CONSTRAINTS ------------------ +CREATE TABLE hyper_unique ( + time BIGINT NOT NULL UNIQUE, + device_id TEXT NOT NULL, + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10) +); +SELECT * FROM create_hypertable('hyper_unique', 'time', chunk_time_interval => 10); + create_hypertable +------------------- + +(1 row) + +INSERT INTO hyper_unique(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +INSERT INTO hyper_unique(time, device_id,sensor_1) VALUES +(1257987800000000000, 'dev2', 11); +\set ON_ERROR_STOP 0 +INSERT INTO hyper_unique(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +ERROR: duplicate key value violates unique constraint "4_1_hyper_unique_time_key" +\set ON_ERROR_STOP 1 +\d+ hyper + Table "public.hyper" + Column | Type | Modifiers | Storage | Stats target | Description +-----------+---------+-----------+----------+--------------+------------- + time | bigint | not null | plain | | + device_id | text | not null | extended | | + sensor_1 | numeric | default 1 | main | | +Indexes: + "hyper_time_idx" btree ("time" DESC) +Check constraints: + "hyper_sensor_1_check" CHECK (sensor_1 > 10::numeric) +Child tables: _timescaledb_internal._hyper_1_3_chunk + +\d+ hyper_unique + Table "public.hyper_unique" + Column | Type | Modifiers | Storage | Stats target | Description +-----------+---------+-----------+----------+--------------+------------- + time | bigint | not null | plain | | + device_id | text | not null | extended | | + sensor_1 | numeric | default 1 | main | | +Indexes: + "hyper_unique_time_key" UNIQUE CONSTRAINT, btree ("time") +Check constraints: + "hyper_unique_sensor_1_check" CHECK (sensor_1 > 10::numeric) +Child tables: _timescaledb_internal._hyper_2_4_chunk, + _timescaledb_internal._hyper_2_5_chunk + +--should have unique constraint not just unique index +\d+ _timescaledb_internal._hyper_2_4_chunk + Table "_timescaledb_internal._hyper_2_4_chunk" + Column | Type | Modifiers | Storage | Stats target | Description +-----------+---------+-----------+----------+--------------+------------- + time | bigint | not null | plain | | + device_id | text | not null | extended | | + sensor_1 | numeric | default 1 | main | | +Indexes: + "4_1_hyper_unique_time_key" UNIQUE CONSTRAINT, btree ("time") +Check constraints: + "constraint_4" CHECK ("time" >= '1257987700000000000'::bigint AND "time" < '1257987700000000010'::bigint) + "hyper_unique_sensor_1_check" CHECK (sensor_1 > 10::numeric) +Inherits: hyper_unique + +ALTER TABLE hyper_unique DROP CONSTRAINT hyper_unique_time_key; +\d+ _timescaledb_internal._hyper_2_4_chunk + Table "_timescaledb_internal._hyper_2_4_chunk" + Column | Type | Modifiers | Storage | Stats target | Description +-----------+---------+-----------+----------+--------------+------------- + time | bigint | not null | plain | | + device_id | text | not null | extended | | + sensor_1 | numeric | default 1 | main | | +Check constraints: + "constraint_4" CHECK ("time" >= '1257987700000000000'::bigint AND "time" < '1257987700000000010'::bigint) + "hyper_unique_sensor_1_check" CHECK (sensor_1 > 10::numeric) +Inherits: hyper_unique + +--uniqueness not enforced +INSERT INTO hyper_unique(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev3', 11); +--shouldn't be able to create constraint +\set ON_ERROR_STOP 0 +ALTER TABLE hyper_unique ADD CONSTRAINT hyper_unique_time_key UNIQUE (time); +ERROR: could not create unique index "4_3_hyper_unique_time_key" +\set ON_ERROR_STOP 1 +DELETE FROM hyper_unique WHERE device_id = 'dev3'; +--now can create +ALTER TABLE hyper_unique ADD CONSTRAINT hyper_unique_time_key UNIQUE (time); +\d+ _timescaledb_internal._hyper_2_4_chunk + Table "_timescaledb_internal._hyper_2_4_chunk" + Column | Type | Modifiers | Storage | Stats target | Description +-----------+---------+-----------+----------+--------------+------------- + time | bigint | not null | plain | | + device_id | text | not null | extended | | + sensor_1 | numeric | default 1 | main | | +Indexes: + "4_4_hyper_unique_time_key" UNIQUE CONSTRAINT, btree ("time") +Check constraints: + "constraint_4" CHECK ("time" >= '1257987700000000000'::bigint AND "time" < '1257987700000000010'::bigint) + "hyper_unique_sensor_1_check" CHECK (sensor_1 > 10::numeric) +Inherits: hyper_unique + +--test adding constraint with same name to different table -- should fail +\set ON_ERROR_STOP 0 +ALTER TABLE hyper ADD CONSTRAINT hyper_unique_time_key UNIQUE (time); +ERROR: relation "hyper_unique_time_key" already exists +\set ON_ERROR_STOP 1 +--uniquness violation fails +\set ON_ERROR_STOP 0 +INSERT INTO hyper_unique(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +ERROR: duplicate key value violates unique constraint "4_4_hyper_unique_time_key" +\set ON_ERROR_STOP 1 +--cannot create unique constraint on non-partition column +\set ON_ERROR_STOP 0 +ALTER TABLE hyper_unique ADD CONSTRAINT hyper_unique_invalid UNIQUE (device_id); +ERROR: Cannot create a unique index without the column: time (used in partitioning) +\set ON_ERROR_STOP 1 +----------------------- PRIMARY KEY ------------------ +CREATE TABLE hyper_pk ( + time BIGINT NOT NULL PRIMARY KEY, + device_id TEXT NOT NULL, + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10) +); +SELECT * FROM create_hypertable('hyper_pk', 'time', chunk_time_interval => 10); + create_hypertable +------------------- + +(1 row) + +INSERT INTO hyper_pk(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +\set ON_ERROR_STOP 0 +INSERT INTO hyper_pk(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +ERROR: duplicate key value violates unique constraint "6_6_hyper_pk_pkey" +\set ON_ERROR_STOP 1 +--should have unique constraint not just unique index +\d+ _timescaledb_internal._hyper_3_6_chunk + Table "_timescaledb_internal._hyper_3_6_chunk" + Column | Type | Modifiers | Storage | Stats target | Description +-----------+---------+-----------+----------+--------------+------------- + time | bigint | not null | plain | | + device_id | text | not null | extended | | + sensor_1 | numeric | default 1 | main | | +Indexes: + "6_6_hyper_pk_pkey" PRIMARY KEY, btree ("time") +Check constraints: + "constraint_6" CHECK ("time" >= '1257987700000000000'::bigint AND "time" < '1257987700000000010'::bigint) + "hyper_pk_sensor_1_check" CHECK (sensor_1 > 10::numeric) +Inherits: hyper_pk + +ALTER TABLE hyper_pk DROP CONSTRAINT hyper_pk_pkey; +\d+ _timescaledb_internal._hyper_3_6_chunk + Table "_timescaledb_internal._hyper_3_6_chunk" + Column | Type | Modifiers | Storage | Stats target | Description +-----------+---------+-----------+----------+--------------+------------- + time | bigint | not null | plain | | + device_id | text | not null | extended | | + sensor_1 | numeric | default 1 | main | | +Check constraints: + "constraint_6" CHECK ("time" >= '1257987700000000000'::bigint AND "time" < '1257987700000000010'::bigint) + "hyper_pk_sensor_1_check" CHECK (sensor_1 > 10::numeric) +Inherits: hyper_pk + +--uniqueness not enforced +INSERT INTO hyper_pk(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev3', 11); +--shouldn't be able to create pk +\set ON_ERROR_STOP 0 +ALTER TABLE hyper_pk ADD CONSTRAINT hyper_pk_pkey PRIMARY KEY (time); +ERROR: could not create unique index "6_7_hyper_pk_pkey" +\set ON_ERROR_STOP 1 +DELETE FROM hyper_pk WHERE device_id = 'dev3'; +--cannot create pk constraint on non-partition column +\set ON_ERROR_STOP 0 +ALTER TABLE hyper_pk ADD CONSTRAINT hyper_pk_invalid PRIMARY KEY (device_id); +ERROR: Cannot create a unique index without the column: time (used in partitioning) +\set ON_ERROR_STOP 1 +--now can create +ALTER TABLE hyper_pk ADD CONSTRAINT hyper_pk_pkey PRIMARY KEY (time); +\d+ _timescaledb_internal._hyper_3_6_chunk + Table "_timescaledb_internal._hyper_3_6_chunk" + Column | Type | Modifiers | Storage | Stats target | Description +-----------+---------+-----------+----------+--------------+------------- + time | bigint | not null | plain | | + device_id | text | not null | extended | | + sensor_1 | numeric | default 1 | main | | +Indexes: + "6_8_hyper_pk_pkey" PRIMARY KEY, btree ("time") +Check constraints: + "constraint_6" CHECK ("time" >= '1257987700000000000'::bigint AND "time" < '1257987700000000010'::bigint) + "hyper_pk_sensor_1_check" CHECK (sensor_1 > 10::numeric) +Inherits: hyper_pk + +--test adding constraint with same name to different table -- should fail +\set ON_ERROR_STOP 0 +ALTER TABLE hyper ADD CONSTRAINT hyper_pk_pkey UNIQUE (time); +ERROR: relation "hyper_pk_pkey" already exists +\set ON_ERROR_STOP 1 +--uniquness violation fails +\set ON_ERROR_STOP 0 +INSERT INTO hyper_pk(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +ERROR: duplicate key value violates unique constraint "6_8_hyper_pk_pkey" +\set ON_ERROR_STOP 1 +----------------------- FOREIGN KEY ------------------ +CREATE TABLE devices( + device_id TEXT NOT NULL, + PRIMARY KEY (device_id) +); +CREATE TABLE hyper_fk ( + time BIGINT NOT NULL PRIMARY KEY, + device_id TEXT NOT NULL REFERENCES devices(device_id), + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10) +); +SELECT * FROM create_hypertable('hyper_fk', 'time', chunk_time_interval => 10); + create_hypertable +------------------- + +(1 row) + +--fail fk constraint +\set ON_ERROR_STOP 0 +INSERT INTO hyper_fk(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +ERROR: insert or update on table "_hyper_4_7_chunk" violates foreign key constraint "7_10_hyper_fk_device_id_fkey" +\set ON_ERROR_STOP 1 +INSERT INTO devices VALUES ('dev2'); +INSERT INTO hyper_fk(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +--delete should fail +\set ON_ERROR_STOP 0 +DELETE FROM devices; +ERROR: update or delete on table "devices" violates foreign key constraint "8_12_hyper_fk_device_id_fkey" on table "_hyper_4_8_chunk" +\set ON_ERROR_STOP 1 +ALTER TABLE hyper_fk DROP CONSTRAINT hyper_fk_device_id_fkey; +--should now be able to add non-fk rows +INSERT INTO hyper_fk(time, device_id,sensor_1) VALUES +(1257987700000000001, 'dev3', 11); +--can't add fk because of dev3 row +\set ON_ERROR_STOP 0 +ALTER TABLE hyper_fk ADD CONSTRAINT hyper_fk_device_id_fkey +FOREIGN KEY (device_id) REFERENCES devices(device_id); +ERROR: insert or update on table "_hyper_4_8_chunk" violates foreign key constraint "8_13_hyper_fk_device_id_fkey" +\set ON_ERROR_STOP 1 +DELETE FROM hyper_fk WHERE device_id = 'dev3'; +ALTER TABLE hyper_fk ADD CONSTRAINT hyper_fk_device_id_fkey +FOREIGN KEY (device_id) REFERENCES devices(device_id); +\set ON_ERROR_STOP 0 +INSERT INTO hyper_fk(time, device_id,sensor_1) VALUES +(1257987700000000002, 'dev3', 11); +ERROR: insert or update on table "_hyper_4_8_chunk" violates foreign key constraint "8_14_hyper_fk_device_id_fkey" +\set ON_ERROR_STOP 1 +----------------------- FOREIGN KEY INTO A HYPERTABLE ------------------ +--FOREIGN KEY references into a hypertable are currently broken. +--The referencing table will never find the corresponding row in the hypertable +--since it will only search the parent. Thus any insert will result in an ERROR +--TODO: block such foreign keys or fix. (Hard to block on create table so punting for now) +CREATE TABLE hyper_for_ref ( + time BIGINT NOT NULL PRIMARY KEY, + device_id TEXT NOT NULL, + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10) +); +SELECT * FROM create_hypertable('hyper_for_ref', 'time', chunk_time_interval => 10); + create_hypertable +------------------- + +(1 row) + +\set ON_ERROR_STOP 0 +CREATE TABLE referrer ( + time BIGINT NOT NULL REFERENCES hyper_for_ref(time) +); +ERROR: Foreign keys to hypertables are not supported. +\set ON_ERROR_STOP 1 +CREATE TABLE referrer2 ( + time BIGINT NOT NULL +); +\set ON_ERROR_STOP 0 +ALTER TABLE referrer2 ADD CONSTRAINT hyper_fk_device_id_fkey +FOREIGN KEY (time) REFERENCES hyper_for_ref(time); +ERROR: Foreign keys to hypertables are not supported. +\set ON_ERROR_STOP 1 +----------------------- EXCLUSION CONSTRAINT ------------------ +CREATE TABLE hyper_ex ( + time BIGINT, + device_id TEXT NOT NULL REFERENCES devices(device_id), + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10), + canceled boolean DEFAULT false, + EXCLUDE USING btree ( + time WITH =, device_id WITH = + ) WHERE (not canceled) +); +SELECT * FROM create_hypertable('hyper_ex', 'time', chunk_time_interval=>_timescaledb_internal.interval_to_usec('1 month')); + create_hypertable +------------------- + +(1 row) + +INSERT INTO hyper_ex(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +\set ON_ERROR_STOP 0 +INSERT INTO hyper_ex(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 12); +ERROR: conflicting key value violates exclusion constraint "9_15_hyper_ex_time_device_id_excl" +\set ON_ERROR_STOP 1 +ALTER TABLE hyper_ex DROP CONSTRAINT hyper_ex_time_device_id_excl; +--can now add +INSERT INTO hyper_ex(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 12); +--cannot add because of conflicts +\set ON_ERROR_STOP 0 +ALTER TABLE hyper_ex ADD CONSTRAINT hyper_ex_time_device_id_excl + EXCLUDE USING btree ( + time WITH =, device_id WITH = + ) WHERE (not canceled) +; +ERROR: could not create exclusion constraint "9_17_hyper_ex_time_device_id_excl" +\set ON_ERROR_STOP 1 +DELETE FROM hyper_ex WHERE sensor_1 = 12; +ALTER TABLE hyper_ex ADD CONSTRAINT hyper_ex_time_device_id_excl + EXCLUDE USING btree ( + time WITH =, device_id WITH = + ) WHERE (not canceled) +; +\set ON_ERROR_STOP 0 +INSERT INTO hyper_ex(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 12); +ERROR: conflicting key value violates exclusion constraint "9_18_hyper_ex_time_device_id_excl" +\set ON_ERROR_STOP 1 +--cannot add exclusion constraint without partition key. +CREATE TABLE hyper_ex_invalid ( + time BIGINT, + device_id TEXT NOT NULL REFERENCES devices(device_id), + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10), + canceled boolean DEFAULT false, + EXCLUDE USING btree ( + device_id WITH = + ) WHERE (not canceled) +); +\set ON_ERROR_STOP 0 +SELECT * FROM create_hypertable('hyper_ex_invalid', 'time', chunk_time_interval=>_timescaledb_internal.interval_to_usec('1 month')); +ERROR: Cannot create a unique index without the column: time (used in partitioning) +\set ON_ERROR_STOP 1 diff --git a/test/expected/create_chunks.out b/test/expected/create_chunks.out index 90429be6e..eace7f2d1 100644 --- a/test/expected/create_chunks.out +++ b/test/expected/create_chunks.out @@ -71,13 +71,95 @@ SELECT set_chunk_time_interval('chunk_test', 1::bigint); (1 row) +<<<<<<< HEAD INSERT INTO chunk_test VALUES (8, 24.3, 79669, 1); SELECT set_chunk_time_interval('chunk_test', 5::bigint); +======= +INSERT INTO chunk_test VALUES(23, 3, 'dev3'); +SELECT * FROM chunk_test order by time, metric, device_id; + time | metric | device_id +------+--------+----------- + 1 | 1 | dev1 + 2 | 2 | dev2 + 23 | 3 | dev3 + 45 | 2 | dev2 + 46 | 2 | dev2 +(5 rows) + +SELECT * FROM _timescaledb_catalog.chunk; + id | hypertable_id | schema_name | table_name +----+---------------+-----------------------+------------------ + 1 | 1 | _timescaledb_internal | _hyper_1_1_chunk + 2 | 1 | _timescaledb_internal | _hyper_1_2_chunk + 3 | 1 | _timescaledb_internal | _hyper_1_3_chunk + 4 | 1 | _timescaledb_internal | _hyper_1_4_chunk +(4 rows) + +SELECT * FROM _timescaledb_catalog.hypertable; + id | schema_name | table_name | associated_schema_name | associated_table_prefix | num_dimensions +----+-------------+------------+------------------------+-------------------------+---------------- + 1 | public | chunk_test | _timescaledb_internal | _hyper_1 | 2 +(1 row) + +SELECT * FROM ONLY chunk_test; + time | metric | device_id +------+--------+----------- +(0 rows) + +SELECT * FROM _timescaledb_catalog.chunk c + LEFT JOIN _timescaledb_catalog.chunk_constraint cc ON (c.id = cc.chunk_id) + LEFT JOIN _timescaledb_catalog.dimension_slice ds ON (ds.id = cc.dimension_slice_id) + LEFT JOIN _timescaledb_catalog.dimension d ON (d.id = ds.dimension_id) + LEFT JOIN _timescaledb_catalog.hypertable h ON (d.hypertable_id = h.id) + WHERE h.schema_name = 'public' AND h.table_name = 'chunk_test' + ORDER BY c.id, d.id; + id | hypertable_id | schema_name | table_name | chunk_id | dimension_slice_id | constraint_name | hypertable_constraint_name | id | dimension_id | range_start | range_end | id | hypertable_id | column_name | column_type | aligned | num_slices | partitioning_func_schema | partitioning_func | interval_length | id | schema_name | table_name | associated_schema_name | associated_table_prefix | num_dimensions +----+---------------+-----------------------+------------------+----------+--------------------+-----------------+----------------------------+----+--------------+-------------+------------+----+---------------+-------------+-------------+---------+------------+--------------------------+-----------------------+-----------------+----+-------------+------------+------------------------+-------------------------+---------------- + 1 | 1 | _timescaledb_internal | _hyper_1_1_chunk | 1 | 1 | constraint_1 | | 1 | 1 | 0 | 10 | 1 | 1 | time | bigint | t | | | | 40 | 1 | public | chunk_test | _timescaledb_internal | _hyper_1 | 2 + 1 | 1 | _timescaledb_internal | _hyper_1_1_chunk | 1 | 2 | constraint_2 | | 2 | 2 | 1073741823 | 2147483647 | 2 | 1 | device_id | text | f | 2 | _timescaledb_internal | get_partition_for_key | | 1 | public | chunk_test | _timescaledb_internal | _hyper_1 | 2 + 2 | 1 | _timescaledb_internal | _hyper_1_2_chunk | 2 | 1 | constraint_1 | | 1 | 1 | 0 | 10 | 1 | 1 | time | bigint | t | | | | 40 | 1 | public | chunk_test | _timescaledb_internal | _hyper_1 | 2 + 2 | 1 | _timescaledb_internal | _hyper_1_2_chunk | 2 | 4 | constraint_4 | | 4 | 2 | 0 | 1073741823 | 2 | 1 | device_id | text | f | 2 | _timescaledb_internal | get_partition_for_key | | 1 | public | chunk_test | _timescaledb_internal | _hyper_1 | 2 + 3 | 1 | _timescaledb_internal | _hyper_1_3_chunk | 3 | 5 | constraint_5 | | 5 | 1 | 40 | 50 | 1 | 1 | time | bigint | t | | | | 40 | 1 | public | chunk_test | _timescaledb_internal | _hyper_1 | 2 + 3 | 1 | _timescaledb_internal | _hyper_1_3_chunk | 3 | 4 | constraint_4 | | 4 | 2 | 0 | 1073741823 | 2 | 1 | device_id | text | f | 2 | _timescaledb_internal | get_partition_for_key | | 1 | public | chunk_test | _timescaledb_internal | _hyper_1 | 2 + 4 | 1 | _timescaledb_internal | _hyper_1_4_chunk | 4 | 7 | constraint_7 | | 7 | 1 | 10 | 40 | 1 | 1 | time | bigint | t | | | | 40 | 1 | public | chunk_test | _timescaledb_internal | _hyper_1 | 2 + 4 | 1 | _timescaledb_internal | _hyper_1_4_chunk | 4 | 4 | constraint_4 | | 4 | 2 | 0 | 1073741823 | 2 | 1 | device_id | text | f | 2 | _timescaledb_internal | get_partition_for_key | | 1 | public | chunk_test | _timescaledb_internal | _hyper_1 | 2 +(8 rows) + +-- Test chunk aligning between partitions +CREATE TABLE chunk_align_test( + time BIGINT, + metric INTEGER, + device_id TEXT + ); +SELECT * FROM create_hypertable('chunk_align_test', 'time', 'device_id', 2, chunk_time_interval => 10); + create_hypertable +------------------- + +(1 row) + +INSERT INTO chunk_align_test VALUES (1, 1, 'dev1'); -- this should create a 10 wide chunk +SELECT * FROM _timescaledb_catalog.chunk c + LEFT JOIN _timescaledb_catalog.chunk_constraint cc ON (c.id = cc.chunk_id) + LEFT JOIN _timescaledb_catalog.dimension_slice ds ON (ds.id = cc.dimension_slice_id) + LEFT JOIN _timescaledb_catalog.dimension d ON (d.id = ds.dimension_id) + LEFT JOIN _timescaledb_catalog.hypertable h ON (d.hypertable_id = h.id) + WHERE h.schema_name = 'public' AND h.table_name = 'chunk_align_test' + AND d.column_name = 'time' + ORDER BY c.id, d.id; + id | hypertable_id | schema_name | table_name | chunk_id | dimension_slice_id | constraint_name | hypertable_constraint_name | id | dimension_id | range_start | range_end | id | hypertable_id | column_name | column_type | aligned | num_slices | partitioning_func_schema | partitioning_func | interval_length | id | schema_name | table_name | associated_schema_name | associated_table_prefix | num_dimensions +----+---------------+-----------------------+------------------+----------+--------------------+-----------------+----------------------------+----+--------------+-------------+-----------+----+---------------+-------------+-------------+---------+------------+--------------------------+-------------------+-----------------+----+-------------+------------------+------------------------+-------------------------+---------------- + 5 | 2 | _timescaledb_internal | _hyper_2_5_chunk | 5 | 9 | constraint_9 | | 9 | 3 | 0 | 10 | 3 | 2 | time | bigint | t | | | | 10 | 2 | public | chunk_align_test | _timescaledb_internal | _hyper_2 | 2 +(1 row) + + +SELECT * FROM set_chunk_time_interval('chunk_align_test', 40::bigint); +>>>>>>> 98565c0... Add support for hypertable constraints set_chunk_time_interval ------------------------- (1 row) +<<<<<<< HEAD SELECT * FROM _timescaledb_catalog.dimension; id | hypertable_id | column_name | column_type | aligned | num_slices | partitioning_func_schema | partitioning_func | interval_length ----+---------------+-------------+-------------+---------+------------+--------------------------+-----------------------+----------------- @@ -115,4 +197,46 @@ ORDER BY c.id, d.id; _hyper_1_8_chunk | 1 | 9 | 15 | 20 _hyper_1_8_chunk | 2 | 6 | 0 | 715827882 (16 rows) +======= +INSERT INTO chunk_align_test VALUES (5, 1, 'dev2'); -- this should still create a 10 wide chunk +INSERT INTO chunk_align_test VALUES (45, 1, 'dev2'); -- this should create a 40 wide chunk +SELECT * FROM _timescaledb_catalog.chunk c + LEFT JOIN _timescaledb_catalog.chunk_constraint cc ON (c.id = cc.chunk_id) + LEFT JOIN _timescaledb_catalog.dimension_slice ds ON (ds.id = cc.dimension_slice_id) + LEFT JOIN _timescaledb_catalog.dimension d ON (d.id = ds.dimension_id) + LEFT JOIN _timescaledb_catalog.hypertable h ON (d.hypertable_id = h.id) + WHERE h.schema_name = 'public' AND h.table_name = 'chunk_align_test' + AND d.column_name = 'time' + ORDER BY c.id, d.id; + id | hypertable_id | schema_name | table_name | chunk_id | dimension_slice_id | constraint_name | hypertable_constraint_name | id | dimension_id | range_start | range_end | id | hypertable_id | column_name | column_type | aligned | num_slices | partitioning_func_schema | partitioning_func | interval_length | id | schema_name | table_name | associated_schema_name | associated_table_prefix | num_dimensions +----+---------------+-----------------------+------------------+----------+--------------------+-----------------+----------------------------+----+--------------+-------------+-----------+----+---------------+-------------+-------------+---------+------------+--------------------------+-------------------+-----------------+----+-------------+------------------+------------------------+-------------------------+---------------- + 5 | 2 | _timescaledb_internal | _hyper_2_5_chunk | 5 | 9 | constraint_9 | | 9 | 3 | 0 | 10 | 3 | 2 | time | bigint | t | | | | 40 | 2 | public | chunk_align_test | _timescaledb_internal | _hyper_2 | 2 + 6 | 2 | _timescaledb_internal | _hyper_2_6_chunk | 6 | 9 | constraint_9 | | 9 | 3 | 0 | 10 | 3 | 2 | time | bigint | t | | | | 40 | 2 | public | chunk_align_test | _timescaledb_internal | _hyper_2 | 2 + 7 | 2 | _timescaledb_internal | _hyper_2_7_chunk | 7 | 13 | constraint_13 | | 13 | 3 | 40 | 80 | 3 | 2 | time | bigint | t | | | | 40 | 2 | public | chunk_align_test | _timescaledb_internal | _hyper_2 | 2 +(3 rows) + + --check the cut-to-size with aligned dimensions code + INSERT INTO chunk_align_test VALUES (35, 1, 'dev1'); + INSERT INTO chunk_align_test VALUES (35, 1, 'dev2'); + INSERT INTO chunk_align_test VALUES (81, 1, 'dev1'); + INSERT INTO chunk_align_test VALUES (81, 1, 'dev2'); +SELECT * FROM _timescaledb_catalog.chunk c + LEFT JOIN _timescaledb_catalog.chunk_constraint cc ON (c.id = cc.chunk_id) + LEFT JOIN _timescaledb_catalog.dimension_slice ds ON (ds.id = cc.dimension_slice_id) + LEFT JOIN _timescaledb_catalog.dimension d ON (d.id = ds.dimension_id) + LEFT JOIN _timescaledb_catalog.hypertable h ON (d.hypertable_id = h.id) + WHERE h.schema_name = 'public' AND h.table_name = 'chunk_align_test' + AND d.column_name = 'time' + ORDER BY c.id, d.id; + id | hypertable_id | schema_name | table_name | chunk_id | dimension_slice_id | constraint_name | hypertable_constraint_name | id | dimension_id | range_start | range_end | id | hypertable_id | column_name | column_type | aligned | num_slices | partitioning_func_schema | partitioning_func | interval_length | id | schema_name | table_name | associated_schema_name | associated_table_prefix | num_dimensions +----+---------------+-----------------------+-------------------+----------+--------------------+-----------------+----------------------------+----+--------------+-------------+-----------+----+---------------+-------------+-------------+---------+------------+--------------------------+-------------------+-----------------+----+-------------+------------------+------------------------+-------------------------+---------------- + 5 | 2 | _timescaledb_internal | _hyper_2_5_chunk | 5 | 9 | constraint_9 | | 9 | 3 | 0 | 10 | 3 | 2 | time | bigint | t | | | | 40 | 2 | public | chunk_align_test | _timescaledb_internal | _hyper_2 | 2 + 6 | 2 | _timescaledb_internal | _hyper_2_6_chunk | 6 | 9 | constraint_9 | | 9 | 3 | 0 | 10 | 3 | 2 | time | bigint | t | | | | 40 | 2 | public | chunk_align_test | _timescaledb_internal | _hyper_2 | 2 + 7 | 2 | _timescaledb_internal | _hyper_2_7_chunk | 7 | 13 | constraint_13 | | 13 | 3 | 40 | 80 | 3 | 2 | time | bigint | t | | | | 40 | 2 | public | chunk_align_test | _timescaledb_internal | _hyper_2 | 2 + 8 | 2 | _timescaledb_internal | _hyper_2_8_chunk | 8 | 15 | constraint_15 | | 15 | 3 | 10 | 40 | 3 | 2 | time | bigint | t | | | | 40 | 2 | public | chunk_align_test | _timescaledb_internal | _hyper_2 | 2 + 9 | 2 | _timescaledb_internal | _hyper_2_9_chunk | 9 | 15 | constraint_15 | | 15 | 3 | 10 | 40 | 3 | 2 | time | bigint | t | | | | 40 | 2 | public | chunk_align_test | _timescaledb_internal | _hyper_2 | 2 + 10 | 2 | _timescaledb_internal | _hyper_2_10_chunk | 10 | 19 | constraint_19 | | 19 | 3 | 80 | 120 | 3 | 2 | time | bigint | t | | | | 40 | 2 | public | chunk_align_test | _timescaledb_internal | _hyper_2 | 2 + 11 | 2 | _timescaledb_internal | _hyper_2_11_chunk | 11 | 19 | constraint_19 | | 19 | 3 | 80 | 120 | 3 | 2 | time | bigint | t | | | | 40 | 2 | public | chunk_align_test | _timescaledb_internal | _hyper_2 | 2 +(7 rows) +>>>>>>> 98565c0... Add support for hypertable constraints diff --git a/test/expected/ddl.out b/test/expected/ddl.out index afa47d3fc..09fe1c49f 100644 --- a/test/expected/ddl.out +++ b/test/expected/ddl.out @@ -53,13 +53,13 @@ SELECT * FROM _timescaledb_catalog.hypertable; 2 | customSchema | Hypertable_1 | _timescaledb_internal | _hyper_2 | 1 (2 rows) -SELECT * FROM _timescaledb_catalog.hypertable_index; +SELECT * FROM _timescaledb_catalog.hypertable_index ORDER BY format('%I.%I', main_schema_name, main_index_name)::regclass; hypertable_id | main_schema_name | main_index_name | definition ---------------+------------------+---------------------------------+-------------------------------------------------------------------------------------- 1 | public | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") + 2 | customSchema | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") 1 | public | Hypertable_1_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) 1 | public | Hypertable_1_Device_id_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("Device_id", "time" DESC) - 2 | customSchema | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") 2 | customSchema | Hypertable_1_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) (5 rows) @@ -74,13 +74,13 @@ INSERT INTO "customSchema"."Hypertable_1"(time, "Device_id", temp_c, humidity, s VALUES(1257894000000000000, 'dev1', 30, 70, 1, 2, 3, 100); INSERT INTO "customSchema"."Hypertable_1"(time, "Device_id", temp_c, humidity, sensor_1, sensor_2, sensor_3, sensor_4) VALUES(1257894000000000001, 'dev1', 30, 70, 1, 2, 3, 100); -SELECT * FROM _timescaledb_catalog.hypertable_index; +SELECT * FROM _timescaledb_catalog.hypertable_index ORDER BY format('%I.%I', main_schema_name, main_index_name)::regclass; hypertable_id | main_schema_name | main_index_name | definition ---------------+------------------+---------------------------------+---------------------------------------------------------------------------------------- 1 | public | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") + 2 | customSchema | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") 1 | public | Hypertable_1_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) 1 | public | Hypertable_1_Device_id_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("Device_id", "time" DESC) - 2 | customSchema | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") 2 | customSchema | Hypertable_1_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) 1 | public | Hypertable_1_time_temp_c_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", temp_c) 1 | public | ind_humidity | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", humidity) diff --git a/test/expected/ddl_single.out b/test/expected/ddl_single.out index 009613cb2..9485f57b5 100644 --- a/test/expected/ddl_single.out +++ b/test/expected/ddl_single.out @@ -53,13 +53,13 @@ SELECT * FROM _timescaledb_catalog.hypertable; 2 | customSchema | Hypertable_1 | _timescaledb_internal | _hyper_2 | 1 (2 rows) -SELECT * FROM _timescaledb_catalog.hypertable_index; +SELECT * FROM _timescaledb_catalog.hypertable_index ORDER BY format('%I.%I', main_schema_name, main_index_name)::regclass; hypertable_id | main_schema_name | main_index_name | definition ---------------+------------------+---------------------------------+-------------------------------------------------------------------------------------- 1 | public | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") + 2 | customSchema | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") 1 | public | Hypertable_1_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) 1 | public | Hypertable_1_Device_id_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("Device_id", "time" DESC) - 2 | customSchema | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") 2 | customSchema | Hypertable_1_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) (5 rows) @@ -74,13 +74,13 @@ INSERT INTO "customSchema"."Hypertable_1"(time, "Device_id", temp_c, humidity, s VALUES(1257894000000000000, 'dev1', 30, 70, 1, 2, 3, 100); INSERT INTO "customSchema"."Hypertable_1"(time, "Device_id", temp_c, humidity, sensor_1, sensor_2, sensor_3, sensor_4) VALUES(1257894000000000001, 'dev1', 30, 70, 1, 2, 3, 100); -SELECT * FROM _timescaledb_catalog.hypertable_index; +SELECT * FROM _timescaledb_catalog.hypertable_index ORDER BY format('%I.%I', main_schema_name, main_index_name)::regclass; hypertable_id | main_schema_name | main_index_name | definition ---------------+------------------+---------------------------------+---------------------------------------------------------------------------------------- 1 | public | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") + 2 | customSchema | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") 1 | public | Hypertable_1_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) 1 | public | Hypertable_1_Device_id_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("Device_id", "time" DESC) - 2 | customSchema | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") 2 | customSchema | Hypertable_1_time_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time" DESC) 1 | public | Hypertable_1_time_temp_c_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", temp_c) 1 | public | ind_humidity | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", humidity) diff --git a/test/expected/insert_single.out b/test/expected/insert_single.out index 388fe3b61..9b4be7ca0 100644 --- a/test/expected/insert_single.out +++ b/test/expected/insert_single.out @@ -407,7 +407,7 @@ INSERT INTO "3dim" VALUES('2017-01-20T09:00:47', 25.1, 'yellow', 'la'); time | integer | | plain | | temp | double precision | | plain | | Indexes: - "25-1dim_neg_time_idx" btree ("time" DESC) + "21-1dim_neg_time_idx" btree ("time" DESC) Check constraints: "constraint_10" CHECK ("time" >= 0 AND "time" < 10) Inherits: "1dim_neg" diff --git a/test/expected/pg_dump.out b/test/expected/pg_dump.out index 79bfb28bf..cb1ba3ace 100644 --- a/test/expected/pg_dump.out +++ b/test/expected/pg_dump.out @@ -43,7 +43,15 @@ SELECT count(*) AND refobjid = (SELECT oid FROM pg_extension WHERE extname = 'timescaledb'); count ------- +<<<<<<< HEAD 121 +======= +<<<<<<< HEAD + 120 +======= + 135 +>>>>>>> 98565c0... Add support for hypertable constraints +>>>>>>> Add support for hypertable constraints (1 row) \c postgres @@ -67,7 +75,15 @@ SELECT count(*) AND refobjid = (SELECT oid FROM pg_extension WHERE extname = 'timescaledb'); count ------- +<<<<<<< HEAD 121 +======= +<<<<<<< HEAD + 120 +======= + 135 +>>>>>>> 98565c0... Add support for hypertable constraints +>>>>>>> Add support for hypertable constraints (1 row) \c single diff --git a/test/expected/upsert.out b/test/expected/upsert.out index 3662d2d68..cfe2fd56a 100644 --- a/test/expected/upsert.out +++ b/test/expected/upsert.out @@ -46,7 +46,7 @@ INSERT INTO upsert_test VALUES ('2017-01-21T09:00:01', 22.5, 'yellow') RETURNING (1 row) UPDATE upsert_test SET time = '2017-01-20T09:00:01'; -ERROR: duplicate key value violates unique constraint "1-upsert_test_pkey" +ERROR: duplicate key value violates unique constraint "1_1_upsert_test_pkey" \set ON_ERROR_STOP 1 -- Test with UNIQUE index on multiple columns instead of PRIMARY KEY constraint CREATE TABLE upsert_test_unique(time timestamp, temp float, color text); @@ -126,5 +126,5 @@ SELECT * FROM upsert_test_multi_unique ORDER BY time, color DESC; \set ON_ERROR_STOP 0 INSERT INTO upsert_test_multi_unique VALUES ('2017-01-20T09:00:01', 23.5, 'purple') ON CONFLICT (time, color) DO UPDATE set temp = 23.5; -ERROR: duplicate key value violates unique constraint "5-multi_time_temp_idx" +ERROR: duplicate key value violates unique constraint "4-multi_time_temp_idx" \set ON_ERROR_STOP 1 diff --git a/test/sql/constraint.sql b/test/sql/constraint.sql new file mode 100644 index 000000000..781e1e3ac --- /dev/null +++ b/test/sql/constraint.sql @@ -0,0 +1,301 @@ +\o /dev/null +\ir include/create_single_db.sql +\o + +CREATE TABLE hyper ( + time BIGINT NOT NULL, + device_id TEXT NOT NULL, + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10) +); + + +SELECT * FROM create_hypertable('hyper', 'time', chunk_time_interval => 10); + +--check and not-null constraints are inherited through regular inheritance. + +\set ON_ERROR_STOP 0 +INSERT INTO hyper(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 9); + +INSERT INTO hyper(time, device_id,sensor_1) VALUES +(1257987700000000000, NULL, 11); + +\set ON_ERROR_STOP 1 + +INSERT INTO hyper(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); + +INSERT INTO hyper(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); + +----------------------- UNIQUE CONSTRAINTS ------------------ + +CREATE TABLE hyper_unique ( + time BIGINT NOT NULL UNIQUE, + device_id TEXT NOT NULL, + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10) +); + + +SELECT * FROM create_hypertable('hyper_unique', 'time', chunk_time_interval => 10); + +INSERT INTO hyper_unique(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); + +INSERT INTO hyper_unique(time, device_id,sensor_1) VALUES +(1257987800000000000, 'dev2', 11); + + +\set ON_ERROR_STOP 0 +INSERT INTO hyper_unique(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +\set ON_ERROR_STOP 1 + +\d+ hyper +\d+ hyper_unique +--should have unique constraint not just unique index +\d+ _timescaledb_internal._hyper_2_4_chunk + +ALTER TABLE hyper_unique DROP CONSTRAINT hyper_unique_time_key; +\d+ _timescaledb_internal._hyper_2_4_chunk + +--uniqueness not enforced +INSERT INTO hyper_unique(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev3', 11); + + +--shouldn't be able to create constraint +\set ON_ERROR_STOP 0 +ALTER TABLE hyper_unique ADD CONSTRAINT hyper_unique_time_key UNIQUE (time); +\set ON_ERROR_STOP 1 + +DELETE FROM hyper_unique WHERE device_id = 'dev3'; + +--now can create +ALTER TABLE hyper_unique ADD CONSTRAINT hyper_unique_time_key UNIQUE (time); +\d+ _timescaledb_internal._hyper_2_4_chunk + +--test adding constraint with same name to different table -- should fail +\set ON_ERROR_STOP 0 +ALTER TABLE hyper ADD CONSTRAINT hyper_unique_time_key UNIQUE (time); +\set ON_ERROR_STOP 1 + +--uniquness violation fails +\set ON_ERROR_STOP 0 +INSERT INTO hyper_unique(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +\set ON_ERROR_STOP 1 + +--cannot create unique constraint on non-partition column +\set ON_ERROR_STOP 0 +ALTER TABLE hyper_unique ADD CONSTRAINT hyper_unique_invalid UNIQUE (device_id); +\set ON_ERROR_STOP 1 + + +----------------------- PRIMARY KEY ------------------ + +CREATE TABLE hyper_pk ( + time BIGINT NOT NULL PRIMARY KEY, + device_id TEXT NOT NULL, + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10) +); + + +SELECT * FROM create_hypertable('hyper_pk', 'time', chunk_time_interval => 10); + +INSERT INTO hyper_pk(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); + +\set ON_ERROR_STOP 0 +INSERT INTO hyper_pk(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +\set ON_ERROR_STOP 1 + +--should have unique constraint not just unique index +\d+ _timescaledb_internal._hyper_3_6_chunk + +ALTER TABLE hyper_pk DROP CONSTRAINT hyper_pk_pkey; +\d+ _timescaledb_internal._hyper_3_6_chunk + +--uniqueness not enforced +INSERT INTO hyper_pk(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev3', 11); + + +--shouldn't be able to create pk +\set ON_ERROR_STOP 0 +ALTER TABLE hyper_pk ADD CONSTRAINT hyper_pk_pkey PRIMARY KEY (time); +\set ON_ERROR_STOP 1 + +DELETE FROM hyper_pk WHERE device_id = 'dev3'; + +--cannot create pk constraint on non-partition column +\set ON_ERROR_STOP 0 +ALTER TABLE hyper_pk ADD CONSTRAINT hyper_pk_invalid PRIMARY KEY (device_id); +\set ON_ERROR_STOP 1 + +--now can create +ALTER TABLE hyper_pk ADD CONSTRAINT hyper_pk_pkey PRIMARY KEY (time); +\d+ _timescaledb_internal._hyper_3_6_chunk + +--test adding constraint with same name to different table -- should fail +\set ON_ERROR_STOP 0 +ALTER TABLE hyper ADD CONSTRAINT hyper_pk_pkey UNIQUE (time); +\set ON_ERROR_STOP 1 + +--uniquness violation fails +\set ON_ERROR_STOP 0 +INSERT INTO hyper_pk(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +\set ON_ERROR_STOP 1 + + +----------------------- FOREIGN KEY ------------------ + +CREATE TABLE devices( + device_id TEXT NOT NULL, + PRIMARY KEY (device_id) +); + +CREATE TABLE hyper_fk ( + time BIGINT NOT NULL PRIMARY KEY, + device_id TEXT NOT NULL REFERENCES devices(device_id), + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10) +); + + +SELECT * FROM create_hypertable('hyper_fk', 'time', chunk_time_interval => 10); + +--fail fk constraint +\set ON_ERROR_STOP 0 +INSERT INTO hyper_fk(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); +\set ON_ERROR_STOP 1 + +INSERT INTO devices VALUES ('dev2'); + +INSERT INTO hyper_fk(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); + +--delete should fail +\set ON_ERROR_STOP 0 +DELETE FROM devices; +\set ON_ERROR_STOP 1 + +ALTER TABLE hyper_fk DROP CONSTRAINT hyper_fk_device_id_fkey; + +--should now be able to add non-fk rows +INSERT INTO hyper_fk(time, device_id,sensor_1) VALUES +(1257987700000000001, 'dev3', 11); + +--can't add fk because of dev3 row +\set ON_ERROR_STOP 0 +ALTER TABLE hyper_fk ADD CONSTRAINT hyper_fk_device_id_fkey +FOREIGN KEY (device_id) REFERENCES devices(device_id); +\set ON_ERROR_STOP 1 + +DELETE FROM hyper_fk WHERE device_id = 'dev3'; + +ALTER TABLE hyper_fk ADD CONSTRAINT hyper_fk_device_id_fkey +FOREIGN KEY (device_id) REFERENCES devices(device_id); + +\set ON_ERROR_STOP 0 +INSERT INTO hyper_fk(time, device_id,sensor_1) VALUES +(1257987700000000002, 'dev3', 11); +\set ON_ERROR_STOP 1 + +----------------------- FOREIGN KEY INTO A HYPERTABLE ------------------ + + +--FOREIGN KEY references into a hypertable are currently broken. +--The referencing table will never find the corresponding row in the hypertable +--since it will only search the parent. Thus any insert will result in an ERROR +--TODO: block such foreign keys or fix. (Hard to block on create table so punting for now) +CREATE TABLE hyper_for_ref ( + time BIGINT NOT NULL PRIMARY KEY, + device_id TEXT NOT NULL, + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10) +); + +SELECT * FROM create_hypertable('hyper_for_ref', 'time', chunk_time_interval => 10); + +\set ON_ERROR_STOP 0 +CREATE TABLE referrer ( + time BIGINT NOT NULL REFERENCES hyper_for_ref(time) +); +\set ON_ERROR_STOP 1 + +CREATE TABLE referrer2 ( + time BIGINT NOT NULL +); + +\set ON_ERROR_STOP 0 +ALTER TABLE referrer2 ADD CONSTRAINT hyper_fk_device_id_fkey +FOREIGN KEY (time) REFERENCES hyper_for_ref(time); +\set ON_ERROR_STOP 1 + +----------------------- EXCLUSION CONSTRAINT ------------------ + +CREATE TABLE hyper_ex ( + time BIGINT, + device_id TEXT NOT NULL REFERENCES devices(device_id), + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10), + canceled boolean DEFAULT false, + EXCLUDE USING btree ( + time WITH =, device_id WITH = + ) WHERE (not canceled) +); + +SELECT * FROM create_hypertable('hyper_ex', 'time', chunk_time_interval=>_timescaledb_internal.interval_to_usec('1 month')); + +INSERT INTO hyper_ex(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 11); + +\set ON_ERROR_STOP 0 +INSERT INTO hyper_ex(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 12); +\set ON_ERROR_STOP 1 + +ALTER TABLE hyper_ex DROP CONSTRAINT hyper_ex_time_device_id_excl; + +--can now add +INSERT INTO hyper_ex(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 12); + +--cannot add because of conflicts +\set ON_ERROR_STOP 0 +ALTER TABLE hyper_ex ADD CONSTRAINT hyper_ex_time_device_id_excl + EXCLUDE USING btree ( + time WITH =, device_id WITH = + ) WHERE (not canceled) +; +\set ON_ERROR_STOP 1 + +DELETE FROM hyper_ex WHERE sensor_1 = 12; + +ALTER TABLE hyper_ex ADD CONSTRAINT hyper_ex_time_device_id_excl + EXCLUDE USING btree ( + time WITH =, device_id WITH = + ) WHERE (not canceled) +; + +\set ON_ERROR_STOP 0 +INSERT INTO hyper_ex(time, device_id,sensor_1) VALUES +(1257987700000000000, 'dev2', 12); +\set ON_ERROR_STOP 1 + + +--cannot add exclusion constraint without partition key. +CREATE TABLE hyper_ex_invalid ( + time BIGINT, + device_id TEXT NOT NULL REFERENCES devices(device_id), + sensor_1 NUMERIC NULL DEFAULT 1 CHECK (sensor_1 > 10), + canceled boolean DEFAULT false, + EXCLUDE USING btree ( + device_id WITH = + ) WHERE (not canceled) +); + +\set ON_ERROR_STOP 0 +SELECT * FROM create_hypertable('hyper_ex_invalid', 'time', chunk_time_interval=>_timescaledb_internal.interval_to_usec('1 month')); +\set ON_ERROR_STOP 1 diff --git a/test/sql/include/ddl_ops_1.sql b/test/sql/include/ddl_ops_1.sql index 909ae182b..3dee93f16 100644 --- a/test/sql/include/ddl_ops_1.sql +++ b/test/sql/include/ddl_ops_1.sql @@ -31,7 +31,7 @@ SELECT * FROM create_hypertable('"public"."Hypertable_1"', 'time', 'Device_id', SELECT * FROM create_hypertable('"customSchema"."Hypertable_1"', 'time', NULL, 1, chunk_time_interval=>_timescaledb_internal.interval_to_usec('1 month')); SELECT * FROM _timescaledb_catalog.hypertable; -SELECT * FROM _timescaledb_catalog.hypertable_index; +SELECT * FROM _timescaledb_catalog.hypertable_index ORDER BY format('%I.%I', main_schema_name, main_index_name)::regclass; CREATE INDEX ON PUBLIC."Hypertable_1" (time, "temp_c"); CREATE INDEX "ind_humidity" ON PUBLIC."Hypertable_1" (time, "humidity"); @@ -49,7 +49,7 @@ VALUES(1257894000000000000, 'dev1', 30, 70, 1, 2, 3, 100); INSERT INTO "customSchema"."Hypertable_1"(time, "Device_id", temp_c, humidity, sensor_1, sensor_2, sensor_3, sensor_4) VALUES(1257894000000000001, 'dev1', 30, 70, 1, 2, 3, 100); -SELECT * FROM _timescaledb_catalog.hypertable_index; +SELECT * FROM _timescaledb_catalog.hypertable_index ORDER BY format('%I.%I', main_schema_name, main_index_name)::regclass; --expect error cases \set ON_ERROR_STOP 0 diff --git a/test/sql/updates/setup.sql b/test/sql/updates/setup.sql index e4432feb2..fa62cbbc0 100644 --- a/test/sql/updates/setup.sql +++ b/test/sql/updates/setup.sql @@ -9,7 +9,8 @@ CREATE TABLE PUBLIC."two_Partitions" ( series_0 DOUBLE PRECISION NULL, series_1 DOUBLE PRECISION NULL, series_2 DOUBLE PRECISION NULL, - series_bool BOOLEAN NULL + series_bool BOOLEAN NULL, + UNIQUE("timeCustom", device_id, series_2) ); CREATE INDEX ON PUBLIC."two_Partitions" (device_id, "timeCustom" DESC NULLS LAST) WHERE device_id IS NOT NULL; CREATE INDEX ON PUBLIC."two_Partitions" ("timeCustom" DESC NULLS LAST, series_0) WHERE series_0 IS NOT NULL; @@ -21,11 +22,11 @@ CREATE INDEX ON PUBLIC."two_Partitions" ("timeCustom" DESC NULLS LAST, device_id SELECT * FROM create_hypertable('"public"."two_Partitions"'::regclass, 'timeCustom'::name, 'device_id'::name, associated_schema_name=>'_timescaledb_internal'::text, number_partitions => 2, chunk_time_interval=>_timescaledb_internal.interval_to_usec('1 month')); -INSERT INTO public."two_Partitions"("timeCustom", device_id, series_0, series_1) VALUES -(1257987600000000000, 'dev1', 1.5, 1), -(1257987600000000000, 'dev1', 1.5, 2), -(1257894000000000000, 'dev2', 1.5, 1), -(1257894002000000000, 'dev1', 2.5, 3); +INSERT INTO public."two_Partitions"("timeCustom", device_id, series_0, series_1, series_2) VALUES +(1257987600000000000, 'dev1', 1.5, 1, 1), +(1257987600000000000, 'dev1', 1.5, 2, 2), +(1257894000000000000, 'dev2', 1.5, 1, 3), +(1257894002000000000, 'dev1', 2.5, 3, 4); -INSERT INTO "two_Partitions"("timeCustom", device_id, series_0, series_1) VALUES -(1257894000000000000, 'dev2', 1.5, 2); +INSERT INTO "two_Partitions"("timeCustom", device_id, series_0, series_1, series_2) VALUES +(1257894000000000000, 'dev2', 1.5, 2, 6); diff --git a/test/sql/updates/test-0.1.1.sql b/test/sql/updates/test-0.1.1.sql index 4632bbb82..9dd6600b6 100644 --- a/test/sql/updates/test-0.1.1.sql +++ b/test/sql/updates/test-0.1.1.sql @@ -1,6 +1,7 @@ \d+ _timescaledb_catalog.*; \df+ _timescaledb_internal.*; \dy +\d+ PUBLIC.* SELECT count(*) FROM pg_depend @@ -9,3 +10,9 @@ SELECT count(*) -- The list of tables configured to be dumped. SELECT unnest(extconfig)::regclass::text c from pg_extension where extname='timescaledb' ORDER BY c; + +SELECT * FROM _timescaledb_catalog.chunk_constraint ORDER BY chunk_id, dimension_slice_id, constraint_name; +SELECT * FROM _timescaledb_catalog.hypertable_index ORDER BY hypertable_id, main_index_name; +SELECT schema_name, table_name, main_schema_name, main_index_name FROM _timescaledb_catalog.chunk_index ORDER BY schema_name, table_name, main_schema_name, main_index_name; + +SELECT * FROM public."two_Partitions";