diff --git a/src/backend/distributed/commands/database.c b/src/backend/distributed/commands/database.c index 944ff627d..ade604d17 100644 --- a/src/backend/distributed/commands/database.c +++ b/src/backend/distributed/commands/database.c @@ -10,34 +10,76 @@ */ #include "postgres.h" +#include "miscadmin.h" +#include "access/heapam.h" #include "access/htup_details.h" #include "access/xact.h" #include "catalog/objectaddress.h" +#include "catalog/pg_collation.h" #include "catalog/pg_database.h" +#include "catalog/pg_database_d.h" +#include "catalog/pg_tablespace.h" #include "commands/dbcommands.h" -#include "miscadmin.h" +#include "commands/defrem.h" #include "nodes/parsenodes.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/rel.h" +#include "utils/relcache.h" #include "utils/syscache.h" +#include "distributed/adaptive_executor.h" #include "distributed/commands.h" #include "distributed/commands/utility_hook.h" +#include "distributed/deparse_shard_query.h" #include "distributed/deparser.h" +#include "distributed/listutils.h" +#include "distributed/metadata/distobject.h" #include "distributed/metadata_sync.h" #include "distributed/metadata_utility.h" #include "distributed/multi_executor.h" #include "distributed/relation_access_tracking.h" +#include "distributed/worker_protocol.h" #include "distributed/worker_transaction.h" + +/* + * DatabaseCollationInfo is used to store collation related information of a database. + */ +typedef struct DatabaseCollationInfo +{ + char *datcollate; + char *datctype; + +#if PG_VERSION_NUM >= PG_VERSION_15 + char *daticulocale; + char *datcollversion; +#endif + +#if PG_VERSION_NUM >= PG_VERSION_16 + char *daticurules; +#endif +} DatabaseCollationInfo; + +static char * GenerateCreateDatabaseStatementFromPgDatabase(Form_pg_database + databaseForm); +static DatabaseCollationInfo GetDatabaseCollation(Oid dbOid); static AlterOwnerStmt * RecreateAlterDatabaseOwnerStmt(Oid databaseOid); -static Oid get_database_owner(Oid db_oid); -List * PreprocessGrantOnDatabaseStmt(Node *node, const char *queryString, - ProcessUtilityContext processUtilityContext); +#if PG_VERSION_NUM >= PG_VERSION_15 +static char * GetLocaleProviderString(char datlocprovider); +#endif +static char * GetTablespaceName(Oid tablespaceOid); +static ObjectAddress * GetDatabaseAddressFromDatabaseName(char *databaseName, + bool missingOk); + +static Oid get_database_owner(Oid dbId); + /* controlled via GUC */ +bool EnableCreateDatabasePropagation = false; bool EnableAlterDatabaseOwner = true; - /* * AlterDatabaseOwnerObjectAddress returns the ObjectAddress of the database that is the * object of the AlterOwnerStmt. Errors if missing_ok is false. @@ -94,13 +136,13 @@ RecreateAlterDatabaseOwnerStmt(Oid databaseOid) * get_database_owner returns the Oid of the role owning the database */ static Oid -get_database_owner(Oid db_oid) +get_database_owner(Oid dbId) { - HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(db_oid)); + HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbId)); if (!HeapTupleIsValid(tuple)) { ereport(ERROR, (errcode(ERRCODE_UNDEFINED_DATABASE), - errmsg("database with OID %u does not exist", db_oid))); + errmsg("database with OID %u does not exist", dbId))); } Oid dba = ((Form_pg_database) GETSTRUCT(tuple))->datdba; @@ -213,7 +255,6 @@ PreprocessAlterDatabaseRefreshCollStmt(Node *node, const char *queryString, #endif - /* * PreprocessAlterDatabaseSetStmt is executed before the statement is applied to the local * postgres instance. @@ -242,3 +283,420 @@ PreprocessAlterDatabaseSetStmt(Node *node, const char *queryString, return NodeDDLTaskList(NON_COORDINATOR_NODES, commands); } + + +/* + * PostprocessAlterDatabaseStmt is executed before the statement is applied to the local + * Postgres instance. + * + * In this stage, we perform validations that we want to ensure before delegating to + * previous utility hooks because it might not be convenient to throw an error in an + * implicit transaction that creates a database. + */ +List * +PreprocessCreateDatabaseStmt(Node *node, const char *queryString, + ProcessUtilityContext processUtilityContext) +{ + if (!EnableCreateDatabasePropagation || !ShouldPropagate()) + { + return NIL; + } + + EnsureCoordinator(); + + CreatedbStmt *stmt = castNode(CreatedbStmt, node); + EnsureSupportedCreateDatabaseCommand(stmt); + + return NIL; +} + + +/* + * PostprocessCreateDatabaseStmt is executed after the statement is applied to the local + * postgres instance. In this stage we prepare the commands that need to be run on + * all workers to create the database. Since the CREATE DATABASE statement gives error + * in a transaction block, we need to use NontransactionalNodeDDLTaskList to send the + * CREATE DATABASE statement to the workers. + * + */ +List * +PostprocessCreateDatabaseStmt(Node *node, const char *queryString) +{ + if (!EnableCreateDatabasePropagation || !ShouldPropagate()) + { + return NIL; + } + + EnsureCoordinator(); + + /* + * Given that CREATE DATABASE doesn't support "IF NOT EXISTS" and we're + * in the post-process, database must exist, hence missingOk = false. + */ + bool missingOk = false; + bool isPostProcess = true; + List *addresses = GetObjectAddressListFromParseTree(node, missingOk, + isPostProcess); + EnsureAllObjectDependenciesExistOnAllNodes(addresses); + + char *createDatabaseCommand = DeparseTreeNode(node); + + List *commands = list_make3(DISABLE_DDL_PROPAGATION, + (void *) createDatabaseCommand, + ENABLE_DDL_PROPAGATION); + + return NontransactionalNodeDDLTaskList(NON_COORDINATOR_NODES, commands); +} + + +/* + * PreprocessDropDatabaseStmt is executed after the statement is applied to the local + * postgres instance. In this stage we can prepare the commands that need to be run on + * all workers to drop the database. Since the DROP DATABASE statement gives error in + * transaction context, we need to use NontransactionalNodeDDLTaskList to send the + * DROP DATABASE statement to the workers. + */ +List * +PreprocessDropDatabaseStmt(Node *node, const char *queryString, + ProcessUtilityContext processUtilityContext) +{ + if (!EnableCreateDatabasePropagation || !ShouldPropagate()) + { + return NIL; + } + + EnsureCoordinator(); + + DropdbStmt *stmt = (DropdbStmt *) node; + + bool isPostProcess = false; + List *addresses = GetObjectAddressListFromParseTree(node, stmt->missing_ok, + isPostProcess); + + if (list_length(addresses) != 1) + { + ereport(ERROR, (errmsg("unexpected number of objects found when " + "executing DROP DATABASE command"))); + } + + ObjectAddress *address = (ObjectAddress *) linitial(addresses); + if (address->objectId == InvalidOid || !IsAnyObjectDistributed(list_make1(address))) + { + return NIL; + } + + char *dropDatabaseCommand = DeparseTreeNode(node); + + List *commands = list_make3(DISABLE_DDL_PROPAGATION, + (void *) dropDatabaseCommand, + ENABLE_DDL_PROPAGATION); + + return NontransactionalNodeDDLTaskList(NON_COORDINATOR_NODES, commands); +} + + +/* + * DropDatabaseStmtObjectAddress gets the ObjectAddress of the database that is the + * object of the DropdbStmt. + */ +List * +DropDatabaseStmtObjectAddress(Node *node, bool missingOk, bool isPostprocess) +{ + DropdbStmt *stmt = castNode(DropdbStmt, node); + ObjectAddress *dbAddress = GetDatabaseAddressFromDatabaseName(stmt->dbname, + missingOk); + return list_make1(dbAddress); +} + + +/* + * CreateDatabaseStmtObjectAddress gets the ObjectAddress of the database that is the + * object of the CreatedbStmt. + */ +List * +CreateDatabaseStmtObjectAddress(Node *node, bool missingOk, bool isPostprocess) +{ + CreatedbStmt *stmt = castNode(CreatedbStmt, node); + ObjectAddress *dbAddress = GetDatabaseAddressFromDatabaseName(stmt->dbname, + missingOk); + return list_make1(dbAddress); +} + + +/* + * EnsureSupportedCreateDatabaseCommand validates the options provided for the CREATE + * DATABASE command. + * + * Parameters: + * stmt: A CreatedbStmt struct representing a CREATE DATABASE command. + * The options field is a list of DefElem structs, each representing an option. + * + * Currently, this function checks for the following: + * - The "oid" option is not supported. + * - The "template" option is only supported with the value "template1". + * - The "strategy" option is only supported with the value "wal_log". + */ +void +EnsureSupportedCreateDatabaseCommand(CreatedbStmt *stmt) +{ + DefElem *option = NULL; + foreach_ptr(option, stmt->options) + { + if (strcmp(option->defname, "oid") == 0) + { + ereport(ERROR, + errmsg("CREATE DATABASE option \"%s\" is not supported", + option->defname)); + } + + char *optionValue = defGetString(option); + + if (strcmp(option->defname, "template") == 0 && + strcmp(optionValue, "template1") != 0) + { + ereport(ERROR, errmsg("Only template1 is supported as template " + "parameter for CREATE DATABASE")); + } + + if (strcmp(option->defname, "strategy") == 0 && + strcmp(optionValue, "wal_log") != 0) + { + ereport(ERROR, errmsg("Only wal_log is supported as strategy " + "parameter for CREATE DATABASE")); + } + } +} + + +/* + * GetDatabaseAddressFromDatabaseName gets the database name and returns the ObjectAddress + * of the database. + */ +static ObjectAddress * +GetDatabaseAddressFromDatabaseName(char *databaseName, bool missingOk) +{ + Oid databaseOid = get_database_oid(databaseName, missingOk); + ObjectAddress *dbObjectAddress = palloc0(sizeof(ObjectAddress)); + ObjectAddressSet(*dbObjectAddress, DatabaseRelationId, databaseOid); + return dbObjectAddress; +} + + +/* + * GetTablespaceName gets the tablespace oid and returns the tablespace name. + */ +static char * +GetTablespaceName(Oid tablespaceOid) +{ + HeapTuple tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tablespaceOid)); + if (!HeapTupleIsValid(tuple)) + { + return NULL; + } + + Form_pg_tablespace tablespaceForm = (Form_pg_tablespace) GETSTRUCT(tuple); + char *tablespaceName = pstrdup(NameStr(tablespaceForm->spcname)); + + ReleaseSysCache(tuple); + + return tablespaceName; +} + + +/* + * GetDatabaseCollation gets oid of a database and returns all the collation related information + * We need this method since collation related info in Form_pg_database is not accessible. + */ +static DatabaseCollationInfo +GetDatabaseCollation(Oid dbOid) +{ + DatabaseCollationInfo info; + memset(&info, 0, sizeof(DatabaseCollationInfo)); + + Relation rel = table_open(DatabaseRelationId, AccessShareLock); + HeapTuple tup = get_catalog_object_by_oid(rel, Anum_pg_database_oid, dbOid); + if (!HeapTupleIsValid(tup)) + { + elog(ERROR, "cache lookup failed for database %u", dbOid); + } + + bool isNull = false; + + TupleDesc tupdesc = RelationGetDescr(rel); + + Datum collationDatum = heap_getattr(tup, Anum_pg_database_datcollate, tupdesc, + &isNull); + info.datcollate = TextDatumGetCString(collationDatum); + + Datum ctypeDatum = heap_getattr(tup, Anum_pg_database_datctype, tupdesc, &isNull); + info.datctype = TextDatumGetCString(ctypeDatum); + +#if PG_VERSION_NUM >= PG_VERSION_15 + + Datum icuLocaleDatum = heap_getattr(tup, Anum_pg_database_daticulocale, tupdesc, + &isNull); + if (!isNull) + { + info.daticulocale = TextDatumGetCString(icuLocaleDatum); + } + + Datum collverDatum = heap_getattr(tup, Anum_pg_database_datcollversion, tupdesc, + &isNull); + if (!isNull) + { + info.datcollversion = TextDatumGetCString(collverDatum); + } +#endif + +#if PG_VERSION_NUM >= PG_VERSION_16 + Datum icurulesDatum = heap_getattr(tup, Anum_pg_database_daticurules, tupdesc, + &isNull); + if (!isNull) + { + info.daticurules = TextDatumGetCString(icurulesDatum); + } +#endif + + table_close(rel, AccessShareLock); + heap_freetuple(tup); + + return info; +} + + +#if PG_VERSION_NUM >= PG_VERSION_15 + +/* + * GetLocaleProviderString gets the datlocprovider stored in pg_database + * and returns the string representation of the datlocprovider + */ +static char * +GetLocaleProviderString(char datlocprovider) +{ + switch (datlocprovider) + { + case 'c': + { + return "libc"; + } + + case 'i': + { + return "icu"; + } + + default: + { + ereport(ERROR, (errmsg("unexpected datlocprovider value: %c", + datlocprovider))); + } + } +} + + +#endif + + +/* + * GenerateCreateDatabaseStatementFromPgDatabase gets the pg_database tuple and returns the + * CREATE DATABASE statement that can be used to create given database. + * + * Note that this doesn't deparse OID of the database and this is not a + * problem as we anyway don't allow specifying custom OIDs for databases + * when creating them. + */ +static char * +GenerateCreateDatabaseStatementFromPgDatabase(Form_pg_database databaseForm) +{ + DatabaseCollationInfo collInfo = GetDatabaseCollation(databaseForm->oid); + + StringInfoData str; + initStringInfo(&str); + + appendStringInfo(&str, "CREATE DATABASE %s", + quote_identifier(NameStr(databaseForm->datname))); + + appendStringInfo(&str, " CONNECTION LIMIT %d", databaseForm->datconnlimit); + + appendStringInfo(&str, " ALLOW_CONNECTIONS = %s", + quote_literal_cstr(databaseForm->datallowconn ? "true" : "false")); + + appendStringInfo(&str, " IS_TEMPLATE = %s", + quote_literal_cstr(databaseForm->datistemplate ? "true" : "false")); + + appendStringInfo(&str, " LC_COLLATE = %s", + quote_literal_cstr(collInfo.datcollate)); + + appendStringInfo(&str, " LC_CTYPE = %s", quote_literal_cstr(collInfo.datctype)); + + appendStringInfo(&str, " OWNER = %s", + quote_identifier(GetUserNameFromId(databaseForm->datdba, false))); + + appendStringInfo(&str, " TABLESPACE = %s", + quote_identifier(GetTablespaceName(databaseForm->dattablespace))); + + appendStringInfo(&str, " ENCODING = %s", + quote_literal_cstr(pg_encoding_to_char(databaseForm->encoding))); + +#if PG_VERSION_NUM >= PG_VERSION_15 + if (collInfo.datcollversion != NULL) + { + appendStringInfo(&str, " COLLATION_VERSION = %s", + quote_identifier(collInfo.datcollversion)); + } + + if (collInfo.daticulocale != NULL) + { + appendStringInfo(&str, " ICU_LOCALE = %s", quote_identifier( + collInfo.daticulocale)); + } + + appendStringInfo(&str, " LOCALE_PROVIDER = %s", + quote_identifier(GetLocaleProviderString( + databaseForm->datlocprovider))); +#endif + +#if PG_VERSION_NUM >= PG_VERSION_16 + if (collInfo.daticurules != NULL) + { + appendStringInfo(&str, " ICU_RULES = %s", quote_identifier( + collInfo.daticurules)); + } +#endif + + return str.data; +} + + +/* + * CreateDatabaseDDLCommand returns a CREATE DATABASE command to create given + * database + * + * Command is wrapped by citus_internal_database_command() UDF + * to avoid from transaction block restrictions that apply to database commands. + */ +char * +CreateDatabaseDDLCommand(Oid dbId) +{ + HeapTuple tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbId)); + if (!HeapTupleIsValid(tuple)) + { + ereport(ERROR, (errcode(ERRCODE_UNDEFINED_DATABASE), + errmsg("database with OID %u does not exist", dbId))); + } + + Form_pg_database databaseForm = (Form_pg_database) GETSTRUCT(tuple); + + char *createStmt = GenerateCreateDatabaseStatementFromPgDatabase(databaseForm); + + StringInfo outerDbStmt = makeStringInfo(); + + /* Generate the CREATE DATABASE statement */ + appendStringInfo(outerDbStmt, + "SELECT pg_catalog.citus_internal_database_command(%s)", + quote_literal_cstr(createStmt)); + + ReleaseSysCache(tuple); + + return outerDbStmt->data; +} diff --git a/src/backend/distributed/commands/dependencies.c b/src/backend/distributed/commands/dependencies.c index e309ee86c..6ce699cf5 100644 --- a/src/backend/distributed/commands/dependencies.c +++ b/src/backend/distributed/commands/dependencies.c @@ -457,16 +457,37 @@ GetDependencyCreateDDLCommands(const ObjectAddress *dependency) case OCLASS_DATABASE: { - List *databaseDDLCommands = NIL; - - /* only propagate the ownership of the database when the feature is on */ - if (EnableAlterDatabaseOwner) + /* + * For the database where Citus is installed, only propagate the ownership of the + * database, only when the feature is on. + * + * This is because this database must exist on all nodes already so we shouldn't + * need to "CREATE" it on other nodes. However, we still need to correctly reflect + * its owner on other nodes too. + */ + if (dependency->objectId == MyDatabaseId && EnableAlterDatabaseOwner) { - List *ownerDDLCommands = DatabaseOwnerDDLCommands(dependency); - databaseDDLCommands = list_concat(databaseDDLCommands, ownerDDLCommands); + return DatabaseOwnerDDLCommands(dependency); } - return databaseDDLCommands; + /* + * For the other databases, create the database on all nodes, only when the feature + * is on. + */ + if (dependency->objectId != MyDatabaseId && EnableCreateDatabasePropagation) + { + char *databaseDDLCommand = CreateDatabaseDDLCommand(dependency->objectId); + + List *ddlCommands = list_make1(databaseDDLCommand); + + List *grantDDLCommands = GrantOnDatabaseDDLCommands(dependency->objectId); + + ddlCommands = list_concat(ddlCommands, grantDDLCommands); + + return ddlCommands; + } + + return NIL; } case OCLASS_PROC: diff --git a/src/backend/distributed/commands/distribute_object_ops.c b/src/backend/distributed/commands/distribute_object_ops.c index 72ea5beb4..06390f65d 100644 --- a/src/backend/distributed/commands/distribute_object_ops.c +++ b/src/backend/distributed/commands/distribute_object_ops.c @@ -475,6 +475,28 @@ static DistributeObjectOps Database_Alter = { .markDistributed = false, }; +static DistributeObjectOps Database_Create = { + .deparse = DeparseCreateDatabaseStmt, + .qualify = NULL, + .preprocess = PreprocessCreateDatabaseStmt, + .postprocess = PostprocessCreateDatabaseStmt, + .objectType = OBJECT_DATABASE, + .operationType = DIST_OPS_CREATE, + .address = CreateDatabaseStmtObjectAddress, + .markDistributed = true, +}; + +static DistributeObjectOps Database_Drop = { + .deparse = DeparseDropDatabaseStmt, + .qualify = NULL, + .preprocess = PreprocessDropDatabaseStmt, + .postprocess = NULL, + .objectType = OBJECT_DATABASE, + .operationType = DIST_OPS_DROP, + .address = DropDatabaseStmtObjectAddress, + .markDistributed = false, +}; + #if PG_VERSION_NUM >= PG_VERSION_15 static DistributeObjectOps Database_RefreshColl = { .deparse = DeparseAlterDatabaseRefreshCollStmt, @@ -1343,6 +1365,16 @@ GetDistributeObjectOps(Node *node) return &Database_Alter; } + case T_CreatedbStmt: + { + return &Database_Create; + } + + case T_DropdbStmt: + { + return &Database_Drop; + } + #if PG_VERSION_NUM >= PG_VERSION_15 case T_AlterDatabaseRefreshCollStmt: { diff --git a/src/backend/distributed/commands/utility_hook.c b/src/backend/distributed/commands/utility_hook.c index afc8fa9fd..29d7e08da 100644 --- a/src/backend/distributed/commands/utility_hook.c +++ b/src/backend/distributed/commands/utility_hook.c @@ -35,6 +35,7 @@ #include "access/htup_details.h" #include "catalog/catalog.h" #include "catalog/dependency.h" +#include "catalog/pg_database.h" #include "citus_version.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -694,7 +695,7 @@ citus_ProcessUtilityInternal(PlannedStmt *pstmt, } /* inform the user about potential caveats */ - if (IsA(parsetree, CreatedbStmt)) + if (IsA(parsetree, CreatedbStmt) && !EnableCreateDatabasePropagation) { if (EnableUnsupportedFeatureMessages) { @@ -724,22 +725,13 @@ citus_ProcessUtilityInternal(PlannedStmt *pstmt, } /* - * Make sure that dropping the role deletes the pg_dist_object entries. There is a - * separate logic for roles, since roles are not included as dropped objects in the - * drop event trigger. To handle it both on worker and coordinator nodes, it is not - * implemented as a part of process functions but here. + * Make sure that dropping node-wide objects deletes the pg_dist_object + * entries. There is a separate logic for node-wide objects (such as role + * and databases), since they are not included as dropped objects in the + * drop event trigger. To handle it both on worker and coordinator nodes, + * it is not implemented as a part of process functions but here. */ - if (IsA(parsetree, DropRoleStmt)) - { - DropRoleStmt *stmt = castNode(DropRoleStmt, parsetree); - List *allDropRoles = stmt->roles; - - List *distributedDropRoles = FilterDistributedRoles(allDropRoles); - if (list_length(distributedDropRoles) > 0) - { - UnmarkRolesDistributed(distributedDropRoles); - } - } + UnmarkNodeWideObjectsDistributed(parsetree); pstmt->utilityStmt = parsetree; @@ -1275,9 +1267,12 @@ ExecuteDistributedDDLJob(DDLJob *ddlJob) { ereport(WARNING, (errmsg( - "CONCURRENTLY-enabled index commands can fail partially, " - "leaving behind an INVALID index.\n Use DROP INDEX " - "CONCURRENTLY IF EXISTS to remove the invalid index."))); + "Commands that are not transaction-safe may result in " + "partial failure, potentially leading to an inconsistent " + "state.\nIf the problematic command is a CREATE operation, " + "consider using the 'IF EXISTS' syntax to drop the object," + "\nif applicable, and then re-attempt the original command."))); + PG_RE_THROW(); } } @@ -1491,6 +1486,28 @@ DDLTaskList(Oid relationId, const char *commandString) } +/* + * NontransactionalNodeDDLTaskList builds a list of tasks to execute a DDL command on a + * given target set of nodes with cannotBeExecutedInTransaction is set to make sure + * that task list is executed outside a transaction block. + */ +List * +NontransactionalNodeDDLTaskList(TargetWorkerSet targets, List *commands) +{ + List *ddlJobs = NodeDDLTaskList(targets, commands); + DDLJob *ddlJob = NULL; + foreach_ptr(ddlJob, ddlJobs) + { + Task *task = NULL; + foreach_ptr(task, ddlJob->taskList) + { + task->cannotBeExecutedInTransaction = true; + } + } + return ddlJobs; +} + + /* * NodeDDLTaskList builds a list of tasks to execute a DDL command on a * given target set of nodes. diff --git a/src/backend/distributed/deparser/citus_deparseutils.c b/src/backend/distributed/deparser/citus_deparseutils.c new file mode 100644 index 000000000..c0de4ae7c --- /dev/null +++ b/src/backend/distributed/deparser/citus_deparseutils.c @@ -0,0 +1,89 @@ +/*------------------------------------------------------------------------- + * + * citus_deparseutils.c + * + * This file contains common functions used for deparsing PostgreSQL + * statements to their equivalent SQL representation. + * + * Copyright (c) Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + +#include "pg_version_constants.h" + +#include "postgres.h" + +#include "commands/defrem.h" +#include "distributed/deparser.h" +#include "utils/builtins.h" +#include "utils/elog.h" +#include "utils/rel.h" +#include "utils/relcache.h" +#include "utils/syscache.h" +#include "utils/typcache.h" + + +/** + * DefElemOptionToStatement converts a DefElem option to a SQL statement and + * appends it to the given StringInfo buffer. + * + * @param buf The StringInfo buffer to append the SQL statement to. + * @param option The DefElem option to convert to a SQL statement. + * @param optionFormats The option format specification to use for the conversion. + * @param optionFormatsLen The number of option formats in the opt_formats array. + */ +void +DefElemOptionToStatement(StringInfo buf, DefElem *option, + const DefElemOptionFormat *optionFormats, + int optionFormatsLen) +{ + const char *name = option->defname; + int i; + + for (i = 0; i < optionFormatsLen; i++) + { + if (strcmp(name, optionFormats[i].name) == 0) + { + switch (optionFormats[i].type) + { + case OPTION_FORMAT_STRING: + { + char *value = defGetString(option); + appendStringInfo(buf, optionFormats[i].format, quote_identifier( + value)); + break; + } + + case OPTION_FORMAT_INTEGER: + { + int32 value = defGetInt32(option); + appendStringInfo(buf, optionFormats[i].format, value); + break; + } + + case OPTION_FORMAT_BOOLEAN: + { + bool value = defGetBoolean(option); + appendStringInfo(buf, optionFormats[i].format, value ? "true" : + "false"); + break; + } + + case OPTION_FORMAT_LITERAL_CSTR: + { + char *value = defGetString(option); + appendStringInfo(buf, optionFormats[i].format, quote_literal_cstr( + value)); + break; + } + + default: + { + elog(ERROR, "unrecognized option type: %d", optionFormats[i].type); + break; + } + } + } + } +} diff --git a/src/backend/distributed/deparser/deparse_database_stmts.c b/src/backend/distributed/deparser/deparse_database_stmts.c index d3d3ce633..1ae45f920 100644 --- a/src/backend/distributed/deparser/deparse_database_stmts.c +++ b/src/backend/distributed/deparser/deparse_database_stmts.c @@ -12,23 +12,46 @@ #include "postgres.h" #include "pg_version_compat.h" - #include "catalog/namespace.h" +#include "commands/defrem.h" #include "lib/stringinfo.h" #include "nodes/parsenodes.h" +#include "parser/parse_type.h" #include "utils/builtins.h" #include "distributed/deparser.h" +#include "distributed/commands.h" #include "distributed/citus_ruleutils.h" -#include "commands/defrem.h" #include "distributed/deparser.h" +#include "distributed/listutils.h" #include "distributed/log_utils.h" -#include "parser/parse_type.h" static void AppendAlterDatabaseOwnerStmt(StringInfo buf, AlterOwnerStmt *stmt); +static void AppendAlterDatabaseSetStmt(StringInfo buf, AlterDatabaseSetStmt *stmt); static void AppendAlterDatabaseStmt(StringInfo buf, AlterDatabaseStmt *stmt); static void AppendDefElemConnLimit(StringInfo buf, DefElem *def); +static void AppendCreateDatabaseStmt(StringInfo buf, CreatedbStmt *stmt); +static void AppendDropDatabaseStmt(StringInfo buf, DropdbStmt *stmt); +static void AppendGrantOnDatabaseStmt(StringInfo buf, GrantStmt *stmt); + +const DefElemOptionFormat create_database_option_formats[] = { + { "owner", " OWNER %s", OPTION_FORMAT_STRING }, + { "template", " TEMPLATE %s", OPTION_FORMAT_STRING }, + { "encoding", " ENCODING %s", OPTION_FORMAT_LITERAL_CSTR }, + { "strategy", " STRATEGY %s", OPTION_FORMAT_LITERAL_CSTR }, + { "locale", " LOCALE %s", OPTION_FORMAT_LITERAL_CSTR }, + { "lc_collate", " LC_COLLATE %s", OPTION_FORMAT_LITERAL_CSTR }, + { "lc_ctype", " LC_CTYPE %s", OPTION_FORMAT_LITERAL_CSTR }, + { "icu_locale", " ICU_LOCALE %s", OPTION_FORMAT_LITERAL_CSTR }, + { "icu_rules", " ICU_RULES %s", OPTION_FORMAT_LITERAL_CSTR }, + { "locale_provider", " LOCALE_PROVIDER %s", OPTION_FORMAT_LITERAL_CSTR }, + { "collation_version", " COLLATION_VERSION %s", OPTION_FORMAT_LITERAL_CSTR }, + { "tablespace", " TABLESPACE %s", OPTION_FORMAT_STRING }, + { "allow_connections", " ALLOW_CONNECTIONS %s", OPTION_FORMAT_BOOLEAN }, + { "connection_limit", " CONNECTION LIMIT %d", OPTION_FORMAT_INTEGER }, + { "is_template", " IS_TEMPLATE %s", OPTION_FORMAT_BOOLEAN } +}; char * DeparseAlterDatabaseOwnerStmt(Node *node) @@ -205,3 +228,87 @@ DeparseAlterDatabaseSetStmt(Node *node) return str.data; } + + +static void +AppendCreateDatabaseStmt(StringInfo buf, CreatedbStmt *stmt) +{ + /* + * Make sure that we don't try to deparse something that this + * function doesn't expect. + */ + EnsureSupportedCreateDatabaseCommand(stmt); + + appendStringInfo(buf, + "CREATE DATABASE %s", + quote_identifier(stmt->dbname)); + + DefElem *option = NULL; + foreach_ptr(option, stmt->options) + { + DefElemOptionToStatement(buf, option, create_database_option_formats, + lengthof(create_database_option_formats)); + } +} + + +char * +DeparseCreateDatabaseStmt(Node *node) +{ + CreatedbStmt *stmt = castNode(CreatedbStmt, node); + StringInfoData str = { 0 }; + initStringInfo(&str); + + AppendCreateDatabaseStmt(&str, stmt); + + return str.data; +} + + +static void +AppendDropDatabaseStmt(StringInfo buf, DropdbStmt *stmt) +{ + char *ifExistsStatement = stmt->missing_ok ? "IF EXISTS" : ""; + appendStringInfo(buf, + "DROP DATABASE %s %s", + ifExistsStatement, + quote_identifier(stmt->dbname)); + + if (list_length(stmt->options) > 1) + { + /* FORCE is the only option that can be provided for this command */ + elog(ERROR, "got unexpected number of options for DROP DATABASE"); + } + else if (list_length(stmt->options) == 1) + { + DefElem *option = linitial(stmt->options); + appendStringInfo(buf, " WITH ( "); + + if (strcmp(option->defname, "force") == 0) + { + appendStringInfo(buf, "FORCE"); + } + else + { + /* FORCE is the only option that can be provided for this command */ + ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unrecognized DROP DATABASE option \"%s\"", + option->defname))); + } + + appendStringInfo(buf, " )"); + } +} + + +char * +DeparseDropDatabaseStmt(Node *node) +{ + DropdbStmt *stmt = castNode(DropdbStmt, node); + StringInfoData str = { 0 }; + initStringInfo(&str); + + AppendDropDatabaseStmt(&str, stmt); + + return str.data; +} diff --git a/src/backend/distributed/metadata/dependency.c b/src/backend/distributed/metadata/dependency.c index 989e957af..be2ceb3e3 100644 --- a/src/backend/distributed/metadata/dependency.c +++ b/src/backend/distributed/metadata/dependency.c @@ -698,7 +698,6 @@ SupportedDependencyByCitus(const ObjectAddress *address) case OCLASS_DATABASE: { - /* only to propagate its owner */ return true; } diff --git a/src/backend/distributed/metadata/distobject.c b/src/backend/distributed/metadata/distobject.c index fa9da8b75..9f31add60 100644 --- a/src/backend/distributed/metadata/distobject.c +++ b/src/backend/distributed/metadata/distobject.c @@ -22,11 +22,13 @@ #include "catalog/dependency.h" #include "catalog/namespace.h" #include "catalog/objectaddress.h" +#include "catalog/pg_database.h" #include "catalog/pg_extension_d.h" #include "catalog/pg_namespace.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "citus_version.h" +#include "commands/dbcommands.h" #include "commands/extension.h" #include "distributed/listutils.h" #include "distributed/colocation_utils.h" @@ -49,7 +51,6 @@ #include "utils/regproc.h" #include "utils/rel.h" - static char * CreatePgDistObjectEntryCommand(const ObjectAddress *objectAddress); static int ExecuteCommandAsSuperuser(char *query, int paramCount, Oid *paramTypes, Datum *paramValues); @@ -357,6 +358,42 @@ ExecuteCommandAsSuperuser(char *query, int paramCount, Oid *paramTypes, } +/* + * UnmarkNodeWideObjectsDistributed deletes pg_dist_object records + * for all distributed objects in given Drop stmt node. + * + * Today we only expect DropRoleStmt and DropdbStmt to get here. + */ +void +UnmarkNodeWideObjectsDistributed(Node *node) +{ + if (IsA(node, DropRoleStmt)) + { + DropRoleStmt *stmt = castNode(DropRoleStmt, node); + List *allDropRoles = stmt->roles; + + List *distributedDropRoles = FilterDistributedRoles(allDropRoles); + if (list_length(distributedDropRoles) > 0) + { + UnmarkRolesDistributed(distributedDropRoles); + } + } + else if (IsA(node, DropdbStmt)) + { + DropdbStmt *stmt = castNode(DropdbStmt, node); + char *dbName = stmt->dbname; + + Oid dbOid = get_database_oid(dbName, stmt->missing_ok); + ObjectAddress *dbObjectAddress = palloc0(sizeof(ObjectAddress)); + ObjectAddressSet(*dbObjectAddress, DatabaseRelationId, dbOid); + if (IsAnyObjectDistributed(list_make1(dbObjectAddress))) + { + UnmarkObjectDistributed(dbObjectAddress); + } + } +} + + /* * UnmarkObjectDistributed removes the entry from pg_dist_object that marks this object as * distributed. This will prevent updates to that object to be propagated to the worker. diff --git a/src/backend/distributed/metadata/metadata_sync.c b/src/backend/distributed/metadata/metadata_sync.c index 53dc7e747..f04768457 100644 --- a/src/backend/distributed/metadata/metadata_sync.c +++ b/src/backend/distributed/metadata/metadata_sync.c @@ -30,12 +30,15 @@ #include "catalog/pg_attrdef.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" +#include "catalog/pg_database.h" +#include "catalog/pg_database_d.h" #include "catalog/pg_depend.h" #include "catalog/pg_foreign_server.h" #include "catalog/pg_namespace.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "commands/async.h" +#include "commands/dbcommands.h" #include "distributed/argutils.h" #include "distributed/backend_data.h" #include "distributed/citus_ruleutils.h" @@ -120,6 +123,7 @@ static List * GetObjectsForGrantStmt(ObjectType objectType, Oid objectId); static AccessPriv * GetAccessPrivObjectForGrantStmt(char *permission); static List * GenerateGrantOnSchemaQueriesFromAclItem(Oid schemaOid, AclItem *aclItem); +static List * GenerateGrantOnDatabaseFromAclItem(Oid databaseOid, AclItem *aclItem); static List * GenerateGrantOnFunctionQueriesFromAclItem(Oid schemaOid, AclItem *aclItem); static List * GrantOnSequenceDDLCommands(Oid sequenceOid); @@ -179,6 +183,7 @@ PG_FUNCTION_INFO_V1(citus_internal_delete_colocation_metadata); PG_FUNCTION_INFO_V1(citus_internal_add_tenant_schema); PG_FUNCTION_INFO_V1(citus_internal_delete_tenant_schema); PG_FUNCTION_INFO_V1(citus_internal_update_none_dist_table_metadata); +PG_FUNCTION_INFO_V1(citus_internal_database_command); static bool got_SIGTERM = false; @@ -2043,6 +2048,92 @@ GenerateGrantOnSchemaQueriesFromAclItem(Oid schemaOid, AclItem *aclItem) } +/* + * GrantOnDatabaseDDLCommands creates a list of ddl command for replicating the permissions + * of roles on databases. + */ +List * +GrantOnDatabaseDDLCommands(Oid databaseOid) +{ + HeapTuple databaseTuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(databaseOid)); + bool isNull = true; + Datum aclDatum = SysCacheGetAttr(DATABASEOID, databaseTuple, Anum_pg_database_datacl, + &isNull); + if (isNull) + { + ReleaseSysCache(databaseTuple); + return NIL; + } + Acl *acl = DatumGetAclPCopy(aclDatum); + AclItem *aclDat = ACL_DAT(acl); + int aclNum = ACL_NUM(acl); + List *commands = NIL; + + ReleaseSysCache(databaseTuple); + + for (int i = 0; i < aclNum; i++) + { + commands = list_concat(commands, + GenerateGrantOnDatabaseFromAclItem( + databaseOid, &aclDat[i])); + } + + return commands; +} + + +/* + * GenerateGrantOnDatabaseFromAclItem generates a query string for replicating a users permissions + * on a database. + */ +List * +GenerateGrantOnDatabaseFromAclItem(Oid databaseOid, AclItem *aclItem) +{ + AclMode permissions = ACLITEM_GET_PRIVS(*aclItem) & ACL_ALL_RIGHTS_DATABASE; + AclMode grants = ACLITEM_GET_GOPTIONS(*aclItem) & ACL_ALL_RIGHTS_DATABASE; + + /* + * seems unlikely but we check if there is a grant option in the list without the actual permission + */ + Assert(!(grants & ACL_CONNECT) || (permissions & ACL_CONNECT)); + Assert(!(grants & ACL_CREATE) || (permissions & ACL_CREATE)); + Assert(!(grants & ACL_CREATE_TEMP) || (permissions & ACL_CREATE_TEMP)); + Oid granteeOid = aclItem->ai_grantee; + List *queries = NIL; + + queries = lappend(queries, GenerateSetRoleQuery(aclItem->ai_grantor)); + + if (permissions & ACL_CONNECT) + { + char *query = DeparseTreeNode((Node *) GenerateGrantStmtForRights( + OBJECT_DATABASE, granteeOid, databaseOid, + "CONNECT", + grants & ACL_CONNECT)); + queries = lappend(queries, query); + } + if (permissions & ACL_CREATE) + { + char *query = DeparseTreeNode((Node *) GenerateGrantStmtForRights( + OBJECT_DATABASE, granteeOid, databaseOid, + "CREATE", + grants & ACL_CREATE)); + queries = lappend(queries, query); + } + if (permissions & ACL_CREATE_TEMP) + { + char *query = DeparseTreeNode((Node *) GenerateGrantStmtForRights( + OBJECT_DATABASE, granteeOid, databaseOid, + "TEMPORARY", + grants & ACL_CREATE_TEMP)); + queries = lappend(queries, query); + } + + queries = lappend(queries, "RESET ROLE"); + + return queries; +} + + /* * GenerateGrantStmtForRights is the function for creating GrantStmt's for all * types of objects that are supported. It takes parameters to fill a GrantStmt's @@ -2116,6 +2207,11 @@ GetObjectsForGrantStmt(ObjectType objectType, Oid objectId) return list_make1(sequence); } + case OBJECT_DATABASE: + { + return list_make1(makeString(get_database_name(objectId))); + } + default: { elog(ERROR, "unsupported object type for GRANT"); @@ -3889,6 +3985,70 @@ citus_internal_update_none_dist_table_metadata(PG_FUNCTION_ARGS) } +/* + * citus_internal_database_command is an internal UDF to + * create a database in an idempotent maner without + * transaction block restrictions. + */ +Datum +citus_internal_database_command(PG_FUNCTION_ARGS) +{ + CheckCitusVersion(ERROR); + + if (!ShouldSkipMetadataChecks()) + { + EnsureCitusInitiatedOperation(); + } + + PG_ENSURE_ARGNOTNULL(0, "command"); + + text *commandText = PG_GETARG_TEXT_P(0); + char *command = text_to_cstring(commandText); + Node *parseTree = ParseTreeNode(command); + + int saveNestLevel = NewGUCNestLevel(); + + set_config_option("citus.enable_ddl_propagation", "off", + (superuser() ? PGC_SUSET : PGC_USERSET), PGC_S_SESSION, + GUC_ACTION_LOCAL, true, 0, false); + + set_config_option("citus.enable_create_database_propagation", "off", + (superuser() ? PGC_SUSET : PGC_USERSET), PGC_S_SESSION, + GUC_ACTION_LOCAL, true, 0, false); + + /* + * createdb() uses ParseState to report the error position for the + * input command and the position is reported to be 0 when it's provided as NULL. + * We're okay with that because we don't expect this UDF to be called with an incorrect + * DDL command. + */ + ParseState *pstate = NULL; + + if (IsA(parseTree, CreatedbStmt)) + { + CreatedbStmt *stmt = castNode(CreatedbStmt, parseTree); + + bool missingOk = true; + Oid databaseOid = get_database_oid(stmt->dbname, missingOk); + + if (!OidIsValid(databaseOid)) + { + createdb(pstate, (CreatedbStmt *) parseTree); + } + } + else + { + ereport(ERROR, (errmsg("citus_internal_database_command() can only be used " + "for CREATE DATABASE command by Citus."))); + } + + /* rollback GUCs to the state before this session */ + AtEOXact_GUC(true, saveNestLevel); + + PG_RETURN_VOID(); +} + + /* * SyncNewColocationGroup synchronizes a new pg_dist_colocation entry to a worker. */ diff --git a/src/backend/distributed/shared_library_init.c b/src/backend/distributed/shared_library_init.c index 22037c82b..dccc30e15 100644 --- a/src/backend/distributed/shared_library_init.c +++ b/src/backend/distributed/shared_library_init.c @@ -1274,6 +1274,17 @@ RegisterCitusConfigVariables(void) GUC_NO_SHOW_ALL | GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); + DefineCustomBoolVariable( + "citus.enable_create_database_propagation", + gettext_noop("Enables propagating CREATE DATABASE " + "and DROP DATABASE statements to workers."), + NULL, + &EnableCreateDatabasePropagation, + false, + PGC_USERSET, + GUC_STANDARD, + NULL, NULL, NULL); + DefineCustomBoolVariable( "citus.enable_create_role_propagation", gettext_noop("Enables propagating CREATE ROLE " diff --git a/src/backend/distributed/sql/citus--12.1-1--12.2-1.sql b/src/backend/distributed/sql/citus--12.1-1--12.2-1.sql index ec4cc7134..63c4a527f 100644 --- a/src/backend/distributed/sql/citus--12.1-1--12.2-1.sql +++ b/src/backend/distributed/sql/citus--12.1-1--12.2-1.sql @@ -1,5 +1,5 @@ -- citus--12.1-1--12.2-1 - -- bump version to 12.2-1 +#include "udfs/citus_internal_database_command/12.2-1.sql" #include "udfs/citus_add_rebalance_strategy/12.2-1.sql" diff --git a/src/backend/distributed/sql/downgrades/citus--12.2-1--12.1-1.sql b/src/backend/distributed/sql/downgrades/citus--12.2-1--12.1-1.sql index 93d121a12..d18f7257b 100644 --- a/src/backend/distributed/sql/downgrades/citus--12.2-1--12.1-1.sql +++ b/src/backend/distributed/sql/downgrades/citus--12.2-1--12.1-1.sql @@ -1,3 +1,5 @@ -- citus--12.2-1--12.1-1 +DROP FUNCTION pg_catalog.citus_internal_database_command(text); + #include "../udfs/citus_add_rebalance_strategy/10.1-1.sql" diff --git a/src/backend/distributed/sql/udfs/citus_internal_database_command/12.2-1.sql b/src/backend/distributed/sql/udfs/citus_internal_database_command/12.2-1.sql new file mode 100644 index 000000000..9f6d873cc --- /dev/null +++ b/src/backend/distributed/sql/udfs/citus_internal_database_command/12.2-1.sql @@ -0,0 +1,10 @@ +-- +-- citus_internal_database_command run given database command without transaction block restriction. + +CREATE OR REPLACE FUNCTION pg_catalog.citus_internal_database_command(command text) + RETURNS void + LANGUAGE C + VOLATILE +AS 'MODULE_PATHNAME', $$citus_internal_database_command$$; +COMMENT ON FUNCTION pg_catalog.citus_internal_database_command(text) IS + 'run a database command without transaction block restrictions'; diff --git a/src/backend/distributed/sql/udfs/citus_internal_database_command/latest.sql b/src/backend/distributed/sql/udfs/citus_internal_database_command/latest.sql new file mode 100644 index 000000000..9f6d873cc --- /dev/null +++ b/src/backend/distributed/sql/udfs/citus_internal_database_command/latest.sql @@ -0,0 +1,10 @@ +-- +-- citus_internal_database_command run given database command without transaction block restriction. + +CREATE OR REPLACE FUNCTION pg_catalog.citus_internal_database_command(command text) + RETURNS void + LANGUAGE C + VOLATILE +AS 'MODULE_PATHNAME', $$citus_internal_database_command$$; +COMMENT ON FUNCTION pg_catalog.citus_internal_database_command(text) IS + 'run a database command without transaction block restrictions'; diff --git a/src/include/distributed/commands.h b/src/include/distributed/commands.h index df98e66a5..501fefedd 100644 --- a/src/include/distributed/commands.h +++ b/src/include/distributed/commands.h @@ -234,6 +234,18 @@ extern List * PreprocessAlterDatabaseRefreshCollStmt(Node *node, const char *que extern List * PreprocessAlterDatabaseSetStmt(Node *node, const char *queryString, ProcessUtilityContext processUtilityContext); +extern List * PreprocessCreateDatabaseStmt(Node *node, const char *queryString, + ProcessUtilityContext processUtilityContext); +extern List * PostprocessCreateDatabaseStmt(Node *node, const char *queryString); +extern List * PreprocessDropDatabaseStmt(Node *node, const char *queryString, + ProcessUtilityContext processUtilityContext); +extern List * DropDatabaseStmtObjectAddress(Node *node, bool missingOk, + bool isPostprocess); +extern List * CreateDatabaseStmtObjectAddress(Node *node, bool missingOk, + bool isPostprocess); +extern void EnsureSupportedCreateDatabaseCommand(CreatedbStmt *stmt); +extern char * CreateDatabaseDDLCommand(Oid dbId); + /* domain.c - forward declarations */ extern List * CreateDomainStmtObjectAddress(Node *node, bool missing_ok, bool diff --git a/src/include/distributed/commands/utility_hook.h b/src/include/distributed/commands/utility_hook.h index c474dcc43..caac002ed 100644 --- a/src/include/distributed/commands/utility_hook.h +++ b/src/include/distributed/commands/utility_hook.h @@ -40,6 +40,7 @@ typedef enum extern PropSetCmdBehavior PropagateSetCommands; extern bool EnableDDLPropagation; extern int CreateObjectPropagationMode; +extern bool EnableCreateDatabasePropagation; extern bool EnableCreateTypePropagation; extern bool EnableCreateRolePropagation; extern bool EnableAlterRolePropagation; @@ -93,6 +94,7 @@ extern void ProcessUtilityParseTree(Node *node, const char *queryString, extern void MarkInvalidateForeignKeyGraph(void); extern void InvalidateForeignKeyGraphForDDL(void); extern List * DDLTaskList(Oid relationId, const char *commandString); +extern List * NontransactionalNodeDDLTaskList(TargetWorkerSet targets, List *commands); extern List * NodeDDLTaskList(TargetWorkerSet targets, List *commands); extern bool AlterTableInProgress(void); extern bool DropSchemaOrDBInProgress(void); diff --git a/src/include/distributed/deparser.h b/src/include/distributed/deparser.h index aae526df4..f67e53238 100644 --- a/src/include/distributed/deparser.h +++ b/src/include/distributed/deparser.h @@ -121,6 +121,28 @@ extern void AppendGrantedByInGrant(StringInfo buf, GrantStmt *stmt); extern void AppendGrantSharedPrefix(StringInfo buf, GrantStmt *stmt); extern void AppendGrantSharedSuffix(StringInfo buf, GrantStmt *stmt); +/* Common deparser utils */ + +typedef struct DefElemOptionFormat +{ + char *name; + char *format; + int type; +} DefElemOptionFormat; + +typedef enum OptionFormatType +{ + OPTION_FORMAT_STRING, + OPTION_FORMAT_LITERAL_CSTR, + OPTION_FORMAT_BOOLEAN, + OPTION_FORMAT_INTEGER +} OptionFormatType; + + +extern void DefElemOptionToStatement(StringInfo buf, DefElem *option, + const DefElemOptionFormat *opt_formats, + int opt_formats_len); + /* forward declarations for deparse_statistics_stmts.c */ extern char * DeparseCreateStatisticsStmt(Node *node); @@ -227,6 +249,8 @@ extern char * DeparseGrantOnDatabaseStmt(Node *node); extern char * DeparseAlterDatabaseStmt(Node *node); extern char * DeparseAlterDatabaseRefreshCollStmt(Node *node); extern char * DeparseAlterDatabaseSetStmt(Node *node); +extern char * DeparseCreateDatabaseStmt(Node *node); +extern char * DeparseDropDatabaseStmt(Node *node); /* forward declaration for deparse_publication_stmts.c */ diff --git a/src/include/distributed/metadata/distobject.h b/src/include/distributed/metadata/distobject.h index de56c0e1f..cf24a8c81 100644 --- a/src/include/distributed/metadata/distobject.h +++ b/src/include/distributed/metadata/distobject.h @@ -26,6 +26,7 @@ extern void MarkObjectDistributed(const ObjectAddress *distAddress); extern void MarkObjectDistributedViaSuperUser(const ObjectAddress *distAddress); extern void MarkObjectDistributedLocally(const ObjectAddress *distAddress); extern void UnmarkObjectDistributed(const ObjectAddress *address); +extern void UnmarkNodeWideObjectsDistributed(Node *node); extern bool IsTableOwnedByExtension(Oid relationId); extern bool ObjectAddressDependsOnExtension(const ObjectAddress *target); extern bool IsAnyObjectAddressOwnedByExtension(const List *targets, diff --git a/src/include/distributed/metadata_sync.h b/src/include/distributed/metadata_sync.h index 237df363a..7b993ec31 100644 --- a/src/include/distributed/metadata_sync.h +++ b/src/include/distributed/metadata_sync.h @@ -107,6 +107,7 @@ extern char * ColocationIdUpdateCommand(Oid relationId, uint32 colocationId); extern char * CreateSchemaDDLCommand(Oid schemaId); extern List * GrantOnSchemaDDLCommands(Oid schemaId); extern List * GrantOnFunctionDDLCommands(Oid functionOid); +extern List * GrantOnDatabaseDDLCommands(Oid databaseOid); extern List * GrantOnForeignServerDDLCommands(Oid serverId); extern List * GenerateGrantOnForeignServerQueriesFromAclItem(Oid serverId, AclItem *aclItem); diff --git a/src/test/regress/expected/create_drop_database_propagation.out b/src/test/regress/expected/create_drop_database_propagation.out new file mode 100644 index 000000000..e0172f3e8 --- /dev/null +++ b/src/test/regress/expected/create_drop_database_propagation.out @@ -0,0 +1,962 @@ +-- Test for create/drop database propagation. +-- This test is only executes for Postgres versions < 15. +-- For versions >= 15, pg15_create_drop_database_propagation.sql is used. +-- For versions >= 16, pg16_create_drop_database_propagation.sql is used. +-- Test the UDF that we use to issue database command during metadata sync. +SELECT pg_catalog.citus_internal_database_command(null); +ERROR: This is an internal Citus function can only be used in a distributed transaction +CREATE ROLE test_db_commands WITH LOGIN; +ALTER SYSTEM SET citus.enable_manual_metadata_changes_for_user TO 'test_db_commands'; +SELECT pg_reload_conf(); + pg_reload_conf +--------------------------------------------------------------------- + t +(1 row) + +SELECT pg_sleep(0.1); + pg_sleep +--------------------------------------------------------------------- + +(1 row) + +SET ROLE test_db_commands; +-- fails on null input +SELECT pg_catalog.citus_internal_database_command(null); +ERROR: command cannot be NULL +-- fails on non create / drop db command +SELECT pg_catalog.citus_internal_database_command('CREATE TABLE foo_bar(a int)'); +ERROR: citus_internal_database_command() can only be used for CREATE DATABASE command by Citus. +SELECT pg_catalog.citus_internal_database_command('SELECT 1'); +ERROR: citus_internal_database_command() can only be used for CREATE DATABASE command by Citus. +SELECT pg_catalog.citus_internal_database_command('asfsfdsg'); +ERROR: syntax error at or near "asfsfdsg" +SELECT pg_catalog.citus_internal_database_command(''); +ERROR: cannot execute multiple utility events +RESET ROLE; +ALTER ROLE test_db_commands nocreatedb; +SET ROLE test_db_commands; +-- make sure that pg_catalog.citus_internal_database_command doesn't cause privilege escalation +SELECT pg_catalog.citus_internal_database_command('CREATE DATABASE no_permissions'); +ERROR: permission denied to create database +RESET ROLE; +DROP USER test_db_commands; +ALTER SYSTEM RESET citus.enable_manual_metadata_changes_for_user; +SELECT pg_reload_conf(); + pg_reload_conf +--------------------------------------------------------------------- + t +(1 row) + +SELECT pg_sleep(0.1); + pg_sleep +--------------------------------------------------------------------- + +(1 row) + +\set create_drop_db_tablespace :abs_srcdir '/tmp_check/ts3' +CREATE TABLESPACE create_drop_db_tablespace LOCATION :'create_drop_db_tablespace'; +\c - - - :worker_1_port +\set create_drop_db_tablespace :abs_srcdir '/tmp_check/ts4' +CREATE TABLESPACE create_drop_db_tablespace LOCATION :'create_drop_db_tablespace'; +\c - - - :worker_2_port +\set create_drop_db_tablespace :abs_srcdir '/tmp_check/ts5' +CREATE TABLESPACE create_drop_db_tablespace LOCATION :'create_drop_db_tablespace'; +\c - - - :master_port +CREATE DATABASE local_database; +NOTICE: Citus partially supports CREATE DATABASE for distributed databases +DETAIL: Citus does not propagate CREATE DATABASE command to workers +HINT: You can manually create a database and its extensions on workers. +-- check that it's only created for coordinator +SELECT * FROM public.check_database_on_all_nodes('local_database') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "local_database", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +DROP DATABASE local_database; +-- and is dropped +SELECT * FROM public.check_database_on_all_nodes('local_database') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +\c - - - :worker_1_port +CREATE DATABASE local_database; +NOTICE: Citus partially supports CREATE DATABASE for distributed databases +DETAIL: Citus does not propagate CREATE DATABASE command to workers +HINT: You can manually create a database and its extensions on workers. +-- check that it's only created for coordinator +SELECT * FROM public.check_database_on_all_nodes('local_database') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (local) | {"database_properties": {"datacl": null, "datname": "local_database", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +DROP DATABASE local_database; +-- and is dropped +SELECT * FROM public.check_database_on_all_nodes('local_database') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (local) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +\c - - - :master_port +create user create_drop_db_test_user; +set citus.enable_create_database_propagation=on; +-- Tests for create database propagation with template0 which should fail +CREATE DATABASE mydatabase + WITH OWNER = create_drop_db_test_user + TEMPLATE = 'template0' + ENCODING = 'UTF8' + CONNECTION LIMIT = 10 + TABLESPACE = create_drop_db_tablespace + ALLOW_CONNECTIONS = true + IS_TEMPLATE = false; +ERROR: Only template1 is supported as template parameter for CREATE DATABASE +CREATE DATABASE mydatabase_1 + WITH template=template1 + OWNER = create_drop_db_test_user + ENCODING = 'UTF8' + CONNECTION LIMIT = 10 + TABLESPACE = create_drop_db_tablespace + ALLOW_CONNECTIONS = true + IS_TEMPLATE = false; +SELECT * FROM public.check_database_on_all_nodes('mydatabase_1') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "mydatabase_1", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "create_drop_db_tablespace", "daticurules": null, "datallowconn": true, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase_1", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "create_drop_db_tablespace", "daticurules": null, "datallowconn": true, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase_1", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "create_drop_db_tablespace", "daticurules": null, "datallowconn": true, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +-- Test LC / LOCALE settings that don't match the ones provided in template db. +-- All should throw an error on the coordinator. +CREATE DATABASE lc_collate_test LC_COLLATE = 'C.UTF-8'; +ERROR: new collation (C.UTF-8) is incompatible with the collation of the template database (C) +HINT: Use the same collation as in the template database, or use template0 as template. +CREATE DATABASE lc_ctype_test LC_CTYPE = 'C.UTF-8'; +ERROR: new LC_CTYPE (C.UTF-8) is incompatible with the LC_CTYPE of the template database (C) +HINT: Use the same LC_CTYPE as in the template database, or use template0 as template. +CREATE DATABASE locale_test LOCALE = 'C.UTF-8'; +ERROR: new collation (C.UTF-8) is incompatible with the collation of the template database (C) +HINT: Use the same collation as in the template database, or use template0 as template. +CREATE DATABASE lc_collate_lc_ctype_test LC_COLLATE = 'C.UTF-8' LC_CTYPE = 'C.UTF-8'; +ERROR: new collation (C.UTF-8) is incompatible with the collation of the template database (C) +HINT: Use the same collation as in the template database, or use template0 as template. +-- Test LC / LOCALE settings that match the ones provided in template db. +CREATE DATABASE lc_collate_test LC_COLLATE = 'C'; +CREATE DATABASE lc_ctype_test LC_CTYPE = 'C'; +CREATE DATABASE locale_test LOCALE = 'C'; +CREATE DATABASE lc_collate_lc_ctype_test LC_COLLATE = 'C' LC_CTYPE = 'C'; +SELECT * FROM public.check_database_on_all_nodes('lc_collate_test') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "lc_collate_test", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "lc_collate_test", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "lc_collate_test", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SELECT * FROM public.check_database_on_all_nodes('lc_ctype_test') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "lc_ctype_test", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "lc_ctype_test", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "lc_ctype_test", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SELECT * FROM public.check_database_on_all_nodes('locale_test') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "locale_test", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "locale_test", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "locale_test", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SELECT * FROM public.check_database_on_all_nodes('lc_collate_lc_ctype_test') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "lc_collate_lc_ctype_test", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "lc_collate_lc_ctype_test", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "lc_collate_lc_ctype_test", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +DROP DATABASE lc_collate_test; +DROP DATABASE lc_ctype_test; +DROP DATABASE locale_test; +DROP DATABASE lc_collate_lc_ctype_test; +-- ALTER TABLESPACE .. RENAME TO .. is not supported, so we need to rename it manually. +SELECT result FROM run_command_on_all_nodes( + $$ + ALTER TABLESPACE create_drop_db_tablespace RENAME TO "ts-needs\!escape" + $$ +); + result +--------------------------------------------------------------------- + ALTER TABLESPACE + ALTER TABLESPACE + ALTER TABLESPACE +(3 rows) + +CREATE USER "role-needs\!escape"; +CREATE DATABASE "db-needs\!escape" owner "role-needs\!escape" tablespace "ts-needs\!escape"; +-- Rename it to make check_database_on_all_nodes happy. +-- Today we don't support ALTER DATABASE .. RENAME TO .., so need to propagate it manually. +SELECT result FROM run_command_on_all_nodes( + $$ + ALTER DATABASE "db-needs\!escape" RENAME TO db_needs_escape + $$ +); + result +--------------------------------------------------------------------- + ALTER DATABASE + ALTER DATABASE + ALTER DATABASE +(3 rows) + +SELECT * FROM public.check_database_on_all_nodes('db_needs_escape') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "db_needs_escape", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "role-needs\\!escape", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "db_needs_escape", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "role-needs\\!escape", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "db_needs_escape", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "role-needs\\!escape", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +-- test database syncing after node addition +select 1 from citus_remove_node('localhost', :worker_2_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +--test with is_template true and allow connections false +CREATE DATABASE mydatabase + OWNER = create_drop_db_test_user + CONNECTION LIMIT = 10 + ENCODING = 'UTF8' + TABLESPACE = "ts-needs\!escape" + ALLOW_CONNECTIONS = false + IS_TEMPLATE = false; +SELECT * FROM public.check_database_on_all_nodes('mydatabase') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "mydatabase", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(2 rows) + +SET citus.metadata_sync_mode to 'transactional'; +select 1 from citus_add_node('localhost', :worker_2_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +SELECT * FROM public.check_database_on_all_nodes('mydatabase') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "mydatabase", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SELECT * FROM public.check_database_on_all_nodes('mydatabase_1') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "mydatabase_1", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase_1", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase_1", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SELECT * FROM public.check_database_on_all_nodes('db_needs_escape') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "db_needs_escape", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "role-needs\\!escape", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "db_needs_escape", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "role-needs\\!escape", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "db_needs_escape", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "role-needs\\!escape", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +select 1 from citus_remove_node('localhost', :worker_2_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +SET citus.metadata_sync_mode to 'nontransactional'; +select 1 from citus_add_node('localhost', :worker_2_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +RESET citus.metadata_sync_mode; +SELECT * FROM public.check_database_on_all_nodes('mydatabase') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "mydatabase", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SELECT * FROM public.check_database_on_all_nodes('mydatabase_1') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "mydatabase_1", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase_1", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase_1", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SELECT * FROM public.check_database_on_all_nodes('db_needs_escape') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "db_needs_escape", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "role-needs\\!escape", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "db_needs_escape", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "role-needs\\!escape", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "db_needs_escape", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "role-needs\\!escape", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SELECT citus_disable_node_and_wait('localhost', :worker_1_port, true); + citus_disable_node_and_wait +--------------------------------------------------------------------- + +(1 row) + +CREATE DATABASE test_node_activation; +SELECT 1 FROM citus_activate_node('localhost', :worker_1_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +SELECT * FROM public.check_database_on_all_nodes('mydatabase') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "mydatabase", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SELECT * FROM public.check_database_on_all_nodes('mydatabase_1') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "mydatabase_1", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase_1", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "mydatabase_1", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": 10, "daticulocale": null, "datistemplate": false, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SELECT * FROM public.check_database_on_all_nodes('db_needs_escape') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "db_needs_escape", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "role-needs\\!escape", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "db_needs_escape", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "role-needs\\!escape", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "db_needs_escape", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "role-needs\\!escape", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SELECT * FROM public.check_database_on_all_nodes('test_node_activation') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "test_node_activation", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "test_node_activation", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "test_node_activation", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SET citus.log_remote_commands = true; +set citus.grep_remote_commands = '%DROP DATABASE%'; +drop database mydatabase; +NOTICE: issuing DROP DATABASE mydatabase +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +NOTICE: issuing DROP DATABASE mydatabase +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +SET citus.log_remote_commands = false; +-- check that we actually drop the database +drop database mydatabase_1; +SELECT * FROM public.check_database_on_all_nodes('mydatabase_1') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +SELECT * FROM public.check_database_on_all_nodes('mydatabase') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +-- create a template database with all options set and allow connections false +CREATE DATABASE my_template_database + WITH OWNER = create_drop_db_test_user + ENCODING = 'UTF8' + TABLESPACE = "ts-needs\!escape" + ALLOW_CONNECTIONS = false + IS_TEMPLATE = true; +SELECT * FROM public.check_database_on_all_nodes('my_template_database') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "my_template_database", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": -1, "daticulocale": null, "datistemplate": true, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "my_template_database", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": -1, "daticulocale": null, "datistemplate": true, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "my_template_database", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "ts-needs\\!escape", "daticurules": null, "datallowconn": false, "datconnlimit": -1, "daticulocale": null, "datistemplate": true, "database_owner": "create_drop_db_test_user", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +--template databases could not be dropped so we need to change the template flag +SELECT result from run_command_on_all_nodes( + $$ + UPDATE pg_database SET datistemplate = false WHERE datname = 'my_template_database' + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + UPDATE 1 + UPDATE 1 + UPDATE 1 +(3 rows) + +SET citus.log_remote_commands = true; +set citus.grep_remote_commands = '%DROP DATABASE%'; +drop database my_template_database; +NOTICE: issuing DROP DATABASE my_template_database +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +NOTICE: issuing DROP DATABASE my_template_database +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +SET citus.log_remote_commands = false; +SELECT * FROM public.check_database_on_all_nodes('my_template_database') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +--tests for special characters in database name +set citus.enable_create_database_propagation=on; +SET citus.log_remote_commands = true; +set citus.grep_remote_commands = '%CREATE DATABASE%'; +create database "mydatabase#1'2"; +NOTICE: issuing CREATE DATABASE "mydatabase#1'2" +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +NOTICE: issuing CREATE DATABASE "mydatabase#1'2" +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +set citus.grep_remote_commands = '%DROP DATABASE%'; +drop database if exists "mydatabase#1'2"; +NOTICE: issuing DROP DATABASE IF EXISTS "mydatabase#1'2" +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +NOTICE: issuing DROP DATABASE IF EXISTS "mydatabase#1'2" +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +reset citus.grep_remote_commands; +reset citus.log_remote_commands; +-- it doesn't fail thanks to "if exists" +drop database if exists "mydatabase#1'2"; +NOTICE: database "mydatabase#1'2" does not exist, skipping +-- recreate it to verify that it's actually dropped +create database "mydatabase#1'2"; +drop database "mydatabase#1'2"; +-- second time we try to drop it, it fails due to lack of "if exists" +drop database "mydatabase#1'2"; +ERROR: database "mydatabase#1'2" does not exist +\c - - - :worker_1_port +SET citus.enable_create_database_propagation TO ON; +-- show that dropping the database from workers is not allowed when citus.enable_create_database_propagation is on +DROP DATABASE db_needs_escape; +ERROR: operation is not allowed on this node +HINT: Connect to the coordinator and run it again. +-- and the same applies to create database too +create database error_test; +ERROR: operation is not allowed on this node +HINT: Connect to the coordinator and run it again. +\c - - - :master_port +SET citus.enable_create_database_propagation TO ON; +DROP DATABASE test_node_activation; +DROP DATABASE db_needs_escape; +DROP USER "role-needs\!escape"; +-- drop database with force options test +create database db_force_test; +SET citus.log_remote_commands = true; +set citus.grep_remote_commands = '%DROP DATABASE%'; +drop database db_force_test with (force); +NOTICE: issuing DROP DATABASE db_force_test WITH ( FORCE ) +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +NOTICE: issuing DROP DATABASE db_force_test WITH ( FORCE ) +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +reset citus.log_remote_commands; +reset citus.grep_remote_commands; +SELECT * FROM public.check_database_on_all_nodes('db_force_test') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +-- test that we won't propagate non-distributed databases in citus_add_node +select 1 from citus_remove_node('localhost', :worker_2_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +SET citus.enable_create_database_propagation TO off; +CREATE DATABASE non_distributed_db; +NOTICE: Citus partially supports CREATE DATABASE for distributed databases +DETAIL: Citus does not propagate CREATE DATABASE command to workers +HINT: You can manually create a database and its extensions on workers. +SET citus.enable_create_database_propagation TO on; +create database distributed_db; +select 1 from citus_add_node('localhost', :worker_2_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +--non_distributed_db should not be propagated to worker_2 +SELECT * FROM public.check_database_on_all_nodes('non_distributed_db') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "non_distributed_db", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": null, "pg_dist_object_record_for_db_exists": false, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +--distributed_db should be propagated to worker_2 +SELECT * FROM public.check_database_on_all_nodes('distributed_db') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "distributed_db", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "distributed_db", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "distributed_db", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +--clean up resources created by this test +drop database distributed_db; +set citus.enable_create_database_propagation TO off; +drop database non_distributed_db; +-- test role grants on DATABASE in metadata sync +SELECT result from run_command_on_all_nodes( + $$ + create database db_role_grants_test_non_distributed + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + CREATE DATABASE + CREATE DATABASE + CREATE DATABASE +(3 rows) + +SELECT result from run_command_on_all_nodes( + $$ + revoke connect,temp,temporary,create on database db_role_grants_test_non_distributed from public + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + ERROR: operation is not allowed on this node + ERROR: operation is not allowed on this node + REVOKE +(3 rows) + +SET citus.enable_create_database_propagation TO on; +CREATE ROLE db_role_grants_test_role_exists_on_node_2; +select 1 from citus_remove_node('localhost', :worker_2_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +CREATE DATABASE db_role_grants_test; +revoke connect,temp,temporary,create on database db_role_grants_test from public; +SET citus.log_remote_commands = true; +set citus.grep_remote_commands = '%CREATE ROLE%'; +CREATE ROLE db_role_grants_test_role_missing_on_node_2; +NOTICE: issuing SELECT worker_create_or_alter_role('db_role_grants_test_role_missing_on_node_2', 'CREATE ROLE db_role_grants_test_role_missing_on_node_2', 'ALTER ROLE db_role_grants_test_role_missing_on_node_2') +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +RESET citus.log_remote_commands ; +RESET citus.grep_remote_commands; +SET citus.log_remote_commands = true; +set citus.grep_remote_commands = '%GRANT%'; +grant CONNECT,TEMPORARY,CREATE on DATABASE db_role_grants_test to db_role_grants_test_role_exists_on_node_2; +NOTICE: issuing GRANT connect, temporary, create ON DATABASE db_role_grants_test TO db_role_grants_test_role_exists_on_node_2; +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +grant CONNECT,TEMPORARY,CREATE on DATABASE db_role_grants_test to db_role_grants_test_role_missing_on_node_2; +NOTICE: issuing GRANT connect, temporary, create ON DATABASE db_role_grants_test TO db_role_grants_test_role_missing_on_node_2; +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +grant CONNECT,TEMPORARY,CREATE on DATABASE db_role_grants_test_non_distributed to db_role_grants_test_role_exists_on_node_2; +NOTICE: issuing GRANT connect, temporary, create ON DATABASE db_role_grants_test_non_distributed TO db_role_grants_test_role_exists_on_node_2; +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +grant CONNECT,TEMPORARY,CREATE on DATABASE db_role_grants_test_non_distributed to db_role_grants_test_role_missing_on_node_2; +NOTICE: issuing GRANT connect, temporary, create ON DATABASE db_role_grants_test_non_distributed TO db_role_grants_test_role_missing_on_node_2; +DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx +-- check the privileges before add_node for database db_role_grants_test, +-- role db_role_grants_test_role_exists_on_node_2 +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test', 'CREATE') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t +(2 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test', 'TEMPORARY') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t +(2 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test', 'CONNECT') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t +(2 rows) + +-- check the privileges before add_node for database db_role_grants_test, +-- role db_role_grants_test_role_missing_on_node_2 +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test', 'CREATE') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t +(2 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test', 'TEMPORARY') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t +(2 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test', 'CONNECT') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t +(2 rows) + +-- check the privileges before add_node for database db_role_grants_test_non_distributed, +-- role db_role_grants_test_role_exists_on_node_2 +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test_non_distributed', 'CREATE') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t +(2 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test_non_distributed', 'TEMPORARY') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t +(2 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test_non_distributed', 'CONNECT') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t +(2 rows) + +-- check the privileges before add_node for database db_role_grants_test_non_distributed, +-- role db_role_grants_test_role_missing_on_node_2 +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test_non_distributed', 'CREATE') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t +(2 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test_non_distributed', 'TEMPORARY') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t +(2 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test_non_distributed', 'CONNECT') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t +(2 rows) + +RESET citus.log_remote_commands; +RESET citus.grep_remote_commands; +select 1 from citus_add_node('localhost', :worker_2_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +-- check the privileges after add_node for database db_role_grants_test, +-- role db_role_grants_test_role_exists_on_node_2 +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test', 'CREATE') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t + t +(3 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test', 'TEMPORARY') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t + t +(3 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test', 'CONNECT') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t + t +(3 rows) + +-- check the privileges after add_node for database db_role_grants_test, +-- role db_role_grants_test_role_missing_on_node_2 +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test', 'CREATE') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t + t +(3 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test', 'TEMPORARY') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t + t +(3 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test', 'CONNECT') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + t + t + t +(3 rows) + +-- check the privileges after add_node for database db_role_grants_test_non_distributed, +-- role db_role_grants_test_role_exists_on_node_2 +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test_non_distributed', 'CREATE') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + f + t + t +(3 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test_non_distributed', 'TEMPORARY') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + f + t + t +(3 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test_non_distributed', 'CONNECT') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + f + t + t +(3 rows) + +-- check the privileges after add_node for database db_role_grants_test_non_distributed, +-- role db_role_grants_test_role_missing_on_node_2 +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test_non_distributed', 'CREATE') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + f + t + t +(3 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test_non_distributed', 'TEMPORARY') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + f + t + t +(3 rows) + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test_non_distributed', 'CONNECT') + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + f + t + t +(3 rows) + +grant connect,temp,temporary,create on database db_role_grants_test to public; +DROP DATABASE db_role_grants_test; +SELECT result from run_command_on_all_nodes( + $$ + drop database db_role_grants_test_non_distributed + $$ +) ORDER BY result; + result +--------------------------------------------------------------------- + DROP DATABASE + DROP DATABASE + DROP DATABASE +(3 rows) + +DROP ROLE db_role_grants_test_role_exists_on_node_2; +DROP ROLE db_role_grants_test_role_missing_on_node_2; +select 1 from citus_remove_node('localhost', :worker_2_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +set citus.enable_create_role_propagation TO off; +create role non_propagated_role; +NOTICE: not propagating CREATE ROLE/USER commands to other nodes +HINT: Connect to other nodes directly to manually create all necessary users and roles. +set citus.enable_create_role_propagation TO on; +set citus.enable_create_database_propagation TO on; +-- Make sure that we propagate non_propagated_role because it's a dependency of test_db. +-- And hence it becomes a distributed object. +create database test_db OWNER non_propagated_role; +create role propagated_role; +grant connect on database test_db to propagated_role; +SELECT 1 FROM citus_add_node('localhost', :worker_2_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +SELECT * FROM public.check_database_on_all_nodes('test_db') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": ["=Tc/non_propagated_role", "non_propagated_role=CTc/non_propagated_role", "propagated_role=c/non_propagated_role"], "datname": "test_db", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "non_propagated_role", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": ["=Tc/non_propagated_role", "non_propagated_role=CTc/non_propagated_role", "propagated_role=c/non_propagated_role"], "datname": "test_db", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "non_propagated_role", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": ["=Tc/non_propagated_role", "non_propagated_role=CTc/non_propagated_role", "propagated_role=c/non_propagated_role"], "datname": "test_db", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "non_propagated_role", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +REVOKE CONNECT ON DATABASE test_db FROM propagated_role; +DROP DATABASE test_db; +DROP ROLE propagated_role, non_propagated_role; +--clean up resources created by this test +-- DROP TABLESPACE is not supported, so we need to drop it manually. +SELECT result FROM run_command_on_all_nodes( + $$ + drop tablespace "ts-needs\!escape" + $$ +); + result +--------------------------------------------------------------------- + DROP TABLESPACE + DROP TABLESPACE + DROP TABLESPACE +(3 rows) + +drop user create_drop_db_test_user; +reset citus.enable_create_database_propagation; diff --git a/src/test/regress/expected/create_drop_database_propagation_pg15.out b/src/test/regress/expected/create_drop_database_propagation_pg15.out new file mode 100644 index 000000000..9a501558a --- /dev/null +++ b/src/test/regress/expected/create_drop_database_propagation_pg15.out @@ -0,0 +1,82 @@ +-- +-- PG15 +-- +SHOW server_version \gset +SELECT substring(:'server_version', '\d+')::int >= 15 AS server_version_ge_15 +\gset +\if :server_version_ge_15 +\else +\q +\endif +-- create/drop database for pg >= 15 +set citus.enable_create_database_propagation=on; +CREATE DATABASE mydatabase + WITH OID = 966345; +ERROR: CREATE DATABASE option "oid" is not supported +CREATE DATABASE mydatabase + WITH strategy file_copy; +ERROR: Only wal_log is supported as strategy parameter for CREATE DATABASE +CREATE DATABASE st_wal_log + WITH strategy WaL_LoG; +SELECT * FROM public.check_database_on_all_nodes('st_wal_log') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "st_wal_log", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "st_wal_log", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "st_wal_log", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +drop database st_wal_log; +select 1 from citus_remove_node('localhost', :worker_2_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +-- test COLLATION_VERSION +CREATE DATABASE test_collation_version + WITH ENCODING = 'UTF8' + COLLATION_VERSION = '1.0' + ALLOW_CONNECTIONS = false; +select 1 from citus_add_node('localhost', :worker_2_port); + ?column? +--------------------------------------------------------------------- + 1 +(1 row) + +SELECT * FROM public.check_database_on_all_nodes('test_collation_version') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "test_collation_version", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": false, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": "1.0", "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "test_collation_version", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": false, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": "1.0", "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "test_collation_version", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": false, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": "1.0", "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +drop database test_collation_version; +SET client_min_messages TO WARNING; +-- test LOCALE_PROVIDER & ICU_LOCALE +CREATE DATABASE test_locale_provider + WITH ENCODING = 'UTF8' + LOCALE_PROVIDER = 'icu' + ICU_LOCALE = 'en_US'; +ERROR: new locale provider (icu) does not match locale provider of the template database (libc) +HINT: Use the same locale provider as in the template database, or use template0 as template. +RESET client_min_messages; +CREATE DATABASE test_locale_provider + WITH ENCODING = 'UTF8' + LOCALE_PROVIDER = 'libc' + ICU_LOCALE = 'en_US'; +ERROR: ICU locale cannot be specified unless locale provider is ICU +CREATE DATABASE test_locale_provider + WITH ENCODING = 'UTF8' + LOCALE_PROVIDER = 'libc'; +SELECT * FROM public.check_database_on_all_nodes('test_locale_provider') ORDER BY node_type; + node_type | result +--------------------------------------------------------------------- + coordinator (local) | {"database_properties": {"datacl": null, "datname": "test_locale_provider", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "test_locale_provider", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} + worker node (remote) | {"database_properties": {"datacl": null, "datname": "test_locale_provider", "datctype": "C", "encoding": "UTF8", "datcollate": "C", "tablespace": "pg_default", "daticurules": null, "datallowconn": true, "datconnlimit": -1, "daticulocale": null, "datistemplate": false, "database_owner": "postgres", "datcollversion": null, "datlocprovider": "c"}, "pg_dist_object_record_for_db_exists": true, "stale_pg_dist_object_record_for_a_db_exists": false} +(3 rows) + +drop database test_locale_provider; +\c - - - :master_port diff --git a/src/test/regress/expected/create_drop_database_propagation_pg15_0.out b/src/test/regress/expected/create_drop_database_propagation_pg15_0.out new file mode 100644 index 000000000..b1ed9cc5b --- /dev/null +++ b/src/test/regress/expected/create_drop_database_propagation_pg15_0.out @@ -0,0 +1,9 @@ +-- +-- PG15 +-- +SHOW server_version \gset +SELECT substring(:'server_version', '\d+')::int >= 15 AS server_version_ge_15 +\gset +\if :server_version_ge_15 +\else +\q diff --git a/src/test/regress/expected/create_drop_database_propagation_pg16.out b/src/test/regress/expected/create_drop_database_propagation_pg16.out new file mode 100644 index 000000000..75cd99e61 --- /dev/null +++ b/src/test/regress/expected/create_drop_database_propagation_pg16.out @@ -0,0 +1,23 @@ +-- +-- PG16 +-- +SHOW server_version \gset +SELECT substring(:'server_version', '\d+')::int >= 16 AS server_version_ge_16 +\gset +\if :server_version_ge_16 +\else +\q +\endif +-- create/drop database for pg >= 16 +set citus.enable_create_database_propagation=on; +-- test icu_rules +-- +-- practically we don't support it but better to test +CREATE DATABASE citus_icu_rules_test WITH icu_rules='de_DE@collation=phonebook'; +ERROR: ICU rules cannot be specified unless locale provider is ICU +CREATE DATABASE citus_icu_rules_test WITH icu_rules='de_DE@collation=phonebook' locale_provider='icu'; +ERROR: LOCALE or ICU_LOCALE must be specified +CREATE DATABASE citus_icu_rules_test WITH icu_rules='de_DE@collation=phonebook' locale_provider='icu' icu_locale = 'de_DE'; +NOTICE: using standard form "de-DE" for ICU locale "de_DE" +ERROR: new locale provider (icu) does not match locale provider of the template database (libc) +HINT: Use the same locale provider as in the template database, or use template0 as template. diff --git a/src/test/regress/expected/create_drop_database_propagation_pg16_0.out b/src/test/regress/expected/create_drop_database_propagation_pg16_0.out new file mode 100644 index 000000000..730c916ca --- /dev/null +++ b/src/test/regress/expected/create_drop_database_propagation_pg16_0.out @@ -0,0 +1,9 @@ +-- +-- PG16 +-- +SHOW server_version \gset +SELECT substring(:'server_version', '\d+')::int >= 16 AS server_version_ge_16 +\gset +\if :server_version_ge_16 +\else +\q diff --git a/src/test/regress/expected/failure_create_index_concurrently.out b/src/test/regress/expected/failure_create_index_concurrently.out index a198ddc70..784c91aec 100644 --- a/src/test/regress/expected/failure_create_index_concurrently.out +++ b/src/test/regress/expected/failure_create_index_concurrently.out @@ -26,8 +26,9 @@ SELECT citus.mitmproxy('conn.onQuery(query="CREATE").kill()'); (1 row) CREATE INDEX CONCURRENTLY idx_index_test ON index_test(id, value_1); -WARNING: CONCURRENTLY-enabled index commands can fail partially, leaving behind an INVALID index. -Use DROP INDEX CONCURRENTLY IF EXISTS to remove the invalid index. +WARNING: Commands that are not transaction-safe may result in partial failure, potentially leading to an inconsistent state. +If the problematic command is a CREATE operation, consider using the 'IF EXISTS' syntax to drop the object, +if applicable, and then re-attempt the original command. ERROR: connection to the remote node localhost:xxxxx failed with the following error: connection not open SELECT citus.mitmproxy('conn.allow()'); mitmproxy @@ -59,8 +60,9 @@ SELECT citus.mitmproxy('conn.onQuery(query="CREATE").kill()'); (1 row) CREATE INDEX CONCURRENTLY idx_index_test ON index_test(id, value_1); -WARNING: CONCURRENTLY-enabled index commands can fail partially, leaving behind an INVALID index. -Use DROP INDEX CONCURRENTLY IF EXISTS to remove the invalid index. +WARNING: Commands that are not transaction-safe may result in partial failure, potentially leading to an inconsistent state. +If the problematic command is a CREATE operation, consider using the 'IF EXISTS' syntax to drop the object, +if applicable, and then re-attempt the original command. ERROR: connection to the remote node localhost:xxxxx failed with the following error: connection not open SELECT citus.mitmproxy('conn.allow()'); mitmproxy @@ -86,8 +88,9 @@ SELECT citus.mitmproxy('conn.onQuery(query="CREATE").cancel(' || pg_backend_pid( (1 row) CREATE INDEX CONCURRENTLY idx_index_test ON index_test(id, value_1); -WARNING: CONCURRENTLY-enabled index commands can fail partially, leaving behind an INVALID index. -Use DROP INDEX CONCURRENTLY IF EXISTS to remove the invalid index. +WARNING: Commands that are not transaction-safe may result in partial failure, potentially leading to an inconsistent state. +If the problematic command is a CREATE operation, consider using the 'IF EXISTS' syntax to drop the object, +if applicable, and then re-attempt the original command. ERROR: canceling statement due to user request SELECT citus.mitmproxy('conn.allow()'); mitmproxy @@ -111,8 +114,9 @@ SELECT citus.mitmproxy('conn.onQuery(query="CREATE").cancel(' || pg_backend_pid( (1 row) CREATE INDEX CONCURRENTLY idx_index_test ON index_test(id, value_1); -WARNING: CONCURRENTLY-enabled index commands can fail partially, leaving behind an INVALID index. -Use DROP INDEX CONCURRENTLY IF EXISTS to remove the invalid index. +WARNING: Commands that are not transaction-safe may result in partial failure, potentially leading to an inconsistent state. +If the problematic command is a CREATE operation, consider using the 'IF EXISTS' syntax to drop the object, +if applicable, and then re-attempt the original command. ERROR: canceling statement due to user request SELECT citus.mitmproxy('conn.allow()'); mitmproxy @@ -137,8 +141,9 @@ SELECT citus.mitmproxy('conn.onQuery(query="DROP INDEX CONCURRENTLY").kill()'); (1 row) DROP INDEX CONCURRENTLY IF EXISTS idx_index_test; -WARNING: CONCURRENTLY-enabled index commands can fail partially, leaving behind an INVALID index. -Use DROP INDEX CONCURRENTLY IF EXISTS to remove the invalid index. +WARNING: Commands that are not transaction-safe may result in partial failure, potentially leading to an inconsistent state. +If the problematic command is a CREATE operation, consider using the 'IF EXISTS' syntax to drop the object, +if applicable, and then re-attempt the original command. ERROR: connection to the remote node localhost:xxxxx failed with the following error: connection not open SELECT citus.mitmproxy('conn.allow()'); mitmproxy @@ -164,8 +169,9 @@ SELECT create_distributed_table('index_test_2', 'a'); INSERT INTO index_test_2 VALUES (1, 1), (1, 2); CREATE UNIQUE INDEX CONCURRENTLY index_test_2_a_idx ON index_test_2(a); -WARNING: CONCURRENTLY-enabled index commands can fail partially, leaving behind an INVALID index. -Use DROP INDEX CONCURRENTLY IF EXISTS to remove the invalid index. +WARNING: Commands that are not transaction-safe may result in partial failure, potentially leading to an inconsistent state. +If the problematic command is a CREATE operation, consider using the 'IF EXISTS' syntax to drop the object, +if applicable, and then re-attempt the original command. ERROR: could not create unique index "index_test_2_a_idx_1880019" DETAIL: Key (a)=(1) is duplicated. CONTEXT: while executing command on localhost:xxxxx diff --git a/src/test/regress/expected/multi_extension.out b/src/test/regress/expected/multi_extension.out index 295b10c76..43f9c3b98 100644 --- a/src/test/regress/expected/multi_extension.out +++ b/src/test/regress/expected/multi_extension.out @@ -1422,7 +1422,8 @@ ALTER EXTENSION citus UPDATE TO '12.2-1'; SELECT * FROM multi_extension.print_extension_changes(); previous_object | current_object --------------------------------------------------------------------- -(0 rows) + | function citus_internal_database_command(text) void +(1 row) DROP TABLE multi_extension.prev_objects, multi_extension.extension_diff; -- show running version diff --git a/src/test/regress/expected/multi_test_helpers.out b/src/test/regress/expected/multi_test_helpers.out index 4b621b968..70a541d2a 100644 --- a/src/test/regress/expected/multi_test_helpers.out +++ b/src/test/regress/expected/multi_test_helpers.out @@ -556,3 +556,73 @@ BEGIN ORDER BY node_type; END; $func$ LANGUAGE plpgsql; +-- For all nodes, returns database properties of given database, except +-- oid, datfrozenxid and datminmxid. +-- +-- Also returns whether the node has a pg_dist_object record for the database +-- and whether there are any stale pg_dist_object records for a database. +CREATE OR REPLACE FUNCTION check_database_on_all_nodes(p_database_name text) +RETURNS TABLE (node_type text, result text) +AS $func$ +DECLARE + pg_ge_15_options text := ''; + pg_ge_16_options text := ''; +BEGIN + IF EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'pg_database'::regclass AND attname = 'datlocprovider') THEN + pg_ge_15_options := ', daticulocale, datcollversion, datlocprovider'; + ELSE + pg_ge_15_options := $$, null as daticulocale, null as datcollversion, 'c' as datlocprovider$$; + END IF; + + IF EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'pg_database'::regclass AND attname = 'daticurules') THEN + pg_ge_16_options := ', daticurules'; + ELSE + pg_ge_16_options := ', null as daticurules'; + END IF; + + RETURN QUERY + SELECT + CASE WHEN (groupid = 0 AND groupid = (SELECT groupid FROM pg_dist_local_group)) THEN 'coordinator (local)' + WHEN (groupid = 0) THEN 'coordinator (remote)' + WHEN (groupid = (SELECT groupid FROM pg_dist_local_group)) THEN 'worker node (local)' + ELSE 'worker node (remote)' + END AS node_type, + q2.result + FROM run_command_on_all_nodes( + format( + $$ + SELECT to_jsonb(q.*) + FROM ( + SELECT + ( + SELECT to_jsonb(database_properties.*) + FROM ( + SELECT datname, pa.rolname as database_owner, + pg_encoding_to_char(pd.encoding) as encoding, + datistemplate, datallowconn, datconnlimit, datacl, + pt.spcname AS tablespace, datcollate, datctype + %2$s -- >= pg15 options + %3$s -- >= pg16 options + FROM pg_database pd + JOIN pg_authid pa ON pd.datdba = pa.oid + JOIN pg_tablespace pt ON pd.dattablespace = pt.oid + WHERE datname = '%1$s' + ) database_properties + ) AS database_properties, + ( + SELECT COUNT(*)=1 + FROM pg_dist_object WHERE objid = (SELECT oid FROM pg_database WHERE datname = '%1$s') + ) AS pg_dist_object_record_for_db_exists, + ( + SELECT COUNT(*) > 0 + FROM pg_dist_object + WHERE classid = 1262 AND objid NOT IN (SELECT oid FROM pg_database) + ) AS stale_pg_dist_object_record_for_a_db_exists + ) q + $$, + p_database_name, pg_ge_15_options, pg_ge_16_options + ) + ) q2 + JOIN pg_dist_node USING (nodeid); +END; +$func$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/src/test/regress/expected/single_node.out b/src/test/regress/expected/single_node.out index f485763c5..522ffb8e8 100644 --- a/src/test/regress/expected/single_node.out +++ b/src/test/regress/expected/single_node.out @@ -88,8 +88,9 @@ SELECT create_distributed_table('failover_to_local', 'a', shard_count=>32); (1 row) CREATE INDEX CONCURRENTLY ON failover_to_local(a); -WARNING: CONCURRENTLY-enabled index commands can fail partially, leaving behind an INVALID index. - Use DROP INDEX CONCURRENTLY IF EXISTS to remove the invalid index. +WARNING: Commands that are not transaction-safe may result in partial failure, potentially leading to an inconsistent state. +If the problematic command is a CREATE operation, consider using the 'IF EXISTS' syntax to drop the object, +if applicable, and then re-attempt the original command. ERROR: the total number of connections on the server is more than max_connections(100) HINT: Consider using a higher value for max_connections -- reset global GUC changes diff --git a/src/test/regress/expected/single_node_0.out b/src/test/regress/expected/single_node_0.out index 321d283f8..12b385e96 100644 --- a/src/test/regress/expected/single_node_0.out +++ b/src/test/regress/expected/single_node_0.out @@ -88,8 +88,9 @@ SELECT create_distributed_table('failover_to_local', 'a', shard_count=>32); (1 row) CREATE INDEX CONCURRENTLY ON failover_to_local(a); -WARNING: CONCURRENTLY-enabled index commands can fail partially, leaving behind an INVALID index. - Use DROP INDEX CONCURRENTLY IF EXISTS to remove the invalid index. +WARNING: Commands that are not transaction-safe may result in partial failure, potentially leading to an inconsistent state. +If the problematic command is a CREATE operation, consider using the 'IF EXISTS' syntax to drop the object, +if applicable, and then re-attempt the original command. ERROR: the total number of connections on the server is more than max_connections(100) HINT: Consider using a higher value for max_connections -- reset global GUC changes diff --git a/src/test/regress/expected/upgrade_list_citus_objects.out b/src/test/regress/expected/upgrade_list_citus_objects.out index 36bd504e8..942e0336f 100644 --- a/src/test/regress/expected/upgrade_list_citus_objects.out +++ b/src/test/regress/expected/upgrade_list_citus_objects.out @@ -71,6 +71,7 @@ ORDER BY 1; function citus_internal_add_shard_metadata(regclass,bigint,"char",text,text) function citus_internal_add_tenant_schema(oid,integer) function citus_internal_adjust_local_clock_to_remote(cluster_clock) + function citus_internal_database_command(text) function citus_internal_delete_colocation_metadata(integer) function citus_internal_delete_partition_metadata(regclass) function citus_internal_delete_placement_metadata(bigint) @@ -343,5 +344,5 @@ ORDER BY 1; view citus_stat_tenants_local view pg_dist_shard_placement view time_partitions -(333 rows) +(334 rows) diff --git a/src/test/regress/multi_1_schedule b/src/test/regress/multi_1_schedule index aed751aa0..9528cc704 100644 --- a/src/test/regress/multi_1_schedule +++ b/src/test/regress/multi_1_schedule @@ -35,6 +35,9 @@ test: alter_database_owner test: seclabel test: distributed_triggers test: create_single_shard_table +test: create_drop_database_propagation +test: create_drop_database_propagation_pg15 +test: create_drop_database_propagation_pg16 # don't parallelize single_shard_table_udfs to make sure colocation ids are sequential test: single_shard_table_udfs test: schema_based_sharding diff --git a/src/test/regress/pg_regress_multi.pl b/src/test/regress/pg_regress_multi.pl index 3acde4c3c..95330c638 100755 --- a/src/test/regress/pg_regress_multi.pl +++ b/src/test/regress/pg_regress_multi.pl @@ -634,7 +634,7 @@ for my $port (@followerWorkerPorts) } } -for my $tablespace ("ts0", "ts1", "ts2") +for my $tablespace ("ts0", "ts1", "ts2", "ts3", "ts4", "ts5") { if (-e catfile($TMP_CHECKDIR, $tablespace)) { diff --git a/src/test/regress/sql/create_drop_database_propagation.sql b/src/test/regress/sql/create_drop_database_propagation.sql new file mode 100644 index 000000000..c83548d68 --- /dev/null +++ b/src/test/regress/sql/create_drop_database_propagation.sql @@ -0,0 +1,554 @@ + +-- Test for create/drop database propagation. +-- This test is only executes for Postgres versions < 15. +-- For versions >= 15, pg15_create_drop_database_propagation.sql is used. +-- For versions >= 16, pg16_create_drop_database_propagation.sql is used. + +-- Test the UDF that we use to issue database command during metadata sync. +SELECT pg_catalog.citus_internal_database_command(null); + +CREATE ROLE test_db_commands WITH LOGIN; +ALTER SYSTEM SET citus.enable_manual_metadata_changes_for_user TO 'test_db_commands'; +SELECT pg_reload_conf(); +SELECT pg_sleep(0.1); +SET ROLE test_db_commands; + +-- fails on null input +SELECT pg_catalog.citus_internal_database_command(null); + +-- fails on non create / drop db command +SELECT pg_catalog.citus_internal_database_command('CREATE TABLE foo_bar(a int)'); +SELECT pg_catalog.citus_internal_database_command('SELECT 1'); +SELECT pg_catalog.citus_internal_database_command('asfsfdsg'); +SELECT pg_catalog.citus_internal_database_command(''); + +RESET ROLE; +ALTER ROLE test_db_commands nocreatedb; +SET ROLE test_db_commands; + +-- make sure that pg_catalog.citus_internal_database_command doesn't cause privilege escalation +SELECT pg_catalog.citus_internal_database_command('CREATE DATABASE no_permissions'); + +RESET ROLE; +DROP USER test_db_commands; +ALTER SYSTEM RESET citus.enable_manual_metadata_changes_for_user; +SELECT pg_reload_conf(); +SELECT pg_sleep(0.1); + +\set create_drop_db_tablespace :abs_srcdir '/tmp_check/ts3' +CREATE TABLESPACE create_drop_db_tablespace LOCATION :'create_drop_db_tablespace'; + +\c - - - :worker_1_port +\set create_drop_db_tablespace :abs_srcdir '/tmp_check/ts4' +CREATE TABLESPACE create_drop_db_tablespace LOCATION :'create_drop_db_tablespace'; + +\c - - - :worker_2_port +\set create_drop_db_tablespace :abs_srcdir '/tmp_check/ts5' +CREATE TABLESPACE create_drop_db_tablespace LOCATION :'create_drop_db_tablespace'; + +\c - - - :master_port +CREATE DATABASE local_database; + +-- check that it's only created for coordinator +SELECT * FROM public.check_database_on_all_nodes('local_database') ORDER BY node_type; + +DROP DATABASE local_database; + +-- and is dropped +SELECT * FROM public.check_database_on_all_nodes('local_database') ORDER BY node_type; + +\c - - - :worker_1_port +CREATE DATABASE local_database; + +-- check that it's only created for coordinator +SELECT * FROM public.check_database_on_all_nodes('local_database') ORDER BY node_type; + +DROP DATABASE local_database; + +-- and is dropped +SELECT * FROM public.check_database_on_all_nodes('local_database') ORDER BY node_type; + +\c - - - :master_port +create user create_drop_db_test_user; + +set citus.enable_create_database_propagation=on; + +-- Tests for create database propagation with template0 which should fail +CREATE DATABASE mydatabase + WITH OWNER = create_drop_db_test_user + TEMPLATE = 'template0' + ENCODING = 'UTF8' + CONNECTION LIMIT = 10 + TABLESPACE = create_drop_db_tablespace + ALLOW_CONNECTIONS = true + IS_TEMPLATE = false; + +CREATE DATABASE mydatabase_1 + WITH template=template1 + OWNER = create_drop_db_test_user + ENCODING = 'UTF8' + CONNECTION LIMIT = 10 + TABLESPACE = create_drop_db_tablespace + ALLOW_CONNECTIONS = true + IS_TEMPLATE = false; + +SELECT * FROM public.check_database_on_all_nodes('mydatabase_1') ORDER BY node_type; + +-- Test LC / LOCALE settings that don't match the ones provided in template db. +-- All should throw an error on the coordinator. +CREATE DATABASE lc_collate_test LC_COLLATE = 'C.UTF-8'; +CREATE DATABASE lc_ctype_test LC_CTYPE = 'C.UTF-8'; +CREATE DATABASE locale_test LOCALE = 'C.UTF-8'; +CREATE DATABASE lc_collate_lc_ctype_test LC_COLLATE = 'C.UTF-8' LC_CTYPE = 'C.UTF-8'; + +-- Test LC / LOCALE settings that match the ones provided in template db. +CREATE DATABASE lc_collate_test LC_COLLATE = 'C'; +CREATE DATABASE lc_ctype_test LC_CTYPE = 'C'; +CREATE DATABASE locale_test LOCALE = 'C'; +CREATE DATABASE lc_collate_lc_ctype_test LC_COLLATE = 'C' LC_CTYPE = 'C'; + +SELECT * FROM public.check_database_on_all_nodes('lc_collate_test') ORDER BY node_type; +SELECT * FROM public.check_database_on_all_nodes('lc_ctype_test') ORDER BY node_type; +SELECT * FROM public.check_database_on_all_nodes('locale_test') ORDER BY node_type; +SELECT * FROM public.check_database_on_all_nodes('lc_collate_lc_ctype_test') ORDER BY node_type; + +DROP DATABASE lc_collate_test; +DROP DATABASE lc_ctype_test; +DROP DATABASE locale_test; +DROP DATABASE lc_collate_lc_ctype_test; + +-- ALTER TABLESPACE .. RENAME TO .. is not supported, so we need to rename it manually. +SELECT result FROM run_command_on_all_nodes( + $$ + ALTER TABLESPACE create_drop_db_tablespace RENAME TO "ts-needs\!escape" + $$ +); + +CREATE USER "role-needs\!escape"; + +CREATE DATABASE "db-needs\!escape" owner "role-needs\!escape" tablespace "ts-needs\!escape"; + +-- Rename it to make check_database_on_all_nodes happy. +-- Today we don't support ALTER DATABASE .. RENAME TO .., so need to propagate it manually. +SELECT result FROM run_command_on_all_nodes( + $$ + ALTER DATABASE "db-needs\!escape" RENAME TO db_needs_escape + $$ +); + +SELECT * FROM public.check_database_on_all_nodes('db_needs_escape') ORDER BY node_type; + +-- test database syncing after node addition + +select 1 from citus_remove_node('localhost', :worker_2_port); + +--test with is_template true and allow connections false +CREATE DATABASE mydatabase + OWNER = create_drop_db_test_user + CONNECTION LIMIT = 10 + ENCODING = 'UTF8' + TABLESPACE = "ts-needs\!escape" + ALLOW_CONNECTIONS = false + IS_TEMPLATE = false; + +SELECT * FROM public.check_database_on_all_nodes('mydatabase') ORDER BY node_type; + +SET citus.metadata_sync_mode to 'transactional'; +select 1 from citus_add_node('localhost', :worker_2_port); + +SELECT * FROM public.check_database_on_all_nodes('mydatabase') ORDER BY node_type; +SELECT * FROM public.check_database_on_all_nodes('mydatabase_1') ORDER BY node_type; +SELECT * FROM public.check_database_on_all_nodes('db_needs_escape') ORDER BY node_type; + +select 1 from citus_remove_node('localhost', :worker_2_port); + +SET citus.metadata_sync_mode to 'nontransactional'; +select 1 from citus_add_node('localhost', :worker_2_port); + +RESET citus.metadata_sync_mode; + +SELECT * FROM public.check_database_on_all_nodes('mydatabase') ORDER BY node_type; +SELECT * FROM public.check_database_on_all_nodes('mydatabase_1') ORDER BY node_type; +SELECT * FROM public.check_database_on_all_nodes('db_needs_escape') ORDER BY node_type; + +SELECT citus_disable_node_and_wait('localhost', :worker_1_port, true); + +CREATE DATABASE test_node_activation; +SELECT 1 FROM citus_activate_node('localhost', :worker_1_port); + +SELECT * FROM public.check_database_on_all_nodes('mydatabase') ORDER BY node_type; +SELECT * FROM public.check_database_on_all_nodes('mydatabase_1') ORDER BY node_type; +SELECT * FROM public.check_database_on_all_nodes('db_needs_escape') ORDER BY node_type; +SELECT * FROM public.check_database_on_all_nodes('test_node_activation') ORDER BY node_type; + +SET citus.log_remote_commands = true; +set citus.grep_remote_commands = '%DROP DATABASE%'; +drop database mydatabase; + +SET citus.log_remote_commands = false; + +-- check that we actually drop the database +drop database mydatabase_1; + +SELECT * FROM public.check_database_on_all_nodes('mydatabase_1') ORDER BY node_type; + +SELECT * FROM public.check_database_on_all_nodes('mydatabase') ORDER BY node_type; + +-- create a template database with all options set and allow connections false +CREATE DATABASE my_template_database + WITH OWNER = create_drop_db_test_user + ENCODING = 'UTF8' + TABLESPACE = "ts-needs\!escape" + ALLOW_CONNECTIONS = false + IS_TEMPLATE = true; + +SELECT * FROM public.check_database_on_all_nodes('my_template_database') ORDER BY node_type; + +--template databases could not be dropped so we need to change the template flag +SELECT result from run_command_on_all_nodes( + $$ + UPDATE pg_database SET datistemplate = false WHERE datname = 'my_template_database' + $$ +) ORDER BY result; + +SET citus.log_remote_commands = true; + +set citus.grep_remote_commands = '%DROP DATABASE%'; +drop database my_template_database; + +SET citus.log_remote_commands = false; + +SELECT * FROM public.check_database_on_all_nodes('my_template_database') ORDER BY node_type; + +--tests for special characters in database name +set citus.enable_create_database_propagation=on; +SET citus.log_remote_commands = true; +set citus.grep_remote_commands = '%CREATE DATABASE%'; + +create database "mydatabase#1'2"; + +set citus.grep_remote_commands = '%DROP DATABASE%'; +drop database if exists "mydatabase#1'2"; + +reset citus.grep_remote_commands; +reset citus.log_remote_commands; + +-- it doesn't fail thanks to "if exists" +drop database if exists "mydatabase#1'2"; + +-- recreate it to verify that it's actually dropped +create database "mydatabase#1'2"; +drop database "mydatabase#1'2"; + +-- second time we try to drop it, it fails due to lack of "if exists" +drop database "mydatabase#1'2"; + +\c - - - :worker_1_port + +SET citus.enable_create_database_propagation TO ON; + +-- show that dropping the database from workers is not allowed when citus.enable_create_database_propagation is on +DROP DATABASE db_needs_escape; + +-- and the same applies to create database too +create database error_test; + +\c - - - :master_port + +SET citus.enable_create_database_propagation TO ON; + +DROP DATABASE test_node_activation; +DROP DATABASE db_needs_escape; +DROP USER "role-needs\!escape"; + +-- drop database with force options test + +create database db_force_test; + +SET citus.log_remote_commands = true; +set citus.grep_remote_commands = '%DROP DATABASE%'; + +drop database db_force_test with (force); + +reset citus.log_remote_commands; +reset citus.grep_remote_commands; + +SELECT * FROM public.check_database_on_all_nodes('db_force_test') ORDER BY node_type; + +-- test that we won't propagate non-distributed databases in citus_add_node + +select 1 from citus_remove_node('localhost', :worker_2_port); +SET citus.enable_create_database_propagation TO off; +CREATE DATABASE non_distributed_db; +SET citus.enable_create_database_propagation TO on; +create database distributed_db; + +select 1 from citus_add_node('localhost', :worker_2_port); + +--non_distributed_db should not be propagated to worker_2 +SELECT * FROM public.check_database_on_all_nodes('non_distributed_db') ORDER BY node_type; +--distributed_db should be propagated to worker_2 +SELECT * FROM public.check_database_on_all_nodes('distributed_db') ORDER BY node_type; + +--clean up resources created by this test +drop database distributed_db; + +set citus.enable_create_database_propagation TO off; +drop database non_distributed_db; + +-- test role grants on DATABASE in metadata sync + +SELECT result from run_command_on_all_nodes( + $$ + create database db_role_grants_test_non_distributed + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + revoke connect,temp,temporary,create on database db_role_grants_test_non_distributed from public + $$ +) ORDER BY result; + +SET citus.enable_create_database_propagation TO on; + +CREATE ROLE db_role_grants_test_role_exists_on_node_2; + +select 1 from citus_remove_node('localhost', :worker_2_port); + +CREATE DATABASE db_role_grants_test; + +revoke connect,temp,temporary,create on database db_role_grants_test from public; + +SET citus.log_remote_commands = true; +set citus.grep_remote_commands = '%CREATE ROLE%'; +CREATE ROLE db_role_grants_test_role_missing_on_node_2; + +RESET citus.log_remote_commands ; +RESET citus.grep_remote_commands; + +SET citus.log_remote_commands = true; +set citus.grep_remote_commands = '%GRANT%'; +grant CONNECT,TEMPORARY,CREATE on DATABASE db_role_grants_test to db_role_grants_test_role_exists_on_node_2; +grant CONNECT,TEMPORARY,CREATE on DATABASE db_role_grants_test to db_role_grants_test_role_missing_on_node_2; + +grant CONNECT,TEMPORARY,CREATE on DATABASE db_role_grants_test_non_distributed to db_role_grants_test_role_exists_on_node_2; +grant CONNECT,TEMPORARY,CREATE on DATABASE db_role_grants_test_non_distributed to db_role_grants_test_role_missing_on_node_2; + +-- check the privileges before add_node for database db_role_grants_test, +-- role db_role_grants_test_role_exists_on_node_2 + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test', 'CREATE') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test', 'TEMPORARY') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test', 'CONNECT') + $$ +) ORDER BY result; + +-- check the privileges before add_node for database db_role_grants_test, +-- role db_role_grants_test_role_missing_on_node_2 + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test', 'CREATE') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test', 'TEMPORARY') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test', 'CONNECT') + $$ +) ORDER BY result; + +-- check the privileges before add_node for database db_role_grants_test_non_distributed, +-- role db_role_grants_test_role_exists_on_node_2 +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test_non_distributed', 'CREATE') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test_non_distributed', 'TEMPORARY') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test_non_distributed', 'CONNECT') + $$ +) ORDER BY result; + +-- check the privileges before add_node for database db_role_grants_test_non_distributed, +-- role db_role_grants_test_role_missing_on_node_2 + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test_non_distributed', 'CREATE') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test_non_distributed', 'TEMPORARY') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test_non_distributed', 'CONNECT') + $$ +) ORDER BY result; + +RESET citus.log_remote_commands; +RESET citus.grep_remote_commands; + +select 1 from citus_add_node('localhost', :worker_2_port); + +-- check the privileges after add_node for database db_role_grants_test, +-- role db_role_grants_test_role_exists_on_node_2 + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test', 'CREATE') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test', 'TEMPORARY') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test', 'CONNECT') + $$ +) ORDER BY result; + +-- check the privileges after add_node for database db_role_grants_test, +-- role db_role_grants_test_role_missing_on_node_2 + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test', 'CREATE') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test', 'TEMPORARY') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test', 'CONNECT') + $$ +) ORDER BY result; + +-- check the privileges after add_node for database db_role_grants_test_non_distributed, +-- role db_role_grants_test_role_exists_on_node_2 +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test_non_distributed', 'CREATE') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test_non_distributed', 'TEMPORARY') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_exists_on_node_2','db_role_grants_test_non_distributed', 'CONNECT') + $$ +) ORDER BY result; + +-- check the privileges after add_node for database db_role_grants_test_non_distributed, +-- role db_role_grants_test_role_missing_on_node_2 + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test_non_distributed', 'CREATE') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test_non_distributed', 'TEMPORARY') + $$ +) ORDER BY result; + +SELECT result from run_command_on_all_nodes( + $$ + select has_database_privilege('db_role_grants_test_role_missing_on_node_2','db_role_grants_test_non_distributed', 'CONNECT') + $$ +) ORDER BY result; + +grant connect,temp,temporary,create on database db_role_grants_test to public; + +DROP DATABASE db_role_grants_test; + +SELECT result from run_command_on_all_nodes( + $$ + drop database db_role_grants_test_non_distributed + $$ +) ORDER BY result; +DROP ROLE db_role_grants_test_role_exists_on_node_2; +DROP ROLE db_role_grants_test_role_missing_on_node_2; + +select 1 from citus_remove_node('localhost', :worker_2_port); + +set citus.enable_create_role_propagation TO off; +create role non_propagated_role; +set citus.enable_create_role_propagation TO on; + +set citus.enable_create_database_propagation TO on; + +-- Make sure that we propagate non_propagated_role because it's a dependency of test_db. +-- And hence it becomes a distributed object. +create database test_db OWNER non_propagated_role; + +create role propagated_role; +grant connect on database test_db to propagated_role; + +SELECT 1 FROM citus_add_node('localhost', :worker_2_port); + +SELECT * FROM public.check_database_on_all_nodes('test_db') ORDER BY node_type; + +REVOKE CONNECT ON DATABASE test_db FROM propagated_role; +DROP DATABASE test_db; +DROP ROLE propagated_role, non_propagated_role; + +--clean up resources created by this test + +-- DROP TABLESPACE is not supported, so we need to drop it manually. +SELECT result FROM run_command_on_all_nodes( + $$ + drop tablespace "ts-needs\!escape" + $$ +); + +drop user create_drop_db_test_user; +reset citus.enable_create_database_propagation; diff --git a/src/test/regress/sql/create_drop_database_propagation_pg15.sql b/src/test/regress/sql/create_drop_database_propagation_pg15.sql new file mode 100644 index 000000000..40d1b9e09 --- /dev/null +++ b/src/test/regress/sql/create_drop_database_propagation_pg15.sql @@ -0,0 +1,65 @@ +-- +-- PG15 +-- +SHOW server_version \gset +SELECT substring(:'server_version', '\d+')::int >= 15 AS server_version_ge_15 +\gset +\if :server_version_ge_15 +\else +\q +\endif + +-- create/drop database for pg >= 15 + +set citus.enable_create_database_propagation=on; + +CREATE DATABASE mydatabase + WITH OID = 966345; + +CREATE DATABASE mydatabase + WITH strategy file_copy; + +CREATE DATABASE st_wal_log + WITH strategy WaL_LoG; + +SELECT * FROM public.check_database_on_all_nodes('st_wal_log') ORDER BY node_type; + +drop database st_wal_log; + +select 1 from citus_remove_node('localhost', :worker_2_port); + +-- test COLLATION_VERSION + +CREATE DATABASE test_collation_version + WITH ENCODING = 'UTF8' + COLLATION_VERSION = '1.0' + ALLOW_CONNECTIONS = false; + +select 1 from citus_add_node('localhost', :worker_2_port); + +SELECT * FROM public.check_database_on_all_nodes('test_collation_version') ORDER BY node_type; + +drop database test_collation_version; + +SET client_min_messages TO WARNING; +-- test LOCALE_PROVIDER & ICU_LOCALE +CREATE DATABASE test_locale_provider + WITH ENCODING = 'UTF8' + LOCALE_PROVIDER = 'icu' + ICU_LOCALE = 'en_US'; +RESET client_min_messages; + +CREATE DATABASE test_locale_provider + WITH ENCODING = 'UTF8' + LOCALE_PROVIDER = 'libc' + ICU_LOCALE = 'en_US'; + +CREATE DATABASE test_locale_provider + WITH ENCODING = 'UTF8' + LOCALE_PROVIDER = 'libc'; + +SELECT * FROM public.check_database_on_all_nodes('test_locale_provider') ORDER BY node_type; + +drop database test_locale_provider; + +\c - - - :master_port diff --git a/src/test/regress/sql/create_drop_database_propagation_pg16.sql b/src/test/regress/sql/create_drop_database_propagation_pg16.sql new file mode 100644 index 000000000..cec553813 --- /dev/null +++ b/src/test/regress/sql/create_drop_database_propagation_pg16.sql @@ -0,0 +1,22 @@ +-- +-- PG16 +-- +SHOW server_version \gset +SELECT substring(:'server_version', '\d+')::int >= 16 AS server_version_ge_16 +\gset +\if :server_version_ge_16 +\else +\q +\endif + +-- create/drop database for pg >= 16 + +set citus.enable_create_database_propagation=on; + +-- test icu_rules +-- +-- practically we don't support it but better to test + +CREATE DATABASE citus_icu_rules_test WITH icu_rules='de_DE@collation=phonebook'; +CREATE DATABASE citus_icu_rules_test WITH icu_rules='de_DE@collation=phonebook' locale_provider='icu'; +CREATE DATABASE citus_icu_rules_test WITH icu_rules='de_DE@collation=phonebook' locale_provider='icu' icu_locale = 'de_DE'; diff --git a/src/test/regress/sql/multi_test_helpers.sql b/src/test/regress/sql/multi_test_helpers.sql index 7f0346d14..e67b782a5 100644 --- a/src/test/regress/sql/multi_test_helpers.sql +++ b/src/test/regress/sql/multi_test_helpers.sql @@ -581,3 +581,74 @@ BEGIN ORDER BY node_type; END; $func$ LANGUAGE plpgsql; + +-- For all nodes, returns database properties of given database, except +-- oid, datfrozenxid and datminmxid. +-- +-- Also returns whether the node has a pg_dist_object record for the database +-- and whether there are any stale pg_dist_object records for a database. +CREATE OR REPLACE FUNCTION check_database_on_all_nodes(p_database_name text) +RETURNS TABLE (node_type text, result text) +AS $func$ +DECLARE + pg_ge_15_options text := ''; + pg_ge_16_options text := ''; +BEGIN + IF EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'pg_database'::regclass AND attname = 'datlocprovider') THEN + pg_ge_15_options := ', daticulocale, datcollversion, datlocprovider'; + ELSE + pg_ge_15_options := $$, null as daticulocale, null as datcollversion, 'c' as datlocprovider$$; + END IF; + + IF EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'pg_database'::regclass AND attname = 'daticurules') THEN + pg_ge_16_options := ', daticurules'; + ELSE + pg_ge_16_options := ', null as daticurules'; + END IF; + + RETURN QUERY + SELECT + CASE WHEN (groupid = 0 AND groupid = (SELECT groupid FROM pg_dist_local_group)) THEN 'coordinator (local)' + WHEN (groupid = 0) THEN 'coordinator (remote)' + WHEN (groupid = (SELECT groupid FROM pg_dist_local_group)) THEN 'worker node (local)' + ELSE 'worker node (remote)' + END AS node_type, + q2.result + FROM run_command_on_all_nodes( + format( + $$ + SELECT to_jsonb(q.*) + FROM ( + SELECT + ( + SELECT to_jsonb(database_properties.*) + FROM ( + SELECT datname, pa.rolname as database_owner, + pg_encoding_to_char(pd.encoding) as encoding, + datistemplate, datallowconn, datconnlimit, datacl, + pt.spcname AS tablespace, datcollate, datctype + %2$s -- >= pg15 options + %3$s -- >= pg16 options + FROM pg_database pd + JOIN pg_authid pa ON pd.datdba = pa.oid + JOIN pg_tablespace pt ON pd.dattablespace = pt.oid + WHERE datname = '%1$s' + ) database_properties + ) AS database_properties, + ( + SELECT COUNT(*)=1 + FROM pg_dist_object WHERE objid = (SELECT oid FROM pg_database WHERE datname = '%1$s') + ) AS pg_dist_object_record_for_db_exists, + ( + SELECT COUNT(*) > 0 + FROM pg_dist_object + WHERE classid = 1262 AND objid NOT IN (SELECT oid FROM pg_database) + ) AS stale_pg_dist_object_record_for_a_db_exists + ) q + $$, + p_database_name, pg_ge_15_options, pg_ge_16_options + ) + ) q2 + JOIN pg_dist_node USING (nodeid); +END; +$func$ LANGUAGE plpgsql;