Skip to content

Commit

Permalink
Keep track of catalog version
Browse files Browse the repository at this point in the history
This patch stores the current catalog version in an internal
table to allow us to verify catalog and code version match.
When doing dump/restore people occasionally report very unusual
errors and during investigation it is discovered that they loaded
a dump from an older version and run it with a later code version.
This allows to detect mismatches between installed code version
and loaded dump version. The version number in the metadata table
will be kept updated in upgrade and downgrade scripts.
  • Loading branch information
svenklemm committed Oct 14, 2023
1 parent 332bbdb commit a664e68
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 3 deletions.
1 change: 1 addition & 0 deletions .unreleased/pr_6185
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implements: #6185 Keep track of catalog version
21 changes: 21 additions & 0 deletions sql/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,23 @@ function(version_files SRC_FILE_LIST OUTPUT_FILE_LIST)
PARENT_SCOPE)
endfunction()

# Function to replace @VARIABLE@ in source files, producing an output file in
# the build dir. Unlike the previous function we need to build a versioned file
# for every version we create an update script for.
function(version_check_file SRC_FILE_LIST OUTPUT_FILE_LIST)
set(result "")
foreach(unversioned_file ${SRC_FILE_LIST})
set(versioned_file ${unversioned_file}-${START_VERSION})
list(APPEND result ${CMAKE_CURRENT_BINARY_DIR}/${versioned_file})
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${unversioned_file})
configure_file(${unversioned_file} ${versioned_file} @ONLY)
endif()
endforeach(unversioned_file)
set(${OUTPUT_FILE_LIST}
"${result}"
PARENT_SCOPE)
endfunction()

# Create versioned files (replacing MODULE_PATHNAME) in the build directory of
# all our source files
version_files("${PRE_UPDATE_FILES}" PRE_UPDATE_FILES_VERSIONED)
Expand Down Expand Up @@ -250,6 +267,9 @@ foreach(transition_mod_file ${MOD_FILES_LIST})
list(APPEND PRE_FILES ${ORIGIN_MOD_FILE})
endif()

version_check_file("updates/version_check.sql" VERSION_CHECK_VERSIONED)
list(PREPEND PRE_FILES ${VERSION_CHECK_VERSIONED})

# There might not have been any changes in the modfile, in which case the
# modfile need not be present
if(EXISTS ${transition_mod_file})
Expand All @@ -271,6 +291,7 @@ foreach(transition_mod_file ${MOD_FILES_LIST})
"${PRE_FILES};${PRE_INSTALL_FUNCTION_FILES_VERSIONED};${SOURCE_FILES_VERSIONED};${POST_FILES_PROCESSED}"
${UPDATE_SCRIPT})
endif()

endforeach(transition_mod_file)

add_custom_target(sqlupdatescripts ALL DEPENDS ${UPDATE_SCRIPTS})
Expand Down
6 changes: 6 additions & 0 deletions sql/metadata.sql
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ INSERT INTO _timescaledb_catalog.metadata
SELECT 'uuid', _timescaledb_functions.generate_uuid(), TRUE ON CONFLICT DO NOTHING;
INSERT INTO _timescaledb_catalog.metadata
SELECT 'install_timestamp', now(), TRUE ON CONFLICT DO NOTHING;

-- Install catalog version on database installation and upgrade.
-- This allows us to detect catalog mismatches in dump/restore cycle.
INSERT INTO _timescaledb_catalog.metadata (key, value, include_in_telemetry)
SELECT 'timescaledb_version', '@PROJECT_VERSION_MOD@', FALSE ON CONFLICT (key) DO UPDATE SET value = excluded.value;

10 changes: 10 additions & 0 deletions sql/restoring.sql
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ CREATE OR REPLACE FUNCTION @[email protected]_post_restore() RETURNS BOOL A
$BODY$
DECLARE
db text;
catalog_version text;
BEGIN
SELECT m.value INTO catalog_version FROM pg_extension x
JOIN _timescaledb_catalog.metadata m ON m.key='timescaledb_version'
WHERE x.extname='timescaledb' AND x.extversion <> m.value;

-- check that a loaded dump is compatible with the currently running code
IF FOUND THEN
RAISE EXCEPTION 'catalog version mismatch, expected "%" seen "%"', '@PROJECT_VERSION_MOD@', catalog_version;
END IF;

SELECT current_database() INTO db;
EXECUTE format($$ALTER DATABASE %I RESET timescaledb.restoring $$, db);
-- we cannot use reset here because the reset_val might not be off
Expand Down
14 changes: 14 additions & 0 deletions sql/updates/version_check.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- 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.

DO $$
DECLARE
catalog_version TEXT;
BEGIN
SELECT value INTO catalog_version FROM _timescaledb_catalog.metadata WHERE key='timescaledb_version' AND value <> '@START_VERSION@';
IF FOUND THEN
RAISE EXCEPTION 'catalog version mismatch, expected "%" seen "%"', '@START_VERSION@', catalog_version;
END IF;
END$$;

30 changes: 28 additions & 2 deletions test/expected/metadata.out
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ CREATE OR REPLACE FUNCTION _timescaledb_internal.test_install_timestamp() RETURN
SELECT COUNT(*) from _timescaledb_catalog.metadata;
count
-------
2
3
(1 row)

SELECT _timescaledb_internal.test_uuid() as uuid_1 \gset
Expand Down Expand Up @@ -90,7 +90,7 @@ ALTER DATABASE :TEST_DBNAME SET timescaledb.restoring='off';
SELECT COUNT(*) FROM _timescaledb_catalog.metadata;
count
-------
3
4
(1 row)

-- Verify that this is the old exported_uuid
Expand All @@ -113,3 +113,29 @@ SELECT _timescaledb_internal.test_install_timestamp() = :'timestamp_1' as export
f
(1 row)

-- check metadata version matches expected value
SELECT x.extversion = m.value AS "version match"
FROM pg_extension x
JOIN _timescaledb_catalog.metadata m ON m.key='timescaledb_version'
WHERE x.extname='timescaledb';
version match
---------------
t
(1 row)

-- test version check in post_restore
\c :TEST_DBNAME :ROLE_SUPERUSER
UPDATE _timescaledb_catalog.metadata SET value = '1.2.3' WHERE key = 'timescaledb_version';
\set ON_ERROR_STOP 0
-- set verbosity to sqlstate to suppress version dependant error message
\set VERBOSITY sqlstate
SELECT timescaledb_post_restore();
ERROR: P0001
\set ON_ERROR_STOP 1
UPDATE _timescaledb_catalog.metadata m SET value = x.extversion FROM pg_extension x WHERE m.key = 'timescaledb_version' AND x.extname='timescaledb';
SELECT timescaledb_post_restore();
timescaledb_post_restore
--------------------------
t
(1 row)

20 changes: 20 additions & 0 deletions test/sql/metadata.sql
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,23 @@ SELECT _timescaledb_internal.test_exported_uuid() = :'uuid_ex_1' as exported_uui
-- Verify that the uuid and timestamp are new
SELECT _timescaledb_internal.test_uuid() = :'uuid_1' as exported_uuids_diff;
SELECT _timescaledb_internal.test_install_timestamp() = :'timestamp_1' as exported_uuids_diff;

-- check metadata version matches expected value
SELECT x.extversion = m.value AS "version match"
FROM pg_extension x
JOIN _timescaledb_catalog.metadata m ON m.key='timescaledb_version'
WHERE x.extname='timescaledb';

-- test version check in post_restore
\c :TEST_DBNAME :ROLE_SUPERUSER
UPDATE _timescaledb_catalog.metadata SET value = '1.2.3' WHERE key = 'timescaledb_version';
\set ON_ERROR_STOP 0
-- set verbosity to sqlstate to suppress version dependant error message
\set VERBOSITY sqlstate
SELECT timescaledb_post_restore();
\set ON_ERROR_STOP 1

UPDATE _timescaledb_catalog.metadata m SET value = x.extversion FROM pg_extension x WHERE m.key = 'timescaledb_version' AND x.extname='timescaledb';
SELECT timescaledb_post_restore();


1 change: 0 additions & 1 deletion tsl/test/shared/sql/build_info.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@ SELECT pg_typeof(commit_tag) AS commit_tag_type,
length(commit_hash) AS commit_hash_length,
pg_typeof(commit_time) AS commit_time_type
FROM _timescaledb_functions.get_git_commit();

0 comments on commit a664e68

Please sign in to comment.