Skip to content

Commit

Permalink
Adjust tests from time_for_series
Browse files Browse the repository at this point in the history
Improve the checking when activating foreign keys for tables with
data accordingly.
Add the C functions from time_for_keys and periods, for future usage.
The `completely_covers` function from time_for_keys is probably
a lot faster than the code ported from periods.
  • Loading branch information
jhf committed Nov 3, 2023
1 parent 63a9e99 commit 2e4d768
Show file tree
Hide file tree
Showing 32 changed files with 2,445 additions and 912 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
sql_saga.o
sql_saga.so
completely_covers.o
periods.o
sql_saga.dylib
regression.diffs
regression.out
results/
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#MODULE_big = sql_saga
MODULE_big = sql_saga
EXTENSION = sql_saga
EXTENSION_VERSION = 1.0
DATA = $(EXTENSION)--$(EXTENSION_VERSION).sql
Expand All @@ -11,6 +11,8 @@ SQL_FILES = $(wildcard sql/[0-9]*_*.sql)

REGRESS = $(patsubst sql/%.sql,%,$(SQL_FILES))

OBJS = sql_saga.o periods.o completely_covers.o $(WIN32RES)

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
Expand Down
189 changes: 189 additions & 0 deletions completely_covers.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/**
* completely_covers.c -
* Provides an aggregate function
* that tells whether a bunch of input ranges competely cover a target range.
*/

#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/uio.h>
#include <unistd.h>
#include <string.h>
#include <time.h>

#include <postgres.h>
#include <fmgr.h>
#include <pg_config.h>
#include <miscadmin.h>
#include <utils/array.h>
#include <utils/guc.h>
#include <utils/acl.h>
#include <utils/lsyscache.h>
#include <utils/builtins.h>
#include <utils/rangetypes.h>
#include <utils/timestamp.h>
#include <catalog/pg_type.h>
#include <catalog/catalog.h>
#include <catalog/pg_tablespace.h>
#include <commands/tablespace.h>

#include "completely_covers.h"

typedef struct completely_covers_state {
TimestampTz covered_to;
TimestampTz target_start;
TimestampTz target_end;
bool target_start_unbounded;
bool target_end_unbounded;
bool answer_is_null;
bool finished; // Used to avoid further processing if we have already succeeded/failed.
bool completely_covered;
} completely_covers_state;


Datum completely_covers_transfn(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(completely_covers_transfn);

Datum completely_covers_transfn(PG_FUNCTION_ARGS)
{
MemoryContext aggContext;
completely_covers_state *state;
RangeType *current_range,
*target_range;
RangeBound current_start, current_end, target_start, target_end;
TypeCacheEntry *typcache;
bool current_empty, target_empty;
bool first_time;

if (!AggCheckCallContext(fcinfo, &aggContext)) {
elog(ERROR, "completely_covers called in non-aggregate context");
}

if (PG_ARGISNULL(0)) {
// Need to use MemoryContextAlloc with aggContext, not just palloc0,
// or the state will get cleared in between invocations:
state = (completely_covers_state *)MemoryContextAlloc(aggContext, sizeof(completely_covers_state));
state->finished = false;
state->completely_covered = false;
first_time = true;

// Need to find out the target range:

// TODO: Technically this will fail to detect an inconsistent target
// if only the first row is NULL:
if (PG_ARGISNULL(2)) {
// return NULL from the whole thing
state->answer_is_null = true;
state->finished = true;
PG_RETURN_POINTER(state);
}
state->answer_is_null = false;

target_range = PG_GETARG_RANGE_P(2);
typcache = range_get_typcache(fcinfo, RangeTypeGetOid(target_range));
range_deserialize(typcache, target_range, &target_start, &target_end, &target_empty);

state->target_start_unbounded = target_start.infinite;
state->target_end_unbounded = target_end.infinite;
state->target_start = DatumGetTimestampTz(target_start.val);
state->target_end = DatumGetTimestampTz(target_end.val);
// ereport(NOTICE, (errmsg("STARTING: state is [%ld, %ld) target is [%ld, %ld)", state->target_start, state->target_end, DatumGetTimestampTz(target_start.val), DatumGetTimestampTz(target_end.val))));

state->covered_to = 0;

} else {
// ereport(NOTICE, (errmsg("looking up state....")));
state = (completely_covers_state *)PG_GETARG_POINTER(0);

// TODO: Is there any better way to exit an aggregation early?
// Even https://pgxn.org/dist/first_last_agg/ hits all the input rows:
if (state->finished) PG_RETURN_POINTER(state);

first_time = false;

// Make sure the second arg is always the same:
if (PG_ARGISNULL(2)) {
ereport(ERROR, (errmsg("completely_covers second argument must be constant across the group")));
}
target_range = PG_GETARG_RANGE_P(2);
typcache = range_get_typcache(fcinfo, RangeTypeGetOid(target_range));
range_deserialize(typcache, target_range, &target_start, &target_end, &target_empty);

// ereport(NOTICE, (errmsg("state is [%ld, %ld) target is [%ld, %ld)", state->target_start, state->target_end, DatumGetTimestampTz(target_start.val), DatumGetTimestampTz(target_end.val))));
if (DatumGetTimestampTz(target_start.val) != state->target_start || DatumGetTimestampTz(target_end.val) != state->target_end) {
ereport(ERROR, (errmsg("completely_covers second argument must be constant across the group")));
}
}

if (PG_ARGISNULL(1)) PG_RETURN_POINTER(state);
current_range = PG_GETARG_RANGE_P(1);
typcache = range_get_typcache(fcinfo, RangeTypeGetOid(current_range));
range_deserialize(typcache, current_range, &current_start, &current_end, &current_empty);

// ereport(NOTICE, (errmsg("current is [%ld, %ld)", DatumGetTimestampTz(current_start.val), DatumGetTimestampTz(current_end.val))));

if (first_time) {
if (state->target_start_unbounded && !current_start.infinite) {
state->finished = true;
state->completely_covered = false;
PG_RETURN_POINTER(state);
}
if (DatumGetTimestampTz(current_start.val) > state->target_start) {
state->finished = true;
state->completely_covered = false;
PG_RETURN_POINTER(state);
}

} else {
// If there is a gap then fail:
if (DatumGetTimestampTz(current_start.val) > state->covered_to) {
// ereport(NOTICE, (errmsg("found a gap")));
state->finished = true;
state->completely_covered = false;
PG_RETURN_POINTER(state);
}
}

// This check is why we set covered_to to 0 above on the first pass:
// Note this check will not check unsorted inputs in some cases:
// - the inputs cover the target before we hit an out-of-order input.
if (DatumGetTimestampTz(current_start.val) < state->covered_to) {
// Right? Maybe this should be a warning....
ereport(ERROR, (errmsg("completely_covered first argument should be sorted")));
// ereport(ERROR, (errmsg("completely_covered first argument should be sorted but got %ld after covering up to %ld", DatumGetTimestampTz(current_start.val), state->covered_to)));
}

if (current_end.infinite) {
state->completely_covered = true;
state->finished = true;

} else {
state->covered_to = DatumGetTimestampTz(current_end.val);

if (!state->target_end_unbounded && state->covered_to >= state->target_end) {
state->completely_covered = true;
state->finished = true;
}
}

PG_RETURN_POINTER(state);
}

Datum completely_covers_finalfn(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(completely_covers_finalfn);

Datum completely_covers_finalfn(PG_FUNCTION_ARGS)
{
completely_covers_state *state;

if (PG_ARGISNULL(0)) PG_RETURN_NULL();

state = (completely_covers_state *)PG_GETARG_POINTER(0);
if (state->answer_is_null) {
PG_RETURN_NULL();
} else {
PG_RETURN_BOOL(state->completely_covered);
}
}
Empty file added completely_covers.h
Empty file.
10 changes: 5 additions & 5 deletions expected/06_unique_foreign.out
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
SET ROLE TO sql_saga_unprivileged_user;
-- Unique keys are already pretty much guaranteed by the underlying features of
-- PostgreSQL, but test them anyway.
CREATE TABLE uk (id integer, s integer, e integer, CONSTRAINT uk_pkey PRIMARY KEY (id, s, e));
CREATE TABLE uk (id integer, s integer, e integer, CONSTRAINT uk_pkey PRIMARY KEY (id, s, e) DEFERRABLE);
SELECT sql_saga.add_era('uk', 's', 'e', 'p');
add_era
---------
Expand Down Expand Up @@ -71,25 +71,25 @@ TABLE sql_saga.foreign_keys;
-- INSERT
INSERT INTO fk VALUES (0, 100, 0, 1); -- fail
ERROR: insert or update on table "fk" violates foreign key constraint "fk_uk_id_q"
CONTEXT: PL/pgSQL function sql_saga.validate_foreign_key_new_row(name,jsonb) line 130 at RAISE
CONTEXT: PL/pgSQL function sql_saga.validate_foreign_key_new_row(name,jsonb) line 133 at RAISE
SQL statement "SELECT sql_saga.validate_foreign_key_new_row(TG_ARGV[0], jnew)"
PL/pgSQL function sql_saga.fk_insert_check() line 20 at PERFORM
INSERT INTO fk VALUES (0, 100, 0, 10); -- fail
ERROR: insert or update on table "fk" violates foreign key constraint "fk_uk_id_q"
CONTEXT: PL/pgSQL function sql_saga.validate_foreign_key_new_row(name,jsonb) line 130 at RAISE
CONTEXT: PL/pgSQL function sql_saga.validate_foreign_key_new_row(name,jsonb) line 133 at RAISE
SQL statement "SELECT sql_saga.validate_foreign_key_new_row(TG_ARGV[0], jnew)"
PL/pgSQL function sql_saga.fk_insert_check() line 20 at PERFORM
INSERT INTO fk VALUES (0, 100, 1, 11); -- fail
ERROR: insert or update on table "fk" violates foreign key constraint "fk_uk_id_q"
CONTEXT: PL/pgSQL function sql_saga.validate_foreign_key_new_row(name,jsonb) line 130 at RAISE
CONTEXT: PL/pgSQL function sql_saga.validate_foreign_key_new_row(name,jsonb) line 133 at RAISE
SQL statement "SELECT sql_saga.validate_foreign_key_new_row(TG_ARGV[0], jnew)"
PL/pgSQL function sql_saga.fk_insert_check() line 20 at PERFORM
INSERT INTO fk VALUES (1, 100, 1, 3); -- success
INSERT INTO fk VALUES (2, 100, 1, 10); -- success
-- UPDATE
UPDATE fk SET e = 20 WHERE id = 1; -- fail
ERROR: insert or update on table "fk" violates foreign key constraint "fk_uk_id_q"
CONTEXT: PL/pgSQL function sql_saga.validate_foreign_key_new_row(name,jsonb) line 130 at RAISE
CONTEXT: PL/pgSQL function sql_saga.validate_foreign_key_new_row(name,jsonb) line 133 at RAISE
SQL statement "SELECT sql_saga.validate_foreign_key_new_row(TG_ARGV[0], jnew)"
PL/pgSQL function sql_saga.fk_update_check() line 19 at PERFORM
UPDATE fk SET e = 6 WHERE id = 1; -- success
Expand Down
16 changes: 8 additions & 8 deletions expected/09_drop_protection.out
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,19 @@ SELECT sql_saga.drop_api('dp', 'p');
ALTER TABLE dp DROP CONSTRAINT dp_pkey;
/* unique_keys */
ALTER TABLE dp
ADD CONSTRAINT u UNIQUE (id, s, e),
ADD CONSTRAINT x EXCLUDE USING gist (id WITH =, integerrange(s, e, '[)') WITH &&);
ADD CONSTRAINT u UNIQUE (id, s, e) DEFERRABLE,
ADD CONSTRAINT x EXCLUDE USING gist (id WITH =, integerrange(s, e, '[)') WITH &&) DEFERRABLE;
SELECT sql_saga.add_unique_key('dp', ARRAY['id'], 'p', 'k', 'u', 'x');
add_unique_key
----------------
k
(1 row)

ALTER TABLE dp DROP CONSTRAINT u; -- fails
ERROR: cannot drop constraint "u" on table "dp" because it is used in period unique key "k"
ERROR: cannot drop constraint "u" on table "dp" because it is used in era unique key "k"
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 182 at RAISE
ALTER TABLE dp DROP CONSTRAINT x; -- fails
ERROR: cannot drop constraint "x" on table "dp" because it is used in period unique key "k"
ERROR: cannot drop constraint "x" on table "dp" because it is used in era unique key "k"
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 193 at RAISE
ALTER TABLE dp DROP CONSTRAINT dp_p_check; -- fails
/* foreign_keys */
Expand All @@ -74,16 +74,16 @@ SELECT sql_saga.add_foreign_key('dp_ref', ARRAY['id'], 'p', 'k', key_name => 'f'
(1 row)

DROP TRIGGER f_fk_insert ON dp_ref; -- fails
ERROR: cannot drop trigger "f_fk_insert" on table "dp_ref" because it is used in period foreign key "f"
ERROR: cannot drop trigger "f_fk_insert" on table "dp_ref" because it is used in era foreign key "f"
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 209 at RAISE
DROP TRIGGER f_fk_update ON dp_ref; -- fails
ERROR: cannot drop trigger "f_fk_update" on table "dp_ref" because it is used in period foreign key "f"
ERROR: cannot drop trigger "f_fk_update" on table "dp_ref" because it is used in era foreign key "f"
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 220 at RAISE
DROP TRIGGER f_uk_update ON dp; -- fails
ERROR: cannot drop trigger "f_uk_update" on table "dp" because it is used in period foreign key "f"
ERROR: cannot drop trigger "f_uk_update" on table "dp" because it is used in era foreign key "f"
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 232 at RAISE
DROP TRIGGER f_uk_delete ON dp; -- fails
ERROR: cannot drop trigger "f_uk_delete" on table "dp" because it is used in period foreign key "f"
ERROR: cannot drop trigger "f_uk_delete" on table "dp" because it is used in era foreign key "f"
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 244 at RAISE
SELECT sql_saga.drop_foreign_key('dp_ref', 'f');
drop_foreign_key
Expand Down
6 changes: 3 additions & 3 deletions expected/10_rename_following.out
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ ALTER TABLE rename_test RENAME COLUMN col1 TO "COLUMN1";
ALTER TABLE rename_test RENAME CONSTRAINT "rename_test_col2_col1_col3_s < e_embedded "" symbols_key" TO unconst;
ALTER TABLE rename_test RENAME CONSTRAINT rename_test_col2_col1_col3_int4range_excl TO exconst;
TABLE sql_saga.unique_keys;
key_name | table_name | column_names | era_name | unique_constraint | exclude_constraint
------------------------------+-------------+---------------------+----------+-------------------+--------------------
rename_test_col2_col1_col3_p | rename_test | {col2,COLUMN1,col3} | p | unconst | exconst
key_name | table_name | column_names | era_name | unique_constraint | exclude_constraint
------------------------------+-------------+---------------------+----------+-------------------+-------------------------------------------
rename_test_col2_col1_col3_p | rename_test | {col2,COLUMN1,col3} | p | unconst | rename_test_col2_col1_col3_int4range_excl
(1 row)

/* foreign_keys */
Expand Down
2 changes: 1 addition & 1 deletion expected/11_health_checks.out
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ SELECT sql_saga.add_era('log', 's', 'e', 'p'); -- passes
(1 row)

ALTER TABLE log SET UNLOGGED; -- fails
ERROR: table "log" must remain persistent because it has periods
ERROR: table "log" must remain persistent because it has an era
CONTEXT: PL/pgSQL function sql_saga.health_checks() line 15 at RAISE
DROP TABLE log;
2 changes: 1 addition & 1 deletion expected/13_issues.out
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ INSERT INTO uk(id, s, e) VALUES (3, 1, 3),
-- Reference over non contiguous time - should fail
INSERT INTO fk(id, uk_id, s, e) VALUES (5, 3, 1, 5);
ERROR: insert or update on table "fk" violates foreign key constraint "fk_uk_id_q"
CONTEXT: PL/pgSQL function sql_saga.validate_foreign_key_new_row(name,jsonb) line 130 at RAISE
CONTEXT: PL/pgSQL function sql_saga.validate_foreign_key_new_row(name,jsonb) line 133 at RAISE
SQL statement "SELECT sql_saga.validate_foreign_key_new_row(TG_ARGV[0], jnew)"
PL/pgSQL function sql_saga.fk_insert_check() line 20 at PERFORM
-- Create overlappig range - should fail
Expand Down
Loading

0 comments on commit 2e4d768

Please sign in to comment.