/*------------------------------------------------------------------------- * * database.c * Commands to interact with the database object in a distributed * environment. * * Copyright (c) Citus Data, Inc. * *------------------------------------------------------------------------- */ #include "postgres.h" #include "miscadmin.h" #include "access/genam.h" #include "access/heapam.h" #include "access/htup_details.h" #include "access/table.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 "commands/defrem.h" #include "nodes/parsenodes.h" #include "utils/builtins.h" #include "utils/fmgroids.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/serialize_distributed_ddls.h" #include "distributed/commands/utility_hook.h" #include "distributed/comment.h" #include "distributed/deparse_shard_query.h" #include "distributed/deparser.h" #include "distributed/listutils.h" #include "distributed/local_executor.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/shard_cleaner.h" #include "distributed/worker_protocol.h" #include "distributed/worker_transaction.h" /* * Used to save original name of the database before it is replaced with a * temporary name for failure handling purposes in PreprocessCreateDatabaseStmt(). */ static char *CreateDatabaseCommandOriginalDbName = NULL; /* * The format string used when creating a temporary databases for failure * handling purposes. * * The fields are as follows to ensure using a unique name for each temporary * database: * - operationId: The operation id returned by RegisterOperationNeedingCleanup(). * - groupId: The group id of the worker node where CREATE DATABASE command * is issued from. */ #define TEMP_DATABASE_NAME_FMT "citus_temp_database_%lu_%d" /* * DatabaseCollationInfo is used to store collation related information of a database. */ typedef struct DatabaseCollationInfo { char *datcollate; char *datctype; char *daticulocale; char *datcollversion; #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 char * GetLocaleProviderString(char datlocprovider); static char * GetTablespaceName(Oid tablespaceOid); static ObjectAddress * GetDatabaseAddressFromDatabaseName(char *databaseName, bool missingOk); static List * FilterDistributedDatabases(List *databases); 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. */ List * AlterDatabaseOwnerObjectAddress(Node *node, bool missing_ok, bool isPostprocess) { AlterOwnerStmt *stmt = castNode(AlterOwnerStmt, node); Assert(stmt->objectType == OBJECT_DATABASE); Oid databaseOid = get_database_oid(strVal((String *) stmt->object), missing_ok); ObjectAddress *address = palloc0(sizeof(ObjectAddress)); ObjectAddressSet(*address, DatabaseRelationId, databaseOid); return list_make1(address); } /* * DatabaseOwnerDDLCommands returns a list of sql statements to idempotently apply a * change of the database owner on the workers so that the database is owned by the same * user on all nodes in the cluster. */ List * DatabaseOwnerDDLCommands(const ObjectAddress *address) { Node *stmt = (Node *) RecreateAlterDatabaseOwnerStmt(address->objectId); return list_make1(DeparseTreeNode(stmt)); } /* * RecreateAlterDatabaseOwnerStmt creates an AlterOwnerStmt that represents the operation * of changing the owner of the database to its current owner. */ static AlterOwnerStmt * RecreateAlterDatabaseOwnerStmt(Oid databaseOid) { AlterOwnerStmt *stmt = makeNode(AlterOwnerStmt); stmt->objectType = OBJECT_DATABASE; stmt->object = (Node *) makeString(get_database_name(databaseOid)); Oid ownerOid = get_database_owner(databaseOid); stmt->newowner = makeNode(RoleSpec); stmt->newowner->roletype = ROLESPEC_CSTRING; stmt->newowner->rolename = GetUserNameFromId(ownerOid, false); return stmt; } /* * get_database_owner returns the Oid of the role owning the database */ static Oid get_database_owner(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))); } Oid dba = ((Form_pg_database) GETSTRUCT(tuple))->datdba; ReleaseSysCache(tuple); return dba; } /* * PreprocessGrantOnDatabaseStmt is executed before 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 grant * on databases. */ List * PreprocessGrantOnDatabaseStmt(Node *node, const char *queryString, ProcessUtilityContext processUtilityContext) { if (!ShouldPropagate()) { return NIL; } GrantStmt *stmt = castNode(GrantStmt, node); Assert(stmt->objtype == OBJECT_DATABASE); List *distributedDatabases = FilterDistributedDatabases(stmt->objects); if (list_length(distributedDatabases) == 0) { return NIL; } EnsureCoordinator(); List *originalObjects = stmt->objects; stmt->objects = distributedDatabases; char *sql = DeparseTreeNode((Node *) stmt); stmt->objects = originalObjects; List *commands = list_make3(DISABLE_DDL_PROPAGATION, (void *) sql, ENABLE_DDL_PROPAGATION); return NodeDDLTaskList(NON_COORDINATOR_NODES, commands); } /* * FilterDistributedDatabases filters the database list and returns the distributed ones, * as a list. */ static List * FilterDistributedDatabases(List *databases) { List *distributedDatabases = NIL; String *databaseName = NULL; foreach_declared_ptr(databaseName, databases) { bool missingOk = true; ObjectAddress *dbAddress = GetDatabaseAddressFromDatabaseName(strVal(databaseName), missingOk); if (IsAnyObjectDistributed(list_make1(dbAddress))) { distributedDatabases = lappend(distributedDatabases, databaseName); } } return distributedDatabases; } /* * IsSetTablespaceStatement returns true if the statement is a SET TABLESPACE statement, * false otherwise. */ static bool IsSetTablespaceStatement(AlterDatabaseStmt *stmt) { DefElem *def = NULL; foreach_declared_ptr(def, stmt->options) { if (strcmp(def->defname, "tablespace") == 0) { return true; } } return false; } /* * PreprocessAlterDatabaseStmt is executed before 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 grant * on databases. * * We also serialize database commands globally by acquiring a Citus specific advisory * lock based on OCLASS_DATABASE on the first primary worker node. */ List * PreprocessAlterDatabaseStmt(Node *node, const char *queryString, ProcessUtilityContext processUtilityContext) { bool missingOk = false; AlterDatabaseStmt *stmt = castNode(AlterDatabaseStmt, node); ObjectAddress *dbAddress = GetDatabaseAddressFromDatabaseName(stmt->dbname, missingOk); if (!ShouldPropagate() || !IsAnyObjectDistributed(list_make1(dbAddress))) { return NIL; } EnsureCoordinator(); SerializeDistributedDDLsOnObjectClassObject(OCLASS_DATABASE, stmt->dbname); char *sql = DeparseTreeNode((Node *) stmt); List *commands = list_make3(DISABLE_DDL_PROPAGATION, sql, ENABLE_DDL_PROPAGATION); if (IsSetTablespaceStatement(stmt)) { /* * Set tablespace does not work inside a transaction.Therefore, we need to use * NontransactionalNodeDDLTask to run the command on the workers outside * the transaction block. */ bool warnForPartialFailure = true; return NontransactionalNodeDDLTaskList(NON_COORDINATOR_NODES, commands, warnForPartialFailure); } else { return NodeDDLTaskList(NON_COORDINATOR_NODES, commands); } } /* * PreprocessAlterDatabaseRefreshCollStmt is executed before 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 grant * on databases. * * We also serialize database commands globally by acquiring a Citus specific advisory * lock based on OCLASS_DATABASE on the first primary worker node. */ List * PreprocessAlterDatabaseRefreshCollStmt(Node *node, const char *queryString, ProcessUtilityContext processUtilityContext) { bool missingOk = true; AlterDatabaseRefreshCollStmt *stmt = castNode(AlterDatabaseRefreshCollStmt, node); ObjectAddress *dbAddress = GetDatabaseAddressFromDatabaseName(stmt->dbname, missingOk); if (!ShouldPropagate() || !IsAnyObjectDistributed(list_make1(dbAddress))) { return NIL; } EnsureCoordinator(); SerializeDistributedDDLsOnObjectClassObject(OCLASS_DATABASE, stmt->dbname); char *sql = DeparseTreeNode((Node *) stmt); List *commands = list_make3(DISABLE_DDL_PROPAGATION, (void *) sql, ENABLE_DDL_PROPAGATION); return NodeDDLTaskList(NON_COORDINATOR_NODES, commands); } /* * PreprocessAlterDatabaseRenameStmt is executed before the statement is applied to * the local postgres instance. * * We also serialize database commands globally by acquiring a Citus specific advisory * lock based on OCLASS_DATABASE on the first primary worker node. * * We acquire this lock here instead of PostprocessAlterDatabaseRenameStmt because the * command renames the database and SerializeDistributedDDLsOnObjectClass resolves the * object on workers based on database name. For this reason, we need to acquire the lock * before the command is applied to the local postgres instance. */ List * PreprocessAlterDatabaseRenameStmt(Node *node, const char *queryString, ProcessUtilityContext processUtilityContext) { bool missingOk = true; RenameStmt *stmt = castNode(RenameStmt, node); ObjectAddress *dbAddress = GetDatabaseAddressFromDatabaseName(stmt->subname, missingOk); if (!ShouldPropagate() || !IsAnyObjectDistributed(list_make1(dbAddress))) { return NIL; } EnsureCoordinator(); /* * Different than other ALTER DATABASE commands, we first acquire a lock * by providing InvalidOid because we want ALTER TABLE .. RENAME TO .. * commands to block not only with ALTER DATABASE operations but also * with CREATE DATABASE operations because they might cause name conflicts * and that could also cause deadlocks too. */ SerializeDistributedDDLsOnObjectClass(OCLASS_DATABASE); SerializeDistributedDDLsOnObjectClassObject(OCLASS_DATABASE, stmt->subname); return NIL; } /* * PostprocessAlterDatabaseRenameStmt is executed after the statement is applied to the local * postgres instance. In this stage we prepare ALTER DATABASE RENAME statement to be run on * all workers. */ List * PostprocessAlterDatabaseRenameStmt(Node *node, const char *queryString) { bool missingOk = false; RenameStmt *stmt = castNode(RenameStmt, node); ObjectAddress *dbAddress = GetDatabaseAddressFromDatabaseName(stmt->newname, missingOk); if (!ShouldPropagate() || !IsAnyObjectDistributed(list_make1(dbAddress))) { return NIL; } EnsureCoordinator(); char *sql = DeparseTreeNode((Node *) stmt); List *commands = list_make3(DISABLE_DDL_PROPAGATION, (void *) sql, ENABLE_DDL_PROPAGATION); return NodeDDLTaskList(NON_COORDINATOR_NODES, commands); } /* * PreprocessAlterDatabaseSetStmt is executed before 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 grant * on databases. * * We also serialize database commands globally by acquiring a Citus specific advisory * lock based on OCLASS_DATABASE on the first primary worker node. */ List * PreprocessAlterDatabaseSetStmt(Node *node, const char *queryString, ProcessUtilityContext processUtilityContext) { AlterDatabaseSetStmt *stmt = castNode(AlterDatabaseSetStmt, node); bool missingOk = true; ObjectAddress *dbAddress = GetDatabaseAddressFromDatabaseName(stmt->dbname, missingOk); if (!ShouldPropagate() || !IsAnyObjectDistributed(list_make1(dbAddress))) { return NIL; } EnsureCoordinator(); SerializeDistributedDDLsOnObjectClassObject(OCLASS_DATABASE, stmt->dbname); char *sql = DeparseTreeNode((Node *) stmt); List *commands = list_make3(DISABLE_DDL_PROPAGATION, (void *) sql, ENABLE_DDL_PROPAGATION); return NodeDDLTaskList(NON_COORDINATOR_NODES, commands); } /* * PreprocessCreateDatabaseStmt 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. Also in this stage, we save the original * database name and replace dbname field with a temporary name for failure handling * purposes. We let Postgres create the database with the temporary name, insert a cleanup * record for the temporary database name on all nodes and let PostprocessCreateDatabaseStmt() * to return the distributed DDL job that both creates the database with the temporary name * and then renames it back to its original name. * * We also serialize database commands globally by acquiring a Citus specific advisory * lock based on OCLASS_DATABASE on the first primary worker node. */ List * PreprocessCreateDatabaseStmt(Node *node, const char *queryString, ProcessUtilityContext processUtilityContext) { if (!EnableCreateDatabasePropagation || !ShouldPropagate()) { return NIL; } EnsureCoordinatorIsInMetadata(); CreatedbStmt *stmt = castNode(CreatedbStmt, node); EnsureSupportedCreateDatabaseCommand(stmt); SerializeDistributedDDLsOnObjectClass(OCLASS_DATABASE); OperationId operationId = RegisterOperationNeedingCleanup(); char *tempDatabaseName = psprintf(TEMP_DATABASE_NAME_FMT, operationId, GetLocalGroupId()); List *remoteNodes = TargetWorkerSetNodeList(ALL_SHARD_NODES, RowShareLock); WorkerNode *remoteNode = NULL; foreach_declared_ptr(remoteNode, remoteNodes) { InsertCleanupRecordOutsideTransaction( CLEANUP_OBJECT_DATABASE, pstrdup(quote_identifier(tempDatabaseName)), remoteNode->groupId, CLEANUP_ON_FAILURE ); } CreateDatabaseCommandOriginalDbName = stmt->dbname; stmt->dbname = tempDatabaseName; /* * Delete cleanup records in the same transaction so that if the current * transactions fails for some reason, then the cleanup records won't be * deleted. In the happy path, we will delete the cleanup records without * deferring them to the background worker. */ FinalizeOperationNeedingCleanupOnSuccess("create database"); return NIL; } /* * PostprocessCreateDatabaseStmt is executed after the statement is applied to the local * postgres instance. * * In this stage, we first rename the temporary database back to its original name for * local node and then return a list of distributed DDL jobs to create the database with * the temporary name and then to rename it back to its original name. That way, if CREATE * DATABASE fails on any of the nodes, the temporary database will be cleaned up by the * cleanup records that we inserted in PreprocessCreateDatabaseStmt() and in case of a * failure, we won't leak any databases called as the name that user intended to use for * the database. */ List * PostprocessCreateDatabaseStmt(Node *node, const char *queryString) { if (!EnableCreateDatabasePropagation || !ShouldPropagate()) { return NIL; } EnsurePropagationToCoordinator(); /* * 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 *createDatabaseCommands = list_make3(DISABLE_DDL_PROPAGATION, (void *) createDatabaseCommand, ENABLE_DDL_PROPAGATION); /* * Since the CREATE DATABASE statements cannot be executed in a transaction * block, we need to use NontransactionalNodeDDLTaskList() to send the CREATE * DATABASE statement to the workers. */ bool warnForPartialFailure = false; List *createDatabaseDDLJobList = NontransactionalNodeDDLTaskList(REMOTE_NODES, createDatabaseCommands, warnForPartialFailure); CreatedbStmt *stmt = castNode(CreatedbStmt, node); char *renameDatabaseCommand = psprintf("ALTER DATABASE %s RENAME TO %s", quote_identifier(stmt->dbname), quote_identifier(CreateDatabaseCommandOriginalDbName)); List *renameDatabaseCommands = list_make3(DISABLE_DDL_PROPAGATION, renameDatabaseCommand, ENABLE_DDL_PROPAGATION); /* * We use NodeDDLTaskList() to send the RENAME DATABASE statement to the * workers because we want to execute it in a coordinated transaction. */ List *renameDatabaseDDLJobList = NodeDDLTaskList(REMOTE_NODES, renameDatabaseCommands); /* * Temporarily disable citus.enable_ddl_propagation before issuing * rename command locally because we don't want to execute it on remote * nodes yet. We will execute it on remote nodes by returning it as a * distributed DDL job. * * The reason why we don't want to execute it on remote nodes yet is that * the database is not created on remote nodes yet. */ 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); ExecuteUtilityCommand(renameDatabaseCommand); AtEOXact_GUC(true, saveNestLevel); /* * Restore the original database name because MarkObjectDistributed() * resolves oid of the object based on the database name and is called * after executing the distributed DDL job that renames temporary database. */ stmt->dbname = CreateDatabaseCommandOriginalDbName; return list_concat(createDatabaseDDLJobList, renameDatabaseDDLJobList); } /* * PreprocessDropDatabaseStmt is executed before 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. * * We also serialize database commands globally by acquiring a Citus specific advisory * lock based on OCLASS_DATABASE on the first primary worker node. */ List * PreprocessDropDatabaseStmt(Node *node, const char *queryString, ProcessUtilityContext processUtilityContext) { if (!EnableCreateDatabasePropagation || !ShouldPropagate()) { return NIL; } EnsurePropagationToCoordinator(); 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; } SerializeDistributedDDLsOnObjectClassObject(OCLASS_DATABASE, stmt->dbname); char *dropDatabaseCommand = DeparseTreeNode(node); List *dropDatabaseCommands = list_make3(DISABLE_DDL_PROPAGATION, (void *) dropDatabaseCommand, ENABLE_DDL_PROPAGATION); /* * Due to same reason stated in PostprocessCreateDatabaseStmt(), we need to * use NontransactionalNodeDDLTaskList() to send the DROP DATABASE statement * to the workers. */ bool warnForPartialFailure = true; List *dropDatabaseDDLJobList = NontransactionalNodeDDLTaskList(REMOTE_NODES, dropDatabaseCommands, warnForPartialFailure); return dropDatabaseDDLJobList; } /* * 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_declared_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; } /* * GetDatabaseMetadataSyncCommands returns a list of sql statements * for the given database id. The list contains the database ddl command, * grant commands and comment propagation commands. */ List * GetDatabaseMetadataSyncCommands(Oid dbOid) { char *databaseName = get_database_name(dbOid); char *databaseDDLCommand = CreateDatabaseDDLCommand(dbOid); List *ddlCommands = list_make1(databaseDDLCommand); List *grantDDLCommands = GrantOnDatabaseDDLCommands(dbOid); List *commentDDLCommands = GetCommentPropagationCommands(DatabaseRelationId, dbOid, databaseName, OBJECT_DATABASE); ddlCommands = list_concat(ddlCommands, grantDDLCommands); ddlCommands = list_concat(ddlCommands, commentDDLCommands); return ddlCommands; } /* * 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); Datum icuLocaleDatum = heap_getattr(tup, Anum_pg_database_datlocale, 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); } #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; } /* * 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))); } } } /* * 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 (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))); #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 citus_internal.database_command(%s)", quote_literal_cstr(createStmt)); ReleaseSysCache(tuple); return outerDbStmt->data; }