Skip to content

Commit

Permalink
Fix issue creating dimensional constraints
Browse files Browse the repository at this point in the history
During chunk creation, the chunk's dimensional CHECK constraints are
created via an "upcall" to PL/pgSQL code. However, creating
dimensional constraints in PL/pgSQL code sometimes fails, especially
during high-concurrency inserts, because PL/pgSQL code scans metadata
using a snapshot that might not see the same metadata as the C
code. As a result, chunk creation sometimes fail during constraint
creation.

To fix this issue, implement dimensional CHECK-constraint creation in
C code. Other constraints (FK, PK, etc.) are still created via an
upcall, but should probably also be rewritten in C. However, since
these constraints don't depend on recently updated metadata, this is
left to a future change.

Fixes #5456
  • Loading branch information
erimatnor committed Mar 23, 2023
1 parent 72c0f5b commit 2105a06
Show file tree
Hide file tree
Showing 14 changed files with 412 additions and 215 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -24,6 +24,7 @@ accidentally triggering the load of a previous DB version.**
* #5442 Decompression may have lost DEFAULT values
* #5446 Add checks for malloc failure in libpq calls
* #5470 Ensure superuser perms during copy/move chunk
* #5459 Fix issue creating dimensional constraints

**Thanks**
* @nikolaps for reporting an issue with the COPY fetcher
Expand Down
7 changes: 1 addition & 6 deletions sql/chunk_constraint.sql
Expand Up @@ -22,12 +22,7 @@ BEGIN
SELECT * INTO STRICT hypertable_row FROM _timescaledb_catalog.hypertable h WHERE h.id = chunk_row.hypertable_id;

IF chunk_constraint_row.dimension_slice_id IS NOT NULL THEN
check_sql = _timescaledb_internal.dimension_slice_get_constraint_sql(chunk_constraint_row.dimension_slice_id);
IF check_sql IS NOT NULL THEN
def := format('CHECK (%s)', check_sql);
ELSE
def := NULL;
END IF;
RAISE 'cannot create dimension constraint %', chunk_constraint_row;
ELSIF chunk_constraint_row.hypertable_constraint_name IS NOT NULL THEN

SELECT oid, contype INTO STRICT constraint_oid, constraint_type FROM pg_constraint
Expand Down
3 changes: 2 additions & 1 deletion sql/size_utils.sql
Expand Up @@ -366,7 +366,8 @@ CREATE OR REPLACE FUNCTION _timescaledb_internal.range_value_to_pretty(
$BODY$
DECLARE
BEGIN
IF NOT _timescaledb_internal.dimension_is_finite(time_value) THEN
IF NOT (time_value > (-9223372036854775808)::bigint AND
time_value < 9223372036854775807::bigint) THEN
RETURN '';
END IF;
IF time_value IS NULL THEN
Expand Down
3 changes: 3 additions & 0 deletions sql/updates/latest-dev.sql
Expand Up @@ -21,3 +21,6 @@ CREATE FUNCTION _timescaledb_internal.recompress_chunk_segmentwise(REGCLASS, BOO
AS '@MODULE_PATHNAME@', 'ts_recompress_chunk_segmentwise' LANGUAGE C STRICT VOLATILE;
CREATE FUNCTION _timescaledb_internal.get_compressed_chunk_index_for_recompression(REGCLASS) RETURNS REGCLASS
AS '@MODULE_PATHNAME@', 'ts_get_compressed_chunk_index_for_recompression' LANGUAGE C STRICT VOLATILE;

DROP FUNCTION _timescaledb_internal.dimension_is_finite;
DROP FUNCTION _timescaledb_internal.dimension_slice_get_constraint_sql;
87 changes: 87 additions & 0 deletions sql/updates/reverse-dev.sql
Expand Up @@ -12,3 +12,90 @@ DROP TABLE IF EXISTS _timescaledb_catalog.continuous_aggs_watermark;
DROP FUNCTION IF EXISTS _timescaledb_internal.cagg_watermark_materialized(hypertable_id INTEGER);
DROP FUNCTION _timescaledb_internal.recompress_chunk_segmentwise(REGCLASS, BOOLEAN);
DROP FUNCTION _timescaledb_internal.get_compressed_chunk_index_for_recompression(REGCLASS);

CREATE OR REPLACE FUNCTION _timescaledb_internal.dimension_is_finite(
val BIGINT
)
RETURNS BOOLEAN LANGUAGE SQL IMMUTABLE PARALLEL SAFE AS
$BODY$
--end values of bigint reserved for infinite
SELECT val > (-9223372036854775808)::bigint AND val < 9223372036854775807::bigint
$BODY$ SET search_path TO pg_catalog, pg_temp;

CREATE OR REPLACE FUNCTION _timescaledb_internal.dimension_slice_get_constraint_sql(
dimension_slice_id INTEGER
)
RETURNS TEXT LANGUAGE PLPGSQL VOLATILE AS
$BODY$
DECLARE
dimension_slice_row _timescaledb_catalog.dimension_slice;
dimension_row _timescaledb_catalog.dimension;
dimension_def TEXT;
dimtype REGTYPE;
parts TEXT[];
BEGIN
SELECT * INTO STRICT dimension_slice_row
FROM _timescaledb_catalog.dimension_slice
WHERE id = dimension_slice_id;

SELECT * INTO STRICT dimension_row
FROM _timescaledb_catalog.dimension
WHERE id = dimension_slice_row.dimension_id;

IF dimension_row.partitioning_func_schema IS NOT NULL AND
dimension_row.partitioning_func IS NOT NULL THEN
SELECT prorettype INTO STRICT dimtype
FROM pg_catalog.pg_proc pro
WHERE pro.oid = format('%I.%I', dimension_row.partitioning_func_schema, dimension_row.partitioning_func)::regproc::oid;

dimension_def := format('%1$I.%2$I(%3$I)',
dimension_row.partitioning_func_schema,
dimension_row.partitioning_func,
dimension_row.column_name);
ELSE
dimension_def := format('%1$I', dimension_row.column_name);
dimtype := dimension_row.column_type;
END IF;

IF dimension_row.num_slices IS NOT NULL THEN

IF _timescaledb_internal.dimension_is_finite(dimension_slice_row.range_start) THEN
parts = parts || format(' %1$s >= %2$L ', dimension_def, dimension_slice_row.range_start);
END IF;

IF _timescaledb_internal.dimension_is_finite(dimension_slice_row.range_end) THEN
parts = parts || format(' %1$s < %2$L ', dimension_def, dimension_slice_row.range_end);
END IF;

IF array_length(parts, 1) = 0 THEN
RETURN NULL;
END IF;
return array_to_string(parts, 'AND');
ELSE
-- only works with time for now
IF _timescaledb_internal.time_literal_sql(dimension_slice_row.range_start, dimtype) =
_timescaledb_internal.time_literal_sql(dimension_slice_row.range_end, dimtype) THEN
RAISE 'time-based constraints have the same start and end values for column "%": %',
dimension_row.column_name,
_timescaledb_internal.time_literal_sql(dimension_slice_row.range_end, dimtype);
END IF;

parts = ARRAY[]::text[];

IF _timescaledb_internal.dimension_is_finite(dimension_slice_row.range_start) THEN
parts = parts || format(' %1$s >= %2$s ',
dimension_def,
_timescaledb_internal.time_literal_sql(dimension_slice_row.range_start, dimtype));
END IF;

IF _timescaledb_internal.dimension_is_finite(dimension_slice_row.range_end) THEN
parts = parts || format(' %1$s < %2$s ',
dimension_def,
_timescaledb_internal.time_literal_sql(dimension_slice_row.range_end, dimtype));
END IF;

return array_to_string(parts, 'AND');
END IF;
END
$BODY$ SET search_path TO pg_catalog, pg_temp;

87 changes: 0 additions & 87 deletions sql/util_internal_table_ddl.sql
Expand Up @@ -5,93 +5,6 @@
-- This file contains functions associated with creating new
-- hypertables.

CREATE OR REPLACE FUNCTION _timescaledb_internal.dimension_is_finite(
val BIGINT
)
RETURNS BOOLEAN LANGUAGE SQL IMMUTABLE PARALLEL SAFE AS
$BODY$
--end values of bigint reserved for infinite
SELECT val > (-9223372036854775808)::bigint AND val < 9223372036854775807::bigint
$BODY$ SET search_path TO pg_catalog, pg_temp;


CREATE OR REPLACE FUNCTION _timescaledb_internal.dimension_slice_get_constraint_sql(
dimension_slice_id INTEGER
)
RETURNS TEXT LANGUAGE PLPGSQL VOLATILE AS
$BODY$
DECLARE
dimension_slice_row _timescaledb_catalog.dimension_slice;
dimension_row _timescaledb_catalog.dimension;
dimension_def TEXT;
dimtype REGTYPE;
parts TEXT[];
BEGIN
SELECT * INTO STRICT dimension_slice_row
FROM _timescaledb_catalog.dimension_slice
WHERE id = dimension_slice_id;

SELECT * INTO STRICT dimension_row
FROM _timescaledb_catalog.dimension
WHERE id = dimension_slice_row.dimension_id;

IF dimension_row.partitioning_func_schema IS NOT NULL AND
dimension_row.partitioning_func IS NOT NULL THEN
SELECT prorettype INTO STRICT dimtype
FROM pg_catalog.pg_proc pro
WHERE pro.oid = format('%I.%I', dimension_row.partitioning_func_schema, dimension_row.partitioning_func)::regproc::oid;

dimension_def := format('%1$I.%2$I(%3$I)',
dimension_row.partitioning_func_schema,
dimension_row.partitioning_func,
dimension_row.column_name);
ELSE
dimension_def := format('%1$I', dimension_row.column_name);
dimtype := dimension_row.column_type;
END IF;

IF dimension_row.num_slices IS NOT NULL THEN

IF _timescaledb_internal.dimension_is_finite(dimension_slice_row.range_start) THEN
parts = parts || format(' %1$s >= %2$L ', dimension_def, dimension_slice_row.range_start);
END IF;

IF _timescaledb_internal.dimension_is_finite(dimension_slice_row.range_end) THEN
parts = parts || format(' %1$s < %2$L ', dimension_def, dimension_slice_row.range_end);
END IF;

IF array_length(parts, 1) = 0 THEN
RETURN NULL;
END IF;
return array_to_string(parts, 'AND');
ELSE
-- only works with time for now
IF _timescaledb_internal.time_literal_sql(dimension_slice_row.range_start, dimtype) =
_timescaledb_internal.time_literal_sql(dimension_slice_row.range_end, dimtype) THEN
RAISE 'time-based constraints have the same start and end values for column "%": %',
dimension_row.column_name,
_timescaledb_internal.time_literal_sql(dimension_slice_row.range_end, dimtype);
END IF;

parts = ARRAY[]::text[];

IF _timescaledb_internal.dimension_is_finite(dimension_slice_row.range_start) THEN
parts = parts || format(' %1$s >= %2$s ',
dimension_def,
_timescaledb_internal.time_literal_sql(dimension_slice_row.range_start, dimtype));
END IF;

IF _timescaledb_internal.dimension_is_finite(dimension_slice_row.range_end) THEN
parts = parts || format(' %1$s < %2$s ',
dimension_def,
_timescaledb_internal.time_literal_sql(dimension_slice_row.range_end, dimtype));
END IF;

return array_to_string(parts, 'AND');
END IF;
END
$BODY$ SET search_path TO pg_catalog, pg_temp;

-- Outputs the create_hypertable command to recreate the given hypertable.
--
-- This is currently used internally for our single hypertable backup tool
Expand Down

0 comments on commit 2105a06

Please sign in to comment.