Arrange for timezone names to be recognized case-insensitively; for

example SET TIME ZONE 'america/new_york' works now.  This seems a good
idea on general user-friendliness grounds, and is part of the solution
to the timestamp-input parsing problems I noted recently.
This commit is contained in:
Tom Lane 2006-10-16 19:58:27 +00:00
parent a2ebf81913
commit 0b35b01e7a
5 changed files with 195 additions and 78 deletions

View File

@ -1,4 +1,4 @@
<!-- $PostgreSQL: pgsql/doc/src/sgml/datatype.sgml,v 1.176 2006/09/22 16:20:00 tgl Exp $ -->
<!-- $PostgreSQL: pgsql/doc/src/sgml/datatype.sgml,v 1.177 2006/10/16 19:58:26 tgl Exp $ -->
<chapter id="datatype">
<title id="datatype-title">Data Types</title>
@ -1593,12 +1593,12 @@ SELECT b, char_length(b) FROM test2;
linkend="datatype-datetime-time-table">
and <xref linkend="datatype-timezone-table">.) If a time zone is
specified in the input for <type>time without time zone</type>,
it is silently ignored. You can also always specify a date but it will
be ignored except for when you use a full time zone name like
it is silently ignored. You can also specify a date but it will
be ignored, except when you use a full time zone name like
<literal>America/New_York</literal>. In this case specifying the date
is compulsory in order to tell which time zone offset should be
applied. It will be applied whatever time zone offset was valid at that
date and time at the specified place.
is required in order to determine whether standard or daylight-savings
time applies. The appropriate time zone offset is recorded in the
<type>time with time zone</type> value.
</para>
<table id="datatype-datetime-time-table">
@ -1653,7 +1653,7 @@ SELECT b, char_length(b) FROM test2;
</row>
<row>
<entry><literal>04:05:06 PST</literal></entry>
<entry>time zone specified by name</entry>
<entry>time zone specified by abbreviation</entry>
</row>
<row>
<entry><literal>2003-04-12 04:05:06 America/New_York</literal></entry>
@ -2214,6 +2214,12 @@ January 8 04:05:06 1999 PST
will always know the correct UTC offset for your region.
</para>
<para>
In all cases, timezone names are recognized case-insensitively.
(This is a change from <productname>PostgreSQL</productname> versions
prior to 8.2, which were case-sensitive in some contexts and not others.)
</para>
<para>
Note that timezone names are <emphasis>not</> used for date/time output
&mdash; all supported output formats use numeric timezone displays to

View File

@ -3,7 +3,7 @@
* 1996-06-05 by Arthur David Olson (arthur_david_olson@nih.gov).
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/timezone/localtime.c,v 1.14 2006/06/07 22:24:46 momjian Exp $
* $PostgreSQL: pgsql/src/timezone/localtime.c,v 1.15 2006/10/16 19:58:26 tgl Exp $
*/
/*
@ -122,7 +122,7 @@ detzcode(const char *codep)
}
int
tzload(const char *name, struct state * sp)
tzload(const char *name, char *canonname, struct state *sp)
{
const char *p;
int i;
@ -130,36 +130,11 @@ tzload(const char *name, struct state * sp)
if (name == NULL && (name = TZDEFAULT) == NULL)
return -1;
{
int doaccess;
char fullname[MAXPGPATH];
if (name[0] == ':')
++name;
doaccess = name[0] == '/';
if (!doaccess)
{
p = pg_TZDIR();
if (p == NULL)
return -1;
if ((strlen(p) + strlen(name) + 1) >= sizeof fullname)
return -1;
(void) strcpy(fullname, p);
(void) strcat(fullname, "/");
(void) strcat(fullname, name);
/*
* Set doaccess if '.' (as in "../") shows up in name.
*/
if (strchr(name, '.') != NULL)
doaccess = TRUE;
name = fullname;
}
if (doaccess && access(name, R_OK) != 0)
return -1;
if ((fid = open(name, O_RDONLY | PG_BINARY, 0)) == -1)
return -1;
}
if (name[0] == ':')
++name;
fid = pg_open_tzfile(name, canonname);
if (fid < 0)
return -1;
{
struct tzhead *tzhp;
union
@ -587,7 +562,7 @@ tzparse(const char *name, struct state * sp, int lastditch)
if (name == NULL)
return -1;
}
load_result = tzload(TZDEFRULES, sp);
load_result = tzload(TZDEFRULES, NULL, sp);
if (load_result != 0)
sp->leapcnt = 0; /* so, we're off a little */
if (*name != '\0')
@ -794,7 +769,7 @@ tzparse(const char *name, struct state * sp, int lastditch)
static void
gmtload(struct state * sp)
{
if (tzload(gmt, sp) != 0)
if (tzload(gmt, NULL, sp) != 0)
(void) tzparse(gmt, sp, TRUE);
}

View File

@ -6,7 +6,7 @@
* Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/timezone/pgtz.c,v 1.46 2006/10/04 00:30:14 momjian Exp $
* $PostgreSQL: pgsql/src/timezone/pgtz.c,v 1.47 2006/10/16 19:58:27 tgl Exp $
*
*-------------------------------------------------------------------------
*/
@ -15,6 +15,7 @@
#include "postgres.h"
#include <ctype.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
@ -31,8 +32,11 @@ pg_tz *global_timezone = NULL;
static char tzdir[MAXPGPATH];
static int done_tzdir = 0;
static bool done_tzdir = false;
static bool scan_directory_ci(const char *dirname,
const char *fname, int fnamelen,
char *canonname, int canonnamelen);
static const char *identify_system_timezone(void);
static const char *select_default_timezone(void);
static bool set_global_timezone(const char *tzname);
@ -41,20 +45,123 @@ static bool set_global_timezone(const char *tzname);
/*
* Return full pathname of timezone data directory
*/
char *
static char *
pg_TZDIR(void)
{
if (done_tzdir)
return tzdir;
get_share_path(my_exec_path, tzdir);
strcat(tzdir, "/timezone");
strlcpy(tzdir + strlen(tzdir), "/timezone", MAXPGPATH - strlen(tzdir));
done_tzdir = 1;
done_tzdir = true;
return tzdir;
}
/*
* Given a timezone name, open() the timezone data file. Return the
* file descriptor if successful, -1 if not.
*
* The input name is searched for case-insensitively (we assume that the
* timezone database does not contain case-equivalent names).
*
* If "canonname" is not NULL, then on success the canonical spelling of the
* given name is stored there (it is assumed to be > TZ_STRLEN_MAX bytes!).
*/
int
pg_open_tzfile(const char *name, char *canonname)
{
const char *fname;
char fullname[MAXPGPATH];
int fullnamelen;
int orignamelen;
/*
* Loop to split the given name into directory levels; for each level,
* search using scan_directory_ci().
*/
strcpy(fullname, pg_TZDIR());
orignamelen = fullnamelen = strlen(fullname);
fname = name;
for (;;)
{
const char *slashptr;
int fnamelen;
slashptr = strchr(fname, '/');
if (slashptr)
fnamelen = slashptr - fname;
else
fnamelen = strlen(fname);
if (fullnamelen + 1 + fnamelen >= MAXPGPATH)
return -1; /* not gonna fit */
if (!scan_directory_ci(fullname, fname, fnamelen,
fullname + fullnamelen + 1,
MAXPGPATH - fullnamelen - 1))
return -1;
fullname[fullnamelen++] = '/';
fullnamelen += strlen(fullname + fullnamelen);
if (slashptr)
fname = slashptr + 1;
else
break;
}
if (canonname)
strlcpy(canonname, fullname + orignamelen + 1, TZ_STRLEN_MAX + 1);
return open(fullname, O_RDONLY | PG_BINARY, 0);
}
/*
* Scan specified directory for a case-insensitive match to fname
* (of length fnamelen --- fname may not be null terminated!). If found,
* copy the actual filename into canonname and return true.
*/
static bool
scan_directory_ci(const char *dirname, const char *fname, int fnamelen,
char *canonname, int canonnamelen)
{
bool found = false;
DIR *dirdesc;
struct dirent *direntry;
dirdesc = AllocateDir(dirname);
if (!dirdesc)
{
ereport(LOG,
(errcode_for_file_access(),
errmsg("could not open directory \"%s\": %m", dirname)));
return false;
}
while ((direntry = ReadDir(dirdesc, dirname)) != NULL)
{
/*
* Ignore . and .., plus any other "hidden" files. This is a security
* measure to prevent access to files outside the timezone directory.
*/
if (direntry->d_name[0] == '.')
continue;
if (strlen(direntry->d_name) == fnamelen &&
pg_strncasecmp(direntry->d_name, fname, fnamelen) == 0)
{
/* Found our match */
strlcpy(canonname, direntry->d_name, canonnamelen);
found = true;
break;
}
}
FreeDir(dirdesc);
return found;
}
/*
* The following block of code attempts to determine which timezone in our
* timezone database is the best match for the active system timezone.
@ -167,7 +274,7 @@ score_timezone(const char *tzname, struct tztry * tt)
* Load timezone directly. Don't use pg_tzset, because we don't want all
* timezones loaded in the cache at startup.
*/
if (tzload(tzname, &tz.state) != 0)
if (tzload(tzname, NULL, &tz.state) != 0)
{
if (tzname[0] == ':' || tzparse(tzname, &tz.state, FALSE) != 0)
{
@ -958,10 +1065,20 @@ identify_system_timezone(void)
/*
* We keep loaded timezones in a hashtable so we don't have to
* load and parse the TZ definition file every time it is selected.
* load and parse the TZ definition file every time one is selected.
* Because we want timezone names to be found case-insensitively,
* the hash key is the uppercased name of the zone.
*/
typedef struct
{
/* tznameupper contains the all-upper-case name of the timezone */
char tznameupper[TZ_STRLEN_MAX + 1];
pg_tz tz;
} pg_tz_cache;
static HTAB *timezone_cache = NULL;
static bool
init_timezone_hashtable(void)
{
@ -970,7 +1087,7 @@ init_timezone_hashtable(void)
MemSet(&hash_ctl, 0, sizeof(hash_ctl));
hash_ctl.keysize = TZ_STRLEN_MAX + 1;
hash_ctl.entrysize = sizeof(pg_tz);
hash_ctl.entrysize = sizeof(pg_tz_cache);
timezone_cache = hash_create("Timezones",
4,
@ -989,8 +1106,11 @@ init_timezone_hashtable(void)
struct pg_tz *
pg_tzset(const char *name)
{
pg_tz *tzp;
pg_tz tz;
pg_tz_cache *tzp;
struct state tzstate;
char uppername[TZ_STRLEN_MAX + 1];
char canonname[TZ_STRLEN_MAX + 1];
char *p;
if (strlen(name) > TZ_STRLEN_MAX)
return NULL; /* not going to fit */
@ -999,37 +1119,49 @@ pg_tzset(const char *name)
if (!init_timezone_hashtable())
return NULL;
tzp = (pg_tz *) hash_search(timezone_cache,
name,
HASH_FIND,
NULL);
/*
* Upcase the given name to perform a case-insensitive hashtable search.
* (We could alternatively downcase it, but we prefer upcase so that we
* can get consistently upcased results from tzparse() in case the name
* is a POSIX-style timezone spec.)
*/
p = uppername;
while (*name)
*p++ = pg_toupper((unsigned char) *name++);
*p = '\0';
tzp = (pg_tz_cache *) hash_search(timezone_cache,
uppername,
HASH_FIND,
NULL);
if (tzp)
{
/* Timezone found in cache, nothing more to do */
return tzp;
return &tzp->tz;
}
if (tzload(name, &tz.state) != 0)
if (tzload(uppername, canonname, &tzstate) != 0)
{
if (name[0] == ':' || tzparse(name, &tz.state, FALSE) != 0)
if (uppername[0] == ':' || tzparse(uppername, &tzstate, FALSE) != 0)
{
/* Unknown timezone. Fail our call instead of loading GMT! */
return NULL;
}
/* For POSIX timezone specs, use uppercase name as canonical */
strcpy(canonname, uppername);
}
strcpy(tz.TZname, name);
/* Save timezone in the cache */
tzp = hash_search(timezone_cache,
name,
HASH_ENTER,
NULL);
tzp = (pg_tz_cache *) hash_search(timezone_cache,
uppername,
HASH_ENTER,
NULL);
strcpy(tzp->TZname, tz.TZname);
memcpy(&tzp->state, &tz.state, sizeof(tz.state));
/* hash_search already copied uppername into the hash key */
strcpy(tzp->tz.TZname, canonname);
memcpy(&tzp->tz.state, &tzstate, sizeof(tzstate));
return tzp;
return &tzp->tz;
}
@ -1241,14 +1373,13 @@ pg_tzenumerate_next(pg_tzenum *dir)
* Load this timezone using tzload() not pg_tzset(), so we don't fill
* the cache
*/
if (tzload(fullname + dir->baselen, &dir->tz.state) != 0)
if (tzload(fullname + dir->baselen, dir->tz.TZname, &dir->tz.state) != 0)
{
/* Zone could not be loaded, ignore it */
continue;
}
/* Timezone loaded OK. */
strcpy(dir->tz.TZname, fullname + dir->baselen);
return &dir->tz;
}

View File

@ -9,7 +9,7 @@
* Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/timezone/pgtz.h,v 1.17 2006/07/11 13:54:25 momjian Exp $
* $PostgreSQL: pgsql/src/timezone/pgtz.h,v 1.18 2006/10/16 19:58:27 tgl Exp $
*
*-------------------------------------------------------------------------
*/
@ -19,7 +19,6 @@
#include "tzfile.h"
#include "pgtime.h"
extern char *pg_TZDIR(void);
#define BIGGEST(a, b) (((a) > (b)) ? (a) : (b))
@ -55,11 +54,17 @@ struct state
struct pg_tz
{
/* TZname contains the canonically-cased name of the timezone */
char TZname[TZ_STRLEN_MAX + 1];
struct state state;
};
int tzload(const char *name, struct state * sp);
int tzparse(const char *name, struct state * sp, int lastditch);
/* in pgtz.c */
extern int pg_open_tzfile(const char *name, char *canonname);
/* in localtime.c */
extern int tzload(const char *name, char *canonname, struct state *sp);
extern int tzparse(const char *name, struct state *sp, int lastditch);
#endif /* _PGTZ_H */

View File

@ -3,7 +3,7 @@
* 1996-06-05 by Arthur David Olson (arthur_david_olson@nih.gov).
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/timezone/zic.c,v 1.16 2005/10/15 02:49:51 momjian Exp $
* $PostgreSQL: pgsql/src/timezone/zic.c,v 1.17 2006/10/16 19:58:27 tgl Exp $
*/
#include "postgres.h"
@ -2387,11 +2387,11 @@ link(const char *oldpath, const char *newpath)
#endif
/*
* This allows zic to compile by just assigning a dummy value.
* This allows zic to compile by just returning a dummy value.
* localtime.c references it, but no one uses it from zic.
*/
char *
pg_TZDIR(void)
int
pg_open_tzfile(const char *name, char *canonname)
{
return NULL;
return -1;
}