|
|
|
/*-------------------------------------------------------------------------
|
|
|
|
*
|
|
|
|
* dict_xsyn.c
|
|
|
|
* Extended synonym dictionary
|
|
|
|
*
|
|
|
|
* Copyright (c) 2007-2025, PostgreSQL Global Development Group
|
|
|
|
*
|
|
|
|
* IDENTIFICATION
|
|
|
|
* contrib/dict_xsyn/dict_xsyn.c
|
|
|
|
*
|
|
|
|
*-------------------------------------------------------------------------
|
|
|
|
*/
|
|
|
|
#include "postgres.h"
|
|
|
|
|
|
|
|
#include <ctype.h>
|
|
|
|
|
|
|
|
#include "catalog/pg_collation_d.h"
|
|
|
|
#include "commands/defrem.h"
|
|
|
|
#include "tsearch/ts_locale.h"
|
|
|
|
#include "tsearch/ts_public.h"
|
|
|
|
#include "utils/formatting.h"
|
|
|
|
|
|
|
|
PG_MODULE_MAGIC_EXT(
|
|
|
|
.name = "dict_xsyn",
|
|
|
|
.version = PG_VERSION
|
|
|
|
);
|
|
|
|
|
|
|
|
typedef struct
|
|
|
|
{
|
|
|
|
char *key; /* Word */
|
|
|
|
char *value; /* Unparsed list of synonyms, including the
|
|
|
|
* word itself */
|
|
|
|
} Syn;
|
|
|
|
|
|
|
|
typedef struct
|
|
|
|
{
|
|
|
|
int len;
|
|
|
|
Syn *syn;
|
|
|
|
|
|
|
|
bool matchorig;
|
|
|
|
bool keeporig;
|
|
|
|
bool matchsynonyms;
|
|
|
|
bool keepsynonyms;
|
|
|
|
} DictSyn;
|
|
|
|
|
|
|
|
|
|
|
|
PG_FUNCTION_INFO_V1(dxsyn_init);
|
|
|
|
PG_FUNCTION_INFO_V1(dxsyn_lexize);
|
|
|
|
|
|
|
|
static char *
|
|
|
|
find_word(char *in, char **end)
|
|
|
|
{
|
|
|
|
char *start;
|
|
|
|
|
|
|
|
*end = NULL;
|
Remove ts_locale.c's t_isdigit(), t_isspace(), t_isprint()
These do the same thing as the standard isdigit(), isspace(), and
isprint() but with multibyte and encoding support. But all the
callers are only interested in analyzing single-byte ASCII characters.
So this extra layer is overkill and we can replace the uses with the
standard functions.
All the t_is*() functions in ts_locale.c are under scrutiny because
they don't use the common locale provider framework but instead use
the global libc locale settings. For the functions being touched by
this patch, we don't need all that anyway, as mentioned above, so the
simplest solution is to just remove them. The few remaining t_is*()
functions will need a different treatment in a separate patch.
pg_trgm has some compile-time options with macros such as
KEEPONLYALNUM. These are not documented, and the non-default variant
is not supported by any test cases. As part of this undertaking, I'm
removing the non-default variant, as it is in the way of cleanup. So
in this case, the not-KEEPONLYALNUM code path is gone.
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://www.postgresql.org/message-id/flat/653f3b84-fc87-45a7-9a0c-bfb4fcab3e7d%40eisentraut.org
9 months ago
|
|
|
while (*in && isspace((unsigned char) *in))
|
|
|
|
in += pg_mblen(in);
|
|
|
|
|
|
|
|
if (!*in || *in == '#')
|
|
|
|
return NULL;
|
|
|
|
start = in;
|
|
|
|
|
Remove ts_locale.c's t_isdigit(), t_isspace(), t_isprint()
These do the same thing as the standard isdigit(), isspace(), and
isprint() but with multibyte and encoding support. But all the
callers are only interested in analyzing single-byte ASCII characters.
So this extra layer is overkill and we can replace the uses with the
standard functions.
All the t_is*() functions in ts_locale.c are under scrutiny because
they don't use the common locale provider framework but instead use
the global libc locale settings. For the functions being touched by
this patch, we don't need all that anyway, as mentioned above, so the
simplest solution is to just remove them. The few remaining t_is*()
functions will need a different treatment in a separate patch.
pg_trgm has some compile-time options with macros such as
KEEPONLYALNUM. These are not documented, and the non-default variant
is not supported by any test cases. As part of this undertaking, I'm
removing the non-default variant, as it is in the way of cleanup. So
in this case, the not-KEEPONLYALNUM code path is gone.
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://www.postgresql.org/message-id/flat/653f3b84-fc87-45a7-9a0c-bfb4fcab3e7d%40eisentraut.org
9 months ago
|
|
|
while (*in && !isspace((unsigned char) *in))
|
|
|
|
in += pg_mblen(in);
|
|
|
|
|
|
|
|
*end = in;
|
|
|
|
|
|
|
|
return start;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
compare_syn(const void *a, const void *b)
|
|
|
|
{
|
|
|
|
return strcmp(((const Syn *) a)->key, ((const Syn *) b)->key);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
read_dictionary(DictSyn *d, const char *filename)
|
|
|
|
{
|
|
|
|
char *real_filename = get_tsearch_config_filename(filename, "rules");
|
|
|
|
tsearch_readline_state trst;
|
|
|
|
char *line;
|
|
|
|
int cur = 0;
|
|
|
|
|
|
|
|
if (!tsearch_readline_begin(&trst, real_filename))
|
|
|
|
ereport(ERROR,
|
|
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
|
|
errmsg("could not open synonym file \"%s\": %m",
|
|
|
|
real_filename)));
|
|
|
|
|
|
|
|
while ((line = tsearch_readline(&trst)) != NULL)
|
|
|
|
{
|
|
|
|
char *value;
|
|
|
|
char *key;
|
|
|
|
char *pos;
|
|
|
|
char *end;
|
|
|
|
|
|
|
|
if (*line == '\0')
|
|
|
|
continue;
|
|
|
|
|
|
|
|
value = str_tolower(line, strlen(line), DEFAULT_COLLATION_OID);
|
|
|
|
pfree(line);
|
|
|
|
|
|
|
|
pos = value;
|
|
|
|
while ((key = find_word(pos, &end)) != NULL)
|
|
|
|
{
|
|
|
|
/* Enlarge syn structure if full */
|
|
|
|
if (cur == d->len)
|
|
|
|
{
|
|
|
|
d->len = (d->len > 0) ? 2 * d->len : 16;
|
|
|
|
if (d->syn)
|
|
|
|
d->syn = (Syn *) repalloc(d->syn, sizeof(Syn) * d->len);
|
|
|
|
else
|
|
|
|
d->syn = (Syn *) palloc(sizeof(Syn) * d->len);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Save first word only if we will match it */
|
|
|
|
if (pos != value || d->matchorig)
|
|
|
|
{
|
|
|
|
d->syn[cur].key = pnstrdup(key, end - key);
|
|
|
|
d->syn[cur].value = pstrdup(value);
|
|
|
|
|
|
|
|
cur++;
|
|
|
|
}
|
|
|
|
|
|
|
|
pos = end;
|
|
|
|
|
|
|
|
/* Don't bother scanning synonyms if we will not match them */
|
|
|
|
if (!d->matchsynonyms)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
pfree(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
tsearch_readline_end(&trst);
|
|
|
|
|
|
|
|
d->len = cur;
|
|
|
|
if (cur > 1)
|
|
|
|
qsort(d->syn, d->len, sizeof(Syn), compare_syn);
|
|
|
|
|
|
|
|
pfree(real_filename);
|
|
|
|
}
|
|
|
|
|
|
|
|
Datum
|
|
|
|
dxsyn_init(PG_FUNCTION_ARGS)
|
|
|
|
{
|
|
|
|
List *dictoptions = (List *) PG_GETARG_POINTER(0);
|
|
|
|
DictSyn *d;
|
|
|
|
ListCell *l;
|
|
|
|
char *filename = NULL;
|
|
|
|
|
|
|
|
d = (DictSyn *) palloc0(sizeof(DictSyn));
|
|
|
|
d->len = 0;
|
|
|
|
d->syn = NULL;
|
|
|
|
d->matchorig = true;
|
|
|
|
d->keeporig = true;
|
|
|
|
d->matchsynonyms = false;
|
|
|
|
d->keepsynonyms = true;
|
|
|
|
|
|
|
|
foreach(l, dictoptions)
|
|
|
|
{
|
|
|
|
DefElem *defel = (DefElem *) lfirst(l);
|
|
|
|
|
Avoid unnecessary use of pg_strcasecmp for already-downcased identifiers.
We have a lot of code in which option names, which from the user's
viewpoint are logically keywords, are passed through the grammar as plain
identifiers, and then matched to string literals during command execution.
This approach avoids making words into lexer keywords unnecessarily. Some
places matched these strings using plain strcmp, some using pg_strcasecmp.
But the latter should be unnecessary since identifiers would have been
downcased on their way through the parser. Aside from any efficiency
concerns (probably not a big factor), the lack of consistency in this area
creates a hazard of subtle bugs due to different places coming to different
conclusions about whether two option names are the same or different.
Hence, standardize on using strcmp() to match any option names that are
expected to have been fed through the parser.
This does create a user-visible behavioral change, which is that while
formerly all of these would work:
alter table foo set (fillfactor = 50);
alter table foo set (FillFactor = 50);
alter table foo set ("fillfactor" = 50);
alter table foo set ("FillFactor" = 50);
now the last case will fail because that double-quoted identifier is
different from the others. However, none of our documentation says that
you can use a quoted identifier in such contexts at all, and we should
discourage doing so since it would break if we ever decide to parse such
constructs as true lexer keywords rather than poor man's substitutes.
So this shouldn't create a significant compatibility issue for users.
Daniel Gustafsson, reviewed by Michael Paquier, small changes by me
Discussion: https://postgr.es/m/29405B24-564E-476B-98C0-677A29805B84@yesql.se
8 years ago
|
|
|
if (strcmp(defel->defname, "matchorig") == 0)
|
|
|
|
{
|
|
|
|
d->matchorig = defGetBoolean(defel);
|
|
|
|
}
|
Avoid unnecessary use of pg_strcasecmp for already-downcased identifiers.
We have a lot of code in which option names, which from the user's
viewpoint are logically keywords, are passed through the grammar as plain
identifiers, and then matched to string literals during command execution.
This approach avoids making words into lexer keywords unnecessarily. Some
places matched these strings using plain strcmp, some using pg_strcasecmp.
But the latter should be unnecessary since identifiers would have been
downcased on their way through the parser. Aside from any efficiency
concerns (probably not a big factor), the lack of consistency in this area
creates a hazard of subtle bugs due to different places coming to different
conclusions about whether two option names are the same or different.
Hence, standardize on using strcmp() to match any option names that are
expected to have been fed through the parser.
This does create a user-visible behavioral change, which is that while
formerly all of these would work:
alter table foo set (fillfactor = 50);
alter table foo set (FillFactor = 50);
alter table foo set ("fillfactor" = 50);
alter table foo set ("FillFactor" = 50);
now the last case will fail because that double-quoted identifier is
different from the others. However, none of our documentation says that
you can use a quoted identifier in such contexts at all, and we should
discourage doing so since it would break if we ever decide to parse such
constructs as true lexer keywords rather than poor man's substitutes.
So this shouldn't create a significant compatibility issue for users.
Daniel Gustafsson, reviewed by Michael Paquier, small changes by me
Discussion: https://postgr.es/m/29405B24-564E-476B-98C0-677A29805B84@yesql.se
8 years ago
|
|
|
else if (strcmp(defel->defname, "keeporig") == 0)
|
|
|
|
{
|
|
|
|
d->keeporig = defGetBoolean(defel);
|
|
|
|
}
|
Avoid unnecessary use of pg_strcasecmp for already-downcased identifiers.
We have a lot of code in which option names, which from the user's
viewpoint are logically keywords, are passed through the grammar as plain
identifiers, and then matched to string literals during command execution.
This approach avoids making words into lexer keywords unnecessarily. Some
places matched these strings using plain strcmp, some using pg_strcasecmp.
But the latter should be unnecessary since identifiers would have been
downcased on their way through the parser. Aside from any efficiency
concerns (probably not a big factor), the lack of consistency in this area
creates a hazard of subtle bugs due to different places coming to different
conclusions about whether two option names are the same or different.
Hence, standardize on using strcmp() to match any option names that are
expected to have been fed through the parser.
This does create a user-visible behavioral change, which is that while
formerly all of these would work:
alter table foo set (fillfactor = 50);
alter table foo set (FillFactor = 50);
alter table foo set ("fillfactor" = 50);
alter table foo set ("FillFactor" = 50);
now the last case will fail because that double-quoted identifier is
different from the others. However, none of our documentation says that
you can use a quoted identifier in such contexts at all, and we should
discourage doing so since it would break if we ever decide to parse such
constructs as true lexer keywords rather than poor man's substitutes.
So this shouldn't create a significant compatibility issue for users.
Daniel Gustafsson, reviewed by Michael Paquier, small changes by me
Discussion: https://postgr.es/m/29405B24-564E-476B-98C0-677A29805B84@yesql.se
8 years ago
|
|
|
else if (strcmp(defel->defname, "matchsynonyms") == 0)
|
|
|
|
{
|
|
|
|
d->matchsynonyms = defGetBoolean(defel);
|
|
|
|
}
|
Avoid unnecessary use of pg_strcasecmp for already-downcased identifiers.
We have a lot of code in which option names, which from the user's
viewpoint are logically keywords, are passed through the grammar as plain
identifiers, and then matched to string literals during command execution.
This approach avoids making words into lexer keywords unnecessarily. Some
places matched these strings using plain strcmp, some using pg_strcasecmp.
But the latter should be unnecessary since identifiers would have been
downcased on their way through the parser. Aside from any efficiency
concerns (probably not a big factor), the lack of consistency in this area
creates a hazard of subtle bugs due to different places coming to different
conclusions about whether two option names are the same or different.
Hence, standardize on using strcmp() to match any option names that are
expected to have been fed through the parser.
This does create a user-visible behavioral change, which is that while
formerly all of these would work:
alter table foo set (fillfactor = 50);
alter table foo set (FillFactor = 50);
alter table foo set ("fillfactor" = 50);
alter table foo set ("FillFactor" = 50);
now the last case will fail because that double-quoted identifier is
different from the others. However, none of our documentation says that
you can use a quoted identifier in such contexts at all, and we should
discourage doing so since it would break if we ever decide to parse such
constructs as true lexer keywords rather than poor man's substitutes.
So this shouldn't create a significant compatibility issue for users.
Daniel Gustafsson, reviewed by Michael Paquier, small changes by me
Discussion: https://postgr.es/m/29405B24-564E-476B-98C0-677A29805B84@yesql.se
8 years ago
|
|
|
else if (strcmp(defel->defname, "keepsynonyms") == 0)
|
|
|
|
{
|
|
|
|
d->keepsynonyms = defGetBoolean(defel);
|
|
|
|
}
|
Avoid unnecessary use of pg_strcasecmp for already-downcased identifiers.
We have a lot of code in which option names, which from the user's
viewpoint are logically keywords, are passed through the grammar as plain
identifiers, and then matched to string literals during command execution.
This approach avoids making words into lexer keywords unnecessarily. Some
places matched these strings using plain strcmp, some using pg_strcasecmp.
But the latter should be unnecessary since identifiers would have been
downcased on their way through the parser. Aside from any efficiency
concerns (probably not a big factor), the lack of consistency in this area
creates a hazard of subtle bugs due to different places coming to different
conclusions about whether two option names are the same or different.
Hence, standardize on using strcmp() to match any option names that are
expected to have been fed through the parser.
This does create a user-visible behavioral change, which is that while
formerly all of these would work:
alter table foo set (fillfactor = 50);
alter table foo set (FillFactor = 50);
alter table foo set ("fillfactor" = 50);
alter table foo set ("FillFactor" = 50);
now the last case will fail because that double-quoted identifier is
different from the others. However, none of our documentation says that
you can use a quoted identifier in such contexts at all, and we should
discourage doing so since it would break if we ever decide to parse such
constructs as true lexer keywords rather than poor man's substitutes.
So this shouldn't create a significant compatibility issue for users.
Daniel Gustafsson, reviewed by Michael Paquier, small changes by me
Discussion: https://postgr.es/m/29405B24-564E-476B-98C0-677A29805B84@yesql.se
8 years ago
|
|
|
else if (strcmp(defel->defname, "rules") == 0)
|
|
|
|
{
|
|
|
|
/* we can't read the rules before parsing all options! */
|
|
|
|
filename = defGetString(defel);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ereport(ERROR,
|
|
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
|
|
errmsg("unrecognized xsyn parameter: \"%s\"",
|
|
|
|
defel->defname)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (filename)
|
|
|
|
read_dictionary(d, filename);
|
|
|
|
|
|
|
|
PG_RETURN_POINTER(d);
|
|
|
|
}
|
|
|
|
|
|
|
|
Datum
|
|
|
|
dxsyn_lexize(PG_FUNCTION_ARGS)
|
|
|
|
{
|
|
|
|
DictSyn *d = (DictSyn *) PG_GETARG_POINTER(0);
|
|
|
|
char *in = (char *) PG_GETARG_POINTER(1);
|
|
|
|
int length = PG_GETARG_INT32(2);
|
|
|
|
Syn word;
|
|
|
|
Syn *found;
|
|
|
|
TSLexeme *res = NULL;
|
|
|
|
|
|
|
|
if (!length || d->len == 0)
|
|
|
|
PG_RETURN_POINTER(NULL);
|
|
|
|
|
|
|
|
/* Create search pattern */
|
|
|
|
{
|
|
|
|
char *temp = pnstrdup(in, length);
|
|
|
|
|
|
|
|
word.key = str_tolower(temp, length, DEFAULT_COLLATION_OID);
|
|
|
|
pfree(temp);
|
|
|
|
word.value = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Look for matching syn */
|
|
|
|
found = (Syn *) bsearch(&word, d->syn, d->len, sizeof(Syn), compare_syn);
|
|
|
|
pfree(word.key);
|
|
|
|
|
|
|
|
if (!found)
|
|
|
|
PG_RETURN_POINTER(NULL);
|
|
|
|
|
|
|
|
/* Parse string of synonyms and return array of words */
|
|
|
|
{
|
|
|
|
char *value = found->value;
|
|
|
|
char *syn;
|
|
|
|
char *pos;
|
|
|
|
char *end;
|
|
|
|
int nsyns = 0;
|
|
|
|
|
|
|
|
res = palloc(sizeof(TSLexeme));
|
|
|
|
|
|
|
|
pos = value;
|
|
|
|
while ((syn = find_word(pos, &end)) != NULL)
|
|
|
|
{
|
|
|
|
res = repalloc(res, sizeof(TSLexeme) * (nsyns + 2));
|
|
|
|
|
|
|
|
/* The first word is output only if keeporig=true */
|
|
|
|
if (pos != value || d->keeporig)
|
|
|
|
{
|
|
|
|
res[nsyns].lexeme = pnstrdup(syn, end - syn);
|
|
|
|
res[nsyns].nvariant = 0;
|
|
|
|
res[nsyns].flags = 0;
|
|
|
|
nsyns++;
|
|
|
|
}
|
|
|
|
|
|
|
|
pos = end;
|
|
|
|
|
|
|
|
/* Stop if we are not to output the synonyms */
|
|
|
|
if (!d->keepsynonyms)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
res[nsyns].lexeme = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
PG_RETURN_POINTER(res);
|
|
|
|
}
|