diff --git a/Makefile b/Makefile index 10841d743..43ca841c4 100644 --- a/Makefile +++ b/Makefile @@ -13,14 +13,14 @@ include Makefile.global all: extension # build extension -extension: +extension: $(citus_abs_srcdir)/src/include/citus_version.h $(MAKE) -C src/backend/distributed/ all install-extension: extension $(MAKE) -C src/backend/distributed/ install install-headers: extension $(MKDIR_P) '$(DESTDIR)$(includedir_server)/distributed/' # generated headers are located in the build directory - $(INSTALL_DATA) src/include/citus_config.h '$(DESTDIR)$(includedir_server)/' + $(INSTALL_DATA) $(citus_abs_srcdir)/src/include/citus_version.h '$(DESTDIR)$(includedir_server)/' # the rest in the source tree $(INSTALL_DATA) $(citus_abs_srcdir)/src/include/distributed/*.h '$(DESTDIR)$(includedir_server)/distributed/' clean-extension: diff --git a/Makefile.global.in b/Makefile.global.in index 7cdbcd5d9..57678e137 100644 --- a/Makefile.global.in +++ b/Makefile.global.in @@ -44,8 +44,8 @@ $(citus_top_builddir)/Makefile.global: $(citus_abs_top_srcdir)/configure $(citus # Ensure configuration is generated by the most recent configure, # useful for longer existing build directories. -$(citus_top_builddir)/config.status: $(citus_abs_top_srcdir)/configure - cd @abs_top_builddir@ && ./config.status --recheck +$(citus_top_builddir)/config.status: $(citus_abs_top_srcdir)/configure $(citus_abs_top_srcdir)/src/backend/distributed/citus.control + cd @abs_top_builddir@ && ./config.status --recheck && ./config.status # Regenerate configure if configure.in changed $(citus_abs_top_srcdir)/configure: $(citus_abs_top_srcdir)/configure.in diff --git a/configure b/configure index c46b262b8..9cf33a4bd 100755 --- a/configure +++ b/configure @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.69 for Citus 5.0. +# Generated by GNU Autoconf 2.69 for Citus 6.2devel. # # # Copyright (C) 1992-1996, 1998-2012 Free Software Foundation, Inc. @@ -9,7 +9,7 @@ # This configure script is free software; the Free Software Foundation # gives unlimited permission to copy, distribute and modify it. # -# Copyright (c) 2012-2016, Citus Data, Inc. +# Copyright (c) 2012-2017, Citus Data, Inc. ## -------------------- ## ## M4sh Initialization. ## ## -------------------- ## @@ -579,8 +579,8 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='Citus' PACKAGE_TARNAME='citus' -PACKAGE_VERSION='5.0' -PACKAGE_STRING='Citus 5.0' +PACKAGE_VERSION='6.2devel' +PACKAGE_STRING='Citus 6.2devel' PACKAGE_BUGREPORT='' PACKAGE_URL='' @@ -600,6 +600,7 @@ vpath_build PATH PG_CONFIG FLEX +AWK SED target_alias host_alias @@ -1194,7 +1195,7 @@ if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures Citus 5.0 to adapt to many kinds of systems. +\`configure' configures Citus 6.2devel to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1255,7 +1256,7 @@ fi if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of Citus 5.0:";; + short | recursive ) echo "Configuration of Citus 6.2devel:";; esac cat <<\_ACEOF @@ -1343,14 +1344,14 @@ fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -Citus configure 5.0 +Citus configure 6.2devel generated by GNU Autoconf 2.69 Copyright (C) 2012 Free Software Foundation, Inc. This configure script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it. -Copyright (c) 2012-2016, Citus Data, Inc. +Copyright (c) 2012-2017, Citus Data, Inc. _ACEOF exit fi @@ -1400,7 +1401,7 @@ cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by Citus $as_me 5.0, which was +It was created by Citus $as_me 6.2devel, which was generated by GNU Autoconf 2.69. Invocation command line was $ $0 $@ @@ -1750,6 +1751,7 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu +# we'll need sed and awk for some of the version commands { $as_echo "$as_me:${as_lineno-$LINENO}: checking for a sed that does not truncate output" >&5 $as_echo_n "checking for a sed that does not truncate output... " >&6; } if ${ac_cv_path_SED+:} false; then : @@ -1819,6 +1821,82 @@ $as_echo "$ac_cv_path_SED" >&6; } SED="$ac_cv_path_SED" rm -f conftest.sed +for ac_prog in gawk mawk nawk awk +do + # Extract the first word of "$ac_prog", so it can be a program name with args. +set dummy $ac_prog; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_AWK+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$AWK"; then + ac_cv_prog_AWK="$AWK" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_AWK="$ac_prog" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +AWK=$ac_cv_prog_AWK +if test -n "$AWK"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $AWK" >&5 +$as_echo "$AWK" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + test -n "$AWK" && break +done + + +# CITUS_VERSION definition + +cat >>confdefs.h <<_ACEOF +#define CITUS_VERSION "$PACKAGE_VERSION" +_ACEOF + + +# CITUS_MAJORVERSION definition +CITUS_MAJORVERSION=`expr "$PACKAGE_VERSION" : '\([0-9][0-9]*\.[0-9][0-9]*\)'` + +cat >>confdefs.h <<_ACEOF +#define CITUS_MAJORVERSION "$CITUS_MAJORVERSION" +_ACEOF + + +# CITUS_VERSION_NUM definition +# awk -F is a regex on some platforms, and not on others, so make "." a tab +CITUS_VERSION_NUM="`echo "$PACKAGE_VERSION" | sed 's/[A-Za-z].*$//' | +tr '.' ' ' | +$AWK '{printf "%d%02d%02d", $1, $2, (NF >= 3) ? $3 : 0}'`" + +cat >>confdefs.h <<_ACEOF +#define CITUS_VERSION_NUM $CITUS_VERSION_NUM +_ACEOF + + +# CITUS_EXTENSIONVERSION definition +CITUS_EXTENSIONVERSION="`grep '^default_version' $srcdir/src/backend/distributed/citus.control | cut -d\' -f2`" + +cat >>confdefs.h <<_ACEOF +#define CITUS_EXTENSIONVERSION "$CITUS_EXTENSIONVERSION" +_ACEOF + # Re-check for flex. That allows to compile citus against a postgres # which was built without flex available (possible because generated @@ -2974,7 +3052,7 @@ POSTGRES_BUILDDIR="$POSTGRES_BUILDDIR" ac_config_files="$ac_config_files Makefile.global" -ac_config_headers="$ac_config_headers src/include/citus_config.h" +ac_config_headers="$ac_config_headers src/include/citus_config.h src/include/citus_version.h" cat >confcache <<\_ACEOF @@ -3483,7 +3561,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by Citus $as_me 5.0, which was +This file was extended by Citus $as_me 6.2devel, which was generated by GNU Autoconf 2.69. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -3545,7 +3623,7 @@ _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`" ac_cs_version="\\ -Citus config.status 5.0 +Citus config.status 6.2devel configured by $0, generated by GNU Autoconf 2.69, with options \\"\$ac_cs_config\\" @@ -3555,6 +3633,7 @@ gives unlimited permission to copy, distribute and modify it." ac_pwd='$ac_pwd' srcdir='$srcdir' +AWK='$AWK' test -n "\$AWK" || AWK=awk _ACEOF @@ -3668,6 +3747,7 @@ do case $ac_config_target in "Makefile.global") CONFIG_FILES="$CONFIG_FILES Makefile.global" ;; "src/include/citus_config.h") CONFIG_HEADERS="$CONFIG_HEADERS src/include/citus_config.h" ;; + "src/include/citus_version.h") CONFIG_HEADERS="$CONFIG_HEADERS src/include/citus_version.h" ;; *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;; esac diff --git a/configure.in b/configure.in index 25bcbe666..1bc1a6ee3 100644 --- a/configure.in +++ b/configure.in @@ -5,10 +5,30 @@ # everyone needing autoconf installed, the resulting files are checked # into the SCM. -AC_INIT([Citus], [5.0], [], [citus], []) -AC_COPYRIGHT([Copyright (c) 2012-2016, Citus Data, Inc.]) +AC_INIT([Citus], [6.2devel]) +AC_COPYRIGHT([Copyright (c) 2012-2017, Citus Data, Inc.]) +# we'll need sed and awk for some of the version commands AC_PROG_SED +AC_PROG_AWK + +# CITUS_VERSION definition +AC_DEFINE_UNQUOTED(CITUS_VERSION, "$PACKAGE_VERSION", [Citus version as a string]) + +# CITUS_MAJORVERSION definition +[CITUS_MAJORVERSION=`expr "$PACKAGE_VERSION" : '\([0-9][0-9]*\.[0-9][0-9]*\)'`] +AC_DEFINE_UNQUOTED(CITUS_MAJORVERSION, "$CITUS_MAJORVERSION", [Citus major version as a string]) + +# CITUS_VERSION_NUM definition +# awk -F is a regex on some platforms, and not on others, so make "." a tab +[CITUS_VERSION_NUM="`echo "$PACKAGE_VERSION" | sed 's/[A-Za-z].*$//' | +tr '.' ' ' | +$AWK '{printf "%d%02d%02d", $1, $2, (NF >= 3) ? $3 : 0}'`"] +AC_DEFINE_UNQUOTED(CITUS_VERSION_NUM, $CITUS_VERSION_NUM, [Citus version as a number]) + +# CITUS_EXTENSIONVERSION definition +[CITUS_EXTENSIONVERSION="`grep '^default_version' $srcdir/src/backend/distributed/citus.control | cut -d\' -f2`"] +AC_DEFINE_UNQUOTED([CITUS_EXTENSIONVERSION], "$CITUS_EXTENSIONVERSION", [Extension version expected by this Citus build]) # Re-check for flex. That allows to compile citus against a postgres # which was built without flex available (possible because generated @@ -122,7 +142,7 @@ AC_SUBST(POSTGRES_SRCDIR, "$POSTGRES_SRCDIR") AC_SUBST(POSTGRES_BUILDDIR, "$POSTGRES_BUILDDIR") AC_CONFIG_FILES([Makefile.global]) -AC_CONFIG_HEADERS([src/include/citus_config.h]) +AC_CONFIG_HEADERS([src/include/citus_config.h] [src/include/citus_version.h]) AH_TOP([ /* * citus_config.h.in is generated by autoconf/autoheader and diff --git a/src/backend/distributed/executor/multi_utility.c b/src/backend/distributed/executor/multi_utility.c index d70fd1930..fad9cad76 100644 --- a/src/backend/distributed/executor/multi_utility.c +++ b/src/backend/distributed/executor/multi_utility.c @@ -28,6 +28,7 @@ #include "catalog/namespace.h" #include "catalog/pg_attribute.h" #include "catalog/pg_class.h" +#include "citus_version.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/prepare.h" @@ -82,7 +83,6 @@ bool EnableDDLPropagation = true; /* ddl propagation is enabled */ - /* * This struct defines the state for the callback for drop statements. * It is copied as it is from commands/tablecmds.c in Postgres source. @@ -95,6 +95,10 @@ struct DropRelationCallbackState }; +/* Local functions forward declarations for deciding when to perform processing/checks */ +static bool SkipCitusProcessingForUtility(Node *parsetree); +static bool IsCitusExtensionStmt(Node *parsetree); + /* Local functions forward declarations for Transmit statement */ static bool IsTransmitStmt(Node *parsetree); static void VerifyTransmitStmt(CopyStmt *copyStatement); @@ -120,6 +124,7 @@ static char * DeparseVacuumColumnNames(List *columnNameList); /* Local functions forward declarations for unsupported command checks */ +static void ErrorIfUnstableCreateOrAlterExtensionStmt(Node *parsetree); static void ErrorIfUnsupportedIndexStmt(IndexStmt *createIndexStatement); static void ErrorIfUnsupportedDropIndexStmt(DropStmt *dropIndexStatement); static void ErrorIfUnsupportedAlterTableStmt(AlterTableStmt *alterTableStatement); @@ -130,6 +135,7 @@ static bool OptionsSpecifyOwnedBy(List *optionList, Oid *ownedByTableId); static void ErrorIfDistributedRenameStmt(RenameStmt *renameStatement); /* Local functions forward declarations for helper functions */ +static char * ExtractNewExtensionVersion(Node *parsetree); static void CreateLocalTable(RangeVar *relation, char *nodeName, int32 nodePort); static bool IsAlterTableRenameStmt(RenameStmt *renameStatement); static void ExecuteDistributedDDLJob(DDLJob *ddlJob); @@ -170,18 +176,20 @@ multi_ProcessUtility(Node *parsetree, Oid savedUserId = InvalidOid; int savedSecurityContext = 0; List *ddlJobs = NIL; + bool skipCitusProcessing = SkipCitusProcessingForUtility(parsetree); - if (IsA(parsetree, TransactionStmt)) + if (skipCitusProcessing) { - /* - * Transaction statements (e.g. ABORT, COMMIT) can be run in aborted - * transactions in which case a lot of checks cannot be done safely in - * that state. Since we never need to intercept transaction statements, - * skip our checks and immediately fall into standard_ProcessUtility. - */ + bool checkExtensionVersion = IsCitusExtensionStmt(parsetree); + standard_ProcessUtility(parsetree, queryString, context, params, dest, completionTag); + if (EnableVersionChecks && checkExtensionVersion) + { + ErrorIfUnstableCreateOrAlterExtensionStmt(parsetree); + } + return; } @@ -401,6 +409,86 @@ multi_ProcessUtility(Node *parsetree, } +/* + * SkipCitusProcessingForUtility simply returns whether a given utility should + * bypass Citus processing and checks and be handled exclusively by standard + * PostgreSQL utility processing. At present, CREATE/ALTER/DROP EXTENSION, + * ABORT, COMMIT, ROLLBACK, and SET (GUC) statements are exempt from Citus. + */ +static bool +SkipCitusProcessingForUtility(Node *parsetree) +{ + switch (parsetree->type) + { + /* + * In the CitusHasBeenLoaded check, we compare versions of loaded code, + * the installed extension, and available extension. If they differ, we + * force user to execute ALTER EXTENSION citus UPDATE. To allow this, + * CREATE/DROP/ALTER extension must be omitted from Citus processing. + */ + case T_DropStmt: + { + DropStmt *dropStatement = (DropStmt *) parsetree; + + if (dropStatement->removeType != OBJECT_EXTENSION) + { + return false; + } + } + + /* no break, fall through */ + + case T_CreateExtensionStmt: + case T_AlterExtensionStmt: + + /* + * Transaction statements (e.g. ABORT, COMMIT) can be run in aborted + * transactions in which case a lot of checks cannot be done safely in + * that state. Since we never need to intercept transaction statements, + * skip our checks and immediately fall into standard_ProcessUtility. + */ + case T_TransactionStmt: + + /* + * Skip processing of variable set statements, to allow changing the + * enable_version_checks GUC during testing. + */ + case T_VariableSetStmt: + { + return true; + } + + default: + { + return false; + } + } +} + + +/* + * IsCitusExtensionStmt returns whether a given utility is a CREATE or ALTER + * EXTENSION statement which references the citus extension. This function + * returns false for all other inputs. + */ +static bool +IsCitusExtensionStmt(Node *parsetree) +{ + char *extensionName = ""; + + if (IsA(parsetree, CreateExtensionStmt)) + { + extensionName = ((CreateExtensionStmt *) parsetree)->extname; + } + else if (IsA(parsetree, AlterExtensionStmt)) + { + extensionName = ((AlterExtensionStmt *) parsetree)->extname; + } + + return (strcmp(extensionName, "citus") == 0); +} + + /* Is the passed in statement a transmit statement? */ static bool IsTransmitStmt(Node *parsetree) @@ -1256,6 +1344,83 @@ DeparseVacuumColumnNames(List *columnNameList) } +/* + * ErrorIfUnstableCreateOrAlterExtensionStmt compares CITUS_EXTENSIONVERSION + * and version given CREATE/ALTER EXTENSION statement will create/update to. If + * they are not same in major or minor version numbers, this function errors + * out. It ignores the schema version. + */ +static void +ErrorIfUnstableCreateOrAlterExtensionStmt(Node *parsetree) +{ + char *newExtensionVersion = ExtractNewExtensionVersion(parsetree); + + if (newExtensionVersion != NULL) + { + /* explicit version provided in CREATE or ALTER EXTENSION UPDATE; verify */ + if (!MajorVersionsCompatible(newExtensionVersion, CITUS_EXTENSIONVERSION)) + { + ereport(ERROR, (errmsg("specified version incompatible with loaded " + "Citus library"), + errdetail("Loaded library requires %s, but %s was specified.", + CITUS_MAJORVERSION, newExtensionVersion), + errhint("If a newer library is present, restart the database " + "and try the command again."))); + } + } + else + { + /* + * No version was specified, so PostgreSQL will use the default_version + * from the citus.control file. In case a new default is available, we + * will force a compatibility check of the latest available version. + */ + availableExtensionVersion = NULL; + ErrorIfAvailableVersionMismatch(); + } +} + + +/* + * ExtractNewExtensionVersion returns the new extension version specified by + * a CREATE or ALTER EXTENSION statement. Other inputs are not permitted. This + * function returns NULL for statements with no explicit version specified. + */ +static char * +ExtractNewExtensionVersion(Node *parsetree) +{ + char *newVersion = NULL; + List *optionsList = NIL; + ListCell *optionsCell = NULL; + + if (IsA(parsetree, CreateExtensionStmt)) + { + optionsList = ((CreateExtensionStmt *) parsetree)->options; + } + else if (IsA(parsetree, AlterExtensionStmt)) + { + optionsList = ((AlterExtensionStmt *) parsetree)->options; + } + else + { + /* input must be one of the two above types */ + Assert(false); + } + + foreach(optionsCell, optionsList) + { + DefElem *defElement = (DefElem *) lfirst(optionsCell); + if (strncmp(defElement->defname, "new_version", NAMEDATALEN) == 0) + { + newVersion = strVal(defElement->arg); + break; + } + } + + return newVersion; +} + + /* * ErrorIfUnsupportedIndexStmt checks if the corresponding index statement is * supported for distributed tables and errors out if it is not. diff --git a/src/backend/distributed/shared_library_init.c b/src/backend/distributed/shared_library_init.c index 689296b08..c3dcf4e88 100644 --- a/src/backend/distributed/shared_library_init.c +++ b/src/backend/distributed/shared_library_init.c @@ -16,6 +16,7 @@ #include "fmgr.h" #include "miscadmin.h" +#include "citus_version.h" #include "commands/explain.h" #include "executor/executor.h" #include "distributed/citus_nodefuncs.h" @@ -48,6 +49,8 @@ /* marks shared object as one loadable by the postgres version compiled against */ PG_MODULE_MAGIC; +static char *CitusVersion = CITUS_VERSION; + void _PG_init(void); static void CreateRequiredDirectories(void); @@ -610,6 +613,26 @@ RegisterCitusConfigVariables(void) 0, NULL, NULL, NULL); + DefineCustomStringVariable( + "citus.version", + gettext_noop("Shows the Citus library version"), + NULL, + &CitusVersion, + CITUS_VERSION, + PGC_INTERNAL, + 0, + NULL, NULL, NULL); + + DefineCustomBoolVariable( + "citus.enable_version_checks", + gettext_noop("Enables version checks during CREATE/ALTER EXTENSION commands"), + NULL, + &EnableVersionChecks, + true, + PGC_USERSET, + GUC_NO_SHOW_ALL, + NULL, NULL, NULL); + /* warn about config items in the citus namespace that are not registered above */ EmitWarningsOnPlaceholders("citus"); } diff --git a/src/backend/distributed/utils/metadata_cache.c b/src/backend/distributed/utils/metadata_cache.c index 2939ade41..275441320 100644 --- a/src/backend/distributed/utils/metadata_cache.c +++ b/src/backend/distributed/utils/metadata_cache.c @@ -22,6 +22,7 @@ #include "catalog/pg_extension.h" #include "catalog/pg_namespace.h" #include "catalog/pg_type.h" +#include "citus_version.h" #include "commands/extension.h" #include "commands/trigger.h" #include "distributed/colocation_utils.h" @@ -35,6 +36,7 @@ #include "distributed/shardinterval_utils.h" #include "distributed/worker_manager.h" #include "distributed/worker_protocol.h" +#include "executor/executor.h" #include "parser/parse_func.h" #include "utils/builtins.h" #include "utils/catcache.h" @@ -98,6 +100,12 @@ static Oid distTransactionRelationId = InvalidOid; static Oid distTransactionGroupIndexId = InvalidOid; static Oid extraDataContainerFuncId = InvalidOid; +/* Citus extension version variables */ +bool EnableVersionChecks = true; /* version checks are enabled */ + +char *availableExtensionVersion = NULL; +static char *installedExtensionVersion = NULL; + /* Hash table for informations about each partition */ static HTAB *DistTableCacheHash = NULL; @@ -133,6 +141,9 @@ static bool HasUniformHashDistribution(ShardInterval **shardIntervalArray, int shardIntervalArrayLength); static bool HasUninitializedShardInterval(ShardInterval **sortedShardIntervalArray, int shardCount); +static void ErrorIfInstalledVersionMismatch(void); +static char * AvailableExtensionVersion(void); +static char * InstalledExtensionVersion(void); static void InitializeDistTableCache(void); static void InitializeWorkerNodeCache(void); static uint32 WorkerNodeHashCode(const void *key, Size keySize); @@ -927,7 +938,7 @@ bool CitusHasBeenLoaded(void) { /* recheck presence until citus has been loaded */ - if (!extensionLoaded) + if (!extensionLoaded || creating_extension) { bool extensionPresent = false; bool extensionScriptExecuted = true; @@ -964,13 +975,286 @@ CitusHasBeenLoaded(void) * present during early stages of upgrade operation. */ DistPartitionRelationId(); + + /* + * We also set installedExtensionVersion to NULL so that it will be re-read + * in case of extension update. + */ + installedExtensionVersion = NULL; } } + if (extensionLoaded) + { + ErrorIfAvailableVersionMismatch(); + ErrorIfInstalledVersionMismatch(); + } + return extensionLoaded; } +/* + * ErrorIfAvailableExtensionVersionMismatch compares CITUS_EXTENSIONVERSION and + * currently available version from citus.control file. If they are not same in + * major or minor version numbers, this function errors out. It ignores the schema + * version. + */ +void +ErrorIfAvailableVersionMismatch(void) +{ + char *availableVersion = NULL; + + if (!EnableVersionChecks) + { + return; + } + + availableVersion = AvailableExtensionVersion(); + if (!MajorVersionsCompatible(availableVersion, CITUS_EXTENSIONVERSION)) + { + ereport(ERROR, (errmsg("loaded Citus library version differs from latest " + "available extension version"), + errdetail("Loaded library requires %s, but the latest control " + "file specifies %s.", CITUS_MAJORVERSION, + availableVersion), + errhint("Restart the database to load the latest Citus " + "library."))); + } +} + + +/* + * ErrorIfInstalledVersionMismatch compares CITUS_EXTENSIONVERSION and currently + * and catalog version from pg_extemsion catalog table. If they are not same in + * major or minor version numbers, this function errors out. It ignores the schema + * version. + */ +static void +ErrorIfInstalledVersionMismatch(void) +{ + char *installedVersion = NULL; + + if (!EnableVersionChecks) + { + return; + } + + installedVersion = InstalledExtensionVersion(); + if (!MajorVersionsCompatible(installedVersion, CITUS_EXTENSIONVERSION)) + { + ereport(ERROR, (errmsg("loaded Citus library version differs from installed " + "extension version"), + errdetail("Loaded library requires %s, but the installed " + "extension version is %s.", CITUS_MAJORVERSION, + installedVersion), + errhint("Run ALTER EXTENSION citus UPDATE and try again."))); + } +} + + +/* + * MajorVersionsCompatible compares given two versions. If they are same in major + * and minor version numbers, this function returns true. It ignores the schema + * version. + */ +bool +MajorVersionsCompatible(char *leftVersion, char *rightVersion) +{ + const char schemaVersionSeparator = '-'; + + char *leftSeperatorPosition = strchr(leftVersion, schemaVersionSeparator); + char *rightSeperatorPosition = strchr(rightVersion, schemaVersionSeparator); + int leftComparisionLimit = 0; + int rightComparisionLimit = 0; + + if (leftSeperatorPosition != NULL) + { + leftComparisionLimit = leftSeperatorPosition - leftVersion; + } + else + { + leftComparisionLimit = strlen(leftVersion); + } + + if (rightSeperatorPosition != NULL) + { + rightComparisionLimit = rightSeperatorPosition - rightVersion; + } + else + { + rightComparisionLimit = strlen(leftVersion); + } + + /* we can error out early if hypens are not in the same position */ + if (leftComparisionLimit != rightComparisionLimit) + { + return false; + } + + return strncmp(leftVersion, rightVersion, leftComparisionLimit) == 0; +} + + +/* + * AvailableExtensionVersion returns the Citus version from citus.control file. It also + * saves the result, thus consecutive calls to CitusExtensionAvailableVersion will + * not read the citus.control file again. + */ +static char * +AvailableExtensionVersion(void) +{ + ReturnSetInfo *extensionsResultSet = NULL; + TupleTableSlot *tupleTableSlot = NULL; + FunctionCallInfoData *fcinfo = NULL; + FmgrInfo *flinfo = NULL; + int argumentCount = 0; + EState *estate = NULL; + + bool hasTuple = false; + bool goForward = true; + bool doCopy = false; + + /* if we cached the result before, return it*/ + if (availableExtensionVersion != NULL) + { + return availableExtensionVersion; + } + + estate = CreateExecutorState(); + extensionsResultSet = makeNode(ReturnSetInfo); + extensionsResultSet->econtext = GetPerTupleExprContext(estate); + extensionsResultSet->allowedModes = SFRM_Materialize; + + fcinfo = palloc0(sizeof(FunctionCallInfoData)); + flinfo = palloc0(sizeof(FmgrInfo)); + + fmgr_info(F_PG_AVAILABLE_EXTENSIONS, flinfo); + InitFunctionCallInfoData(*fcinfo, flinfo, argumentCount, InvalidOid, NULL, + (Node *) extensionsResultSet); + + /* pg_available_extensions returns result set containing all available extensions */ + (*pg_available_extensions)(fcinfo); + + tupleTableSlot = MakeSingleTupleTableSlot(extensionsResultSet->setDesc); + hasTuple = tuplestore_gettupleslot(extensionsResultSet->setResult, goForward, doCopy, + tupleTableSlot); + while (hasTuple) + { + Datum extensionNameDatum = 0; + char *extensionName = NULL; + bool isNull = false; + + extensionNameDatum = slot_getattr(tupleTableSlot, 1, &isNull); + extensionName = NameStr(*DatumGetName(extensionNameDatum)); + if (strcmp(extensionName, "citus") == 0) + { + MemoryContext oldMemoryContext = NULL; + Datum availableVersion = slot_getattr(tupleTableSlot, 2, &isNull); + + /* we will cache the result of citus version to prevent catalog access */ + if (CacheMemoryContext == NULL) + { + CreateCacheMemoryContext(); + } + oldMemoryContext = MemoryContextSwitchTo(CacheMemoryContext); + + availableExtensionVersion = text_to_cstring(DatumGetTextPP(availableVersion)); + + MemoryContextSwitchTo(oldMemoryContext); + + ExecClearTuple(tupleTableSlot); + ExecDropSingleTupleTableSlot(tupleTableSlot); + + return availableExtensionVersion; + } + + ExecClearTuple(tupleTableSlot); + hasTuple = tuplestore_gettupleslot(extensionsResultSet->setResult, goForward, + doCopy, tupleTableSlot); + } + + ExecDropSingleTupleTableSlot(tupleTableSlot); + + ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("citus extension is not found"))); + + return NULL; +} + + +/* + * InstalledExtensionVersion returns the Citus version in PostgreSQL pg_extension table. + * It also saves the result, thus consecutive calls to CitusExtensionCatalogVersion + * will not read the catalog tables again. + */ +static char * +InstalledExtensionVersion(void) +{ + Relation relation = NULL; + SysScanDesc scandesc; + ScanKeyData entry[1]; + HeapTuple extensionTuple = NULL; + + /* if we cached the result before, return it*/ + if (installedExtensionVersion != NULL) + { + return installedExtensionVersion; + } + + relation = heap_open(ExtensionRelationId, AccessShareLock); + + ScanKeyInit(&entry[0], Anum_pg_extension_extname, BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum("citus")); + + scandesc = systable_beginscan(relation, ExtensionNameIndexId, true, + NULL, 1, entry); + + extensionTuple = systable_getnext(scandesc); + + /* We assume that there can be at most one matching tuple */ + if (HeapTupleIsValid(extensionTuple)) + { + MemoryContext oldMemoryContext = NULL; + int extensionIndex = Anum_pg_extension_extversion; + TupleDesc tupleDescriptor = RelationGetDescr(relation); + bool isNull = false; + + Datum installedVersion = heap_getattr(extensionTuple, extensionIndex, + tupleDescriptor, &isNull); + + if (isNull) + { + ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("citus extension version is null"))); + } + + /* we will cache the result of citus version to prevent catalog access */ + if (CacheMemoryContext == NULL) + { + CreateCacheMemoryContext(); + } + + oldMemoryContext = MemoryContextSwitchTo(CacheMemoryContext); + + installedExtensionVersion = text_to_cstring(DatumGetTextPP(installedVersion)); + + MemoryContextSwitchTo(oldMemoryContext); + } + else + { + ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("citus extension is not loaded"))); + } + + systable_endscan(scandesc); + + heap_close(relation, AccessShareLock); + + return installedExtensionVersion; +} + + /* return oid of pg_dist_shard relation */ Oid DistShardRelationId(void) diff --git a/src/include/.gitignore b/src/include/.gitignore index 5207873bf..e97125f70 100644 --- a/src/include/.gitignore +++ b/src/include/.gitignore @@ -2,3 +2,5 @@ /stamp-ext-h /citus_config.h /citus_config.h.in~ +/citus_version.h +/citus_version.h.in~ diff --git a/src/include/citus_config.h.in b/src/include/citus_config.h.in index 33da446e7..364d22d47 100644 --- a/src/include/citus_config.h.in +++ b/src/include/citus_config.h.in @@ -10,6 +10,18 @@ */ +/* Extension version expected by this Citus build */ +#undef CITUS_EXTENSIONVERSION + +/* Citus major version as a string */ +#undef CITUS_MAJORVERSION + +/* Citus version as a string */ +#undef CITUS_VERSION + +/* Citus version as a number */ +#undef CITUS_VERSION_NUM + /* Define to the address where bug reports for this package should be sent. */ #undef PACKAGE_BUGREPORT diff --git a/src/include/citus_version.h.in b/src/include/citus_version.h.in new file mode 100644 index 000000000..2367b86fa --- /dev/null +++ b/src/include/citus_version.h.in @@ -0,0 +1,13 @@ +/* This file is created manually */ + +/* Extension version expected by this Citus build */ +#undef CITUS_EXTENSIONVERSION + +/* Citus major version as a string */ +#undef CITUS_MAJORVERSION + +/* Citus version as a string */ +#undef CITUS_VERSION + +/* Citus version as a number */ +#undef CITUS_VERSION_NUM diff --git a/src/include/distributed/metadata_cache.h b/src/include/distributed/metadata_cache.h index abd1861c0..5b87a59a4 100644 --- a/src/include/distributed/metadata_cache.h +++ b/src/include/distributed/metadata_cache.h @@ -17,6 +17,8 @@ #include "distributed/worker_manager.h" #include "utils/hsearch.h" +extern bool EnableVersionChecks; +extern char *availableExtensionVersion; /* * Representation of a table's metadata that is frequently used for @@ -68,6 +70,8 @@ extern void CitusInvalidateRelcacheByRelid(Oid relationId); extern void CitusInvalidateRelcacheByShardId(int64 shardId); extern bool CitusHasBeenLoaded(void); +void ErrorIfAvailableVersionMismatch(void); +bool MajorVersionsCompatible(char *leftVersion, char *rightVersion); /* access WorkerNodeHash */ extern HTAB * GetWorkerNodeHash(void); diff --git a/src/include/distributed/multi_utility.h b/src/include/distributed/multi_utility.h index 7444c10a1..84ed37443 100644 --- a/src/include/distributed/multi_utility.h +++ b/src/include/distributed/multi_utility.h @@ -13,6 +13,7 @@ #include "tcop/utility.h" extern bool EnableDDLPropagation; +extern bool EnableVersionChecks; /* * A DDLJob encapsulates the remote tasks and commands needed to process all or diff --git a/src/test/regress/expected/multi_extension.out b/src/test/regress/expected/multi_extension.out index 8655907f8..3daa82f42 100644 --- a/src/test/regress/expected/multi_extension.out +++ b/src/test/regress/expected/multi_extension.out @@ -24,7 +24,8 @@ WHERE pgd.refclassid = 'pg_extension'::regclass AND -- DROP EXTENSION pre-created by the regression suite DROP EXTENSION citus; \c --- Create extension in oldest version, test every upgrade step +SET citus.enable_version_checks TO 'false'; +-- Create extension in oldest version CREATE EXTENSION citus VERSION '5.0'; ALTER EXTENSION citus UPDATE TO '5.0-1'; ALTER EXTENSION citus UPDATE TO '5.0-2'; @@ -77,6 +78,13 @@ ALTER EXTENSION citus UPDATE TO '6.1-16'; ALTER EXTENSION citus UPDATE TO '6.1-17'; ALTER EXTENSION citus UPDATE TO '6.2-1'; ALTER EXTENSION citus UPDATE TO '6.2-2'; +-- show running version +SHOW citus.version; + citus.version +--------------- + 6.2devel +(1 row) + -- ensure no objects were created outside pg_catalog SELECT COUNT(*) FROM pg_depend AS pgd, @@ -91,7 +99,13 @@ WHERE pgd.refclassid = 'pg_extension'::regclass AND 0 (1 row) --- drop extension an re-create in newest version +-- see incompatible version errors out +RESET citus.enable_version_checks; DROP EXTENSION citus; +CREATE EXTENSION citus VERSION '5.0'; +ERROR: specified version incompatible with loaded Citus library +DETAIL: Loaded library requires 6.2, but 5.0 was specified. +HINT: If a newer library is present, restart the database and try the command again. +-- re-create in newest version \c CREATE EXTENSION citus; diff --git a/src/test/regress/sql/multi_extension.sql b/src/test/regress/sql/multi_extension.sql index d1a1ff559..0be9c5096 100644 --- a/src/test/regress/sql/multi_extension.sql +++ b/src/test/regress/sql/multi_extension.sql @@ -24,7 +24,9 @@ WHERE pgd.refclassid = 'pg_extension'::regclass AND DROP EXTENSION citus; \c --- Create extension in oldest version, test every upgrade step +SET citus.enable_version_checks TO 'false'; + +-- Create extension in oldest version CREATE EXTENSION citus VERSION '5.0'; ALTER EXTENSION citus UPDATE TO '5.0-1'; ALTER EXTENSION citus UPDATE TO '5.0-2'; @@ -78,6 +80,9 @@ ALTER EXTENSION citus UPDATE TO '6.1-17'; ALTER EXTENSION citus UPDATE TO '6.2-1'; ALTER EXTENSION citus UPDATE TO '6.2-2'; +-- show running version +SHOW citus.version; + -- ensure no objects were created outside pg_catalog SELECT COUNT(*) FROM pg_depend AS pgd, @@ -88,7 +93,11 @@ WHERE pgd.refclassid = 'pg_extension'::regclass AND pge.extname = 'citus' AND pgio.schema NOT IN ('pg_catalog', 'citus'); --- drop extension an re-create in newest version +-- see incompatible version errors out +RESET citus.enable_version_checks; DROP EXTENSION citus; +CREATE EXTENSION citus VERSION '5.0'; + +-- re-create in newest version \c CREATE EXTENSION citus;