diff --git a/tsl/src/dist_util.c b/tsl/src/dist_util.c index 55c5b7abe..37b0f4549 100644 --- a/tsl/src/dist_util.c +++ b/tsl/src/dist_util.c @@ -296,27 +296,54 @@ validate_data_node_settings(void) MaxConnections))); } -int -dist_util_version_compare(const char *lhs, const char *rhs) +/* + * Check that the data node version is compatible with the version on this + * node by checking that all of the following are true: + * + * - The major version is identical on the data node and the access node. + * - The minor version on the data node is before or the same as on the access + * node. + * + * We explicitly do *not* check the patch version since changes between patch + * versions will only fix bugs and there should be no problem using an older + * patch version of the extension on the data node. + * + * We also check if the version on the data node is older and set + * `old_version` to `true` or `false` so that caller can print a warning. + */ +bool +dist_util_is_compatible_version(const char *data_node_version, const char *access_node_version, + bool *is_old_version) { - unsigned int lhs_major, lhs_minor, lhs_patch; - unsigned int rhs_major, rhs_minor, rhs_patch; + unsigned int data_node_major, data_node_minor, data_node_patch; + unsigned int access_node_major, access_node_minor, access_node_patch; - if (sscanf(lhs, "%u.%u.%u", &lhs_major, &lhs_minor, &lhs_patch) != 3) - ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("invalid version %s", lhs))); + Assert(is_old_version); - if (sscanf(rhs, "%u.%u.%u", &rhs_major, &rhs_minor, &rhs_patch) != 3) - ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("invalid version %s", rhs))); + if (sscanf(data_node_version, + "%u.%u.%u", + &data_node_major, + &data_node_minor, + &data_node_patch) != 3) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("invalid data node version %s", data_node_version))); + if (sscanf(access_node_version, + "%u.%u.%u", + &access_node_major, + &access_node_minor, + &access_node_patch) != 3) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("invalid access node version %s", access_node_version))); - if (lhs_major == rhs_major) - { - if (lhs_minor == rhs_minor) - { - if (lhs_patch == rhs_patch) - return 0; - return (lhs_patch < rhs_patch) ? -1 : 1; - } - return (lhs_minor < rhs_minor) ? -1 : 1; - } - return (lhs_major < rhs_major) ? -1 : 1; + if (data_node_major == access_node_major) + if (data_node_minor == access_node_minor) + *is_old_version = (data_node_patch < access_node_patch); + else + *is_old_version = (data_node_minor < access_node_minor); + else + *is_old_version = (data_node_major < access_node_major); + + return (data_node_major == access_node_major) && (data_node_minor <= access_node_minor); } diff --git a/tsl/src/dist_util.h b/tsl/src/dist_util.h index 3b0c8ec6c..ac2ce3e6f 100644 --- a/tsl/src/dist_util.h +++ b/tsl/src/dist_util.h @@ -32,6 +32,7 @@ bool dist_util_is_frontend_session(void); Datum dist_util_remote_hypertable_info(PG_FUNCTION_ARGS); void validate_data_node_settings(void); -int dist_util_version_compare(const char *lhs, const char *rhs); +bool dist_util_is_compatible_version(const char *data_node_version, const char *access_node_version, + bool *is_old_version); #endif /* TIMESCALEDB_TSL_CHUNK_API_H */ diff --git a/tsl/src/remote/connection.c b/tsl/src/remote/connection.c index 90861b090..e248be0a0 100644 --- a/tsl/src/remote/connection.c +++ b/tsl/src/remote/connection.c @@ -851,8 +851,8 @@ bool remote_connection_check_extension(TSConnection *conn, const char **owner_name, Oid *owner_oid) { PGresult *res; - int rc; const char *data_node_version; + bool old_version; res = remote_connection_execf(conn, "SELECT usename, extowner, extversion FROM pg_extension JOIN " @@ -878,13 +878,20 @@ remote_connection_check_extension(TSConnection *conn, const char **owner_name, O break; } - /* compare extension version */ + /* check extension version on data node and make sure that it is + * compatible */ data_node_version = PQgetvalue(res, 0, 2); - rc = dist_util_version_compare(data_node_version, TIMESCALEDB_VERSION_MOD); - if (rc < 0) - ereport(WARNING, + if (!dist_util_is_compatible_version(data_node_version, TIMESCALEDB_VERSION, &old_version)) + ereport(ERROR, (errcode(ERRCODE_TS_DATA_NODE_INVALID_CONFIG), - errmsg("data node \"%s\" has an outdated timescaledb extension version", + errmsg("data node \"%s\" has an incompatible timescaledb extension version", + NameStr(conn->node_name)), + errdetail_internal("Access node version: %s, data node version: %s.", + TIMESCALEDB_VERSION_MOD, + data_node_version))); + if (old_version) + ereport(WARNING, + (errmsg("data node \"%s\" has an outdated timescaledb extension version", NameStr(conn->node_name)), errdetail_internal("Access node version: %s, data node version: %s.", TIMESCALEDB_VERSION_MOD, diff --git a/tsl/test/expected/data_node.out b/tsl/test/expected/data_node.out index 1a939f457..39f526d92 100644 --- a/tsl/test/expected/data_node.out +++ b/tsl/test/expected/data_node.out @@ -1252,24 +1252,10 @@ END $BODY$; CREATE EXTENSION timescaledb VERSION '0.0.0'; \c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER; +\set ON_ERROR_STOP 0 SELECT * FROM add_data_node('data_node_1', 'localhost', database => 'data_node_1', bootstrap => false); -WARNING: data node "data_node_1" has an outdated timescaledb extension version -WARNING: data node "data_node_1" has an outdated timescaledb extension version - node_name | host | port | database | node_created | database_created | extension_created --------------+-----------+-------+-------------+--------------+------------------+------------------- - data_node_1 | localhost | 15432 | data_node_1 | t | f | f -(1 row) - -GRANT USAGE ON FOREIGN SERVER data_node_1 TO :ROLE_1; -SET ROLE :ROLE_1; -CREATE TABLE test_disttable(time timestamptz); -\set ON_ERROR_STOP 0 -SELECT * FROM create_distributed_hypertable('test_disttable', 'time'); -WARNING: only one data node was assigned to the hypertable -NOTICE: adding not-null constraint to column "time" -WARNING: data node "data_node_1" has an outdated timescaledb extension version -ERROR: [data_node_1]: function public.create_hypertable(unknown, time_column_name => unknown, associated_schema_name => unknown, associated_table_prefix => unknown, chunk_time_interval => bigint, chunk_sizing_func => unknown, chunk_target_size => unknown, if_not_exists => boolean, migrate_data => boolean, create_default_indexes => boolean, replication_factor => integer) does not exist +ERROR: data node "data_node_1" has an incompatible timescaledb extension version \set ON_ERROR_STOP 1 RESET ROLE; DROP DATABASE data_node_1; diff --git a/tsl/test/expected/dist_util.out b/tsl/test/expected/dist_util.out index 8852a8162..a2b7005c1 100644 --- a/tsl/test/expected/dist_util.out +++ b/tsl/test/expected/dist_util.out @@ -11,6 +11,67 @@ DROP DATABASE IF EXISTS frontend_1; DROP DATABASE IF EXISTS frontend_2; SET client_min_messages TO NOTICE; ---------------------------------------------------------------- +-- Test version compability function +CREATE OR REPLACE FUNCTION compatible_version(version CSTRING, reference CSTRING) +RETURNS TABLE(is_compatible BOOLEAN, is_old_version BOOLEAN) +AS :TSL_MODULE_PATHNAME, 'ts_test_compatible_version' +LANGUAGE C VOLATILE; +SELECT * FROM compatible_version('2.0.0-beta3.19', reference => '2.0.0-beta3.19'); + is_compatible | is_old_version +---------------+---------------- + t | f +(1 row) + +SELECT * FROM compatible_version('2.0.0', reference => '2.0.0'); + is_compatible | is_old_version +---------------+---------------- + t | f +(1 row) + +SELECT * FROM compatible_version('1.9.9', reference => '2.0.0-beta3.19'); + is_compatible | is_old_version +---------------+---------------- + f | t +(1 row) + +SELECT * FROM compatible_version('1.9.9', reference => '2.0.0'); + is_compatible | is_old_version +---------------+---------------- + f | t +(1 row) + +SELECT * FROM compatible_version('2.0.9', reference => '2.0.0-beta3.19'); + is_compatible | is_old_version +---------------+---------------- + t | f +(1 row) + +SELECT * FROM compatible_version('2.0.9', reference => '2.0.0'); + is_compatible | is_old_version +---------------+---------------- + t | f +(1 row) + +SELECT * FROM compatible_version('2.1.9', reference => '2.0.0-beta3.19'); + is_compatible | is_old_version +---------------+---------------- + f | f +(1 row) + +SELECT * FROM compatible_version('2.1.0', reference => '2.1.19-beta3.19'); + is_compatible | is_old_version +---------------+---------------- + t | t +(1 row) + +-- These should not parse and instead generate an error. +\set ON_ERROR_STOP 0 +SELECT * FROM compatible_version('2.1.*', reference => '2.1.19-beta3.19'); +ERROR: invalid data node version 2.1.* +SELECT * FROM compatible_version('2.1.0', reference => '2.1.*'); +ERROR: invalid access node version 2.1.* +\set ON_ERROR_STOP 1 +---------------------------------------------------------------- -- Create two distributed databases CREATE DATABASE frontend_1; CREATE DATABASE frontend_2; diff --git a/tsl/test/sql/data_node.sql b/tsl/test/sql/data_node.sql index 3a3d8a2e5..00efffcbf 100644 --- a/tsl/test/sql/data_node.sql +++ b/tsl/test/sql/data_node.sql @@ -620,16 +620,9 @@ CREATE EXTENSION timescaledb VERSION '0.0.0'; \c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER; +\set ON_ERROR_STOP 0 SELECT * FROM add_data_node('data_node_1', 'localhost', database => 'data_node_1', bootstrap => false); - -GRANT USAGE ON FOREIGN SERVER data_node_1 TO :ROLE_1; - -SET ROLE :ROLE_1; -CREATE TABLE test_disttable(time timestamptz); - -\set ON_ERROR_STOP 0 -SELECT * FROM create_distributed_hypertable('test_disttable', 'time'); \set ON_ERROR_STOP 1 RESET ROLE; diff --git a/tsl/test/sql/dist_util.sql b/tsl/test/sql/dist_util.sql index 4f96a72b6..e853a7442 100644 --- a/tsl/test/sql/dist_util.sql +++ b/tsl/test/sql/dist_util.sql @@ -12,6 +12,29 @@ DROP DATABASE IF EXISTS frontend_1; DROP DATABASE IF EXISTS frontend_2; SET client_min_messages TO NOTICE; +---------------------------------------------------------------- +-- Test version compability function + +CREATE OR REPLACE FUNCTION compatible_version(version CSTRING, reference CSTRING) +RETURNS TABLE(is_compatible BOOLEAN, is_old_version BOOLEAN) +AS :TSL_MODULE_PATHNAME, 'ts_test_compatible_version' +LANGUAGE C VOLATILE; + +SELECT * FROM compatible_version('2.0.0-beta3.19', reference => '2.0.0-beta3.19'); +SELECT * FROM compatible_version('2.0.0', reference => '2.0.0'); +SELECT * FROM compatible_version('1.9.9', reference => '2.0.0-beta3.19'); +SELECT * FROM compatible_version('1.9.9', reference => '2.0.0'); +SELECT * FROM compatible_version('2.0.9', reference => '2.0.0-beta3.19'); +SELECT * FROM compatible_version('2.0.9', reference => '2.0.0'); +SELECT * FROM compatible_version('2.1.9', reference => '2.0.0-beta3.19'); +SELECT * FROM compatible_version('2.1.0', reference => '2.1.19-beta3.19'); + +-- These should not parse and instead generate an error. +\set ON_ERROR_STOP 0 +SELECT * FROM compatible_version('2.1.*', reference => '2.1.19-beta3.19'); +SELECT * FROM compatible_version('2.1.0', reference => '2.1.*'); +\set ON_ERROR_STOP 1 + ---------------------------------------------------------------- -- Create two distributed databases diff --git a/tsl/test/src/CMakeLists.txt b/tsl/test/src/CMakeLists.txt index 6ca28f4ba..174b752a8 100644 --- a/tsl/test/src/CMakeLists.txt +++ b/tsl/test/src/CMakeLists.txt @@ -8,6 +8,7 @@ set(SOURCES if (PG_VERSION_SUPPORTS_MULTINODE) list(APPEND SOURCES + test_dist_util.c data_node.c deparse.c) endif () diff --git a/tsl/test/src/test_dist_util.c b/tsl/test/src/test_dist_util.c new file mode 100644 index 000000000..ba4e98fc4 --- /dev/null +++ b/tsl/test/src/test_dist_util.c @@ -0,0 +1,50 @@ +/* + * 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 "dist_util.h" + +TS_FUNCTION_INFO_V1(ts_test_compatible_version); + +Datum +ts_test_compatible_version(PG_FUNCTION_ARGS) +{ + const char *checked_version = PG_GETARG_CSTRING(0); + const char *reference_version = PG_GETARG_CSTRING(1); + + TupleDesc tupdesc; + HeapTuple tuple; + Datum values[2]; + bool nulls[2] = { false }; + bool is_old_version; + bool is_compatible; + + if (PG_ARGISNULL(1)) + reference_version = TIMESCALEDB_VERSION; + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("function returning record called in context " + "that cannot accept type record"))); + } + + is_compatible = + dist_util_is_compatible_version(checked_version, reference_version, &is_old_version); + + values[0] = BoolGetDatum(is_compatible); + values[1] = BoolGetDatum(is_old_version); + + tuple = heap_form_tuple(tupdesc, values, nulls); + + PG_RETURN_DATUM(HeapTupleGetDatum(tuple)); +}