diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eb5c52ae1..fa65314a0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,7 @@ set(SOURCES chunk_index.c chunk_insert_state.c chunk_server.c + constraint.c constraint_aware_append.c cross_module_fn.c copy.c diff --git a/src/chunk_constraint.c b/src/chunk_constraint.c index 12c4a001d..06583f184 100644 --- a/src/chunk_constraint.c +++ b/src/chunk_constraint.c @@ -31,6 +31,7 @@ #include "scan_iterator.h" #include "chunk_constraint.h" #include "chunk_index.h" +#include "constraint.h" #include "dimension_vector.h" #include "dimension_slice.h" #include "hypercube.h" @@ -579,36 +580,40 @@ ts_chunk_constraints_add_dimension_constraints(ChunkConstraints *ccs, int32 chun return cube->num_slices; } +typedef struct ConstraintContext +{ + int num_added; + char chunk_relkind; + ChunkConstraints *ccs; + int32 chunk_id; +} ConstraintContext; + +static ConstraintProcessStatus +chunk_constraint_add(HeapTuple constraint_tuple, void *arg) +{ + ConstraintContext *cc = arg; + Form_pg_constraint constraint = (Form_pg_constraint) GETSTRUCT(constraint_tuple); + + if (chunk_constraint_need_on_chunk(cc->chunk_relkind, constraint)) + { + chunk_constraints_add(cc->ccs, cc->chunk_id, 0, NULL, NameStr(constraint->conname)); + return CONSTR_PROCESSED; + } + + return CONSTR_IGNORED; +} + int ts_chunk_constraints_add_inheritable_constraints(ChunkConstraints *ccs, int32 chunk_id, const char chunk_relkind, Oid hypertable_oid) { - ScanKeyData skey; - Relation rel; - SysScanDesc scan; - HeapTuple htup; - int num_added = 0; + ConstraintContext cc = { + .chunk_relkind = chunk_relkind, + .ccs = ccs, + .chunk_id = chunk_id, + }; - ScanKeyInit(&skey, Anum_pg_constraint_conrelid, BTEqualStrategyNumber, F_OIDEQ, hypertable_oid); - - rel = table_open(ConstraintRelationId, AccessShareLock); - scan = systable_beginscan(rel, ConstraintRelidTypidNameIndexId, true, NULL, 1, &skey); - - while (HeapTupleIsValid(htup = systable_getnext(scan))) - { - Form_pg_constraint pg_constraint = (Form_pg_constraint) GETSTRUCT(htup); - - if (chunk_constraint_need_on_chunk(chunk_relkind, pg_constraint)) - { - chunk_constraints_add(ccs, chunk_id, 0, NULL, NameStr(pg_constraint->conname)); - num_added++; - } - } - - systable_endscan(scan); - table_close(rel, AccessShareLock); - - return num_added; + return ts_constraint_process(hypertable_oid, chunk_constraint_add, &cc); } void diff --git a/src/compat.h b/src/compat.h index 3260c1b24..b07cd2ca6 100644 --- a/src/compat.h +++ b/src/compat.h @@ -400,6 +400,8 @@ MakeTupleTableSlotCompat(TupleDesc tupdesc, void *tts_ops) } name##data; \ FunctionCallInfo name = &name##data.fcinfo +#define SizeForFunctionCallInfo(nargs) sizeof(FunctionCallInfoData) + /* convenience macro to allocate FunctionCallInfoData on the heap */ #define HEAP_FCINFO(nargs) palloc(sizeof(FunctionCallInfoData)) diff --git a/src/constraint.c b/src/constraint.c new file mode 100644 index 000000000..308345152 --- /dev/null +++ b/src/constraint.c @@ -0,0 +1,67 @@ +/* + * This file and its contents are licensed under the Apache License 2.0. + * Please see the included NOTICE for copyright information and + * LICENSE-APACHE for a copy of the license. + */ +#include +#include +#include +#include +#include + +#include "compat.h" +#if PG11_LT /* PG11 consolidates pg_foo_fn.h -> pg_foo.h */ +#include +#endif +#if PG12_GE +#include +#else +#include "compat/tableam.h" +#endif + +#include "constraint.h" + +/* + * Process constraints that belong to a given relation. + * + * Returns the number of constraints processed. + */ +int +ts_constraint_process(Oid relid, constraint_func process_func, void *ctx) +{ + ScanKeyData skey; + Relation rel; + SysScanDesc scan; + HeapTuple htup; + bool should_continue = true; + int count = 0; + + ScanKeyInit(&skey, Anum_pg_constraint_conrelid, BTEqualStrategyNumber, F_OIDEQ, relid); + + rel = table_open(ConstraintRelationId, AccessShareLock); + scan = systable_beginscan(rel, ConstraintRelidTypidNameIndexId, true, NULL, 1, &skey); + + while (HeapTupleIsValid(htup = systable_getnext(scan)) && should_continue) + { + switch (process_func(htup, ctx)) + { + case CONSTR_PROCESSED: + count++; + break; + case CONSTR_PROCESSED_DONE: + count++; + should_continue = false; + break; + case CONSTR_IGNORED: + break; + case CONSTR_IGNORED_DONE: + should_continue = false; + break; + } + } + + systable_endscan(scan); + table_close(rel, AccessShareLock); + + return count; +} diff --git a/src/constraint.h b/src/constraint.h new file mode 100644 index 000000000..2049d8a8a --- /dev/null +++ b/src/constraint.h @@ -0,0 +1,32 @@ +/* + * This file and its contents are licensed under the Apache License 2.0. + * Please see the included NOTICE for copyright information and + * LICENSE-APACHE for a copy of the license. + */ +#ifndef TIMESCALEDB_CONSTRAINT_H +#define TIMESCALEDB_CONSTRAINT_H + +#include +#include + +#include "export.h" + +/* + * Return status for constraint processsing function. + * + * PROCESSED - count the constraint as processed + * IGNORED - the constraint wasn't processed + * DONE - stop processing constraints + */ +typedef enum ConstraintProcessStatus +{ + CONSTR_PROCESSED, + CONSTR_PROCESSED_DONE, + CONSTR_IGNORED, + CONSTR_IGNORED_DONE, +} ConstraintProcessStatus; + +typedef ConstraintProcessStatus (*constraint_func)(HeapTuple constraint_tuple, void *ctx); +extern TSDLLEXPORT int ts_constraint_process(Oid relid, constraint_func process_func, void *ctx); + +#endif /* TIMESCALEDB_CONSTRAINT_H */ diff --git a/test/pg_regress.sh b/test/pg_regress.sh index e39f4598a..856f1c8f0 100755 --- a/test/pg_regress.sh +++ b/test/pg_regress.sh @@ -87,6 +87,8 @@ function cleanup() { rm -rf ${TEST_TABLESPACE2_PATH} rm -f ${CURRENT_DIR}/.pg_init rm -f ${TEMP_SCHEDULE} + rm -rf ${TEST_TABLESPACE3_PATH} + rm -f ${TEST_OUTPUT_DIR}/.pg_init } trap cleanup EXIT @@ -94,7 +96,8 @@ trap cleanup EXIT # This mktemp line will work on both OSX and GNU systems TEST_TABLESPACE1_PATH=${TEST_TABLESPACE1_PATH:-$(mktemp -d 2>/dev/null || mktemp -d -t 'timescaledb_regress')} TEST_TABLESPACE2_PATH=${TEST_TABLESPACE2_PATH:-$(mktemp -d 2>/dev/null || mktemp -d -t 'timescaledb_regress')} -export TEST_TABLESPACE1_PATH TEST_TABLESPACE2_PATH +TEST_TABLESPACE3_PATH=${TEST_TABLESPACE3_PATH:-$(mktemp -d 2>/dev/null || mktemp -d -t 'timescaledb_regress')} +export TEST_TABLESPACE1_PATH TEST_TABLESPACE2_PATH TEST_TABLESPACE3_PATH rm -f ${TEST_OUTPUT_DIR}/.pg_init mkdir -p ${EXE_DIR}/sql/dump diff --git a/test/runner.sh b/test/runner.sh index 92d4c507d..f955c1fd4 100755 --- a/test/runner.sh +++ b/test/runner.sh @@ -69,6 +69,7 @@ ${PSQL} -U ${TEST_PGUSER} \ -v TEST_DBNAME="${TEST_DBNAME}" \ -v TEST_TABLESPACE1_PATH=\'${TEST_TABLESPACE1_PATH}\' \ -v TEST_TABLESPACE2_PATH=\'${TEST_TABLESPACE2_PATH}\' \ + -v TEST_TABLESPACE3_PATH=\'${TEST_TABLESPACE3_PATH}\' \ -v TEST_INPUT_DIR=${TEST_INPUT_DIR} \ -v TEST_OUTPUT_DIR=${TEST_OUTPUT_DIR} \ -v TEST_SPINWAIT_ITERS=${TEST_SPINWAIT_ITERS} \ diff --git a/test/sql/utils/testsupport.sql b/test/sql/utils/testsupport.sql index 1eb6a54b8..c2fff1b7e 100644 --- a/test/sql/utils/testsupport.sql +++ b/test/sql/utils/testsupport.sql @@ -272,3 +272,10 @@ $BODY$; -- Used to set a deterministic memory setting during tests CREATE OR REPLACE FUNCTION test.set_memory_cache_size(memory_amount text) RETURNS BIGINT AS :MODULE_PATHNAME, 'ts_set_memory_cache_size' LANGUAGE C VOLATILE STRICT; + +CREATE OR REPLACE FUNCTION test.empty_trigger_func() + RETURNS TRIGGER LANGUAGE PLPGSQL AS +$BODY$ +BEGIN +END +$BODY$; \ No newline at end of file diff --git a/tsl/src/CMakeLists.txt b/tsl/src/CMakeLists.txt index 08718bbb7..09e0df423 100644 --- a/tsl/src/CMakeLists.txt +++ b/tsl/src/CMakeLists.txt @@ -1,5 +1,6 @@ set(SOURCES chunk_api.c + deparse.c hypertable.c init.c license.c diff --git a/tsl/src/deparse.c b/tsl/src/deparse.c new file mode 100644 index 000000000..c090e442b --- /dev/null +++ b/tsl/src/deparse.c @@ -0,0 +1,373 @@ +/* + * This file and its contents are licensed under the Timescale License. + * Please see the included NOTICE for copyright information and + * LICENSE-TIMESCALE for a copy of the license. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "export.h" +#include "compat.h" +#include "constraint.h" +#include "trigger.h" +#include "utils.h" +#include "deparse.h" + +/* + * Deparse a table into a set of SQL commands that can be used to recreate it. + * Together with column definiton it deparses constraints, indexes, triggers + * and rules as well. There are some table types that are not supported: + * temporary, partitioned, foreign, inherited and a table that uses + * options. Row security is also not supported. + */ +typedef const char *(*GetCmdFunc)(Oid oid); + +static const char * +get_index_cmd(Oid oid) +{ + return pg_get_indexdef_string(oid); +} + +static const char * +get_constraint_cmd(Oid oid) +{ + return pg_get_constraintdef_command(oid); +} + +static FunctionCallInfo +build_fcinfo_data(Oid oid) +{ + FunctionCallInfo fcinfo = palloc(SizeForFunctionCallInfo(1)); + + InitFunctionCallInfoData(*fcinfo, NULL, 1, InvalidOid, NULL, NULL); + FC_ARG(fcinfo, 0) = ObjectIdGetDatum(oid); + FC_NULL(fcinfo, 0) = false; + + return fcinfo; +} + +static const char * +get_trigger_cmd(Oid oid) +{ + return TextDatumGetCString(pg_get_triggerdef(build_fcinfo_data(oid))); +} + +static const char * +get_rule_cmd(Oid oid) +{ + return TextDatumGetCString(pg_get_ruledef(build_fcinfo_data(oid))); +} + +static List * +get_cmds(List *oids, GetCmdFunc get_cmd) +{ + List *cmds = NIL; + ListCell *cell; + + foreach (cell, oids) + { + StringInfo cmd = makeStringInfo(); + + appendStringInfo(cmd, "%s;", get_cmd(lfirst_oid(cell))); + cmds = lappend(cmds, cmd->data); + } + return cmds; +} + +static List * +get_constraint_cmds(List *constraint_oids) +{ + return get_cmds(constraint_oids, get_constraint_cmd); +} + +static List * +get_index_cmds(List *index_oids) +{ + return get_cmds(index_oids, get_index_cmd); +} + +static List * +get_trigger_cmds(List *trigger_oids) +{ + return get_cmds(trigger_oids, get_trigger_cmd); +} + +static List * +get_rule_cmds(List *rule_oids) +{ + return get_cmds(rule_oids, get_rule_cmd); +} + +static void +deparse_columns(StringInfo stmt, Relation rel) +{ + int att_idx; + TupleDesc rel_desc = RelationGetDescr(rel); + TupleConstr *constraints = rel_desc->constr; + + for (att_idx = 0; att_idx < rel_desc->natts; att_idx++) + { + int dim_idx; + Form_pg_attribute attr = TupleDescAttr(rel_desc, att_idx); + + if (attr->attisdropped) + continue; + + appendStringInfo(stmt, + "\"%s\" %s", + NameStr(attr->attname), + format_type_with_typemod(attr->atttypid, attr->atttypmod)); + + if (attr->attnotnull) + appendStringInfoString(stmt, " NOT NULL"); + + if (OidIsValid(attr->attcollation)) + appendStringInfo(stmt, " COLLATE \"%s\"", get_collation_name(attr->attcollation)); + + if (attr->atthasdef) + { + int co_idx; + + for (co_idx = 0; co_idx < constraints->num_defval; co_idx++) + { + AttrDefault attr_def = constraints->defval[co_idx]; + + if (attr->attnum == attr_def.adnum) + { + char *attr_default = + TextDatumGetCString(DirectFunctionCall2(pg_get_expr, + CStringGetTextDatum(attr_def.adbin), + ObjectIdGetDatum(rel->rd_id))); + + appendStringInfo(stmt, " DEFAULT %s", attr_default); + break; + } + } + } + + for (dim_idx = 1; dim_idx < attr->attndims; dim_idx++) + appendStringInfoString(stmt, "[]"); + + if (att_idx != (rel_desc->natts - 1)) + appendStringInfoString(stmt, ", "); + } +} + +typedef struct ConstraintContext +{ + List *constraints; + List **constraint_indexes; +} ConstraintContext; + +static ConstraintProcessStatus +add_constraint(HeapTuple constraint_tuple, void *ctx) +{ + ConstraintContext *cc = ctx; + Form_pg_constraint constraint = (Form_pg_constraint) GETSTRUCT(constraint_tuple); + Oid constroid; + + if (OidIsValid(constraint->conindid)) + *cc->constraint_indexes = lappend_oid(*cc->constraint_indexes, constraint->conindid); +#if PG12_GE + constroid = constraint->oid; +#else + constroid = HeapTupleGetOid(constraint_tuple); +#endif + cc->constraints = lappend_oid(cc->constraints, constroid); + return CONSTR_PROCESSED; +} + +static List * +get_constraint_oids(Oid relid, List **constraint_indexes) +{ + ConstraintContext cc = { + .constraints = NIL, + .constraint_indexes = constraint_indexes, + }; + + ts_constraint_process(relid, add_constraint, &cc); + + return cc.constraints; +} + +static List * +get_index_oids(Relation rel, List *exclude_indexes) +{ + List *indexes = NIL; + ListCell *cell; + + foreach (cell, RelationGetIndexList(rel)) + { + Oid indexid = lfirst_oid(cell); + + if (!list_member_oid(exclude_indexes, indexid)) + indexes = lappend_oid(indexes, indexid); + } + return indexes; +} + +static List * +get_trigger_oids(Relation rel) +{ + List *triggers = NIL; + + if (rel->trigdesc != NULL) + { + int i; + + for (i = 0; i < rel->trigdesc->numtriggers; i++) + { + const Trigger trigger = rel->trigdesc->triggers[i]; + + if (!trigger.tgisinternal) + triggers = lappend_oid(triggers, trigger.tgoid); + } + } + return triggers; +} + +static List * +get_rule_oids(Relation rel) +{ + List *rules = NIL; + + if (rel->rd_rules != NULL) + { + int i; + + for (i = 0; i < rel->rd_rules->numLocks; i++) + { + const RewriteRule *rule = rel->rd_rules->rules[i]; + + rules = lappend_oid(rules, rule->ruleId); + } + } + return rules; +} + +static void +validate_relation(Relation rel) +{ + if (rel->rd_rel->relkind != RELKIND_RELATION) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("given relation is not an ordinary table"))); + + if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("temporary table is not supported"))); + + if (rel->rd_rel->relrowsecurity) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("row security is not supported"))); +} + +TableInfo * +deparse_create_table_info(Oid relid) +{ + List *exclude_indexes = NIL; + TableInfo *table_info = palloc0(sizeof(TableInfo)); + Relation rel = table_open(relid, AccessShareLock); + + if (rel == NULL) + ereport(ERROR, (errmsg("relation with id %d not found", relid))); + + validate_relation(rel); + + table_info->relid = relid; + table_info->constraints = get_constraint_oids(relid, &exclude_indexes); + table_info->indexes = get_index_oids(rel, exclude_indexes); + table_info->triggers = get_trigger_oids(rel); + table_info->rules = get_rule_oids(rel); + table_close(rel, AccessShareLock); + return table_info; +} + +TableDef * +deparse_get_tabledef(TableInfo *table_info) +{ + StringInfo create_table = makeStringInfo(); + TableDef *table_def = palloc0(sizeof(TableDef)); + Relation rel = table_open(table_info->relid, AccessShareLock); + Oid tablespace; + + appendStringInfoString(create_table, "CREATE"); + if (rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED) + appendStringInfoString(create_table, " UNLOGGED"); + appendStringInfoString(create_table, " TABLE"); + + appendStringInfo(create_table, + " \"%s\".\"%s\" (", + get_namespace_name(rel->rd_rel->relnamespace), + NameStr(rel->rd_rel->relname)); + + deparse_columns(create_table, rel); + + appendStringInfoChar(create_table, ')'); + + tablespace = get_rel_tablespace(table_info->relid); + if (tablespace != InvalidOid) + appendStringInfo(create_table, " TABLESPACE %s", get_tablespace_name(tablespace)); + + appendStringInfoChar(create_table, ';'); + table_def->create_cmd = create_table->data; + + table_def->constraint_cmds = get_constraint_cmds(table_info->constraints); + table_def->index_cmds = get_index_cmds(table_info->indexes); + table_def->trigger_cmds = get_trigger_cmds(table_info->triggers); + table_def->rule_cmds = get_rule_cmds(table_info->rules); + + table_close(rel, AccessShareLock); + return table_def; +} + +List * +deparse_get_tabledef_commands(Oid relid) +{ + TableInfo *table_info = deparse_create_table_info(relid); + TableDef *table_def = deparse_get_tabledef(table_info); + + return deparse_get_tabledef_commands_from_tabledef(table_def); +} + +List * +deparse_get_tabledef_commands_from_tabledef(TableDef *table_def) +{ + List *cmds = NIL; + + cmds = lappend(cmds, (char *) table_def->create_cmd); + cmds = list_concat(cmds, table_def->constraint_cmds); + cmds = list_concat(cmds, table_def->index_cmds); + cmds = list_concat(cmds, table_def->trigger_cmds); + cmds = list_concat(cmds, table_def->rule_cmds); + return cmds; +} + +const char * +deparse_get_tabledef_commands_concat(Oid relid) +{ + StringInfo tabledef = makeStringInfo(); + ListCell *cell; + + foreach (cell, deparse_get_tabledef_commands(relid)) + appendStringInfoString(tabledef, lfirst(cell)); + + return tabledef->data; +} diff --git a/tsl/src/deparse.h b/tsl/src/deparse.h new file mode 100644 index 000000000..be112bd42 --- /dev/null +++ b/tsl/src/deparse.h @@ -0,0 +1,36 @@ +/* + * This file and its contents are licensed under the Timescale License. + * Please see the included NOTICE for copyright information and + * LICENSE-TIMESCALE for a copy of the license. + */ +#ifndef TIMESCALEDB_DEPARSE_H +#define TIMESCALEDB_DEPARSE_H + +#include +#include + +typedef struct TableInfo +{ + Oid relid; + List *constraints; + List *indexes; + List *triggers; + List *rules; +} TableInfo; + +typedef struct TableDef +{ + const char *create_cmd; + List *constraint_cmds; + List *index_cmds; + List *trigger_cmds; + List *rule_cmds; +} TableDef; + +TableInfo *deparse_create_table_info(Oid relid); +TableDef *deparse_get_tabledef(TableInfo *table_info); +List *deparse_get_tabledef_commands(Oid relid); +List *deparse_get_tabledef_commands_from_tabledef(TableDef *table_def); +const char *deparse_get_tabledef_commands_concat(Oid relid); + +#endif diff --git a/tsl/test/expected/deparse.out b/tsl/test/expected/deparse.out new file mode 100644 index 000000000..b91073def --- /dev/null +++ b/tsl/test/expected/deparse.out @@ -0,0 +1,11 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +-- We compare information(\d+) about manually created tables with the ones that were recreated using deparse_table command. +-- There should be no diff. +\set ECHO errors + ?column? +---------- + DONE +(1 row) + diff --git a/tsl/test/expected/deparse_fail.out b/tsl/test/expected/deparse_fail.out new file mode 100644 index 000000000..cd9ddb91d --- /dev/null +++ b/tsl/test/expected/deparse_fail.out @@ -0,0 +1,24 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +\ir include/deparse_func.sql +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +\c :TEST_DBNAME :ROLE_SUPERUSER +CREATE OR REPLACE FUNCTION _timescaledb_internal.get_tabledef(tbl REGCLASS) RETURNS TEXT +AS :TSL_MODULE_PATHNAME, 'tsl_test_get_tabledef' LANGUAGE C VOLATILE STRICT; +SET ROLE :ROLE_DEFAULT_PERM_USER; +\set ON_ERROR_STOP 0 +CREATE TEMP TABLE fail_table1(x INT); +SELECT _timescaledb_internal.get_tabledef('fail_table1'); +ERROR: temporary table is not supported +CREATE INDEX my_fail_table1_idx ON fail_table1 USING BTREE(x); +SELECT _timescaledb_internal.get_tabledef('my_fail_table1_idx'); +ERROR: "my_fail_table1_idx" is an index +SELECT _timescaledb_internal.get_tabledef('non_existing'); +ERROR: relation "non_existing" does not exist at character 43 +CREATE TABLE row_sec(i INT); +ALTER TABLE row_sec ENABLE ROW LEVEL SECURITY; +SELECT _timescaledb_internal.get_tabledef('row_sec'); +ERROR: row security is not supported diff --git a/tsl/test/sql/CMakeLists.txt b/tsl/test/sql/CMakeLists.txt index 861c9e114..ab45cb760 100644 --- a/tsl/test/sql/CMakeLists.txt +++ b/tsl/test/sql/CMakeLists.txt @@ -100,6 +100,8 @@ if (NOT ${PG_VERSION} VERSION_LESS "10") timescaledb_fdw.sql ) list(APPEND TEST_FILES_DEBUG + deparse.sql + deparse_fail.sql dist_commands.sql remote_connection.sql remote_connection_cache.sql diff --git a/tsl/test/sql/deparse.sql b/tsl/test/sql/deparse.sql new file mode 100644 index 000000000..05ae07329 --- /dev/null +++ b/tsl/test/sql/deparse.sql @@ -0,0 +1,30 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. + +-- We compare information(\d+) about manually created tables with the ones that were recreated using deparse_table command. +-- There should be no diff. + +\set ECHO errors +\ir include/deparse_func.sql + +SELECT format('%s/results/deparse_create.out', :'TEST_OUTPUT_DIR') AS "CREATE_OUT", + format('%s/results/deparse_recreate.out', :'TEST_OUTPUT_DIR') AS "RECREATE_OUT" +\gset +SELECT format('\! diff %s %s', :'CREATE_OUT', :'RECREATE_OUT') as "DIFF_CMD" +\gset + +\ir include/deparse_create.sql + +\o :CREATE_OUT +\d+ "public".* + +\ir include/deparse_recreate.sql +\o :RECREATE_OUT +\d+ "public".* + +\o + +:DIFF_CMD + +SELECT 'DONE' diff --git a/tsl/test/sql/deparse_fail.sql b/tsl/test/sql/deparse_fail.sql new file mode 100644 index 000000000..fa8bf6ae8 --- /dev/null +++ b/tsl/test/sql/deparse_fail.sql @@ -0,0 +1,21 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. + +\ir include/deparse_func.sql + +\set ON_ERROR_STOP 0 + +CREATE TEMP TABLE fail_table1(x INT); + +SELECT _timescaledb_internal.get_tabledef('fail_table1'); + +CREATE INDEX my_fail_table1_idx ON fail_table1 USING BTREE(x); + +SELECT _timescaledb_internal.get_tabledef('my_fail_table1_idx'); + +SELECT _timescaledb_internal.get_tabledef('non_existing'); + +CREATE TABLE row_sec(i INT); +ALTER TABLE row_sec ENABLE ROW LEVEL SECURITY; +SELECT _timescaledb_internal.get_tabledef('row_sec'); diff --git a/tsl/test/sql/include/deparse_create.sql b/tsl/test/sql/include/deparse_create.sql new file mode 100644 index 000000000..9825b0459 --- /dev/null +++ b/tsl/test/sql/include/deparse_create.sql @@ -0,0 +1,51 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. + +-- Lets create some tabels that we will try to deparse and recreate + +\c :TEST_DBNAME :ROLE_SUPERUSER + +SET ROLE :ROLE_DEFAULT_PERM_USER; + +CREATE TABLE table1(time TIMESTAMP, v FLOAT8, c CHAR(10), x NUMERIC(10,4), i interval hour to minute); + +CREATE TABLE table2(time TIMESTAMP NOT NULL, v FLOAT8[], d TEXT COLLATE "POSIX", num INT DEFAULT 100); + +CREATE TABLE table3(time TIMESTAMP PRIMARY KEY, v FLOAT8[][], num INT CHECK (num > 0), d INT UNIQUE, CONSTRAINT validate_num_and_d CHECK ( num > d)); + +CREATE TABLE table4(t TIMESTAMP , d INT, PRIMARY KEY (t, d)); + +CREATE TABLE ref_table(id INT PRIMARY KEY, d TEXT); + +CREATE TABLE table5(t TIMESTAMP PRIMARY KEY, v FLOAT8, d INT REFERENCES ref_table ON DELETE CASCADE); + +CREATE SEQUENCE my_seq; + +CREATE UNLOGGED TABLE table6(id INT NOT NULL DEFAULT nextval('my_seq'), t TEXT); + +CREATE INDEX ON table6 USING BTREE (t); + +RESET ROLE; + +CREATE TABLESPACE mytablespace OWNER :ROLE_DEFAULT_PERM_USER LOCATION :TEST_TABLESPACE3_PATH; + +CREATE TYPE device_status AS ENUM ('OFF', 'ON', 'BROKEN'); + +CREATE SCHEMA myschema AUTHORIZATION :ROLE_DEFAULT_PERM_USER;; + +SET ROLE :ROLE_DEFAULT_PERM_USER; + +CREATE TABLE table7(t TIMESTAMP, v INT) TABLESPACE mytablespace; + +CREATE TABLE table8(id INT, status device_status); + +CREATE TRIGGER test_trigger BEFORE UPDATE OR DELETE ON table8 +FOR EACH STATEMENT EXECUTE PROCEDURE test.empty_trigger_func(); + +CREATE RULE notify_me AS ON UPDATE TO table8 DO ALSO NOTIFY table8; + +CREATE TABLE table9(c CIRCLE, EXCLUDE USING gist (c WITH &&)); + +CREATE TABLE myschema.table10(t TIMESTAMP); + diff --git a/tsl/test/sql/include/deparse_func.sql b/tsl/test/sql/include/deparse_func.sql new file mode 100644 index 000000000..b1979daa4 --- /dev/null +++ b/tsl/test/sql/include/deparse_func.sql @@ -0,0 +1,10 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. + +\c :TEST_DBNAME :ROLE_SUPERUSER + +CREATE OR REPLACE FUNCTION _timescaledb_internal.get_tabledef(tbl REGCLASS) RETURNS TEXT +AS :TSL_MODULE_PATHNAME, 'tsl_test_get_tabledef' LANGUAGE C VOLATILE STRICT; + +SET ROLE :ROLE_DEFAULT_PERM_USER; \ No newline at end of file diff --git a/tsl/test/sql/include/deparse_recreate.sql b/tsl/test/sql/include/deparse_recreate.sql new file mode 100644 index 000000000..e1d9e6192 --- /dev/null +++ b/tsl/test/sql/include/deparse_recreate.sql @@ -0,0 +1,24 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. + +-- deparse each table%, drop it and recreate it using deparse result + +DO +$$DECLARE + tables CURSOR FOR + SELECT tablename, schemaname + FROM pg_tables + WHERE tablename LIKE 'table%' AND (schemaname = 'public' OR schemaname = 'myschema') + ORDER BY tablename; + deparse_stmt text; + tablename text; +BEGIN + FOR table_record IN tables + LOOP + tablename := format('%I.%I', table_record.schemaname, table_record.tablename); + EXECUTE format('SELECT _timescaledb_internal.get_tabledef(%L)', tablename) INTO deparse_stmt; + EXECUTE 'DROP TABLE ' || tablename; + EXECUTE deparse_stmt; + END LOOP; +END$$; diff --git a/tsl/test/src/CMakeLists.txt b/tsl/test/src/CMakeLists.txt index 8e86d70f1..41c87c0e2 100644 --- a/tsl/test/src/CMakeLists.txt +++ b/tsl/test/src/CMakeLists.txt @@ -5,6 +5,7 @@ set(SOURCES test_ddl_hook.c test_continuous_aggs.c server.c + deparse.c ) include(${PROJECT_SOURCE_DIR}/tsl/src/build-defs.cmake) diff --git a/tsl/test/src/deparse.c b/tsl/test/src/deparse.c new file mode 100644 index 000000000..00bba755c --- /dev/null +++ b/tsl/test/src/deparse.c @@ -0,0 +1,19 @@ +/* + * This file and its contents are licensed under the Timescale License. + * Please see the included NOTICE for copyright information and + * LICENSE-TIMESCALE for a copy of the license. + */ +#include +#include +#include "export.h" +#include "deparse.h" + +TS_FUNCTION_INFO_V1(tsl_test_get_tabledef); + +Datum +tsl_test_get_tabledef(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + const char *cmd = deparse_get_tabledef_commands_concat(relid); + PG_RETURN_TEXT_P(cstring_to_text(cmd)); +}