Fix up handling of C/POSIX collations.
authorTom Lane <[email protected]>
Sun, 20 Mar 2011 16:43:39 +0000 (12:43 -0400)
committerTom Lane <[email protected]>
Sun, 20 Mar 2011 16:44:13 +0000 (12:44 -0400)
Install just one instance of the "C" and "POSIX" collations into
pg_collation, rather than one per encoding.  Make these instances exist
and do something useful even in machines without locale_t support: to wit,
it's now possible to force comparisons and case-folding functions to use C
locale in an otherwise non-C database, whether or not the platform has
support for using any additional collations.

Fix up severely broken upper/lower/initcap functions, too: the C/POSIX
fastpath now does what it is supposed to, and non-default collations are
handled correctly in single-byte database encodings.

Merge the two separate collation hashtables that were being maintained in
pg_locale.c, and be more wary of the possibility that we fail partway
through filling a cache entry.

doc/src/sgml/charset.sgml
src/backend/utils/adt/formatting.c
src/backend/utils/adt/pg_locale.c
src/bin/initdb/initdb.c
src/include/catalog/catversion.h
src/include/catalog/pg_collation.h
src/include/port.h
src/port/pgstrcasecmp.c

index dd96d009506401016f6c340322de245ee9ab7107..66f02c619eac9714b1a3a6eeedffed18f53ababd 100644 (file)
@@ -68,7 +68,7 @@ initdb --locale=sv_SE
    <para>
     This example for Unix systems sets the locale to Swedish
     (<literal>sv</>) as spoken
-    in Sweden (<literal>SE</>).  Other possibilities might be
+    in Sweden (<literal>SE</>).  Other possibilities might include
     <literal>en_US</> (U.S. English) and <literal>fr_CA</> (French
     Canadian).  If more than one character set can be used for a
     locale then the specifications can take the form
@@ -133,7 +133,8 @@ initdb --locale=sv_SE
 
    <para>
     If you want the system to behave as if it had no locale support,
-    use the special locale <literal>C</> or <literal>POSIX</>.
+    use the special locale name <literal>C</>, or equivalently
+    <literal>POSIX</>.
    </para>
 
    <para>
@@ -257,7 +258,9 @@ initdb --locale=sv_SE
     operator classes exist. These allow the creation of an index that
     performs a strict character-by-character comparison, ignoring
     locale comparison rules. Refer to <xref linkend="indexes-opclass">
-    for more information.
+    for more information.  Another approach is to create indexes using
+    the <literal>C</> collation, as discussed in
+    <xref linkend="collation">.
    </para>
   </sect2>
 
@@ -321,13 +324,6 @@ initdb --locale=sv_SE
    of a database cannot be changed after its creation.
   </para>
 
-  <note>
-   <para>
-    Collation support is currently only known to work on
-    Linux (glibc) and Mac OS X platforms.
-   </para>
-  </note>
-
   <sect2>
    <title>Concepts</title>
 
@@ -335,7 +331,8 @@ initdb --locale=sv_SE
     Conceptually, every expression of a collatable data type has a
     collation.  (The built-in collatable data types are
     <type>text</type>, <type>varchar</type>, and <type>char</type>.
-    User-defined base types can also be marked collatable.)  If the
+    User-defined base types can also be marked collatable, and of course
+    a domain over a collatable data type is collatable.)  If the
     expression is a column reference, the collation of the expression is the
     defined collation of the column.  If the expression is a constant, the
     collation is the default collation of the data type of the
@@ -346,8 +343,8 @@ initdb --locale=sv_SE
    <para>
     The collation of an expression can be the <quote>default</quote>
     collation, which means the locale settings defined for the
-    database.  In some cases, an expression can also have no known
-    collation.  In such cases, ordering operations and other
+    database.  It is also possible for an expression's collation to be
+    indeterminate.  In such cases, ordering operations and other
     operations that need to know the collation will fail.
    </para>
 
@@ -379,7 +376,7 @@ initdb --locale=sv_SE
     The <firstterm>collation derivation</firstterm> of an expression can be
     implicit or explicit.  This distinction affects how collations are
     combined when multiple different collations appear in an
-    expression.  An explicit collation derivation arises when a
+    expression.  An explicit collation derivation occurs when a
     <literal>COLLATE</literal> clause is used; all other collation
     derivations are implicit.  When multiple collations need to be
     combined, for example in a function call, the following rules are
@@ -399,34 +396,90 @@ initdb --locale=sv_SE
      <listitem>
       <para>
        Otherwise, all input expressions must have the same implicit
-       collation derivation or the default collation.  If any
-       implicitly derived collation is present, that is the result of
-       the collation combination.  Otherwise, the result is the
-       default collation.
+       collation derivation or the default collation.  If any non-default
+       collation is present, that is the result of the collation combination.
+       Otherwise, the result is the default collation.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       If there are conflicting non-default implicit collations among the
+       input expressions, then the combination is deemed to have indeterminate
+       collation.  This is not an error condition unless the particular
+       function being invoked requires knowledge of the collation it should
+       apply.  If it does, an error will be raised at run-time.
       </para>
      </listitem>
     </orderedlist>
 
-    For example, take this table definition:
+    For example, consider this table definition:
 <programlisting>
 CREATE TABLE test1 (
-    a text COLLATE "x",
+    a text COLLATE "de_DE",
+    b text COLLATE "es_ES",
     ...
 );
 </programlisting>
 
     Then in
 <programlisting>
-SELECT a || 'foo' FROM test1;
+SELECT a &lt; 'foo' FROM test1;
 </programlisting>
-    the result collation of the <literal>||</literal> operator is
-    <literal>"x"</literal> because it combines an implicitly derived
-    collation with the default collation.  But in
+    the <literal>&lt;</literal> comparison is performed according to
+    <literal>de_DE</literal> rules, because the expression combines an
+    implicitly derived collation with the default collation.  But in
 <programlisting>
-SELECT a || ('foo' COLLATE "y") FROM test1;
+SELECT a &lt; ('foo' COLLATE "fr_FR") FROM test1;
+</programlisting>
+    the comparison is performed using <literal>fr_FR</literal> rules,
+    because the explicit collation derivation overrides the implicit one.
+    Furthermore, given
+<programlisting>
+SELECT a &lt; b FROM test1;
+</programlisting>
+    the parser cannot determine which collation to apply, since the
+    <structfield>a</> and <structfield>b</> columns have conflicting
+    implicit collations.  Since the <literal>&lt;</literal> operator
+    does need to know which collation to use, this will result in an
+    error.  The error can be resolved by attaching an explicit collation
+    specifier to either input expression, thus:
+<programlisting>
+SELECT a &lt; b COLLATE "de_DE" FROM test1;
+</programlisting>
+    or equivalently
+<programlisting>
+SELECT a COLLATE "de_DE" &lt; b FROM test1;
+</programlisting>
+    On the other hand, the structurally similar case
+<programlisting>
+SELECT a || b FROM test1;
+</programlisting>
+    does not result in an error, because the <literal>||</> operator
+    does not care about collations: its result is the same regardless
+    of the collation.
+   </para>
+
+   <para>
+    The collation assigned to a function or operator's combined input
+    expressions is also considered to apply to the function or operator's
+    result, if the function or operator delivers a result of a collatable
+    data type.  So, in
+<programlisting>
+SELECT * FROM test1 ORDER BY a || 'foo';
+</programlisting>
+    the ordering will be done according to <literal>de_DE</literal> rules.
+    But this query:
+<programlisting>
+SELECT * FROM test1 ORDER BY a || b;
+</programlisting>
+    results in an error, because even though the <literal>||</> operator
+    doesn't need to know a collation, the <literal>ORDER BY</> clause does.
+    As before, the conflict can be resolved with an explicit collation
+    specifier:
+<programlisting>
+SELECT * FROM test1 ORDER BY a || b COLLATE "fr_FR";
 </programlisting>
-    the result collation is <literal>"y"</literal> because the explicit
-    collation derivation overrides the implicit one.
    </para>
   </sect2>
 
@@ -449,7 +502,22 @@ SELECT a || ('foo' COLLATE "y") FROM test1;
    </para>
 
    <para>
-    When a database cluster is initialized, <command>initdb</command>
+    On all platforms, the collations named <literal>default</>,
+    <literal>C</>, and <literal>POSIX</> are available.  Additional
+    collations may be available depending on operating system support.
+    The <literal>default</> collation selects the <symbol>LC_COLLATE</symbol>
+    and <symbol>LC_CTYPE</symbol> values specified at database creation time.
+    The <literal>C</> and <literal>POSIX</> collations both specify
+    <quote>traditional C</> behavior, in which only the ASCII letters
+    <quote><literal>A</></quote> through <quote><literal>Z</></quote>
+    are treated as letters, and sorting is done strictly by character
+    code byte values.
+   </para>
+
+   <para>
+    If the operating system provides support for using multiple locales
+    within a single program (<function>newlocale</> and related functions),
+    then when a database cluster is initialized, <command>initdb</command>
     populates the system catalog <literal>pg_collation</literal> with
     collations based on all the locales it finds on the operating
     system at the time.  For example, the operating system might
@@ -484,7 +552,21 @@ SELECT a || ('foo' COLLATE "y") FROM test1;
     within a given database even though it would not be unique globally.
     Use of the stripped collation names is recommendable, since it will
     make one less thing you need to change if you decide to change to
-    another database encoding.
+    another database encoding.  Note however that the <literal>default</>,
+    <literal>C</>, and <literal>POSIX</> collations can be used
+    regardless of the database encoding.
+   </para>
+
+   <para>
+    <productname>PostgreSQL</productname> considers distinct collation
+    objects to be incompatible even when they have identical properties.
+    Thus for example,
+<programlisting>
+SELECT a COLLATE "C" &lt; b COLLATE "POSIX" FROM test1;
+</programlisting>
+    will draw an error even though the <literal>C</> and <literal>POSIX</>
+    collations have identical behaviors.  Mixing stripped and non-stripped
+    collation names is therefore not recommended.
    </para>
   </sect2>
  </sect1>
index aba11459bb1857a27645b27d9e0388a44267db06..54783103a2c59e6e8fc3822332213c7acc408d5d 100644 (file)
@@ -1462,10 +1462,16 @@ str_numth(char *dest, char *num, int type)
  * in multibyte character sets.  Note that in either case we are effectively
  * assuming that the database character encoding matches the encoding implied
  * by LC_CTYPE.
+ *
+ * If the system provides locale_t and associated functions (which are
+ * standardized by Open Group's XBD), we can support collations that are
+ * neither default nor C.  The code is written to handle both combinations
+ * of have-wide-characters and have-locale_t, though it's rather unlikely
+ * a platform would have the latter without the former.
  */
 
 /*
- * wide-character-aware lower function
+ * collation-aware, wide-character-aware lower function
  *
  * We pass the number of bytes so we can pass varlena and char*
  * to this function.  The result is a palloc'd, null-terminated string.
@@ -1474,21 +1480,31 @@ char *
 str_tolower(const char *buff, size_t nbytes, Oid collid)
 {
        char       *result;
-       pg_locale_t     mylocale = 0;
 
        if (!buff)
                return NULL;
 
-       if (collid != DEFAULT_COLLATION_OID)
-               mylocale = pg_newlocale_from_collation(collid);
+       /* C/POSIX collations use this path regardless of database encoding */
+       if (lc_ctype_is_c(collid))
+       {
+               char       *p;
+
+               result = pnstrdup(buff, nbytes);
 
+               for (p = result; *p; p++)
+                       *p = pg_ascii_tolower((unsigned char) *p);
+       }
 #ifdef USE_WIDE_UPPER_LOWER
-       if (pg_database_encoding_max_length() > 1 && !lc_ctype_is_c(collid))
+       else if (pg_database_encoding_max_length() > 1)
        {
+               pg_locale_t     mylocale = 0;
                wchar_t    *workspace;
                size_t          curr_char;
                size_t          result_size;
 
+               if (collid != DEFAULT_COLLATION_OID)
+                       mylocale = pg_newlocale_from_collation(collid);
+
                /* Overflow paranoia */
                if ((nbytes + 1) > (INT_MAX / sizeof(wchar_t)))
                        ereport(ERROR,
@@ -1501,12 +1517,14 @@ str_tolower(const char *buff, size_t nbytes, Oid collid)
                char2wchar(workspace, nbytes + 1, buff, nbytes, collid);
 
                for (curr_char = 0; workspace[curr_char] != 0; curr_char++)
+               {
 #ifdef HAVE_LOCALE_T
                        if (mylocale)
                                workspace[curr_char] = towlower_l(workspace[curr_char], mylocale);
                        else
 #endif
-                       workspace[curr_char] = towlower(workspace[curr_char]);
+                               workspace[curr_char] = towlower(workspace[curr_char]);
+               }
 
                /* Make result large enough; case change might change number of bytes */
                result_size = curr_char * pg_database_encoding_max_length() + 1;
@@ -1515,22 +1533,40 @@ str_tolower(const char *buff, size_t nbytes, Oid collid)
                wchar2char(result, workspace, result_size, collid);
                pfree(workspace);
        }
-       else
 #endif   /* USE_WIDE_UPPER_LOWER */
+       else
        {
+               pg_locale_t     mylocale = 0;
                char       *p;
 
+               if (collid != DEFAULT_COLLATION_OID)
+                       mylocale = pg_newlocale_from_collation(collid);
+
                result = pnstrdup(buff, nbytes);
 
+               /*
+                * Note: we assume that tolower_l() will not be so broken as to need
+                * an isupper_l() guard test.  When using the default collation, we
+                * apply the traditional Postgres behavior that forces ASCII-style
+                * treatment of I/i, but in non-default collations you get exactly
+                * what the collation says.
+                */
                for (p = result; *p; p++)
-                       *p = pg_tolower((unsigned char) *p);
+               {
+#ifdef HAVE_LOCALE_T
+                       if (mylocale)
+                               *p = tolower_l((unsigned char) *p, mylocale);
+                       else
+#endif
+                               *p = pg_tolower((unsigned char) *p);
+               }
        }
 
        return result;
 }
 
 /*
- * wide-character-aware upper function
+ * collation-aware, wide-character-aware upper function
  *
  * We pass the number of bytes so we can pass varlena and char*
  * to this function.  The result is a palloc'd, null-terminated string.
@@ -1539,21 +1575,31 @@ char *
 str_toupper(const char *buff, size_t nbytes, Oid collid)
 {
        char       *result;
-       pg_locale_t     mylocale = 0;
 
        if (!buff)
                return NULL;
 
-       if (collid != DEFAULT_COLLATION_OID)
-               mylocale = pg_newlocale_from_collation(collid);
+       /* C/POSIX collations use this path regardless of database encoding */
+       if (lc_ctype_is_c(collid))
+       {
+               char       *p;
 
+               result = pnstrdup(buff, nbytes);
+
+               for (p = result; *p; p++)
+                       *p = pg_ascii_toupper((unsigned char) *p);
+       }
 #ifdef USE_WIDE_UPPER_LOWER
-       if (pg_database_encoding_max_length() > 1 && !lc_ctype_is_c(collid))
+       else if (pg_database_encoding_max_length() > 1)
        {
+               pg_locale_t     mylocale = 0;
                wchar_t    *workspace;
                size_t          curr_char;
                size_t          result_size;
 
+               if (collid != DEFAULT_COLLATION_OID)
+                       mylocale = pg_newlocale_from_collation(collid);
+
                /* Overflow paranoia */
                if ((nbytes + 1) > (INT_MAX / sizeof(wchar_t)))
                        ereport(ERROR,
@@ -1566,12 +1612,14 @@ str_toupper(const char *buff, size_t nbytes, Oid collid)
                char2wchar(workspace, nbytes + 1, buff, nbytes, collid);
 
                for (curr_char = 0; workspace[curr_char] != 0; curr_char++)
+               {
 #ifdef HAVE_LOCALE_T
                        if (mylocale)
                                workspace[curr_char] = towupper_l(workspace[curr_char], mylocale);
                        else
 #endif
-                       workspace[curr_char] = towupper(workspace[curr_char]);
+                               workspace[curr_char] = towupper(workspace[curr_char]);
+               }
 
                /* Make result large enough; case change might change number of bytes */
                result_size = curr_char * pg_database_encoding_max_length() + 1;
@@ -1580,22 +1628,40 @@ str_toupper(const char *buff, size_t nbytes, Oid collid)
                wchar2char(result, workspace, result_size, collid);
                pfree(workspace);
        }
-       else
 #endif   /* USE_WIDE_UPPER_LOWER */
+       else
        {
+               pg_locale_t     mylocale = 0;
                char       *p;
 
+               if (collid != DEFAULT_COLLATION_OID)
+                       mylocale = pg_newlocale_from_collation(collid);
+
                result = pnstrdup(buff, nbytes);
 
+               /*
+                * Note: we assume that toupper_l() will not be so broken as to need
+                * an islower_l() guard test.  When using the default collation, we
+                * apply the traditional Postgres behavior that forces ASCII-style
+                * treatment of I/i, but in non-default collations you get exactly
+                * what the collation says.
+                */
                for (p = result; *p; p++)
-                       *p = pg_toupper((unsigned char) *p);
+               {
+#ifdef HAVE_LOCALE_T
+                       if (mylocale)
+                               *p = toupper_l((unsigned char) *p, mylocale);
+                       else
+#endif
+                               *p = pg_toupper((unsigned char) *p);
+               }
        }
 
        return result;
 }
 
 /*
- * wide-character-aware initcap function
+ * collation-aware, wide-character-aware initcap function
  *
  * We pass the number of bytes so we can pass varlena and char*
  * to this function.  The result is a palloc'd, null-terminated string.
@@ -1605,21 +1671,42 @@ str_initcap(const char *buff, size_t nbytes, Oid collid)
 {
        char       *result;
        int                     wasalnum = false;
-       pg_locale_t     mylocale = 0;
 
        if (!buff)
                return NULL;
 
-       if (collid != DEFAULT_COLLATION_OID)
-               mylocale = pg_newlocale_from_collation(collid);
+       /* C/POSIX collations use this path regardless of database encoding */
+       if (lc_ctype_is_c(collid))
+       {
+               char       *p;
+
+               result = pnstrdup(buff, nbytes);
 
+               for (p = result; *p; p++)
+               {
+                       char    c;
+
+                       if (wasalnum)
+                               *p = c = pg_ascii_tolower((unsigned char) *p);
+                       else
+                               *p = c = pg_ascii_toupper((unsigned char) *p);
+                       /* we don't trust isalnum() here */
+                       wasalnum = ((c >= 'A' && c <= 'Z') ||
+                                               (c >= 'a' && c <= 'z') ||
+                                               (c >= '0' && c <= '9'));
+               }
+       }
 #ifdef USE_WIDE_UPPER_LOWER
-       if (pg_database_encoding_max_length() > 1 && !lc_ctype_is_c(collid))
+       else if (pg_database_encoding_max_length() > 1)
        {
+               pg_locale_t     mylocale = 0;
                wchar_t    *workspace;
                size_t          curr_char;
                size_t          result_size;
 
+               if (collid != DEFAULT_COLLATION_OID)
+                       mylocale = pg_newlocale_from_collation(collid);
+
                /* Overflow paranoia */
                if ((nbytes + 1) > (INT_MAX / sizeof(wchar_t)))
                        ereport(ERROR,
@@ -1660,20 +1747,44 @@ str_initcap(const char *buff, size_t nbytes, Oid collid)
                wchar2char(result, workspace, result_size, collid);
                pfree(workspace);
        }
-       else
 #endif   /* USE_WIDE_UPPER_LOWER */
+       else
        {
+               pg_locale_t     mylocale = 0;
                char       *p;
 
+               if (collid != DEFAULT_COLLATION_OID)
+                       mylocale = pg_newlocale_from_collation(collid);
+
                result = pnstrdup(buff, nbytes);
 
+               /*
+                * Note: we assume that toupper_l()/tolower_l() will not be so broken
+                * as to need guard tests.  When using the default collation, we apply
+                * the traditional Postgres behavior that forces ASCII-style treatment
+                * of I/i, but in non-default collations you get exactly what the
+                * collation says.
+                */
                for (p = result; *p; p++)
                {
-                       if (wasalnum)
-                               *p = pg_tolower((unsigned char) *p);
+#ifdef HAVE_LOCALE_T
+                       if (mylocale)
+                       {
+                               if (wasalnum)
+                                       *p = tolower_l((unsigned char) *p, mylocale);
+                               else
+                                       *p = toupper_l((unsigned char) *p, mylocale);
+                               wasalnum = isalnum_l((unsigned char) *p, mylocale);
+                       }
                        else
-                               *p = pg_toupper((unsigned char) *p);
-                       wasalnum = isalnum((unsigned char) *p);
+#endif
+                       {
+                               if (wasalnum)
+                                       *p = pg_tolower((unsigned char) *p);
+                               else
+                                       *p = pg_toupper((unsigned char) *p);
+                               wasalnum = isalnum((unsigned char) *p);
+                       }
                }
        }
 
index 2b9b321b2637cf2d845d9e1460df445b5be2baa8..15d347c4f89ed671ff64ada48d2f2dff2fd263be 100644 (file)
@@ -99,15 +99,24 @@ static char lc_monetary_envbuf[LC_ENV_BUFSIZE];
 static char lc_numeric_envbuf[LC_ENV_BUFSIZE];
 static char lc_time_envbuf[LC_ENV_BUFSIZE];
 
+/* Cache for collation-related knowledge */
+
+typedef struct
+{
+       Oid                     collid;                 /* hash key: pg_collation OID */
+       bool            collate_is_c;   /* is collation's LC_COLLATE C? */
+       bool            ctype_is_c;             /* is collation's LC_CTYPE C? */
+       bool            flags_valid;    /* true if above flags are valid */
+       pg_locale_t     locale;                 /* locale_t struct, or 0 if not valid */
+} collation_cache_entry;
+
+static HTAB *collation_cache = NULL;
+
+
 #if defined(WIN32) && defined(LC_MESSAGES)
 static char *IsoLocaleName(const char *);              /* MSVC specific */
 #endif
 
-static HTAB *locale_cness_cache = NULL;
-#ifdef HAVE_LOCALE_T
-static HTAB *locale_t_cache = NULL;
-#endif
-
 
 /*
  * pg_perm_setlocale
@@ -312,136 +321,6 @@ locale_messages_assign(const char *value, bool doit, GucSource source)
 }
 
 
-/*
- * We'd like to cache whether LC_COLLATE or LC_CTYPE is C (or POSIX),
- * so we can optimize a few code paths in various places.
- *
- * Note that some code relies on this not reporting false negatives
- * (that is, saying it's not C when it is).  For example, char2wchar()
- * could fail if the locale is C, so str_tolower() shouldn't call it
- * in that case.
- */
-
-struct locale_cness_cache_entry
-{
-       Oid                     collid;
-       bool            collate_is_c;
-       bool            ctype_is_c;
-};
-
-static void
-init_locale_cness_cache(void)
-{
-       HASHCTL         ctl;
-
-       memset(&ctl, 0, sizeof(ctl));
-       ctl.keysize = sizeof(Oid);
-       ctl.entrysize = sizeof(struct locale_cness_cache_entry);
-       ctl.hash = oid_hash;
-       locale_cness_cache = hash_create("locale C-ness cache", 1000, &ctl, HASH_ELEM | HASH_FUNCTION);
-}
-
-/*
- * Handle caching of locale "C-ness" for nondefault collation objects.
- * Relying on the system cache directly isn't fast enough.
- */
-static bool
-lookup_collation_cness(Oid collation, int category)
-{
-       struct locale_cness_cache_entry *cache_entry;
-       bool            found;
-       HeapTuple       tp;
-       char       *localeptr;
-
-       Assert(OidIsValid(collation));
-       Assert(category == LC_COLLATE || category == LC_CTYPE);
-
-       if (!locale_cness_cache)
-               init_locale_cness_cache();
-
-       cache_entry = hash_search(locale_cness_cache, &collation, HASH_ENTER, &found);
-       if (found)
-       {
-               if (category == LC_COLLATE)
-                       return cache_entry->collate_is_c;
-               else
-                       return cache_entry->ctype_is_c;
-       }
-
-       tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(collation));
-       if (!HeapTupleIsValid(tp))
-               elog(ERROR, "cache lookup failed for collation %u", collation);
-
-       localeptr = NameStr(((Form_pg_collation) GETSTRUCT(tp))->collcollate);
-       cache_entry->collate_is_c = (strcmp(localeptr, "C") == 0) || (strcmp(localeptr, "POSIX") == 0);
-
-       localeptr = NameStr(((Form_pg_collation) GETSTRUCT(tp))->collctype);
-       cache_entry->ctype_is_c = (strcmp(localeptr, "C") == 0) || (strcmp(localeptr, "POSIX") == 0);
-
-       ReleaseSysCache(tp);
-
-       return category == LC_COLLATE ? cache_entry->collate_is_c : cache_entry->ctype_is_c;
-}
-
-
-bool
-lc_collate_is_c(Oid collation)
-{
-       /* Cache result so we only have to compute it once */
-       static int      result = -1;
-       char       *localeptr;
-
-       if (!OidIsValid(collation))
-               return false;
-
-       if (collation != DEFAULT_COLLATION_OID)
-               return lookup_collation_cness(collation, LC_COLLATE);
-
-       if (result >= 0)
-               return (bool) result;
-       localeptr = setlocale(LC_COLLATE, NULL);
-       if (!localeptr)
-               elog(ERROR, "invalid LC_COLLATE setting");
-
-       if (strcmp(localeptr, "C") == 0)
-               result = true;
-       else if (strcmp(localeptr, "POSIX") == 0)
-               result = true;
-       else
-               result = false;
-       return (bool) result;
-}
-
-
-bool
-lc_ctype_is_c(Oid collation)
-{
-       /* Cache result so we only have to compute it once */
-       static int      result = -1;
-       char       *localeptr;
-
-       if (!OidIsValid(collation))
-               return false;
-
-       if (collation != DEFAULT_COLLATION_OID)
-               return lookup_collation_cness(collation, LC_CTYPE);
-
-       if (result >= 0)
-               return (bool) result;
-       localeptr = setlocale(LC_CTYPE, NULL);
-       if (!localeptr)
-               elog(ERROR, "invalid LC_CTYPE setting");
-
-       if (strcmp(localeptr, "C") == 0)
-               result = true;
-       else if (strcmp(localeptr, "POSIX") == 0)
-               result = true;
-       else
-               result = false;
-       return (bool) result;
-}
-
-
 /*
  * Frees the malloced content of a struct lconv.  (But not the struct
  * itself.)
@@ -844,116 +723,295 @@ IsoLocaleName(const char *winlocname)
 #endif   /* WIN32 && LC_MESSAGES */
 
 
-#ifdef HAVE_LOCALE_T
-struct locale_t_cache_entry
+/*
+ * Cache mechanism for collation information.
+ *
+ * We cache two flags: whether the collation's LC_COLLATE or LC_CTYPE is C
+ * (or POSIX), so we can optimize a few code paths in various places.
+ * For the built-in C and POSIX collations, we can know that without even
+ * doing a cache lookup, but we want to support aliases for C/POSIX too.
+ * For the "default" collation, there are separate static cache variables,
+ * since consulting the pg_collation catalog doesn't tell us what we need.
+ *
+ * Also, if a pg_locale_t has been requested for a collation, we cache that
+ * for the life of a backend.
+ *
+ * Note that some code relies on the flags not reporting false negatives
+ * (that is, saying it's not C when it is).  For example, char2wchar()
+ * could fail if the locale is C, so str_tolower() shouldn't call it
+ * in that case.
+ *
+ * Note that we currently lack any way to flush the cache.  Since we don't
+ * support ALTER COLLATION, this is OK.  The worst case is that someone
+ * drops a collation, and a useless cache entry hangs around in existing
+ * backends.
+ */
+
+static collation_cache_entry *
+lookup_collation_cache(Oid collation, bool set_flags)
 {
-       Oid                     collid;
-       locale_t        locale;
-};
+       collation_cache_entry *cache_entry;
+       bool            found;
 
-static void
-init_locale_t_cache(void)
+       Assert(OidIsValid(collation));
+       Assert(collation != DEFAULT_COLLATION_OID);
+
+       if (collation_cache == NULL)
+       {
+               /* First time through, initialize the hash table */
+               HASHCTL         ctl;
+
+               memset(&ctl, 0, sizeof(ctl));
+               ctl.keysize = sizeof(Oid);
+               ctl.entrysize = sizeof(collation_cache_entry);
+               ctl.hash = oid_hash;
+               collation_cache = hash_create("Collation cache", 100, &ctl,
+                                                                         HASH_ELEM | HASH_FUNCTION);
+       }
+
+       cache_entry = hash_search(collation_cache, &collation, HASH_ENTER, &found);
+       if (!found)
+       {
+               /*
+                * Make sure cache entry is marked invalid, in case we fail before
+                * setting things.
+                */
+               cache_entry->flags_valid = false;
+               cache_entry->locale = 0;
+       }
+
+       if (set_flags && !cache_entry->flags_valid)
+       {
+               /* Attempt to set the flags */
+               HeapTuple       tp;
+               Form_pg_collation collform;
+               const char *collcollate;
+               const char *collctype;
+
+               tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(collation));
+               if (!HeapTupleIsValid(tp))
+                       elog(ERROR, "cache lookup failed for collation %u", collation);
+               collform = (Form_pg_collation) GETSTRUCT(tp);
+
+               collcollate = NameStr(collform->collcollate);
+               collctype = NameStr(collform->collctype);
+
+               cache_entry->collate_is_c = ((strcmp(collcollate, "C") == 0) ||
+                                                                        (strcmp(collcollate, "POSIX") == 0));
+               cache_entry->ctype_is_c = ((strcmp(collctype, "C") == 0) ||
+                                                                  (strcmp(collctype, "POSIX") == 0));
+
+               cache_entry->flags_valid = true;
+
+               ReleaseSysCache(tp);
+       }
+
+       return cache_entry;
+}
+
+
+/*
+ * Detect whether collation's LC_COLLATE property is C
+ */
+bool
+lc_collate_is_c(Oid collation)
 {
-       HASHCTL         ctl;
+       /*
+        * If we're asked about "collation 0", return false, so that the code
+        * will go into the non-C&n