Database-level collation version tracking
This patch adds to database objects the same version tracking that
collation objects have. There is a new pg_database column
datcollversion that stores the version, a new function
pg_database_collation_actual_version() to get the version from the
operating system, and a new subcommand ALTER DATABASE ... REFRESH
COLLATION VERSION.
This was not originally added together with pg_collation.collversion,
since originally version tracking was only supported for ICU, and ICU on
a database-level is not currently supported. But we now have version
tracking for glibc (since PG13), FreeBSD (since PG14), and Windows
(since PG13), so this is useful to have now. And of course ICU on
database-level is being worked on at the moment as well.
This patch is pretty much complete AFAICT. One bonus thing would be to
add a query to the ALTER DATABASE ref page similar to the one on the
ALTER COLLATION ref page that identifies the objects that are affected
by outdated collations. The database version of that might just show
all collation-using objects or something like that. Suggestions welcome.
Also, one curious behavior is that if you get to a situation where the
collation version is mismatched, every time an autovacuum worker
launches you get the collation version mismatch warning in the log.
Maybe that's actually correct, but maybe we want to dial it down a bit
for non-interactive sessions.
Attachments:
0001-Database-level-collation-version-tracking.patchtext/plain; charset=UTF-8; name=0001-Database-level-collation-version-tracking.patchDownload
From c413bd65fb3dbbfea6c724970709501f73946513 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Tue, 1 Feb 2022 16:09:30 +0100
Subject: [PATCH] Database-level collation version tracking
This adds to database objects the same version tracking that collation
objects have. There is a new pg_database column datcollversion that
stores the version, a new function
pg_database_collation_actual_version() to get the version from the
operating system, and a new subcommand ALTER DATABASE ... REFRESH
COLLATION VERSION.
This was not originally added together with pg_collation.collversion,
since originally version tracking was only supported for ICU, and ICU
on a database-level is not currently supported. But we now have
version tracking for glibc (since PG13), FreeBSD (since PG14), and
Windows (since PG13), so this is useful to have now.
---
doc/src/sgml/catalogs.sgml | 11 ++
doc/src/sgml/func.sgml | 18 ++++
doc/src/sgml/ref/alter_collation.sgml | 3 +-
doc/src/sgml/ref/alter_database.sgml | 12 +++
src/backend/commands/dbcommands.c | 146 ++++++++++++++++++++++++--
src/backend/parser/gram.y | 6 ++
src/backend/tcop/utility.c | 14 +--
src/backend/utils/init/postinit.c | 33 ++++++
src/bin/initdb/initdb.c | 12 +++
src/bin/pg_dump/pg_dump.c | 20 ++++
src/bin/psql/tab-complete.c | 2 +-
src/include/catalog/pg_database.h | 3 +
src/include/catalog/pg_proc.dat | 5 +
src/include/commands/dbcommands.h | 1 +
src/include/nodes/nodes.h | 1 +
src/include/nodes/parsenodes.h | 6 ++
16 files changed, 279 insertions(+), 14 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 7d5b0b1656..8d6f33f88c 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3043,6 +3043,17 @@ <title><structname>pg_database</structname> Columns</title>
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>datcollversion</structfield> <type>text</type>
+ </para>
+ <para>
+ Provider-specific version of the collation. This is recorded when the
+ database is created and then checked when it is used, to detect
+ changes in the collation definition that could lead to data corruption.
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>datacl</structfield> <type>aclitem[]</type>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8754f2f89b..49644666bc 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27061,6 +27061,24 @@ <title>Collation Management Functions</title>
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_database_collation_actual_version</primary>
+ </indexterm>
+ <function>pg_database_collation_actual_version</function> ( <type>oid</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the actual version of the database's collation as it is currently
+ installed in the operating system. If this is different from the
+ value in
+ <structname>pg_database</structname>.<structfield>datcollversion</structfield>,
+ then objects depending on the collation might need to be rebuilt. See
+ also <xref linkend="sql-alterdatabase"/>.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/alter_collation.sgml b/doc/src/sgml/ref/alter_collation.sgml
index 892c466565..a8c831d728 100644
--- a/doc/src/sgml/ref/alter_collation.sgml
+++ b/doc/src/sgml/ref/alter_collation.sgml
@@ -151,7 +151,8 @@ <title>Notes</title>
</para>
</note>
<para>
- Currently, there is no version tracking for the database default collation.
+ For the database default collation, there is an analogous command
+ <literal>ALTER DATABASE ... REFRESH COLLATION VERSION</literal>.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_database.sgml b/doc/src/sgml/ref/alter_database.sgml
index 81e37536a3..89ed261b4c 100644
--- a/doc/src/sgml/ref/alter_database.sgml
+++ b/doc/src/sgml/ref/alter_database.sgml
@@ -35,6 +35,8 @@
ALTER DATABASE <replaceable class="parameter">name</replaceable> SET TABLESPACE <replaceable class="parameter">new_tablespace</replaceable>
+ALTER DATABASE <replaceable class="parameter">name</replaceable> REFRESH COLLATION VERSION
+
ALTER DATABASE <replaceable class="parameter">name</replaceable> SET <replaceable>configuration_parameter</replaceable> { TO | = } { <replaceable>value</replaceable> | DEFAULT }
ALTER DATABASE <replaceable class="parameter">name</replaceable> SET <replaceable>configuration_parameter</replaceable> FROM CURRENT
ALTER DATABASE <replaceable class="parameter">name</replaceable> RESET <replaceable>configuration_parameter</replaceable>
@@ -171,6 +173,16 @@ <title>Parameters</title>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>REFRESH COLLATION VERSION</literal></term>
+ <listitem>
+ <para>
+ Update the database collation version. See <xref
+ linkend="sql-altercollation-notes"/> for background.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><replaceable>configuration_parameter</replaceable></term>
<term><replaceable>value</replaceable></term>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 5a6c9beaa4..75d45c8145 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -36,6 +36,7 @@
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_collation.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
#include "catalog/pg_subscription.h"
@@ -85,7 +86,8 @@ static bool get_db_info(const char *name, LOCKMODE lockmode,
Oid *dbIdP, Oid *ownerIdP,
int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
- Oid *dbTablespace, char **dbCollate, char **dbCtype);
+ Oid *dbTablespace, char **dbCollate, char **dbCtype,
+ char **dbCollversion);
static bool have_createdb_privilege(void);
static void remove_dbtablespaces(Oid db_id);
static bool check_db_file_conflict(Oid db_id);
@@ -105,6 +107,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
int src_encoding = -1;
char *src_collate = NULL;
char *src_ctype = NULL;
+ char *src_collversion = NULL;
bool src_istemplate;
bool src_allowconn;
TransactionId src_frozenxid = InvalidTransactionId;
@@ -128,6 +131,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
DefElem *distemplate = NULL;
DefElem *dallowconnections = NULL;
DefElem *dconnlimit = NULL;
+ DefElem *dcollversion = NULL;
char *dbname = stmt->dbname;
char *dbowner = NULL;
const char *dbtemplate = NULL;
@@ -138,6 +142,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
bool dbistemplate = false;
bool dballowconnections = true;
int dbconnlimit = -1;
+ char *dbcollversion = NULL;
int notherbackends;
int npreparedxacts;
createdb_failure_params fparms;
@@ -207,6 +212,12 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
errorConflictingDefElem(defel, pstate);
dconnlimit = defel;
}
+ else if (strcmp(defel->defname, "collation_version") == 0)
+ {
+ if (dcollversion)
+ errorConflictingDefElem(defel, pstate);
+ dcollversion = defel;
+ }
else if (strcmp(defel->defname, "location") == 0)
{
ereport(WARNING,
@@ -305,6 +316,8 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("invalid connection limit: %d", dbconnlimit)));
}
+ if (dcollversion)
+ dbcollversion = defGetString(dcollversion);
/* obtain OID of proposed owner */
if (dbowner)
@@ -342,7 +355,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
&src_dboid, &src_owner, &src_encoding,
&src_istemplate, &src_allowconn,
&src_frozenxid, &src_minmxid, &src_deftablespace,
- &src_collate, &src_ctype))
+ &src_collate, &src_ctype, &src_collversion))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("template database \"%s\" does not exist",
@@ -368,6 +381,8 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
dbcollate = src_collate;
if (dbctype == NULL)
dbctype = src_ctype;
+ if (dbcollversion == NULL)
+ dbcollversion = src_collversion;
/* Some encodings are client only */
if (!PG_VALID_BE_ENCODING(encoding))
@@ -424,6 +439,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
errhint("Use the same LC_CTYPE as in the template database, or use template0 as template.")));
}
+ /*
+ * Normally, we copy the collation version from the template database.
+ * This last resort only applies if the template database does not have a
+ * collation version, which is normally only the case for template0.
+ */
+ if (!dbcollversion)
+ dbcollversion = get_collation_actual_version(COLLPROVIDER_LIBC, dbcollate);
+
/* Resolve default tablespace for new database */
if (dtablespacename && dtablespacename->arg)
{
@@ -578,6 +601,10 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
new_record[Anum_pg_database_datfrozenxid - 1] = TransactionIdGetDatum(src_frozenxid);
new_record[Anum_pg_database_datminmxid - 1] = TransactionIdGetDatum(src_minmxid);
new_record[Anum_pg_database_dattablespace - 1] = ObjectIdGetDatum(dst_deftablespace);
+ if (dbcollversion)
+ new_record[Anum_pg_database_datcollversion - 1] = CStringGetTextDatum(dbcollversion);
+ else
+ new_record_nulls[Anum_pg_database_datcollversion - 1] = true;
/*
* We deliberately set datacl to default (NULL), rather than copying it
@@ -844,7 +871,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
- &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL))
+ &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
{
if (!missing_ok)
{
@@ -1043,7 +1070,7 @@ RenameDatabase(const char *oldname, const char *newname)
rel = table_open(DatabaseRelationId, RowExclusiveLock);
if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL,
- NULL, NULL, NULL, NULL, NULL, NULL, NULL))
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("database \"%s\" does not exist", oldname)));
@@ -1156,7 +1183,7 @@ movedb(const char *dbname, const char *tblspcname)
pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
- NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL))
+ NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL, NULL))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("database \"%s\" does not exist", dbname)));
@@ -1643,6 +1670,76 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
}
+/*
+ * ALTER DATABASE name REFRESH COLLATION VERSION
+ */
+Oid
+AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt)
+{
+ Relation rel;
+ Oid dboid;
+ HeapTuple tup;
+ Datum datum;
+ bool isnull;
+ char *oldversion;
+ char *newversion;
+
+ rel = table_open(DatabaseRelationId, RowExclusiveLock);
+ dboid = get_database_oid(stmt->dbname, false);
+
+ if (!pg_database_ownercheck(dboid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE,
+ stmt->dbname);
+
+ tup = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(dboid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for database %u", dboid);
+
+ datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollversion, &isnull);
+ oldversion = isnull ? NULL : TextDatumGetCString(datum);
+
+ datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollate, &isnull);
+ Assert(!isnull);
+ newversion = get_collation_actual_version(COLLPROVIDER_LIBC, TextDatumGetCString(datum));
+
+ /* cannot change from NULL to non-NULL or vice versa */
+ if ((!oldversion && newversion) || (oldversion && !newversion))
+ elog(ERROR, "invalid collation version change");
+ else if (oldversion && newversion && strcmp(newversion, oldversion) != 0)
+ {
+ bool nulls[Natts_pg_database];
+ bool replaces[Natts_pg_database];
+ Datum values[Natts_pg_database];
+
+ ereport(NOTICE,
+ (errmsg("changing version from %s to %s",
+ oldversion, newversion)));
+
+ memset(values, 0, sizeof(values));
+ memset(nulls, false, sizeof(nulls));
+ memset(replaces, false, sizeof(replaces));
+
+ values[Anum_pg_database_datcollversion - 1] = CStringGetTextDatum(newversion);
+ replaces[Anum_pg_database_datcollversion - 1] = true;
+
+ tup = heap_modify_tuple(tup, RelationGetDescr(rel),
+ values, nulls, replaces);
+ }
+ else
+ ereport(NOTICE,
+ (errmsg("version has not changed")));
+
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ InvokeObjectPostAlterHook(DatabaseRelationId, dboid, 0);
+
+ heap_freetuple(tup);
+ table_close(rel, NoLock);
+
+ return dboid;
+}
+
+
/*
* ALTER DATABASE name SET ...
*/
@@ -1785,6 +1882,34 @@ AlterDatabaseOwner(const char *dbname, Oid newOwnerId)
}
+Datum
+pg_database_collation_actual_version(PG_FUNCTION_ARGS)
+{
+ Oid dbid = PG_GETARG_OID(0);
+ HeapTuple tp;
+ Datum datum;
+ bool isnull;
+ char *version;
+
+ tp = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbid));
+ if (!HeapTupleIsValid(tp))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("database with OID %u does not exist", dbid)));
+
+ datum = SysCacheGetAttr(DATABASEOID, tp, Anum_pg_database_datcollate, &isnull);
+ Assert(!isnull);
+ version = get_collation_actual_version(COLLPROVIDER_LIBC, TextDatumGetCString(datum));
+
+ ReleaseSysCache(tp);
+
+ if (version)
+ PG_RETURN_TEXT_P(cstring_to_text(version));
+ else
+ PG_RETURN_NULL();
+}
+
+
/*
* Helper functions
*/
@@ -1800,7 +1925,8 @@ get_db_info(const char *name, LOCKMODE lockmode,
Oid *dbIdP, Oid *ownerIdP,
int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
- Oid *dbTablespace, char **dbCollate, char **dbCtype)
+ Oid *dbTablespace, char **dbCollate, char **dbCtype,
+ char **dbCollversion)
{
bool result = false;
Relation relation;
@@ -1905,6 +2031,14 @@ get_db_info(const char *name, LOCKMODE lockmode,
Assert(!isnull);
*dbCtype = TextDatumGetCString(datum);
}
+ if (dbCollversion)
+ {
+ datum = SysCacheGetAttr(DATABASEOID, tuple, Anum_pg_database_datcollversion, &isnull);
+ if (isnull)
+ *dbCollversion = NULL;
+ else
+ *dbCollversion = TextDatumGetCString(datum);
+ }
ReleaseSysCache(tuple);
result = true;
break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index b5966712ce..041771f8b7 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10454,6 +10454,12 @@ AlterDatabaseStmt:
(Node *)makeString($6), @6));
$$ = (Node *)n;
}
+ | ALTER DATABASE name REFRESH COLLATION VERSION_P
+ {
+ AlterDatabaseRefreshCollStmt *n = makeNode(AlterDatabaseRefreshCollStmt);
+ n->dbname = $3;
+ $$ = (Node *)n;
+ }
;
AlterDatabaseSetStmt:
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 83e4e37c78..3780c6e812 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -136,6 +136,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
switch (nodeTag(parsetree))
{
case T_AlterCollationStmt:
+ case T_AlterDatabaseRefreshCollStmt:
case T_AlterDatabaseSetStmt:
case T_AlterDatabaseStmt:
case T_AlterDefaultPrivilegesStmt:
@@ -779,6 +780,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
AlterDatabase(pstate, (AlterDatabaseStmt *) parsetree, isTopLevel);
break;
+ case T_AlterDatabaseRefreshCollStmt:
+ /* no event triggers for global objects */
+ AlterDatabaseRefreshColl((AlterDatabaseRefreshCollStmt *) parsetree);
+ break;
+
case T_AlterDatabaseSetStmt:
/* no event triggers for global objects */
AlterDatabaseSet((AlterDatabaseSetStmt *) parsetree);
@@ -2801,9 +2807,7 @@ CreateCommandTag(Node *parsetree)
break;
case T_AlterDatabaseStmt:
- tag = CMDTAG_ALTER_DATABASE;
- break;
-
+ case T_AlterDatabaseRefreshCollStmt:
case T_AlterDatabaseSetStmt:
tag = CMDTAG_ALTER_DATABASE;
break;
@@ -3444,9 +3448,7 @@ GetCommandLogLevel(Node *parsetree)
break;
case T_AlterDatabaseStmt:
- lev = LOGSTMT_DDL;
- break;
-
+ case T_AlterDatabaseRefreshCollStmt:
case T_AlterDatabaseSetStmt:
lev = LOGSTMT_DDL;
break;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 5b9ed2f6f5..228bb63f97 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -31,6 +31,7 @@
#include "catalog/catalog.h"
#include "catalog/namespace.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_collation.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
#include "catalog/pg_tablespace.h"
@@ -414,6 +415,38 @@ CheckMyDatabase(const char *name, bool am_superuser, bool override_allow_connect
" which is not recognized by setlocale().", ctype),
errhint("Recreate the database with another locale or install the missing locale.")));
+ /*
+ * Check collation version. See similar code in
+ * pg_newlocale_from_collation().
+ */
+ datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollversion,
+ &isnull);
+ if (!isnull)
+ {
+ char *actual_versionstr;
+ char *collversionstr;
+
+ collversionstr = TextDatumGetCString(datum);
+
+ actual_versionstr = get_collation_actual_version(COLLPROVIDER_LIBC, collate);
+ if (!actual_versionstr)
+ ereport(ERROR,
+ (errmsg("database \"%s\" has no actual collation version, but a version was specified",
+ name)));
+
+ if (strcmp(actual_versionstr, collversionstr) != 0)
+ ereport(WARNING,
+ (errmsg("database \"%s\" has a collation version mismatch",
+ name),
+ errdetail("The database was created using collation version %s, "
+ "but the operating system provides version %s.",
+ collversionstr, actual_versionstr),
+ errhint("Rebuild all objects affected by collation in this database and run "
+ "ALTER DATABASE %s REFRESH COLLATION VERSION, "
+ "or build PostgreSQL with the right library version.",
+ quote_identifier(name))));
+ }
+
/* Make the locale settings visible as GUC variables, too */
SetConfigOption("lc_collate", collate, PGC_INTERNAL, PGC_S_OVERRIDE);
SetConfigOption("lc_ctype", ctype, PGC_INTERNAL, PGC_S_OVERRIDE);
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index d78e8e67b8..6e70d1b724 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -1857,6 +1857,18 @@ make_template0(FILE *cmdfd)
"CREATE DATABASE template0 IS_TEMPLATE = true ALLOW_CONNECTIONS = false OID = "
CppAsString2(Template0ObjectId) ";\n\n",
+ /*
+ * template0 shouldn't have any collation-dependent objects, so unset
+ * the collation version. This avoids warnings when making a new
+ * database from it.
+ */
+ "UPDATE pg_database SET datcollversion = NULL WHERE datname = 'template0';\n\n",
+
+ /*
+ * While we are here, do set the collation version on template1.
+ */
+ "UPDATE pg_database SET datcollversion = pg_database_collation_actual_version(oid) WHERE datname = 'template1';\n\n",
+
/*
* Explicitly revoke public create-schema and create-temp-table
* privileges in template1 and template0; else the latter would be on
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e3ddf19959..e75dcee62c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2761,6 +2761,7 @@ dumpDatabase(Archive *fout)
i_acldefault,
i_datistemplate,
i_datconnlimit,
+ i_datcollversion,
i_tablespace;
CatalogId dbCatId;
DumpId dbDumpId;
@@ -2792,6 +2793,10 @@ dumpDatabase(Archive *fout)
appendPQExpBuffer(dbQry, "datminmxid, ");
else
appendPQExpBuffer(dbQry, "0 AS datminmxid, ");
+ if (fout->remoteVersion >= 150000)
+ appendPQExpBuffer(dbQry, "datcollversion, ");
+ else
+ appendPQExpBuffer(dbQry, "NULL AS datcollversion, ");
appendPQExpBuffer(dbQry,
"(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
"shobj_description(oid, 'pg_database') AS description "
@@ -2813,6 +2818,7 @@ dumpDatabase(Archive *fout)
i_acldefault = PQfnumber(res, "acldefault");
i_datistemplate = PQfnumber(res, "datistemplate");
i_datconnlimit = PQfnumber(res, "datconnlimit");
+ i_datcollversion = PQfnumber(res, "datcollversion");
i_tablespace = PQfnumber(res, "tablespace");
dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2871,6 +2877,20 @@ dumpDatabase(Archive *fout)
appendStringLiteralAH(creaQry, ctype, fout);
}
}
+ /*
+ * For binary upgrade, carry over the collation version. For normal
+ * dump/restore, omit the version, so that it is computed upon restore.
+ */
+ if (dopt->binary_upgrade)
+ {
+ if (!PQgetisnull(res, 0, i_datcollversion))
+ {
+ appendPQExpBufferStr(creaQry, " COLLATION_VERSION = ");
+ appendStringLiteralAH(creaQry,
+ PQgetvalue(res, 0, i_datcollversion),
+ fout);
+ }
+ }
/*
* Note: looking at dopt->outputNoTablespaces here is completely the wrong
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index b2ec50b4f2..cc9319d434 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1839,7 +1839,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER DATABASE <name> */
else if (Matches("ALTER", "DATABASE", MatchAny))
- COMPLETE_WITH("RESET", "SET", "OWNER TO", "RENAME TO",
+ COMPLETE_WITH("RESET", "SET", "OWNER TO", "REFRESH COLLATION VERSION", "RENAME TO",
"IS_TEMPLATE", "ALLOW_CONNECTIONS",
"CONNECTION LIMIT");
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 90b43a4ecc..76adbd4aad 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -65,6 +65,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
/* LC_CTYPE setting */
text datctype BKI_FORCE_NOT_NULL;
+ /* provider-dependent version of collation data */
+ text datcollversion BKI_DEFAULT(_null_);
+
/* access permissions */
aclitem datacl[1];
#endif
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7024dbe10a..2f45c764e2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11615,6 +11615,11 @@
proname => 'pg_collation_actual_version', procost => '100',
provolatile => 'v', prorettype => 'text', proargtypes => 'oid',
prosrc => 'pg_collation_actual_version' },
+{ oid => '9167',
+ descr => 'get actual version of database collation from operating system',
+ proname => 'pg_database_collation_actual_version', procost => '100',
+ provolatile => 'v', prorettype => 'text', proargtypes => 'oid',
+ prosrc => 'pg_database_collation_actual_version' },
# system management/monitoring related functions
{ oid => '3353', descr => 'list files in the log directory',
diff --git a/src/include/commands/dbcommands.h b/src/include/commands/dbcommands.h
index b1e8b5eb96..7faf6bf41c 100644
--- a/src/include/commands/dbcommands.h
+++ b/src/include/commands/dbcommands.h
@@ -24,6 +24,7 @@ extern void dropdb(const char *dbname, bool missing_ok, bool force);
extern void DropDatabase(ParseState *pstate, DropdbStmt *stmt);
extern ObjectAddress RenameDatabase(const char *oldname, const char *newname);
extern Oid AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel);
+extern Oid AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt);
extern Oid AlterDatabaseSet(AlterDatabaseSetStmt *stmt);
extern ObjectAddress AlterDatabaseOwner(const char *dbname, Oid newOwnerId);
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index da35f2c272..5d075f0c34 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -370,6 +370,7 @@ typedef enum NodeTag
T_CheckPointStmt,
T_CreateSchemaStmt,
T_AlterDatabaseStmt,
+ T_AlterDatabaseRefreshCollStmt,
T_AlterDatabaseSetStmt,
T_AlterRoleSetStmt,
T_CreateConversionStmt,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 3e9bdc781f..d901cf2a35 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3306,6 +3306,12 @@ typedef struct AlterDatabaseStmt
List *options; /* List of DefElem nodes */
} AlterDatabaseStmt;
+typedef struct AlterDatabaseRefreshCollStmt
+{
+ NodeTag type;
+ char *dbname;
+} AlterDatabaseRefreshCollStmt;
+
typedef struct AlterDatabaseSetStmt
{
NodeTag type;
--
2.35.1
On Tue, Feb 01, 2022 at 04:20:14PM +0100, Peter Eisentraut wrote:
This patch adds to database objects the same version tracking that collation
objects have.
This version conflicts with 87669de72c2 (Some cleanup for change of collate and
ctype fields to type text), so I'm attaching a simple rebase of your patch to
make the cfbot happy, no other changes.
There is a new pg_database column datcollversion that stores
the version, a new function pg_database_collation_actual_version() to get
the version from the operating system, and a new subcommand ALTER DATABASE
... REFRESH COLLATION VERSION.This was not originally added together with pg_collation.collversion, since
originally version tracking was only supported for ICU, and ICU on a
database-level is not currently supported. But we now have version tracking
for glibc (since PG13), FreeBSD (since PG14), and Windows (since PG13), so
this is useful to have now. And of course ICU on database-level is being
worked on at the moment as well.
This patch is pretty much complete AFAICT.
Agreed. Here's a review of the patch:
- there should be a mention to the need for a catversion bump in the message
comment
- there is no test
- it's missing some updates in create_database.sgml, and psql tab completion
for CREATE DATABASE with the new collation_version defelem.
- that's not really something new with this patch, but should we output the
collation version info or mismatch info in \l / \dO?
+ if (!actual_versionstr)
+ ereport(ERROR,
+ (errmsg("database \"%s\" has no actual collation version, but a version was specified",
+ name)));-
this means you can't connect on such a database anymore. The level is probably
ok for collation version check, but for db isn't that too much?
+Oid
+AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt)
+{
+ Relation rel;
+ Oid dboid;
+ HeapTuple tup;
+ Datum datum;
+ bool isnull;
+ char *oldversion;
+ char *newversion;
+
+ rel = table_open(DatabaseRelationId, RowExclusiveLock);
+ dboid = get_database_oid(stmt->dbname, false);
+
+ if (!pg_database_ownercheck(dboid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE,
+ stmt->dbname);
+
+ tup = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(dboid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for database %u", dboid);
Is that ok to not obtain a lock on the database when refreshing the collation?
I guess it's not worth bothering as it can only lead to spurious messages for
connection done concurrently, but there should be a comment to clarify it.
Also, it means that someone can drop the database concurrently. So it's
should be a "database does not exist" rather than a cache lookup failed error
message.
+ /*
+ * Check collation version. See similar code in
+ * pg_newlocale_from_collation().
+ */
+ datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollversion,
+ &isnull);
+ if (!isnull)
+ {
This (and pg_newlocale_from_collation()) reports a problem if a recorded
collation version is found but there's no reported collation version.
Shouldn't it also complain if it's the opposite? It's otherwise a backdoor to
make sure there won't be any check about the version anymore, and while it can
probably happen if you mess with the catalogs it still doesn't look great.
+ /*
+ * template0 shouldn't have any collation-dependent objects, so unset
+ * the collation version. This avoids warnings when making a new
+ * database from it.
+ */
+ "UPDATE pg_database SET datcollversion = NULL WHERE datname = 'template0';\n\n",
I'm not opposed, but shouldn't there indeed be a warning in case of discrepancy
in the source database (whether template or not)?
# update pg_database set datcollversion = 'meh' where datname in ('postgres', 'template1');
UPDATE 2
# create database test1 template postgres;
CREATE DATABASE
# create database test2 template template1;
CREATE DATABASE
# \c test2
WARNING: database "test2" has a collation version mismatch
DETAIL: The database was created using collation version meh, but the operating system provides version 2.34.
HINT: Rebuild all objects affected by collation in this database and run ALTER DATABASE test2 REFRESH COLLATION VERSION, or build PostgreSQL with the right library version.
You are now connected to database "test2" as user "rjuju".
The rest of the patch looks good to me. There's notably pg_dump and pg_upgrade
support so it indeed looks complete.
One bonus thing would be to add
a query to the ALTER DATABASE ref page similar to the one on the ALTER
COLLATION ref page that identifies the objects that are affected by outdated
collations. The database version of that might just show all
collation-using objects or something like that. Suggestions welcome.
That would be nice, but that's something quite hard to do and the resulting
query would be somewhat unreadable.
Also, you need to look at custom data types, expression and quals at least, so
I'm not even sure that you can actually do it in pure SQL without additional
infrastructure.
Also, one curious behavior is that if you get to a situation where the
collation version is mismatched, every time an autovacuum worker launches
you get the collation version mismatch warning in the log. Maybe that's
actually correct, but maybe we want to dial it down a bit for
non-interactive sessions.
I'm +0.5 to keep it that way. Index corruption is a real danger, so if you
have enough autovacuum worker launched to have a real problem with that, you
clearly should take care of the problem even faster.
Attachments:
v2-0001-Database-level-collation-version-tracking.patchtext/plain; charset=us-asciiDownload
From 718942496eb0fa19ab092e3c74af42149f86f756 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Tue, 1 Feb 2022 16:09:30 +0100
Subject: [PATCH v2] Database-level collation version tracking
This adds to database objects the same version tracking that collation
objects have. There is a new pg_database column datcollversion that
stores the version, a new function
pg_database_collation_actual_version() to get the version from the
operating system, and a new subcommand ALTER DATABASE ... REFRESH
COLLATION VERSION.
This was not originally added together with pg_collation.collversion,
since originally version tracking was only supported for ICU, and ICU
on a database-level is not currently supported. But we now have
version tracking for glibc (since PG13), FreeBSD (since PG14), and
Windows (since PG13), so this is useful to have now.
---
doc/src/sgml/catalogs.sgml | 11 ++
doc/src/sgml/func.sgml | 18 ++++
doc/src/sgml/ref/alter_collation.sgml | 3 +-
doc/src/sgml/ref/alter_database.sgml | 12 +++
src/backend/commands/dbcommands.c | 146 ++++++++++++++++++++++++--
src/backend/parser/gram.y | 6 ++
src/backend/tcop/utility.c | 14 +--
src/backend/utils/init/postinit.c | 33 ++++++
src/bin/initdb/initdb.c | 12 +++
src/bin/pg_dump/pg_dump.c | 20 ++++
src/bin/psql/tab-complete.c | 2 +-
src/include/catalog/pg_database.h | 3 +
src/include/catalog/pg_proc.dat | 5 +
src/include/commands/dbcommands.h | 1 +
src/include/nodes/nodes.h | 1 +
src/include/nodes/parsenodes.h | 6 ++
16 files changed, 279 insertions(+), 14 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 879d2dbce0..5a1627a394 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3043,6 +3043,17 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>datcollversion</structfield> <type>text</type>
+ </para>
+ <para>
+ Provider-specific version of the collation. This is recorded when the
+ database is created and then checked when it is used, to detect
+ changes in the collation definition that could lead to data corruption.
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>datacl</structfield> <type>aclitem[]</type>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8754f2f89b..49644666bc 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27061,6 +27061,24 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_database_collation_actual_version</primary>
+ </indexterm>
+ <function>pg_database_collation_actual_version</function> ( <type>oid</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the actual version of the database's collation as it is currently
+ installed in the operating system. If this is different from the
+ value in
+ <structname>pg_database</structname>.<structfield>datcollversion</structfield>,
+ then objects depending on the collation might need to be rebuilt. See
+ also <xref linkend="sql-alterdatabase"/>.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/alter_collation.sgml b/doc/src/sgml/ref/alter_collation.sgml
index 892c466565..a8c831d728 100644
--- a/doc/src/sgml/ref/alter_collation.sgml
+++ b/doc/src/sgml/ref/alter_collation.sgml
@@ -151,7 +151,8 @@ HINT: Rebuild all objects affected by this collation and run ALTER COLLATION pg
</para>
</note>
<para>
- Currently, there is no version tracking for the database default collation.
+ For the database default collation, there is an analogous command
+ <literal>ALTER DATABASE ... REFRESH COLLATION VERSION</literal>.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_database.sgml b/doc/src/sgml/ref/alter_database.sgml
index 81e37536a3..89ed261b4c 100644
--- a/doc/src/sgml/ref/alter_database.sgml
+++ b/doc/src/sgml/ref/alter_database.sgml
@@ -35,6 +35,8 @@ ALTER DATABASE <replaceable class="parameter">name</replaceable> OWNER TO { <rep
ALTER DATABASE <replaceable class="parameter">name</replaceable> SET TABLESPACE <replaceable class="parameter">new_tablespace</replaceable>
+ALTER DATABASE <replaceable class="parameter">name</replaceable> REFRESH COLLATION VERSION
+
ALTER DATABASE <replaceable class="parameter">name</replaceable> SET <replaceable>configuration_parameter</replaceable> { TO | = } { <replaceable>value</replaceable> | DEFAULT }
ALTER DATABASE <replaceable class="parameter">name</replaceable> SET <replaceable>configuration_parameter</replaceable> FROM CURRENT
ALTER DATABASE <replaceable class="parameter">name</replaceable> RESET <replaceable>configuration_parameter</replaceable>
@@ -171,6 +173,16 @@ ALTER DATABASE <replaceable class="parameter">name</replaceable> RESET ALL
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>REFRESH COLLATION VERSION</literal></term>
+ <listitem>
+ <para>
+ Update the database collation version. See <xref
+ linkend="sql-altercollation-notes"/> for background.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><replaceable>configuration_parameter</replaceable></term>
<term><replaceable>value</replaceable></term>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index e673138cbd..78010f2198 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -36,6 +36,7 @@
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_collation.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
#include "catalog/pg_subscription.h"
@@ -85,7 +86,8 @@ static bool get_db_info(const char *name, LOCKMODE lockmode,
Oid *dbIdP, Oid *ownerIdP,
int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
- Oid *dbTablespace, char **dbCollate, char **dbCtype);
+ Oid *dbTablespace, char **dbCollate, char **dbCtype,
+ char **dbCollversion);
static bool have_createdb_privilege(void);
static void remove_dbtablespaces(Oid db_id);
static bool check_db_file_conflict(Oid db_id);
@@ -105,6 +107,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
int src_encoding = -1;
char *src_collate = NULL;
char *src_ctype = NULL;
+ char *src_collversion = NULL;
bool src_istemplate;
bool src_allowconn;
TransactionId src_frozenxid = InvalidTransactionId;
@@ -128,6 +131,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
DefElem *distemplate = NULL;
DefElem *dallowconnections = NULL;
DefElem *dconnlimit = NULL;
+ DefElem *dcollversion = NULL;
char *dbname = stmt->dbname;
char *dbowner = NULL;
const char *dbtemplate = NULL;
@@ -138,6 +142,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
bool dbistemplate = false;
bool dballowconnections = true;
int dbconnlimit = -1;
+ char *dbcollversion = NULL;
int notherbackends;
int npreparedxacts;
createdb_failure_params fparms;
@@ -207,6 +212,12 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
errorConflictingDefElem(defel, pstate);
dconnlimit = defel;
}
+ else if (strcmp(defel->defname, "collation_version") == 0)
+ {
+ if (dcollversion)
+ errorConflictingDefElem(defel, pstate);
+ dcollversion = defel;
+ }
else if (strcmp(defel->defname, "location") == 0)
{
ereport(WARNING,
@@ -305,6 +316,8 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("invalid connection limit: %d", dbconnlimit)));
}
+ if (dcollversion)
+ dbcollversion = defGetString(dcollversion);
/* obtain OID of proposed owner */
if (dbowner)
@@ -342,7 +355,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
&src_dboid, &src_owner, &src_encoding,
&src_istemplate, &src_allowconn,
&src_frozenxid, &src_minmxid, &src_deftablespace,
- &src_collate, &src_ctype))
+ &src_collate, &src_ctype, &src_collversion))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("template database \"%s\" does not exist",
@@ -368,6 +381,8 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
dbcollate = src_collate;
if (dbctype == NULL)
dbctype = src_ctype;
+ if (dbcollversion == NULL)
+ dbcollversion = src_collversion;
/* Some encodings are client only */
if (!PG_VALID_BE_ENCODING(encoding))
@@ -424,6 +439,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
errhint("Use the same LC_CTYPE as in the template database, or use template0 as template.")));
}
+ /*
+ * Normally, we copy the collation version from the template database.
+ * This last resort only applies if the template database does not have a
+ * collation version, which is normally only the case for template0.
+ */
+ if (!dbcollversion)
+ dbcollversion = get_collation_actual_version(COLLPROVIDER_LIBC, dbcollate);
+
/* Resolve default tablespace for new database */
if (dtablespacename && dtablespacename->arg)
{
@@ -578,6 +601,10 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
new_record[Anum_pg_database_dattablespace - 1] = ObjectIdGetDatum(dst_deftablespace);
new_record[Anum_pg_database_datcollate - 1] = CStringGetTextDatum(dbcollate);
new_record[Anum_pg_database_datctype - 1] = CStringGetTextDatum(dbctype);
+ if (dbcollversion)
+ new_record[Anum_pg_database_datcollversion - 1] = CStringGetTextDatum(dbcollversion);
+ else
+ new_record_nulls[Anum_pg_database_datcollversion - 1] = true;
/*
* We deliberately set datacl to default (NULL), rather than copying it
@@ -844,7 +871,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
- &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL))
+ &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
{
if (!missing_ok)
{
@@ -1043,7 +1070,7 @@ RenameDatabase(const char *oldname, const char *newname)
rel = table_open(DatabaseRelationId, RowExclusiveLock);
if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL,
- NULL, NULL, NULL, NULL, NULL, NULL, NULL))
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("database \"%s\" does not exist", oldname)));
@@ -1156,7 +1183,7 @@ movedb(const char *dbname, const char *tblspcname)
pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
- NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL))
+ NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL, NULL))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("database \"%s\" does not exist", dbname)));
@@ -1643,6 +1670,76 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
}
+/*
+ * ALTER DATABASE name REFRESH COLLATION VERSION
+ */
+Oid
+AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt)
+{
+ Relation rel;
+ Oid dboid;
+ HeapTuple tup;
+ Datum datum;
+ bool isnull;
+ char *oldversion;
+ char *newversion;
+
+ rel = table_open(DatabaseRelationId, RowExclusiveLock);
+ dboid = get_database_oid(stmt->dbname, false);
+
+ if (!pg_database_ownercheck(dboid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE,
+ stmt->dbname);
+
+ tup = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(dboid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for database %u", dboid);
+
+ datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollversion, &isnull);
+ oldversion = isnull ? NULL : TextDatumGetCString(datum);
+
+ datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollate, &isnull);
+ Assert(!isnull);
+ newversion = get_collation_actual_version(COLLPROVIDER_LIBC, TextDatumGetCString(datum));
+
+ /* cannot change from NULL to non-NULL or vice versa */
+ if ((!oldversion && newversion) || (oldversion && !newversion))
+ elog(ERROR, "invalid collation version change");
+ else if (oldversion && newversion && strcmp(newversion, oldversion) != 0)
+ {
+ bool nulls[Natts_pg_database];
+ bool replaces[Natts_pg_database];
+ Datum values[Natts_pg_database];
+
+ ereport(NOTICE,
+ (errmsg("changing version from %s to %s",
+ oldversion, newversion)));
+
+ memset(values, 0, sizeof(values));
+ memset(nulls, false, sizeof(nulls));
+ memset(replaces, false, sizeof(replaces));
+
+ values[Anum_pg_database_datcollversion - 1] = CStringGetTextDatum(newversion);
+ replaces[Anum_pg_database_datcollversion - 1] = true;
+
+ tup = heap_modify_tuple(tup, RelationGetDescr(rel),
+ values, nulls, replaces);
+ }
+ else
+ ereport(NOTICE,
+ (errmsg("version has not changed")));
+
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ InvokeObjectPostAlterHook(DatabaseRelationId, dboid, 0);
+
+ heap_freetuple(tup);
+ table_close(rel, NoLock);
+
+ return dboid;
+}
+
+
/*
* ALTER DATABASE name SET ...
*/
@@ -1785,6 +1882,34 @@ AlterDatabaseOwner(const char *dbname, Oid newOwnerId)
}
+Datum
+pg_database_collation_actual_version(PG_FUNCTION_ARGS)
+{
+ Oid dbid = PG_GETARG_OID(0);
+ HeapTuple tp;
+ Datum datum;
+ bool isnull;
+ char *version;
+
+ tp = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbid));
+ if (!HeapTupleIsValid(tp))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("database with OID %u does not exist", dbid)));
+
+ datum = SysCacheGetAttr(DATABASEOID, tp, Anum_pg_database_datcollate, &isnull);
+ Assert(!isnull);
+ version = get_collation_actual_version(COLLPROVIDER_LIBC, TextDatumGetCString(datum));
+
+ ReleaseSysCache(tp);
+
+ if (version)
+ PG_RETURN_TEXT_P(cstring_to_text(version));
+ else
+ PG_RETURN_NULL();
+}
+
+
/*
* Helper functions
*/
@@ -1800,7 +1925,8 @@ get_db_info(const char *name, LOCKMODE lockmode,
Oid *dbIdP, Oid *ownerIdP,
int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
- Oid *dbTablespace, char **dbCollate, char **dbCtype)
+ Oid *dbTablespace, char **dbCollate, char **dbCtype,
+ char **dbCollversion)
{
bool result = false;
Relation relation;
@@ -1905,6 +2031,14 @@ get_db_info(const char *name, LOCKMODE lockmode,
Assert(!isnull);
*dbCtype = TextDatumGetCString(datum);
}
+ if (dbCollversion)
+ {
+ datum = SysCacheGetAttr(DATABASEOID, tuple, Anum_pg_database_datcollversion, &isnull);
+ if (isnull)
+ *dbCollversion = NULL;
+ else
+ *dbCollversion = TextDatumGetCString(datum);
+ }
ReleaseSysCache(tuple);
result = true;
break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c4f3242506..92f93cfc72 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10465,6 +10465,12 @@ AlterDatabaseStmt:
(Node *)makeString($6), @6));
$$ = (Node *)n;
}
+ | ALTER DATABASE name REFRESH COLLATION VERSION_P
+ {
+ AlterDatabaseRefreshCollStmt *n = makeNode(AlterDatabaseRefreshCollStmt);
+ n->dbname = $3;
+ $$ = (Node *)n;
+ }
;
AlterDatabaseSetStmt:
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 83e4e37c78..3780c6e812 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -136,6 +136,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
switch (nodeTag(parsetree))
{
case T_AlterCollationStmt:
+ case T_AlterDatabaseRefreshCollStmt:
case T_AlterDatabaseSetStmt:
case T_AlterDatabaseStmt:
case T_AlterDefaultPrivilegesStmt:
@@ -779,6 +780,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
AlterDatabase(pstate, (AlterDatabaseStmt *) parsetree, isTopLevel);
break;
+ case T_AlterDatabaseRefreshCollStmt:
+ /* no event triggers for global objects */
+ AlterDatabaseRefreshColl((AlterDatabaseRefreshCollStmt *) parsetree);
+ break;
+
case T_AlterDatabaseSetStmt:
/* no event triggers for global objects */
AlterDatabaseSet((AlterDatabaseSetStmt *) parsetree);
@@ -2801,9 +2807,7 @@ CreateCommandTag(Node *parsetree)
break;
case T_AlterDatabaseStmt:
- tag = CMDTAG_ALTER_DATABASE;
- break;
-
+ case T_AlterDatabaseRefreshCollStmt:
case T_AlterDatabaseSetStmt:
tag = CMDTAG_ALTER_DATABASE;
break;
@@ -3444,9 +3448,7 @@ GetCommandLogLevel(Node *parsetree)
break;
case T_AlterDatabaseStmt:
- lev = LOGSTMT_DDL;
- break;
-
+ case T_AlterDatabaseRefreshCollStmt:
case T_AlterDatabaseSetStmt:
lev = LOGSTMT_DDL;
break;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 5b9ed2f6f5..228bb63f97 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -31,6 +31,7 @@
#include "catalog/catalog.h"
#include "catalog/namespace.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_collation.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
#include "catalog/pg_tablespace.h"
@@ -414,6 +415,38 @@ CheckMyDatabase(const char *name, bool am_superuser, bool override_allow_connect
" which is not recognized by setlocale().", ctype),
errhint("Recreate the database with another locale or install the missing locale.")));
+ /*
+ * Check collation version. See similar code in
+ * pg_newlocale_from_collation().
+ */
+ datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollversion,
+ &isnull);
+ if (!isnull)
+ {
+ char *actual_versionstr;
+ char *collversionstr;
+
+ collversionstr = TextDatumGetCString(datum);
+
+ actual_versionstr = get_collation_actual_version(COLLPROVIDER_LIBC, collate);
+ if (!actual_versionstr)
+ ereport(ERROR,
+ (errmsg("database \"%s\" has no actual collation version, but a version was specified",
+ name)));
+
+ if (strcmp(actual_versionstr, collversionstr) != 0)
+ ereport(WARNING,
+ (errmsg("database \"%s\" has a collation version mismatch",
+ name),
+ errdetail("The database was created using collation version %s, "
+ "but the operating system provides version %s.",
+ collversionstr, actual_versionstr),
+ errhint("Rebuild all objects affected by collation in this database and run "
+ "ALTER DATABASE %s REFRESH COLLATION VERSION, "
+ "or build PostgreSQL with the right library version.",
+ quote_identifier(name))));
+ }
+
/* Make the locale settings visible as GUC variables, too */
SetConfigOption("lc_collate", collate, PGC_INTERNAL, PGC_S_OVERRIDE);
SetConfigOption("lc_ctype", ctype, PGC_INTERNAL, PGC_S_OVERRIDE);
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index d78e8e67b8..6e70d1b724 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -1857,6 +1857,18 @@ make_template0(FILE *cmdfd)
"CREATE DATABASE template0 IS_TEMPLATE = true ALLOW_CONNECTIONS = false OID = "
CppAsString2(Template0ObjectId) ";\n\n",
+ /*
+ * template0 shouldn't have any collation-dependent objects, so unset
+ * the collation version. This avoids warnings when making a new
+ * database from it.
+ */
+ "UPDATE pg_database SET datcollversion = NULL WHERE datname = 'template0';\n\n",
+
+ /*
+ * While we are here, do set the collation version on template1.
+ */
+ "UPDATE pg_database SET datcollversion = pg_database_collation_actual_version(oid) WHERE datname = 'template1';\n\n",
+
/*
* Explicitly revoke public create-schema and create-temp-table
* privileges in template1 and template0; else the latter would be on
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 3499c0a4d5..7df576961e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2761,6 +2761,7 @@ dumpDatabase(Archive *fout)
i_acldefault,
i_datistemplate,
i_datconnlimit,
+ i_datcollversion,
i_tablespace;
CatalogId dbCatId;
DumpId dbDumpId;
@@ -2792,6 +2793,10 @@ dumpDatabase(Archive *fout)
appendPQExpBuffer(dbQry, "datminmxid, ");
else
appendPQExpBuffer(dbQry, "0 AS datminmxid, ");
+ if (fout->remoteVersion >= 150000)
+ appendPQExpBuffer(dbQry, "datcollversion, ");
+ else
+ appendPQExpBuffer(dbQry, "NULL AS datcollversion, ");
appendPQExpBuffer(dbQry,
"(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
"shobj_description(oid, 'pg_database') AS description "
@@ -2813,6 +2818,7 @@ dumpDatabase(Archive *fout)
i_acldefault = PQfnumber(res, "acldefault");
i_datistemplate = PQfnumber(res, "datistemplate");
i_datconnlimit = PQfnumber(res, "datconnlimit");
+ i_datcollversion = PQfnumber(res, "datcollversion");
i_tablespace = PQfnumber(res, "tablespace");
dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2871,6 +2877,20 @@ dumpDatabase(Archive *fout)
appendStringLiteralAH(creaQry, ctype, fout);
}
}
+ /*
+ * For binary upgrade, carry over the collation version. For normal
+ * dump/restore, omit the version, so that it is computed upon restore.
+ */
+ if (dopt->binary_upgrade)
+ {
+ if (!PQgetisnull(res, 0, i_datcollversion))
+ {
+ appendPQExpBufferStr(creaQry, " COLLATION_VERSION = ");
+ appendStringLiteralAH(creaQry,
+ PQgetvalue(res, 0, i_datcollversion),
+ fout);
+ }
+ }
/*
* Note: looking at dopt->outputNoTablespaces here is completely the wrong
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d1e421bc0f..d8ea142856 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1839,7 +1839,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER DATABASE <name> */
else if (Matches("ALTER", "DATABASE", MatchAny))
- COMPLETE_WITH("RESET", "SET", "OWNER TO", "RENAME TO",
+ COMPLETE_WITH("RESET", "SET", "OWNER TO", "REFRESH COLLATION VERSION", "RENAME TO",
"IS_TEMPLATE", "ALLOW_CONNECTIONS",
"CONNECTION LIMIT");
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 90b43a4ecc..76adbd4aad 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -65,6 +65,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
/* LC_CTYPE setting */
text datctype BKI_FORCE_NOT_NULL;
+ /* provider-dependent version of collation data */
+ text datcollversion BKI_DEFAULT(_null_);
+
/* access permissions */
aclitem datacl[1];
#endif
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7024dbe10a..2f45c764e2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11615,6 +11615,11 @@
proname => 'pg_collation_actual_version', procost => '100',
provolatile => 'v', prorettype => 'text', proargtypes => 'oid',
prosrc => 'pg_collation_actual_version' },
+{ oid => '9167',
+ descr => 'get actual version of database collation from operating system',
+ proname => 'pg_database_collation_actual_version', procost => '100',
+ provolatile => 'v', prorettype => 'text', proargtypes => 'oid',
+ prosrc => 'pg_database_collation_actual_version' },
# system management/monitoring related functions
{ oid => '3353', descr => 'list files in the log directory',
diff --git a/src/include/commands/dbcommands.h b/src/include/commands/dbcommands.h
index b1e8b5eb96..7faf6bf41c 100644
--- a/src/include/commands/dbcommands.h
+++ b/src/include/commands/dbcommands.h
@@ -24,6 +24,7 @@ extern void dropdb(const char *dbname, bool missing_ok, bool force);
extern void DropDatabase(ParseState *pstate, DropdbStmt *stmt);
extern ObjectAddress RenameDatabase(const char *oldname, const char *newname);
extern Oid AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel);
+extern Oid AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt);
extern Oid AlterDatabaseSet(AlterDatabaseSetStmt *stmt);
extern ObjectAddress AlterDatabaseOwner(const char *dbname, Oid newOwnerId);
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index da35f2c272..5d075f0c34 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -370,6 +370,7 @@ typedef enum NodeTag
T_CheckPointStmt,
T_CreateSchemaStmt,
T_AlterDatabaseStmt,
+ T_AlterDatabaseRefreshCollStmt,
T_AlterDatabaseSetStmt,
T_AlterRoleSetStmt,
T_CreateConversionStmt,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 37fcc4c9b5..34218b718c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3308,6 +3308,12 @@ typedef struct AlterDatabaseStmt
List *options; /* List of DefElem nodes */
} AlterDatabaseStmt;
+typedef struct AlterDatabaseRefreshCollStmt
+{
+ NodeTag type;
+ char *dbname;
+} AlterDatabaseRefreshCollStmt;
+
typedef struct AlterDatabaseSetStmt
{
NodeTag type;
--
2.33.1
On 07.02.22 11:29, Julien Rouhaud wrote:
- there should be a mention to the need for a catversion bump in the message
comment
done
- there is no test
Suggestions where to put it? We don't really have tests for the
collation-level versioning either, do we?
- it's missing some updates in create_database.sgml, and psql tab completion
for CREATE DATABASE with the new collation_version defelem.
Added to create_database.sgml, but not to psql. We don't have
completion for the collation option either, since it's only meant to be
used by pg_upgrade, not interactively.
- that's not really something new with this patch, but should we output the
collation version info or mismatch info in \l / \dO?
It's a possibility. Perhaps there is a question of performance if we
show it in \l and people have tons of databases and they have to make a
locale call for each one. As you say, it's more an independent feature,
but it's worth looking into.
+ if (!actual_versionstr) + ereport(ERROR, + (errmsg("database \"%s\" has no actual collation version, but a version was specified", + name)));-this means you can't connect on such a database anymore. The level is probably
ok for collation version check, but for db isn't that too much?
Right, changed to warning.
+Oid +AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt) +{ + Relation rel; + Oid dboid; + HeapTuple tup; + Datum datum; + bool isnull; + char *oldversion; + char *newversion; + + rel = table_open(DatabaseRelationId, RowExclusiveLock); + dboid = get_database_oid(stmt->dbname, false); + + if (!pg_database_ownercheck(dboid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE, + stmt->dbname); + + tup = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(dboid)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for database %u", dboid);Is that ok to not obtain a lock on the database when refreshing the collation?
That code takes a RowExclusiveLock on pg_database. Did you have
something else in mind?
+ /* + * Check collation version. See similar code in + * pg_newlocale_from_collation(). + */ + datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollversion, + &isnull); + if (!isnull) + {This (and pg_newlocale_from_collation()) reports a problem if a recorded
collation version is found but there's no reported collation version.
Shouldn't it also complain if it's the opposite? It's otherwise a backdoor to
make sure there won't be any check about the version anymore, and while it can
probably happen if you mess with the catalogs it still doesn't look great.
get_collation_actual_version() always returns either null or not null
for a given installation. So the situation that the stored version is
null and the actual version is not null can only happen as part of a
software upgrade. In that case, all uses of collations after an upgrade
would immediately start complaining about missing versions, which seems
like a bad experience. Users can explicitly opt in to version tracking
by running REFRESH VERSION once.
+ /* + * template0 shouldn't have any collation-dependent objects, so unset + * the collation version. This avoids warnings when making a new + * database from it. + */ + "UPDATE pg_database SET datcollversion = NULL WHERE datname = 'template0';\n\n",I'm not opposed, but shouldn't there indeed be a warning in case of discrepancy
in the source database (whether template or not)?# update pg_database set datcollversion = 'meh' where datname in ('postgres', 'template1');
UPDATE 2# create database test1 template postgres;
CREATE DATABASE# create database test2 template template1;
CREATE DATABASE# \c test2
WARNING: database "test2" has a collation version mismatch
I don't understand what the complaint is here. It seems to work ok?
Attachments:
v3-0001-Database-level-collation-version-tracking.patchtext/plain; charset=UTF-8; name=v3-0001-Database-level-collation-version-tracking.patchDownload
From c50f4561932f5ff23715a0fb6e3e9554ce395f16 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Mon, 7 Feb 2022 16:32:19 +0100
Subject: [PATCH v3] Database-level collation version tracking
This adds to database objects the same version tracking that collation
objects have. There is a new pg_database column datcollversion that
stores the version, a new function
pg_database_collation_actual_version() to get the version from the
operating system, and a new subcommand ALTER DATABASE ... REFRESH
COLLATION VERSION.
This was not originally added together with pg_collation.collversion,
since originally version tracking was only supported for ICU, and ICU
on a database-level is not currently supported. But we now have
version tracking for glibc (since PG13), FreeBSD (since PG14), and
Windows (since PG13), so this is useful to have now.
Discussion: https://www.postgresql.org/message-id/flat/f0ff3190-29a3-5b39-a179-fa32eee57db6%40enterprisedb.com
XXX catversion bump
---
doc/src/sgml/catalogs.sgml | 11 ++
doc/src/sgml/func.sgml | 18 ++++
doc/src/sgml/ref/alter_collation.sgml | 3 +-
doc/src/sgml/ref/alter_database.sgml | 12 +++
doc/src/sgml/ref/create_database.sgml | 21 ++++
src/backend/commands/dbcommands.c | 146 ++++++++++++++++++++++++--
src/backend/parser/gram.y | 6 ++
src/backend/tcop/utility.c | 14 +--
src/backend/utils/init/postinit.c | 33 ++++++
src/bin/initdb/initdb.c | 12 +++
src/bin/pg_dump/pg_dump.c | 20 ++++
src/bin/psql/tab-complete.c | 2 +-
src/include/catalog/pg_database.h | 3 +
src/include/catalog/pg_proc.dat | 5 +
src/include/commands/dbcommands.h | 1 +
src/include/nodes/nodes.h | 1 +
src/include/nodes/parsenodes.h | 6 ++
17 files changed, 300 insertions(+), 14 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 879d2dbce0..5a1627a394 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3043,6 +3043,17 @@ <title><structname>pg_database</structname> Columns</title>
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>datcollversion</structfield> <type>text</type>
+ </para>
+ <para>
+ Provider-specific version of the collation. This is recorded when the
+ database is created and then checked when it is used, to detect
+ changes in the collation definition that could lead to data corruption.
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>datacl</structfield> <type>aclitem[]</type>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8754f2f89b..49644666bc 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27061,6 +27061,24 @@ <title>Collation Management Functions</title>
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_database_collation_actual_version</primary>
+ </indexterm>
+ <function>pg_database_collation_actual_version</function> ( <type>oid</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the actual version of the database's collation as it is currently
+ installed in the operating system. If this is different from the
+ value in
+ <structname>pg_database</structname>.<structfield>datcollversion</structfield>,
+ then objects depending on the collation might need to be rebuilt. See
+ also <xref linkend="sql-alterdatabase"/>.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/alter_collation.sgml b/doc/src/sgml/ref/alter_collation.sgml
index 892c466565..a8c831d728 100644
--- a/doc/src/sgml/ref/alter_collation.sgml
+++ b/doc/src/sgml/ref/alter_collation.sgml
@@ -151,7 +151,8 @@ <title>Notes</title>
</para>
</note>
<para>
- Currently, there is no version tracking for the database default collation.
+ For the database default collation, there is an analogous command
+ <literal>ALTER DATABASE ... REFRESH COLLATION VERSION</literal>.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_database.sgml b/doc/src/sgml/ref/alter_database.sgml
index 81e37536a3..89ed261b4c 100644
--- a/doc/src/sgml/ref/alter_database.sgml
+++ b/doc/src/sgml/ref/alter_database.sgml
@@ -35,6 +35,8 @@
ALTER DATABASE <replaceable class="parameter">name</replaceable> SET TABLESPACE <replaceable class="parameter">new_tablespace</replaceable>
+ALTER DATABASE <replaceable class="parameter">name</replaceable> REFRESH COLLATION VERSION
+
ALTER DATABASE <replaceable class="parameter">name</replaceable> SET <replaceable>configuration_parameter</replaceable> { TO | = } { <replaceable>value</replaceable> | DEFAULT }
ALTER DATABASE <replaceable class="parameter">name</replaceable> SET <replaceable>configuration_parameter</replaceable> FROM CURRENT
ALTER DATABASE <replaceable class="parameter">name</replaceable> RESET <replaceable>configuration_parameter</replaceable>
@@ -171,6 +173,16 @@ <title>Parameters</title>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>REFRESH COLLATION VERSION</literal></term>
+ <listitem>
+ <para>
+ Update the database collation version. See <xref
+ linkend="sql-altercollation-notes"/> for background.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><replaceable>configuration_parameter</replaceable></term>
<term><replaceable>value</replaceable></term>
diff --git a/doc/src/sgml/ref/create_database.sgml b/doc/src/sgml/ref/create_database.sgml
index f22e28dc81..f70d0c75b4 100644
--- a/doc/src/sgml/ref/create_database.sgml
+++ b/doc/src/sgml/ref/create_database.sgml
@@ -28,6 +28,7 @@
[ LOCALE [=] <replaceable class="parameter">locale</replaceable> ]
[ LC_COLLATE [=] <replaceable class="parameter">lc_collate</replaceable> ]
[ LC_CTYPE [=] <replaceable class="parameter">lc_ctype</replaceable> ]
+ [ COLLATION_VERSION = <replaceable>collation_version</replaceable> ]
[ TABLESPACE [=] <replaceable class="parameter">tablespace_name</replaceable> ]
[ ALLOW_CONNECTIONS [=] <replaceable class="parameter">allowconn</replaceable> ]
[ CONNECTION LIMIT [=] <replaceable class="parameter">connlimit</replaceable> ]
@@ -158,6 +159,26 @@ <title>Parameters</title>
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><replaceable>collation_version</replaceable></term>
+
+ <listitem>
+ <para>
+ Specifies the collation version string to store with the database.
+ Normally, this should be omitted, which will cause the version to be
+ computed from the actual version of the database collation as provided
+ by the operating system. This option is intended to be used by
+ <command>pg_upgrade</command> for copying the version from an existing
+ installation.
+ </para>
+
+ <para>
+ See also <xref linkend="sql-alterdatabase"/> for how to handle
+ database collation version mismatches.
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><replaceable class="parameter">tablespace_name</replaceable></term>
<listitem>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index e673138cbd..78010f2198 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -36,6 +36,7 @@
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_collation.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
#include "catalog/pg_subscription.h"
@@ -85,7 +86,8 @@ static bool get_db_info(const char *name, LOCKMODE lockmode,
Oid *dbIdP, Oid *ownerIdP,
int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
- Oid *dbTablespace, char **dbCollate, char **dbCtype);
+ Oid *dbTablespace, char **dbCollate, char **dbCtype,
+ char **dbCollversion);
static bool have_createdb_privilege(void);
static void remove_dbtablespaces(Oid db_id);
static bool check_db_file_conflict(Oid db_id);
@@ -105,6 +107,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
int src_encoding = -1;
char *src_collate = NULL;
char *src_ctype = NULL;
+ char *src_collversion = NULL;
bool src_istemplate;
bool src_allowconn;
TransactionId src_frozenxid = InvalidTransactionId;
@@ -128,6 +131,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
DefElem *distemplate = NULL;
DefElem *dallowconnections = NULL;
DefElem *dconnlimit = NULL;
+ DefElem *dcollversion = NULL;
char *dbname = stmt->dbname;
char *dbowner = NULL;
const char *dbtemplate = NULL;
@@ -138,6 +142,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
bool dbistemplate = false;
bool dballowconnections = true;
int dbconnlimit = -1;
+ char *dbcollversion = NULL;
int notherbackends;
int npreparedxacts;
createdb_failure_params fparms;
@@ -207,6 +212,12 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
errorConflictingDefElem(defel, pstate);
dconnlimit = defel;
}
+ else if (strcmp(defel->defname, "collation_version") == 0)
+ {
+ if (dcollversion)
+ errorConflictingDefElem(defel, pstate);
+ dcollversion = defel;
+ }
else if (strcmp(defel->defname, "location") == 0)
{
ereport(WARNING,
@@ -305,6 +316,8 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("invalid connection limit: %d", dbconnlimit)));
}
+ if (dcollversion)
+ dbcollversion = defGetString(dcollversion);
/* obtain OID of proposed owner */
if (dbowner)
@@ -342,7 +355,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
&src_dboid, &src_owner, &src_encoding,
&src_istemplate, &src_allowconn,
&src_frozenxid, &src_minmxid, &src_deftablespace,
- &src_collate, &src_ctype))
+ &src_collate, &src_ctype, &src_collversion))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("template database \"%s\" does not exist",
@@ -368,6 +381,8 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
dbcollate = src_collate;
if (dbctype == NULL)
dbctype = src_ctype;
+ if (dbcollversion == NULL)
+ dbcollversion = src_collversion;
/* Some encodings are client only */
if (!PG_VALID_BE_ENCODING(encoding))
@@ -424,6 +439,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
errhint("Use the same LC_CTYPE as in the template database, or use template0 as template.")));
}
+ /*
+ * Normally, we copy the collation version from the template database.
+ * This last resort only applies if the template database does not have a
+ * collation version, which is normally only the case for template0.
+ */
+ if (!dbcollversion)
+ dbcollversion = get_collation_actual_version(COLLPROVIDER_LIBC, dbcollate);
+
/* Resolve default tablespace for new database */
if (dtablespacename && dtablespacename->arg)
{
@@ -578,6 +601,10 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
new_record[Anum_pg_database_dattablespace - 1] = ObjectIdGetDatum(dst_deftablespace);
new_record[Anum_pg_database_datcollate - 1] = CStringGetTextDatum(dbcollate);
new_record[Anum_pg_database_datctype - 1] = CStringGetTextDatum(dbctype);
+ if (dbcollversion)
+ new_record[Anum_pg_database_datcollversion - 1] = CStringGetTextDatum(dbcollversion);
+ else
+ new_record_nulls[Anum_pg_database_datcollversion - 1] = true;
/*
* We deliberately set datacl to default (NULL), rather than copying it
@@ -844,7 +871,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
- &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL))
+ &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
{
if (!missing_ok)
{
@@ -1043,7 +1070,7 @@ RenameDatabase(const char *oldname, const char *newname)
rel = table_open(DatabaseRelationId, RowExclusiveLock);
if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL,
- NULL, NULL, NULL, NULL, NULL, NULL, NULL))
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("database \"%s\" does not exist", oldname)));
@@ -1156,7 +1183,7 @@ movedb(const char *dbname, const char *tblspcname)
pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
- NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL))
+ NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL, NULL))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("database \"%s\" does not exist", dbname)));
@@ -1643,6 +1670,76 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
}
+/*
+ * ALTER DATABASE name REFRESH COLLATION VERSION
+ */
+Oid
+AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt)
+{
+ Relation rel;
+ Oid dboid;
+ HeapTuple tup;
+ Datum datum;
+ bool isnull;
+ char *oldversion;
+ char *newversion;
+
+ rel = table_open(DatabaseRelationId, RowExclusiveLock);
+ dboid = get_database_oid(stmt->dbname, false);
+
+ if (!pg_database_ownercheck(dboid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE,
+ stmt->dbname);
+
+ tup = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(dboid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for database %u", dboid);
+
+ datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollversion, &isnull);
+ oldversion = isnull ? NULL : TextDatumGetCString(datum);
+
+ datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollate, &isnull);
+ Assert(!isnull);
+ newversion = get_collation_actual_version(COLLPROVIDER_LIBC, TextDatumGetCString(datum));
+
+ /* cannot change from NULL to non-NULL or vice versa */
+ if ((!oldversion && newversion) || (oldversion && !newversion))
+ elog(ERROR, "invalid collation version change");
+ else if (oldversion && newversion && strcmp(newversion, oldversion) != 0)
+ {
+ bool nulls[Natts_pg_database];
+ bool replaces[Natts_pg_database];
+ Datum values[Natts_pg_database];
+
+ ereport(NOTICE,
+ (errmsg("changing version from %s to %s",
+ oldversion, newversion)));
+
+ memset(values, 0, sizeof(values));
+ memset(nulls, false, sizeof(nulls));
+ memset(replaces, false, sizeof(replaces));
+
+ values[Anum_pg_database_datcollversion - 1] = CStringGetTextDatum(newversion);
+ replaces[Anum_pg_database_datcollversion - 1] = true;
+
+ tup = heap_modify_tuple(tup, RelationGetDescr(rel),
+ values, nulls, replaces);
+ }
+ else
+ ereport(NOTICE,
+ (errmsg("version has not changed")));
+
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ InvokeObjectPostAlterHook(DatabaseRelationId, dboid, 0);
+
+ heap_freetuple(tup);
+ table_close(rel, NoLock);
+
+ return dboid;
+}
+
+
/*
* ALTER DATABASE name SET ...
*/
@@ -1785,6 +1882,34 @@ AlterDatabaseOwner(const char *dbname, Oid newOwnerId)
}
+Datum
+pg_database_collation_actual_version(PG_FUNCTION_ARGS)
+{
+ Oid dbid = PG_GETARG_OID(0);
+ HeapTuple tp;
+ Datum datum;
+ bool isnull;
+ char *version;
+
+ tp = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbid));
+ if (!HeapTupleIsValid(tp))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("database with OID %u does not exist", dbid)));
+
+ datum = SysCacheGetAttr(DATABASEOID, tp, Anum_pg_database_datcollate, &isnull);
+ Assert(!isnull);
+ version = get_collation_actual_version(COLLPROVIDER_LIBC, TextDatumGetCString(datum));
+
+ ReleaseSysCache(tp);
+
+ if (version)
+ PG_RETURN_TEXT_P(cstring_to_text(version));
+ else
+ PG_RETURN_NULL();
+}
+
+
/*
* Helper functions
*/
@@ -1800,7 +1925,8 @@ get_db_info(const char *name, LOCKMODE lockmode,
Oid *dbIdP, Oid *ownerIdP,
int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
- Oid *dbTablespace, char **dbCollate, char **dbCtype)
+ Oid *dbTablespace, char **dbCollate, char **dbCtype,
+ char **dbCollversion)
{
bool result = false;
Relation relation;
@@ -1905,6 +2031,14 @@ get_db_info(const char *name, LOCKMODE lockmode,
Assert(!isnull);
*dbCtype = TextDatumGetCString(datum);
}
+ if (dbCollversion)
+ {
+ datum = SysCacheGetAttr(DATABASEOID, tuple, Anum_pg_database_datcollversion, &isnull);
+ if (isnull)
+ *dbCollversion = NULL;
+ else
+ *dbCollversion = TextDatumGetCString(datum);
+ }
ReleaseSysCache(tuple);
result = true;
break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c4f3242506..92f93cfc72 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10465,6 +10465,12 @@ AlterDatabaseStmt:
(Node *)makeString($6), @6));
$$ = (Node *)n;
}
+ | ALTER DATABASE name REFRESH COLLATION VERSION_P
+ {
+ AlterDatabaseRefreshCollStmt *n = makeNode(AlterDatabaseRefreshCollStmt);
+ n->dbname = $3;
+ $$ = (Node *)n;
+ }
;
AlterDatabaseSetStmt:
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 83e4e37c78..3780c6e812 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -136,6 +136,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
switch (nodeTag(parsetree))
{
case T_AlterCollationStmt:
+ case T_AlterDatabaseRefreshCollStmt:
case T_AlterDatabaseSetStmt:
case T_AlterDatabaseStmt:
case T_AlterDefaultPrivilegesStmt:
@@ -779,6 +780,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
AlterDatabase(pstate, (AlterDatabaseStmt *) parsetree, isTopLevel);
break;
+ case T_AlterDatabaseRefreshCollStmt:
+ /* no event triggers for global objects */
+ AlterDatabaseRefreshColl((AlterDatabaseRefreshCollStmt *) parsetree);
+ break;
+
case T_AlterDatabaseSetStmt:
/* no event triggers for global objects */
AlterDatabaseSet((AlterDatabaseSetStmt *) parsetree);
@@ -2801,9 +2807,7 @@ CreateCommandTag(Node *parsetree)
break;
case T_AlterDatabaseStmt:
- tag = CMDTAG_ALTER_DATABASE;
- break;
-
+ case T_AlterDatabaseRefreshCollStmt:
case T_AlterDatabaseSetStmt:
tag = CMDTAG_ALTER_DATABASE;
break;
@@ -3444,9 +3448,7 @@ GetCommandLogLevel(Node *parsetree)
break;
case T_AlterDatabaseStmt:
- lev = LOGSTMT_DDL;
- break;
-
+ case T_AlterDatabaseRefreshCollStmt:
case T_AlterDatabaseSetStmt:
lev = LOGSTMT_DDL;
break;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 5b9ed2f6f5..f79e89a915 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -31,6 +31,7 @@
#include "catalog/catalog.h"
#include "catalog/namespace.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_collation.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
#include "catalog/pg_tablespace.h"
@@ -414,6 +415,38 @@ CheckMyDatabase(const char *name, bool am_superuser, bool override_allow_connect
" which is not recognized by setlocale().", ctype),
errhint("Recreate the database with another locale or install the missing locale.")));
+ /*
+ * Check collation version. See similar code in
+ * pg_newlocale_from_collation().
+ */
+ datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollversion,
+ &isnull);
+ if (!isnull)
+ {
+ char *actual_versionstr;
+ char *collversionstr;
+
+ collversionstr = TextDatumGetCString(datum);
+
+ actual_versionstr = get_collation_actual_version(COLLPROVIDER_LIBC, collate);
+ if (!actual_versionstr)
+ ereport(WARNING,
+ (errmsg("database \"%s\" has no actual collation version, but a version was specified",
+ name)));
+
+ if (strcmp(actual_versionstr, collversionstr) != 0)
+ ereport(WARNING,
+ (errmsg("database \"%s\" has a collation version mismatch",
+ name),
+ errdetail("The database was created using collation version %s, "
+ "but the operating system provides version %s.",
+ collversionstr, actual_versionstr),
+ errhint("Rebuild all objects affected by collation in this database and run "
+ "ALTER DATABASE %s REFRESH COLLATION VERSION, "
+ "or build PostgreSQL with the right library version.",
+ quote_identifier(name))));
+ }
+
/* Make the locale settings visible as GUC variables, too */
SetConfigOption("lc_collate", collate, PGC_INTERNAL, PGC_S_OVERRIDE);
SetConfigOption("lc_ctype", ctype, PGC_INTERNAL, PGC_S_OVERRIDE);
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index d78e8e67b8..6e70d1b724 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -1857,6 +1857,18 @@ make_template0(FILE *cmdfd)
"CREATE DATABASE template0 IS_TEMPLATE = true ALLOW_CONNECTIONS = false OID = "
CppAsString2(Template0ObjectId) ";\n\n",
+ /*
+ * template0 shouldn't have any collation-dependent objects, so unset
+ * the collation version. This avoids warnings when making a new
+ * database from it.
+ */
+ "UPDATE pg_database SET datcollversion = NULL WHERE datname = 'template0';\n\n",
+
+ /*
+ * While we are here, do set the collation version on template1.
+ */
+ "UPDATE pg_database SET datcollversion = pg_database_collation_actual_version(oid) WHERE datname = 'template1';\n\n",
+
/*
* Explicitly revoke public create-schema and create-temp-table
* privileges in template1 and template0; else the latter would be on
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 3499c0a4d5..7df576961e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2761,6 +2761,7 @@ dumpDatabase(Archive *fout)
i_acldefault,
i_datistemplate,
i_datconnlimit,
+ i_datcollversion,
i_tablespace;
CatalogId dbCatId;
DumpId dbDumpId;
@@ -2792,6 +2793,10 @@ dumpDatabase(Archive *fout)
appendPQExpBuffer(dbQry, "datminmxid, ");
else
appendPQExpBuffer(dbQry, "0 AS datminmxid, ");
+ if (fout->remoteVersion >= 150000)
+ appendPQExpBuffer(dbQry, "datcollversion, ");
+ else
+ appendPQExpBuffer(dbQry, "NULL AS datcollversion, ");
appendPQExpBuffer(dbQry,
"(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
"shobj_description(oid, 'pg_database') AS description "
@@ -2813,6 +2818,7 @@ dumpDatabase(Archive *fout)
i_acldefault = PQfnumber(res, "acldefault");
i_datistemplate = PQfnumber(res, "datistemplate");
i_datconnlimit = PQfnumber(res, "datconnlimit");
+ i_datcollversion = PQfnumber(res, "datcollversion");
i_tablespace = PQfnumber(res, "tablespace");
dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2871,6 +2877,20 @@ dumpDatabase(Archive *fout)
appendStringLiteralAH(creaQry, ctype, fout);
}
}
+ /*
+ * For binary upgrade, carry over the collation version. For normal
+ * dump/restore, omit the version, so that it is computed upon restore.
+ */
+ if (dopt->binary_upgrade)
+ {
+ if (!PQgetisnull(res, 0, i_datcollversion))
+ {
+ appendPQExpBufferStr(creaQry, " COLLATION_VERSION = ");
+ appendStringLiteralAH(creaQry,
+ PQgetvalue(res, 0, i_datcollversion),
+ fout);
+ }
+ }
/*
* Note: looking at dopt->outputNoTablespaces here is completely the wrong
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d1e421bc0f..d8ea142856 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1839,7 +1839,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER DATABASE <name> */
else if (Matches("ALTER", "DATABASE", MatchAny))
- COMPLETE_WITH("RESET", "SET", "OWNER TO", "RENAME TO",
+ COMPLETE_WITH("RESET", "SET", "OWNER TO", "REFRESH COLLATION VERSION", "RENAME TO",
"IS_TEMPLATE", "ALLOW_CONNECTIONS",
"CONNECTION LIMIT");
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 90b43a4ecc..76adbd4aad 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -65,6 +65,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
/* LC_CTYPE setting */
text datctype BKI_FORCE_NOT_NULL;
+ /* provider-dependent version of collation data */
+ text datcollversion BKI_DEFAULT(_null_);
+
/* access permissions */
aclitem datacl[1];
#endif
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7024dbe10a..2f45c764e2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11615,6 +11615,11 @@
proname => 'pg_collation_actual_version', procost => '100',
provolatile => 'v', prorettype => 'text', proargtypes => 'oid',
prosrc => 'pg_collation_actual_version' },
+{ oid => '9167',
+ descr => 'get actual version of database collation from operating system',
+ proname => 'pg_database_collation_actual_version', procost => '100',
+ provolatile => 'v', prorettype => 'text', proargtypes => 'oid',
+ prosrc => 'pg_database_collation_actual_version' },
# system management/monitoring related functions
{ oid => '3353', descr => 'list files in the log directory',
diff --git a/src/include/commands/dbcommands.h b/src/include/commands/dbcommands.h
index b1e8b5eb96..7faf6bf41c 100644
--- a/src/include/commands/dbcommands.h
+++ b/src/include/commands/dbcommands.h
@@ -24,6 +24,7 @@ extern void dropdb(const char *dbname, bool missing_ok, bool force);
extern void DropDatabase(ParseState *pstate, DropdbStmt *stmt);
extern ObjectAddress RenameDatabase(const char *oldname, const char *newname);
extern Oid AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel);
+extern Oid AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt);
extern Oid AlterDatabaseSet(AlterDatabaseSetStmt *stmt);
extern ObjectAddress AlterDatabaseOwner(const char *dbname, Oid newOwnerId);
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index da35f2c272..5d075f0c34 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -370,6 +370,7 @@ typedef enum NodeTag
T_CheckPointStmt,
T_CreateSchemaStmt,
T_AlterDatabaseStmt,
+ T_AlterDatabaseRefreshCollStmt,
T_AlterDatabaseSetStmt,
T_AlterRoleSetStmt,
T_CreateConversionStmt,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 37fcc4c9b5..34218b718c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3308,6 +3308,12 @@ typedef struct AlterDatabaseStmt
List *options; /* List of DefElem nodes */
} AlterDatabaseStmt;
+typedef struct AlterDatabaseRefreshCollStmt
+{
+ NodeTag type;
+ char *dbname;
+} AlterDatabaseRefreshCollStmt;
+
typedef struct AlterDatabaseSetStmt
{
NodeTag type;
--
2.35.1
On Mon, Feb 07, 2022 at 04:44:24PM +0100, Peter Eisentraut wrote:
On 07.02.22 11:29, Julien Rouhaud wrote:
- there is no test
Suggestions where to put it? We don't really have tests for the
collation-level versioning either, do we?
There's so limited testing in collate.* regression tests, so I thought it would
be ok to add it there. At least some ALTER DATABASE ... REFRESH VERSION would
be good, similarly to collation-level versioning.
- it's missing some updates in create_database.sgml, and psql tab completion
for CREATE DATABASE with the new collation_version defelem.Added to create_database.sgml, but not to psql. We don't have completion
for the collation option either, since it's only meant to be used by
pg_upgrade, not interactively.
Ok.
- that's not really something new with this patch, but should we output the
collation version info or mismatch info in \l / \dO?It's a possibility. Perhaps there is a question of performance if we show
it in \l and people have tons of databases and they have to make a locale
call for each one. As you say, it's more an independent feature, but it's
worth looking into.
Agreed.
+Oid +AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt) +{ + Relation rel; + Oid dboid; + HeapTuple tup; + Datum datum; + bool isnull; + char *oldversion; + char *newversion; + + rel = table_open(DatabaseRelationId, RowExclusiveLock); + dboid = get_database_oid(stmt->dbname, false); + + if (!pg_database_ownercheck(dboid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE, + stmt->dbname); + + tup = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(dboid)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for database %u", dboid);Is that ok to not obtain a lock on the database when refreshing the collation?
That code takes a RowExclusiveLock on pg_database. Did you have something
else in mind?
But that lock won't prevent a concurrent DROP DATABASE, so it's totally
expected to hit that cache lookup failed error. There should either be a
shdepLockAndCheckObject(), or changing the error message to some errmsg("data
xxx does not exist").
+ /* + * Check collation version. See similar code in + * pg_newlocale_from_collation(). + */ + datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollversion, + &isnull); + if (!isnull) + {This (and pg_newlocale_from_collation()) reports a problem if a recorded
collation version is found but there's no reported collation version.
Shouldn't it also complain if it's the opposite? It's otherwise a backdoor to
make sure there won't be any check about the version anymore, and while it can
probably happen if you mess with the catalogs it still doesn't look great.get_collation_actual_version() always returns either null or not null for a
given installation. So the situation that the stored version is null and
the actual version is not null can only happen as part of a software
upgrade. In that case, all uses of collations after an upgrade would
immediately start complaining about missing versions, which seems like a bad
experience. Users can explicitly opt in to version tracking by running
REFRESH VERSION once.
Ah right, I do remember that point which was also discussed in the collation
version tracking. Sorry about the noise.
+ /* + * template0 shouldn't have any collation-dependent objects, so unset + * the collation version. This avoids warnings when making a new + * database from it. + */ + "UPDATE pg_database SET datcollversion = NULL WHERE datname = 'template0';\n\n",I'm not opposed, but shouldn't there indeed be a warning in case of discrepancy
in the source database (whether template or not)?# update pg_database set datcollversion = 'meh' where datname in ('postgres', 'template1');
UPDATE 2# create database test1 template postgres;
CREATE DATABASE# create database test2 template template1;
CREATE DATABASE# \c test2
WARNING: database "test2" has a collation version mismatchI don't understand what the complaint is here. It seems to work ok?
The comment says that you explicitly set a NULL collation version to avoid
warning when creating a database using template0 as a template, presumably
after a collation lib upgrade.
But as far as I can see the source database collation version is not checked
when creating a new database, so it seems to me that either the comment is
wrong, or we need another check for that. The latter seems preferable to me.
On 07.02.22 17:08, Julien Rouhaud wrote:
There's so limited testing in collate.* regression tests, so I thought it would
be ok to add it there. At least some ALTER DATABASE ... REFRESH VERSION would
be good, similarly to collation-level versioning.
I don't think you can run ALTER DATABASE from the regression test
scripts, since the database name is not fixed. You'd have to paste the
command together using psql tricks or something. I guess it could be
done, but maybe there is a better solution. We could put it into the
createdb test suite, or write a new TAP test suite somewhere. There is
no good precedent for this.
That code takes a RowExclusiveLock on pg_database. Did you have something
else in mind?But that lock won't prevent a concurrent DROP DATABASE, so it's totally
expected to hit that cache lookup failed error. There should either be a
shdepLockAndCheckObject(), or changing the error message to some errmsg("data
xxx does not exist").
I was not familiar with that function. AFAICT, it is only used for
database and role settings (AlterDatabaseSet(), AlterRoleSet()), but not
when just updating the role or database catalog (e.g., AlterRole(),
RenameRole(), RenameDatabase()). So I don't think it is needed here.
Maybe I'm missing something.
On Tue, Feb 08, 2022 at 12:14:02PM +0100, Peter Eisentraut wrote:
On 07.02.22 17:08, Julien Rouhaud wrote:
There's so limited testing in collate.* regression tests, so I thought it would
be ok to add it there. At least some ALTER DATABASE ... REFRESH VERSION would
be good, similarly to collation-level versioning.I don't think you can run ALTER DATABASE from the regression test scripts,
since the database name is not fixed. You'd have to paste the command
together using psql tricks or something. I guess it could be done, but
maybe there is a better solution. We could put it into the createdb test
suite, or write a new TAP test suite somewhere. There is no good precedent
for this.
I was thinking using a simple DO command, as there are already some other usage
for that for non deterministic things (like TOAST tables). If it's too
problematic I'm fine with not having tests for that for now.
That code takes a RowExclusiveLock on pg_database. Did you have something
else in mind?But that lock won't prevent a concurrent DROP DATABASE, so it's totally
expected to hit that cache lookup failed error. There should either be a
shdepLockAndCheckObject(), or changing the error message to some errmsg("data
xxx does not exist").I was not familiar with that function. AFAICT, it is only used for database
and role settings (AlterDatabaseSet(), AlterRoleSet()), but not when just
updating the role or database catalog (e.g., AlterRole(), RenameRole(),
RenameDatabase()). So I don't think it is needed here. Maybe I'm missing
something.
I'm just saying that without such a lock you can easily trigger the "cache
lookup" error, and that's something that's supposed to happen with normal
usage I think. So it should be a better message saying that the database has
been concurrently dropped, or actually simply does not exist like it's done in
AlterDatabaseOwner() for the same pattern:
[...]
tuple = systable_getnext(scan);
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("database \"%s\" does not exist", dbname)));
[...]
Apart from that I still think that we should check the collation version of the
source database when creating a new database. It won't cost much but will give
the DBA a chance to recreate the indexes before risking invalid index usage.
On Mon, Feb 07, 2022 at 04:44:24PM +0100, Peter Eisentraut wrote:
On 07.02.22 11:29, Julien Rouhaud wrote:
- that's not really something new with this patch, but should we output the
collation version info or mismatch info in \l / \dO?It's a possibility. Perhaps there is a question of performance if we show
it in \l and people have tons of databases and they have to make a locale
call for each one. As you say, it's more an independent feature, but it's
worth looking into.
Ok, but \l+ shows among others the database size, so I guess at that
level also showing this should be fine? (or is that already the case?)
Michael
On 08.02.22 13:55, Julien Rouhaud wrote:
I'm just saying that without such a lock you can easily trigger the "cache
lookup" error, and that's something that's supposed to happen with normal
usage I think. So it should be a better message saying that the database has
been concurrently dropped, or actually simply does not exist like it's done in
AlterDatabaseOwner() for the same pattern:[...]
tuple = systable_getnext(scan);
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("database \"%s\" does not exist", dbname)));
[...]
In my code, the existence of the database is checked by
dboid = get_database_oid(stmt->dbname, false);
This also issues an appropriate user-facing error message if the
database does not exist.
The flow in AlterDatabaseOwner() is a bit different, it looks up the
pg_database tuple directly from the name. I think both are correct. My
code has been copied from the analogous code in AlterCollation().
On Wed, Feb 09, 2022 at 12:48:35PM +0100, Peter Eisentraut wrote:
On 08.02.22 13:55, Julien Rouhaud wrote:
I'm just saying that without such a lock you can easily trigger the "cache
lookup" error, and that's something that's supposed to happen with normal
usage I think. So it should be a better message saying that the database has
been concurrently dropped, or actually simply does not exist like it's done in
AlterDatabaseOwner() for the same pattern:[...]
tuple = systable_getnext(scan);
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("database \"%s\" does not exist", dbname)));
[...]In my code, the existence of the database is checked by
dboid = get_database_oid(stmt->dbname, false);
This also issues an appropriate user-facing error message if the database
does not exist.
Yes but if you run a DROP DATABASE concurrently you will either get a
"database does not exist" or "cache lookup failed" depending on whether the
DROP is processed before or after the get_database_oid().
I agree that it's not worth trying to make it concurrent-drop safe, but I also
thought that throwing plain elog(ERROR) should be avoided when reasonably
doable. And in that situation we know it can happen in normal situation, so
having a real error message looks like a cost-free improvement. Now if it's
better to have a cache lookup error even in that situation just for safety or
something ok, it's not like trying to refresh a db collation and having someone
else dropping it at the same time is going to be a common practice anway.
The flow in AlterDatabaseOwner() is a bit different, it looks up the
pg_database tuple directly from the name. I think both are correct. My
code has been copied from the analogous code in AlterCollation().
I also think it would be better to have a "collation does not exist" in the
syscache failure message, but same here dropping collation is probably even
less frequent than dropping database, let alone while refreshing the collation
version.
On 08.02.22 13:55, Julien Rouhaud wrote:
Apart from that I still think that we should check the collation version of the
source database when creating a new database. It won't cost much but will give
the DBA a chance to recreate the indexes before risking invalid index usage.
A question on this: In essence, this would be putting code into
createdb() similar to the code in postinit.c:CheckMyDatabase(). But
what should we make it do and say exactly?
After thinking about this for a bit, I suggest: If the actual collation
version of the newly created database does not match the recorded
collation version of the template database, we should error out and make
the user fix the template database (by reindexing there etc.).
The alternative is to warn, as it does now in postinit.c. But then the
phrasing of the message becomes complicated: Should we make the user fix
the new database or the template database or both? And if they don't
fix the template database, they will have the same problem again. So
making it a hard failure seems better to me.
On 2022-Feb-08, Julien Rouhaud wrote:
On Tue, Feb 08, 2022 at 12:14:02PM +0100, Peter Eisentraut wrote:
I don't think you can run ALTER DATABASE from the regression test scripts,
since the database name is not fixed. You'd have to paste the command
together using psql tricks or something. I guess it could be done, but
maybe there is a better solution. We could put it into the createdb test
suite, or write a new TAP test suite somewhere. There is no good precedent
for this.I was thinking using a simple DO command, as there are already some other usage
for that for non deterministic things (like TOAST tables). If it's too
problematic I'm fine with not having tests for that for now.
You can do this:
select current_database() as datname \gset
alter database :"datname" owner to foo;
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
On Wed, Feb 09, 2022 at 05:12:41PM +0100, Peter Eisentraut wrote:
On 08.02.22 13:55, Julien Rouhaud wrote:
Apart from that I still think that we should check the collation version of the
source database when creating a new database. It won't cost much but will give
the DBA a chance to recreate the indexes before risking invalid index usage.A question on this: In essence, this would be putting code into createdb()
similar to the code in postinit.c:CheckMyDatabase(). But what should we
make it do and say exactly?After thinking about this for a bit, I suggest: If the actual collation
version of the newly created database does not match the recorded collation
version of the template database, we should error out and make the user fix
the template database (by reindexing there etc.).
I'm not sure what you really mean by "actual collation version of the newly
created database". Is it really a check after having done all the work or just
checking a discrepancy when computing the to-be-created database version from
the source database, ie. something like
if (dbcollversion == NULL)
+ {
dbcollversion = src_collversion;
+ if src_collversion != get_collation_actual_version(the source db)
+ // raise error or warning
The alternative is to warn, as it does now in postinit.c. But then the
phrasing of the message becomes complicated: Should we make the user fix the
new database or the template database or both? And if they don't fix the
template database, they will have the same problem again. So making it a
hard failure seems better to me.
Agreed, I'm in favor of a hard error. Maybe a message like:
errmsg(cannot create database %s)
errdetail(the template database %s was created using collation version %s, but
the operating system provides version %s)
Also, that check shouldn't be done when using the COLLATION_VERSION option of
create database, since it's there for pg_upgrade usage?
New patch that fixes all reported issues, I think:
- Added test for ALTER DATABASE / REFRESH COLLATION VERSION
- Rewrote AlterDatabaseRefreshCollVersion() with better locking
- Added version checking in createdb()
Attachments:
v4-0001-Database-level-collation-version-tracking.patchtext/plain; charset=UTF-8; name=v4-0001-Database-level-collation-version-tracking.patchDownload
From 290ebb9ca743a2272181f435d5ea76d8a7280a0a Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Thu, 10 Feb 2022 09:44:20 +0100
Subject: [PATCH v4] Database-level collation version tracking
This adds to database objects the same version tracking that collation
objects have. There is a new pg_database column datcollversion that
stores the version, a new function
pg_database_collation_actual_version() to get the version from the
operating system, and a new subcommand ALTER DATABASE ... REFRESH
COLLATION VERSION.
This was not originally added together with pg_collation.collversion,
since originally version tracking was only supported for ICU, and ICU
on a database-level is not currently supported. But we now have
version tracking for glibc (since PG13), FreeBSD (since PG14), and
Windows (since PG13), so this is useful to have now.
Discussion: https://www.postgresql.org/message-id/flat/f0ff3190-29a3-5b39-a179-fa32eee57db6%40enterprisedb.com
XXX catversion bump
---
doc/src/sgml/catalogs.sgml | 11 +
doc/src/sgml/func.sgml | 18 ++
doc/src/sgml/ref/alter_collation.sgml | 3 +-
doc/src/sgml/ref/alter_database.sgml | 12 ++
doc/src/sgml/ref/create_database.sgml | 21 ++
src/backend/commands/dbcommands.c | 194 +++++++++++++++++-
src/backend/parser/gram.y | 6 +
src/backend/tcop/utility.c | 14 +-
src/backend/utils/init/postinit.c | 34 +++
src/bin/initdb/initdb.c | 12 ++
src/bin/pg_dump/pg_dump.c | 21 ++
src/bin/psql/tab-complete.c | 2 +-
src/include/catalog/pg_database.h | 3 +
src/include/catalog/pg_proc.dat | 5 +
src/include/commands/dbcommands.h | 1 +
src/include/nodes/nodes.h | 1 +
src/include/nodes/parsenodes.h | 6 +
.../regress/expected/collate.icu.utf8.out | 4 +
.../regress/expected/collate.linux.utf8.out | 4 +
src/test/regress/sql/collate.icu.utf8.sql | 4 +
src/test/regress/sql/collate.linux.utf8.sql | 4 +
21 files changed, 366 insertions(+), 14 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 879d2dbce0..5a1627a394 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3043,6 +3043,17 @@ <title><structname>pg_database</structname> Columns</title>
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>datcollversion</structfield> <type>text</type>
+ </para>
+ <para>
+ Provider-specific version of the collation. This is recorded when the
+ database is created and then checked when it is used, to detect
+ changes in the collation definition that could lead to data corruption.
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>datacl</structfield> <type>aclitem[]</type>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 1b064b4feb..df3cd5987b 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27061,6 +27061,24 @@ <title>Collation Management Functions</title>
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_database_collation_actual_version</primary>
+ </indexterm>
+ <function>pg_database_collation_actual_version</function> ( <type>oid</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the actual version of the database's collation as it is currently
+ installed in the operating system. If this is different from the
+ value in
+ <structname>pg_database</structname>.<structfield>datcollversion</structfield>,
+ then objects depending on the collation might need to be rebuilt. See
+ also <xref linkend="sql-alterdatabase"/>.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/alter_collation.sgml b/doc/src/sgml/ref/alter_collation.sgml
index 892c466565..a8c831d728 100644
--- a/doc/src/sgml/ref/alter_collation.sgml
+++ b/doc/src/sgml/ref/alter_collation.sgml
@@ -151,7 +151,8 @@ <title>Notes</title>
</para>
</note>
<para>
- Currently, there is no version tracking for the database default collation.
+ For the database default collation, there is an analogous command
+ <literal>ALTER DATABASE ... REFRESH COLLATION VERSION</literal>.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_database.sgml b/doc/src/sgml/ref/alter_database.sgml
index 81e37536a3..89ed261b4c 100644
--- a/doc/src/sgml/ref/alter_database.sgml
+++ b/doc/src/sgml/ref/alter_database.sgml
@@ -35,6 +35,8 @@
ALTER DATABASE <replaceable class="parameter">name</replaceable> SET TABLESPACE <replaceable class="parameter">new_tablespace</replaceable>
+ALTER DATABASE <replaceable class="parameter">name</replaceable> REFRESH COLLATION VERSION
+
ALTER DATABASE <replaceable class="parameter">name</replaceable> SET <replaceable>configuration_parameter</replaceable> { TO | = } { <replaceable>value</replaceable> | DEFAULT }
ALTER DATABASE <replaceable class="parameter">name</replaceable> SET <replaceable>configuration_parameter</replaceable> FROM CURRENT
ALTER DATABASE <replaceable class="parameter">name</replaceable> RESET <replaceable>configuration_parameter</replaceable>
@@ -171,6 +173,16 @@ <title>Parameters</title>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>REFRESH COLLATION VERSION</literal></term>
+ <listitem>
+ <para>
+ Update the database collation version. See <xref
+ linkend="sql-altercollation-notes"/> for background.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><replaceable>configuration_parameter</replaceable></term>
<term><replaceable>value</replaceable></term>
diff --git a/doc/src/sgml/ref/create_database.sgml b/doc/src/sgml/ref/create_database.sgml
index f22e28dc81..f70d0c75b4 100644
--- a/doc/src/sgml/ref/create_database.sgml
+++ b/doc/src/sgml/ref/create_database.sgml
@@ -28,6 +28,7 @@
[ LOCALE [=] <replaceable class="parameter">locale</replaceable> ]
[ LC_COLLATE [=] <replaceable class="parameter">lc_collate</replaceable> ]
[ LC_CTYPE [=] <replaceable class="parameter">lc_ctype</replaceable> ]
+ [ COLLATION_VERSION = <replaceable>collation_version</replaceable> ]
[ TABLESPACE [=] <replaceable class="parameter">tablespace_name</replaceable> ]
[ ALLOW_CONNECTIONS [=] <replaceable class="parameter">allowconn</replaceable> ]
[ CONNECTION LIMIT [=] <replaceable class="parameter">connlimit</replaceable> ]
@@ -158,6 +159,26 @@ <title>Parameters</title>
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><replaceable>collation_version</replaceable></term>
+
+ <listitem>
+ <para>
+ Specifies the collation version string to store with the database.
+ Normally, this should be omitted, which will cause the version to be
+ computed from the actual version of the database collation as provided
+ by the operating system. This option is intended to be used by
+ <command>pg_upgrade</command> for copying the version from an existing
+ installation.
+ </para>
+
+ <para>
+ See also <xref linkend="sql-alterdatabase"/> for how to handle
+ database collation version mismatches.
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><replaceable class="parameter">tablespace_name</replaceable></term>
<listitem>
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index e673138cbd..5397a27754 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -36,6 +36,7 @@
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_collation.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
#include "catalog/pg_subscription.h"
@@ -85,7 +86,8 @@ static bool get_db_info(const char *name, LOCKMODE lockmode,
Oid *dbIdP, Oid *ownerIdP,
int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
- Oid *dbTablespace, char **dbCollate, char **dbCtype);
+ Oid *dbTablespace, char **dbCollate, char **dbCtype,
+ char **dbCollversion);
static bool have_createdb_privilege(void);
static void remove_dbtablespaces(Oid db_id);
static bool check_db_file_conflict(Oid db_id);
@@ -105,6 +107,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
int src_encoding = -1;
char *src_collate = NULL;
char *src_ctype = NULL;
+ char *src_collversion = NULL;
bool src_istemplate;
bool src_allowconn;
TransactionId src_frozenxid = InvalidTransactionId;
@@ -128,6 +131,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
DefElem *distemplate = NULL;
DefElem *dallowconnections = NULL;
DefElem *dconnlimit = NULL;
+ DefElem *dcollversion = NULL;
char *dbname = stmt->dbname;
char *dbowner = NULL;
const char *dbtemplate = NULL;
@@ -138,6 +142,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
bool dbistemplate = false;
bool dballowconnections = true;
int dbconnlimit = -1;
+ char *dbcollversion = NULL;
int notherbackends;
int npreparedxacts;
createdb_failure_params fparms;
@@ -207,6 +212,12 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
errorConflictingDefElem(defel, pstate);
dconnlimit = defel;
}
+ else if (strcmp(defel->defname, "collation_version") == 0)
+ {
+ if (dcollversion)
+ errorConflictingDefElem(defel, pstate);
+ dcollversion = defel;
+ }
else if (strcmp(defel->defname, "location") == 0)
{
ereport(WARNING,
@@ -305,6 +316,8 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("invalid connection limit: %d", dbconnlimit)));
}
+ if (dcollversion)
+ dbcollversion = defGetString(dcollversion);
/* obtain OID of proposed owner */
if (dbowner)
@@ -342,7 +355,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
&src_dboid, &src_owner, &src_encoding,
&src_istemplate, &src_allowconn,
&src_frozenxid, &src_minmxid, &src_deftablespace,
- &src_collate, &src_ctype))
+ &src_collate, &src_ctype, &src_collversion))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("template database \"%s\" does not exist",
@@ -424,6 +437,52 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
errhint("Use the same LC_CTYPE as in the template database, or use template0 as template.")));
}
+ /*
+ * If we got a collation version for the template database, check that it
+ * matches the actual OS collation version. Otherwise error; the user
+ * needs to fix the template database first. Don't complain if a
+ * collation version was specified explicitly as an statement option; that
+ * is used by pg_upgrade to reproduce the old state exactly.
+ *
+ * (If the template database has no collation version, then either the
+ * platform/provider does not support collation versioning, or it's
+ * template0, for which we stipulate that it does not contain
+ * collation-using objects.)
+ */
+ if (src_collversion && !dcollversion)
+ {
+ char *actual_versionstr;
+
+ actual_versionstr = get_collation_actual_version(COLLPROVIDER_LIBC, dbcollate);
+ if (!actual_versionstr)
+ ereport(ERROR,
+ (errmsg("template database \"%s\" has a collation version, but no actual collation version could be determined",
+ dbtemplate)));
+
+ if (strcmp(actual_versionstr, src_collversion) != 0)
+ ereport(ERROR,
+ (errmsg("template database \"%s\" has a collation version mismatch",
+ dbtemplate),
+ errdetail("The template database was created using collation version %s, "
+ "but the operating system provides version %s.",
+ src_collversion, actual_versionstr),
+ errhint("Rebuild all objects affected by collation in the template database and run "
+ "ALTER DATABASE %s REFRESH COLLATION VERSION, "
+ "or build PostgreSQL with the right library version.",
+ quote_identifier(dbtemplate))));
+ }
+
+ if (dbcollversion == NULL)
+ dbcollversion = src_collversion;
+
+ /*
+ * Normally, we copy the collation version from the template database.
+ * This last resort only applies if the template database does not have a
+ * collation version, which is normally only the case for template0.
+ */
+ if (dbcollversion == NULL)
+ dbcollversion = get_collation_actual_version(COLLPROVIDER_LIBC, dbcollate);
+
/* Resolve default tablespace for new database */
if (dtablespacename && dtablespacename->arg)
{
@@ -578,6 +637,10 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
new_record[Anum_pg_database_dattablespace - 1] = ObjectIdGetDatum(dst_deftablespace);
new_record[Anum_pg_database_datcollate - 1] = CStringGetTextDatum(dbcollate);
new_record[Anum_pg_database_datctype - 1] = CStringGetTextDatum(dbctype);
+ if (dbcollversion)
+ new_record[Anum_pg_database_datcollversion - 1] = CStringGetTextDatum(dbcollversion);
+ else
+ new_record_nulls[Anum_pg_database_datcollversion - 1] = true;
/*
* We deliberately set datacl to default (NULL), rather than copying it
@@ -844,7 +907,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
- &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL))
+ &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
{
if (!missing_ok)
{
@@ -1043,7 +1106,7 @@ RenameDatabase(const char *oldname, const char *newname)
rel = table_open(DatabaseRelationId, RowExclusiveLock);
if (!get_db_info(oldname, AccessExclusiveLock, &db_id, NULL, NULL,
- NULL, NULL, NULL, NULL, NULL, NULL, NULL))
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("database \"%s\" does not exist", oldname)));
@@ -1156,7 +1219,7 @@ movedb(const char *dbname, const char *tblspcname)
pgdbrel = table_open(DatabaseRelationId, RowExclusiveLock);
if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
- NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL))
+ NULL, NULL, NULL, NULL, &src_tblspcoid, NULL, NULL, NULL))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_DATABASE),
errmsg("database \"%s\" does not exist", dbname)));
@@ -1643,6 +1706,88 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel)
}
+/*
+ * ALTER DATABASE name REFRESH COLLATION VERSION
+ */
+ObjectAddress
+AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt)
+{
+ Relation rel;
+ ScanKeyData scankey;
+ SysScanDesc scan;
+ Oid db_id;
+ HeapTuple tuple;
+ Form_pg_database datForm;
+ ObjectAddress address;
+ Datum datum;
+ bool isnull;
+ char *oldversion;
+ char *newversion;
+
+ rel = table_open(DatabaseRelationId, RowExclusiveLock);
+ ScanKeyInit(&scankey,
+ Anum_pg_database_datname,
+ BTEqualStrategyNumber, F_NAMEEQ,
+ CStringGetDatum(stmt->dbname));
+ scan = systable_beginscan(rel, DatabaseNameIndexId, true,
+ NULL, 1, &scankey);
+ tuple = systable_getnext(scan);
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_DATABASE),
+ errmsg("database \"%s\" does not exist", stmt->dbname)));
+
+ datForm = (Form_pg_database) GETSTRUCT(tuple);
+ db_id = datForm->oid;
+
+ if (!pg_database_ownercheck(db_id, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE,
+ stmt->dbname);
+
+ datum = heap_getattr(tuple, Anum_pg_database_datcollversion, RelationGetDescr(rel), &isnull);
+ oldversion = isnull ? NULL : TextDatumGetCString(datum);
+
+ datum = heap_getattr(tuple, Anum_pg_database_datcollate, RelationGetDescr(rel), &isnull);
+ Assert(!isnull);
+ newversion = get_collation_actual_version(COLLPROVIDER_LIBC, TextDatumGetCString(datum));
+
+ /* cannot change from NULL to non-NULL or vice versa */
+ if ((!oldversion && newversion) || (oldversion && !newversion))
+ elog(ERROR, "invalid collation version change");
+ else if (oldversion && newversion && strcmp(newversion, oldversion) != 0)
+ {
+ bool nulls[Natts_pg_database] = {0};
+ bool replaces[Natts_pg_database] = {0};
+ Datum values[Natts_pg_database] = {0};
+
+ ereport(NOTICE,
+ (errmsg("changing version from %s to %s",
+ oldversion, newversion)));
+
+ values[Anum_pg_database_datcollversion - 1] = CStringGetTextDatum(newversion);
+ replaces[Anum_pg_database_datcollversion - 1] = true;
+
+ tuple = heap_modify_tuple(tuple, RelationGetDescr(rel),
+ values, nulls, replaces);
+ CatalogTupleUpdate(rel, &tuple->t_self, tuple);
+ heap_freetuple(tuple);
+ }
+ else
+ ereport(NOTICE,
+ (errmsg("version has not changed")));
+
+ InvokeObjectPostAlterHook(DatabaseRelationId, db_id, 0);
+
+ ObjectAddressSet(address, DatabaseRelationId, db_id);
+
+ systable_endscan(scan);
+
+ table_close(rel, NoLock);
+
+ return address;
+}
+
+
/*
* ALTER DATABASE name SET ...
*/
@@ -1785,6 +1930,34 @@ AlterDatabaseOwner(const char *dbname, Oid newOwnerId)
}
+Datum
+pg_database_collation_actual_version(PG_FUNCTION_ARGS)
+{
+ Oid dbid = PG_GETARG_OID(0);
+ HeapTuple tp;
+ Datum datum;
+ bool isnull;
+ char *version;
+
+ tp = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbid));
+ if (!HeapTupleIsValid(tp))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("database with OID %u does not exist", dbid)));
+
+ datum = SysCacheGetAttr(DATABASEOID, tp, Anum_pg_database_datcollate, &isnull);
+ Assert(!isnull);
+ version = get_collation_actual_version(COLLPROVIDER_LIBC, TextDatumGetCString(datum));
+
+ ReleaseSysCache(tp);
+
+ if (version)
+ PG_RETURN_TEXT_P(cstring_to_text(version));
+ else
+ PG_RETURN_NULL();
+}
+
+
/*
* Helper functions
*/
@@ -1800,7 +1973,8 @@ get_db_info(const char *name, LOCKMODE lockmode,
Oid *dbIdP, Oid *ownerIdP,
int *encodingP, bool *dbIsTemplateP, bool *dbAllowConnP,
TransactionId *dbFrozenXidP, MultiXactId *dbMinMultiP,
- Oid *dbTablespace, char **dbCollate, char **dbCtype)
+ Oid *dbTablespace, char **dbCollate, char **dbCtype,
+ char **dbCollversion)
{
bool result = false;
Relation relation;
@@ -1905,6 +2079,14 @@ get_db_info(const char *name, LOCKMODE lockmode,
Assert(!isnull);
*dbCtype = TextDatumGetCString(datum);
}
+ if (dbCollversion)
+ {
+ datum = SysCacheGetAttr(DATABASEOID, tuple, Anum_pg_database_datcollversion, &isnull);
+ if (isnull)
+ *dbCollversion = NULL;
+ else
+ *dbCollversion = TextDatumGetCString(datum);
+ }
ReleaseSysCache(tuple);
result = true;
break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c4f3242506..92f93cfc72 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10465,6 +10465,12 @@ AlterDatabaseStmt:
(Node *)makeString($6), @6));
$$ = (Node *)n;
}
+ | ALTER DATABASE name REFRESH COLLATION VERSION_P
+ {
+ AlterDatabaseRefreshCollStmt *n = makeNode(AlterDatabaseRefreshCollStmt);
+ n->dbname = $3;
+ $$ = (Node *)n;
+ }
;
AlterDatabaseSetStmt:
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 83e4e37c78..3780c6e812 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -136,6 +136,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
switch (nodeTag(parsetree))
{
case T_AlterCollationStmt:
+ case T_AlterDatabaseRefreshCollStmt:
case T_AlterDatabaseSetStmt:
case T_AlterDatabaseStmt:
case T_AlterDefaultPrivilegesStmt:
@@ -779,6 +780,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
AlterDatabase(pstate, (AlterDatabaseStmt *) parsetree, isTopLevel);
break;
+ case T_AlterDatabaseRefreshCollStmt:
+ /* no event triggers for global objects */
+ AlterDatabaseRefreshColl((AlterDatabaseRefreshCollStmt *) parsetree);
+ break;
+
case T_AlterDatabaseSetStmt:
/* no event triggers for global objects */
AlterDatabaseSet((AlterDatabaseSetStmt *) parsetree);
@@ -2801,9 +2807,7 @@ CreateCommandTag(Node *parsetree)
break;
case T_AlterDatabaseStmt:
- tag = CMDTAG_ALTER_DATABASE;
- break;
-
+ case T_AlterDatabaseRefreshCollStmt:
case T_AlterDatabaseSetStmt:
tag = CMDTAG_ALTER_DATABASE;
break;
@@ -3444,9 +3448,7 @@ GetCommandLogLevel(Node *parsetree)
break;
case T_AlterDatabaseStmt:
- lev = LOGSTMT_DDL;
- break;
-
+ case T_AlterDatabaseRefreshCollStmt:
case T_AlterDatabaseSetStmt:
lev = LOGSTMT_DDL;
break;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index ca53912f15..6c9f948dc5 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -32,6 +32,7 @@
#include "catalog/catalog.h"
#include "catalog/namespace.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_collation.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
#include "catalog/pg_tablespace.h"
@@ -418,6 +419,39 @@ CheckMyDatabase(const char *name, bool am_superuser, bool override_allow_connect
" which is not recognized by setlocale().", ctype),
errhint("Recreate the database with another locale or install the missing locale.")));
+ /*
+ * Check collation version. See similar code in
+ * pg_newlocale_from_collation(). Note that here we warn instead of error
+ * in any case, so that we don't prevent connecting.
+ */
+ datum = SysCacheGetAttr(DATABASEOID, tup, Anum_pg_database_datcollversion,
+ &isnull);
+ if (!isnull)
+ {
+ char *actual_versionstr;
+ char *collversionstr;
+
+ collversionstr = TextDatumGetCString(datum);
+
+ actual_versionstr = get_collation_actual_version(COLLPROVIDER_LIBC, collate);
+ if (!actual_versionstr)
+ ereport(WARNING,
+ (errmsg("database \"%s\" has no actual collation version, but a version was recorded",
+ name)));
+
+ if (strcmp(actual_versionstr, collversionstr) != 0)
+ ereport(WARNING,
+ (errmsg("database \"%s\" has a collation version mismatch",
+ name),
+ errdetail("The database was created using collation version %s, "
+ "but the operating system provides version %s.",
+ collversionstr, actual_versionstr),
+ errhint("Rebuild all objects affected by collation in this database and run "
+ "ALTER DATABASE %s REFRESH COLLATION VERSION, "
+ "or build PostgreSQL with the right library version.",
+ quote_identifier(name))));
+ }
+
/* Make the locale settings visible as GUC variables, too */
SetConfigOption("lc_collate", collate, PGC_INTERNAL, PGC_S_OVERRIDE);
SetConfigOption("lc_ctype", ctype, PGC_INTERNAL, PGC_S_OVERRIDE);
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index d78e8e67b8..97f15971e2 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -1857,6 +1857,18 @@ make_template0(FILE *cmdfd)
"CREATE DATABASE template0 IS_TEMPLATE = true ALLOW_CONNECTIONS = false OID = "
CppAsString2(Template0ObjectId) ";\n\n",
+ /*
+ * template0 shouldn't have any collation-dependent objects, so unset
+ * the collation version. This disables collation version checks when
+ * making a new database from it.
+ */
+ "UPDATE pg_database SET datcollversion = NULL WHERE datname = 'template0';\n\n",
+
+ /*
+ * While we are here, do set the collation version on template1.
+ */
+ "UPDATE pg_database SET datcollversion = pg_database_collation_actual_version(oid) WHERE datname = 'template1';\n\n",
+
/*
* Explicitly revoke public create-schema and create-temp-table
* privileges in template1 and template0; else the latter would be on
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 3499c0a4d5..a3b36ecf21 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2761,6 +2761,7 @@ dumpDatabase(Archive *fout)
i_acldefault,
i_datistemplate,
i_datconnlimit,
+ i_datcollversion,
i_tablespace;
CatalogId dbCatId;
DumpId dbDumpId;
@@ -2792,6 +2793,10 @@ dumpDatabase(Archive *fout)
appendPQExpBuffer(dbQry, "datminmxid, ");
else
appendPQExpBuffer(dbQry, "0 AS datminmxid, ");
+ if (fout->remoteVersion >= 150000)
+ appendPQExpBuffer(dbQry, "datcollversion, ");
+ else
+ appendPQExpBuffer(dbQry, "NULL AS datcollversion, ");
appendPQExpBuffer(dbQry,
"(SELECT spcname FROM pg_tablespace t WHERE t.oid = dattablespace) AS tablespace, "
"shobj_description(oid, 'pg_database') AS description "
@@ -2813,6 +2818,7 @@ dumpDatabase(Archive *fout)
i_acldefault = PQfnumber(res, "acldefault");
i_datistemplate = PQfnumber(res, "datistemplate");
i_datconnlimit = PQfnumber(res, "datconnlimit");
+ i_datcollversion = PQfnumber(res, "datcollversion");
i_tablespace = PQfnumber(res, "tablespace");
dbCatId.tableoid = atooid(PQgetvalue(res, 0, i_tableoid));
@@ -2872,6 +2878,21 @@ dumpDatabase(Archive *fout)
}
}
+ /*
+ * For binary upgrade, carry over the collation version. For normal
+ * dump/restore, omit the version, so that it is computed upon restore.
+ */
+ if (dopt->binary_upgrade)
+ {
+ if (!PQgetisnull(res, 0, i_datcollversion))
+ {
+ appendPQExpBufferStr(creaQry, " COLLATION_VERSION = ");
+ appendStringLiteralAH(creaQry,
+ PQgetvalue(res, 0, i_datcollversion),
+ fout);
+ }
+ }
+
/*
* Note: looking at dopt->outputNoTablespaces here is completely the wrong
* thing; the decision whether to specify a tablespace should be left till
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 25d3abbcf1..0b534874d3 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1849,7 +1849,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER DATABASE <name> */
else if (Matches("ALTER", "DATABASE", MatchAny))
- COMPLETE_WITH("RESET", "SET", "OWNER TO", "RENAME TO",
+ COMPLETE_WITH("RESET", "SET", "OWNER TO", "REFRESH COLLATION VERSION", "RENAME TO",
"IS_TEMPLATE", "ALLOW_CONNECTIONS",
"CONNECTION LIMIT");
diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h
index 90b43a4ecc..76adbd4aad 100644
--- a/src/include/catalog/pg_database.h
+++ b/src/include/catalog/pg_database.h
@@ -65,6 +65,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID
/* LC_CTYPE setting */
text datctype BKI_FORCE_NOT_NULL;
+ /* provider-dependent version of collation data */
+ text datcollversion BKI_DEFAULT(_null_);
+
/* access permissions */
aclitem datacl[1];
#endif
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 62f36daa98..7f1ee97f55 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11627,6 +11627,11 @@
proname => 'pg_collation_actual_version', procost => '100',
provolatile => 'v', prorettype => 'text', proargtypes => 'oid',
prosrc => 'pg_collation_actual_version' },
+{ oid => '9167',
+ descr => 'get actual version of database collation from operating system',
+ proname => 'pg_database_collation_actual_version', procost => '100',
+ provolatile => 'v', prorettype => 'text', proargtypes => 'oid',
+ prosrc => 'pg_database_collation_actual_version' },
# system management/monitoring related functions
{ oid => '3353', descr => 'list files in the log directory',
diff --git a/src/include/commands/dbcommands.h b/src/include/commands/dbcommands.h
index b1e8b5eb96..c4947fa71f 100644
--- a/src/include/commands/dbcommands.h
+++ b/src/include/commands/dbcommands.h
@@ -24,6 +24,7 @@ extern void dropdb(const char *dbname, bool missing_ok, bool force);
extern void DropDatabase(ParseState *pstate, DropdbStmt *stmt);
extern ObjectAddress RenameDatabase(const char *oldname, const char *newname);
extern Oid AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel);
+extern ObjectAddress AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt);
extern Oid AlterDatabaseSet(AlterDatabaseSetStmt *stmt);
extern ObjectAddress AlterDatabaseOwner(const char *dbname, Oid newOwnerId);
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index da35f2c272..5d075f0c34 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -370,6 +370,7 @@ typedef enum NodeTag
T_CheckPointStmt,
T_CreateSchemaStmt,
T_AlterDatabaseStmt,
+ T_AlterDatabaseRefreshCollStmt,
T_AlterDatabaseSetStmt,
T_AlterRoleSetStmt,
T_CreateConversionStmt,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 37fcc4c9b5..34218b718c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3308,6 +3308,12 @@ typedef struct AlterDatabaseStmt
List *options; /* List of DefElem nodes */
} AlterDatabaseStmt;
+typedef struct AlterDatabaseRefreshCollStmt
+{
+ NodeTag type;
+ char *dbname;
+} AlterDatabaseRefreshCollStmt;
+
typedef struct AlterDatabaseSetStmt
{
NodeTag type;
diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out
index 70133df804..9699ca16cf 100644
--- a/src/test/regress/expected/collate.icu.utf8.out
+++ b/src/test/regress/expected/collate.icu.utf8.out
@@ -1085,6 +1085,10 @@ DROP ROLE regress_test_role;
-- ALTER
ALTER COLLATION "en-x-icu" REFRESH VERSION;
NOTICE: version has not changed
+-- also test for database while we are here
+SELECT current_database() AS datname \gset
+ALTER DATABASE :"datname" REFRESH COLLATION VERSION;
+NOTICE: version has not changed
-- dependencies
CREATE COLLATION test0 FROM "C";
CREATE TABLE collate_dep_test1 (a int, b text COLLATE test0);
diff --git a/src/test/regress/expected/collate.linux.utf8.out b/src/test/regress/expected/collate.linux.utf8.out
index f06ae543e4..f2d0eb94f2 100644
--- a/src/test/regress/expected/collate.linux.utf8.out
+++ b/src/test/regress/expected/collate.linux.utf8.out
@@ -1096,6 +1096,10 @@ DROP ROLE regress_test_role;
-- ALTER
ALTER COLLATION "en_US" REFRESH VERSION;
NOTICE: version has not changed
+-- also test for database while we are here
+SELECT current_database() AS datname \gset
+ALTER DATABASE :"datname" REFRESH COLLATION VERSION;
+NOTICE: version has not changed
-- dependencies
CREATE COLLATION test0 FROM "C";
CREATE TABLE collate_dep_test1 (a int, b text COLLATE test0);
diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql
index 9cee3d0042..242a7ce6b7 100644
--- a/src/test/regress/sql/collate.icu.utf8.sql
+++ b/src/test/regress/sql/collate.icu.utf8.sql
@@ -409,6 +409,10 @@ CREATE COLLATION test5 FROM test0;
ALTER COLLATION "en-x-icu" REFRESH VERSION;
+-- also test for database while we are here
+SELECT current_database() AS datname \gset
+ALTER DATABASE :"datname" REFRESH COLLATION VERSION;
+
-- dependencies
diff --git a/src/test/regress/sql/collate.linux.utf8.sql b/src/test/regress/sql/collate.linux.utf8.sql
index cbbd2203e4..0f6dd1b02e 100644
--- a/src/test/regress/sql/collate.linux.utf8.sql
+++ b/src/test/regress/sql/collate.linux.utf8.sql
@@ -410,6 +410,10 @@ CREATE COLLATION test5 FROM test0;
ALTER COLLATION "en_US" REFRESH VERSION;
+-- also test for database while we are here
+SELECT current_database() AS datname \gset
+ALTER DATABASE :"datname" REFRESH COLLATION VERSION;
+
-- dependencies
--
2.35.1
On Thu, Feb 10, 2022 at 09:57:59AM +0100, Peter Eisentraut wrote:
New patch that fixes all reported issues, I think:
- Added test for ALTER DATABASE / REFRESH COLLATION VERSION
- Rewrote AlterDatabaseRefreshCollVersion() with better locking
- Added version checking in createdb()
Thanks! All issues are indeed fixed. I just have a few additional comments:
From 290ebb9ca743a2272181f435d5ea76d8a7280a0a Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Thu, 10 Feb 2022 09:44:20 +0100
Subject: [PATCH v4] Database-level collation version tracking
+ * collation version was specified explicitly as an statement option; that
typo, should be "as a statement"
+ actual_versionstr = get_collation_actual_version(COLLPROVIDER_LIBC, dbcollate); + if (!actual_versionstr) + ereport(ERROR, + (errmsg("template database \"%s\" has a collation version, but no actual collation version could be determined", + dbtemplate))); + + if (strcmp(actual_versionstr, src_collversion) != 0) + ereport(ERROR, + (errmsg("template database \"%s\" has a collation version mismatch", + dbtemplate), + errdetail("The template database was created using collation version %s, " + "but the operating system provides version %s.", + src_collversion, actual_versionstr), + errhint("Rebuild all objects affected by collation in the template database and run " + "ALTER DATABASE %s REFRESH COLLATION VERSION, " + "or build PostgreSQL with the right library version.", + quote_identifier(dbtemplate))));
After a second read I think the messages are slightly ambiguous. What do you
think about specifying the problematic collation name and provider?
For now we only support libc default collation so users will probably have to
reindex almost everything on that database (not sure if the versioning is more
fine grained on Windows), but we should probably still specify "affected by
libc collation" in the errhint so they have a chance to avoid unnecessary
reindex.
And this will hopefully become more important to have those information, when
ICU default collations will land :)
+/* + * ALTER DATABASE name REFRESH COLLATION VERSION + */ +ObjectAddress +AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt)
I'm wondering why you changed this function to return an ObjectAddress rather
than an Oid? There's no event trigger support for ALTER DATABASE, and the rest
of similar utility commands also returns Oid.
Other than that it all looks good to me!
On 10.02.22 12:08, Julien Rouhaud wrote:
+ errhint("Rebuild all objects affected by collation in the template database and run " + "ALTER DATABASE %s REFRESH COLLATION VERSION, " + "or build PostgreSQL with the right library version.", + quote_identifier(dbtemplate))));After a second read I think the messages are slightly ambiguous. What do you
think about specifying the problematic collation name and provider?For now we only support libc default collation so users will probably have to
reindex almost everything on that database (not sure if the versioning is more
fine grained on Windows), but we should probably still specify "affected by
libc collation" in the errhint so they have a chance to avoid unnecessary
reindex.
I think accurate would be something like "objects using the default
collation", since objects using a specific collation are not meant, even
if they use the same provider.
+/* + * ALTER DATABASE name REFRESH COLLATION VERSION + */ +ObjectAddress +AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt)I'm wondering why you changed this function to return an ObjectAddress rather
than an Oid? There's no event trigger support for ALTER DATABASE, and the rest
of similar utility commands also returns Oid.
Hmm, I was looking at RenameDatabase() and AlterDatabaseOwner(), which
return ObjectAddress.
On Fri, Feb 11, 2022 at 12:07:02PM +0100, Peter Eisentraut wrote:
On 10.02.22 12:08, Julien Rouhaud wrote:
+ errhint("Rebuild all objects affected by collation in the template database and run " + "ALTER DATABASE %s REFRESH COLLATION VERSION, " + "or build PostgreSQL with the right library version.", + quote_identifier(dbtemplate))));After a second read I think the messages are slightly ambiguous. What do you
think about specifying the problematic collation name and provider?For now we only support libc default collation so users will probably have to
reindex almost everything on that database (not sure if the versioning is more
fine grained on Windows), but we should probably still specify "affected by
libc collation" in the errhint so they have a chance to avoid unnecessary
reindex.I think accurate would be something like "objects using the default
collation", since objects using a specific collation are not meant, even if
they use the same provider.
Technically is the objects explicitly use the same collation as the default
collation they should be impacted the same way, but agreed.
+/* + * ALTER DATABASE name REFRESH COLLATION VERSION + */ +ObjectAddress +AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt)I'm wondering why you changed this function to return an ObjectAddress rather
than an Oid? There's no event trigger support for ALTER DATABASE, and the rest
of similar utility commands also returns Oid.Hmm, I was looking at RenameDatabase() and AlterDatabaseOwner(), which
return ObjectAddress.
Apparently I managed to only check AlterDatabase and AlterDatabaseSet, which
both return an Oid. Maybe we could also update those two to also return an
ObjectAddress, for consistency?
On 11.02.22 13:51, Julien Rouhaud wrote:
I'm wondering why you changed this function to return an ObjectAddress rather
than an Oid? There's no event trigger support for ALTER DATABASE, and the rest
of similar utility commands also returns Oid.Hmm, I was looking at RenameDatabase() and AlterDatabaseOwner(), which
return ObjectAddress.Apparently I managed to only check AlterDatabase and AlterDatabaseSet, which
both return an Oid. Maybe we could also update those two to also return an
ObjectAddress, for consistency?
I have committed this patch.
I didn't address the above issue. I looked at it a bit, but I also
found other (non-database) object types that had a mix of different
return types. It's not clear to me what this is all supposed to mean.
If no one is checking the return, they should really all be turned into
void, IMO. Maybe this should be a separate discussion.
Hi,
On Mon, Feb 14, 2022 at 09:55:19AM +0100, Peter Eisentraut wrote:
I have committed this patch.
Great! Do you plan to send a rebased version of the ICU default collation
soon or should I start looking at the current v4?
I didn't address the above issue. I looked at it a bit, but I also found
other (non-database) object types that had a mix of different return types.
It's not clear to me what this is all supposed to mean. If no one is
checking the return, they should really all be turned into void, IMO. Maybe
this should be a separate discussion.
Agreed.
On 14.02.22 10:14, Julien Rouhaud wrote:
Do you plan to send a rebased version of the ICU default collation
soon or should I start looking at the current v4?
I will send an updated patch in the next few days.
On 2022-Feb-14, Peter Eisentraut wrote:
On 11.02.22 13:51, Julien Rouhaud wrote:
I'm wondering why you changed this function to return an ObjectAddress rather
than an Oid? There's no event trigger support for ALTER DATABASE, and the rest
of similar utility commands also returns Oid.Hmm, I was looking at RenameDatabase() and AlterDatabaseOwner(), which
return ObjectAddress.Apparently I managed to only check AlterDatabase and AlterDatabaseSet, which
both return an Oid. Maybe we could also update those two to also return an
ObjectAddress, for consistency?
I didn't address the above issue. I looked at it a bit, but I also found
other (non-database) object types that had a mix of different return types.
It's not clear to me what this is all supposed to mean. If no one is
checking the return, they should really all be turned into void, IMO. Maybe
this should be a separate discussion.
IIRC we changed the return types of all DDL back when we were doing the
event triggers work (first to OIDs and then to ObjectAddress), but we
didn't realize at the time that shared objects such as databases etc
were not going to be supported by event triggers. So those particular
changes were for naught, but we never reverted them.
Maybe it's OK to have all the functions supporting databases and
tablespaces return void.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
<inflex> really, I see PHP as like a strange amalgamation of C, Perl, Shell
<crab> inflex: you know that "amalgam" means "mixture with mercury",
more or less, right?
<crab> i.e., "deadly poison"