Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 5ac51c8

Browse files
committed
Adjust assorted hint messages that list all valid options.
Instead of listing all valid options, we now try to provide one that looks similar. Since this may be useful elsewhere, this change introduces a new set of functions that can be reused for similar purposes. Author: Nathan Bossart <nathandbossart@gmail.com> Discussion: https://www.postgresql.org/message-id/flat/b1f9f399-3a1a-b554-283f-4ae7f34608e2@enterprisedb.com
1 parent 1e08576 commit 5ac51c8

File tree

10 files changed

+159
-47
lines changed

10 files changed

+159
-47
lines changed

contrib/dblink/dblink.c

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2008,27 +2008,32 @@ dblink_fdw_validator(PG_FUNCTION_ARGS)
20082008
{
20092009
/*
20102010
* Unknown option, or invalid option for the context specified, so
2011-
* complain about it. Provide a hint with list of valid options
2012-
* for the context.
2011+
* complain about it. Provide a hint with a valid option that
2012+
* looks similar, if there is one.
20132013
*/
2014-
StringInfoData buf;
20152014
const PQconninfoOption *opt;
2015+
const char *closest_match;
2016+
ClosestMatchState match_state;
2017+
bool has_valid_options = false;
20162018

2017-
initStringInfo(&buf);
2019+
initClosestMatch(&match_state, def->defname, 4);
20182020
for (opt = options; opt->keyword; opt++)
20192021
{
20202022
if (is_valid_dblink_option(options, opt->keyword, context))
2021-
appendStringInfo(&buf, "%s%s",
2022-
(buf.len > 0) ? ", " : "",
2023-
opt->keyword);
2023+
{
2024+
has_valid_options = true;
2025+
updateClosestMatch(&match_state, opt->keyword);
2026+
}
20242027
}
2028+
2029+
closest_match = getClosestMatch(&match_state);
20252030
ereport(ERROR,
20262031
(errcode(ERRCODE_FDW_OPTION_NAME_NOT_FOUND),
20272032
errmsg("invalid option \"%s\"", def->defname),
2028-
buf.len > 0
2029-
? errhint("Valid options in this context are: %s",
2030-
buf.data)
2031-
: errhint("There are no valid options in this context.")));
2033+
has_valid_options ? closest_match ?
2034+
errhint("Perhaps you meant the option \"%s\".",
2035+
closest_match) : 0 :
2036+
errhint("There are no valid options in this context.")));
20322037
}
20332038
}
20342039

contrib/dblink/expected/dblink.out

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,6 @@ $d$;
897897
CREATE USER MAPPING FOR public SERVER fdtest
898898
OPTIONS (server 'localhost'); -- fail, can't specify server here
899899
ERROR: invalid option "server"
900-
HINT: Valid options in this context are: user, password, sslpassword
901900
CREATE USER MAPPING FOR public SERVER fdtest OPTIONS (user :'USER');
902901
GRANT USAGE ON FOREIGN SERVER fdtest TO regress_dblink_user;
903902
GRANT EXECUTE ON FUNCTION dblink_connect_u(text, text) TO regress_dblink_user;

contrib/file_fdw/expected/file_fdw.out

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ ERROR: invalid option "force_not_null"
166166
HINT: There are no valid options in this context.
167167
CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (force_not_null '*'); -- ERROR
168168
ERROR: invalid option "force_not_null"
169-
HINT: Valid options in this context are: filename, program, format, header, delimiter, quote, escape, null, encoding
170169
-- force_null is not allowed to be specified at any foreign object level:
171170
ALTER FOREIGN DATA WRAPPER file_fdw OPTIONS (ADD force_null '*'); -- ERROR
172171
ERROR: invalid option "force_null"
@@ -179,7 +178,6 @@ ERROR: invalid option "force_null"
179178
HINT: There are no valid options in this context.
180179
CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (force_null '*'); -- ERROR
181180
ERROR: invalid option "force_null"
182-
HINT: Valid options in this context are: filename, program, format, header, delimiter, quote, escape, null, encoding
183181
-- basic query tests
184182
SELECT * FROM agg_text WHERE b > 10.0 ORDER BY a;
185183
a | b

contrib/file_fdw/file_fdw.c

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include "utils/memutils.h"
3838
#include "utils/rel.h"
3939
#include "utils/sampling.h"
40+
#include "utils/varlena.h"
4041

4142
PG_MODULE_MAGIC;
4243

@@ -214,27 +215,32 @@ file_fdw_validator(PG_FUNCTION_ARGS)
214215
if (!is_valid_option(def->defname, catalog))
215216
{
216217
const struct FileFdwOption *opt;
217-
StringInfoData buf;
218+
const char *closest_match;
219+
ClosestMatchState match_state;
220+
bool has_valid_options = false;
218221

219222
/*
220223
* Unknown option specified, complain about it. Provide a hint
221-
* with list of valid options for the object.
224+
* with a valid option that looks similar, if there is one.
222225
*/
223-
initStringInfo(&buf);
226+
initClosestMatch(&match_state, def->defname, 4);
224227
for (opt = valid_options; opt->optname; opt++)
225228
{
226229
if (catalog == opt->optcontext)
227-
appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
228-
opt->optname);
230+
{
231+
has_valid_options = true;
232+
updateClosestMatch(&match_state, opt->optname);
233+
}
229234
}
230235

236+
closest_match = getClosestMatch(&match_state);
231237
ereport(ERROR,
232238
(errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
233239
errmsg("invalid option \"%s\"", def->defname),
234-
buf.len > 0
235-
? errhint("Valid options in this context are: %s",
236-
buf.data)
237-
: errhint("There are no valid options in this context.")));
240+
has_valid_options ? closest_match ?
241+
errhint("Perhaps you meant the option \"%s\".",
242+
closest_match) : 0 :
243+
errhint("There are no valid options in this context.")));
238244
}
239245

240246
/*

contrib/postgres_fdw/expected/postgres_fdw.out

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ ALTER USER MAPPING FOR public SERVER testserver1
188188
ALTER USER MAPPING FOR public SERVER testserver1
189189
OPTIONS (ADD sslmode 'require');
190190
ERROR: invalid option "sslmode"
191-
HINT: Valid options in this context are: user, password, sslpassword, password_required, sslcert, sslkey
192191
-- But we can add valid ones fine
193192
ALTER USER MAPPING FOR public SERVER testserver1
194193
OPTIONS (ADD sslpassword 'dummy');
@@ -9706,7 +9705,7 @@ DO $d$
97069705
END;
97079706
$d$;
97089707
ERROR: invalid option "password"
9709-
HINT: Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
9708+
HINT: Perhaps you meant the option "passfile".
97109709
CONTEXT: SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')"
97119710
PL/pgSQL function inline_code_block line 3 at EXECUTE
97129711
-- If we add a password for our user mapping instead, we should get a different

contrib/postgres_fdw/option.c

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,26 +90,31 @@ postgres_fdw_validator(PG_FUNCTION_ARGS)
9090
{
9191
/*
9292
* Unknown option specified, complain about it. Provide a hint
93-
* with list of valid options for the object.
93+
* with a valid option that looks similar, if there is one.
9494
*/
9595
PgFdwOption *opt;
96-
StringInfoData buf;
96+
const char *closest_match;
97+
ClosestMatchState match_state;
98+
bool has_valid_options = false;
9799

98-
initStringInfo(&buf);
100+
initClosestMatch(&match_state, def->defname, 4);
99101
for (opt = postgres_fdw_options; opt->keyword; opt++)
100102
{
101103
if (catalog == opt->optcontext)
102-
appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
103-
opt->keyword);
104+
{
105+
has_valid_options = true;
106+
updateClosestMatch(&match_state, opt->keyword);
107+
}
104108
}
105109

110+
closest_match = getClosestMatch(&match_state);
106111
ereport(ERROR,
107112
(errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
108113
errmsg("invalid option \"%s\"", def->defname),
109-
buf.len > 0
110-
? errhint("Valid options in this context are: %s",
111-
buf.data)
112-
: errhint("There are no valid options in this context.")));
114+
has_valid_options ? closest_match ?
115+
errhint("Perhaps you meant the option \"%s\".",
116+
closest_match) : 0 :
117+
errhint("There are no valid options in this context.")));
113118
}
114119

115120
/*

src/backend/foreign/foreign.c

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "utils/memutils.h"
2828
#include "utils/rel.h"
2929
#include "utils/syscache.h"
30+
#include "utils/varlena.h"
3031

3132

3233
/*
@@ -621,25 +622,32 @@ postgresql_fdw_validator(PG_FUNCTION_ARGS)
621622
if (!is_conninfo_option(def->defname, catalog))
622623
{
623624
const struct ConnectionOption *opt;
624-
StringInfoData buf;
625+
const char *closest_match;
626+
ClosestMatchState match_state;
627+
bool has_valid_options = false;
625628

626629
/*
627630
* Unknown option specified, complain about it. Provide a hint
628-
* with list of valid options for the object.
631+
* with a valid option that looks similar, if there is one.
629632
*/
630-
initStringInfo(&buf);
633+
initClosestMatch(&match_state, def->defname, 4);
631634
for (opt = libpq_conninfo_options; opt->optname; opt++)
635+
{
632636
if (catalog == opt->optcontext)
633-
appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
634-
opt->optname);
637+
{
638+
has_valid_options = true;
639+
updateClosestMatch(&match_state, opt->optname);
640+
}
641+
}
635642

643+
closest_match = getClosestMatch(&match_state);
636644
ereport(ERROR,
637645
(errcode(ERRCODE_SYNTAX_ERROR),
638646
errmsg("invalid option \"%s\"", def->defname),
639-
buf.len > 0
640-
? errhint("Valid options in this context are: %s",
641-
buf.data)
642-
: errhint("There are no valid options in this context.")));
647+
has_valid_options ? closest_match ?
648+
errhint("Perhaps you meant the option \"%s\".",
649+
closest_match) : 0 :
650+
errhint("There are no valid options in this context.")));
643651

644652
PG_RETURN_BOOL(false);
645653
}

src/backend/utils/adt/varlena.c

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6197,6 +6197,88 @@ rest_of_char_same(const char *s1, const char *s2, int len)
61976197
#include "levenshtein.c"
61986198

61996199

6200+
/*
6201+
* The following *ClosestMatch() functions can be used to determine whether a
6202+
* user-provided string resembles any known valid values, which is useful for
6203+
* providing hints in log messages, among other things. Use these functions
6204+
* like so:
6205+
*
6206+
* initClosestMatch(&state, source_string, max_distance);
6207+
*
6208+
* for (int i = 0; i < num_valid_strings; i++)
6209+
* updateClosestMatch(&state, valid_strings[i]);
6210+
*
6211+
* closestMatch = getClosestMatch(&state);
6212+
*/
6213+
6214+
/*
6215+
* Initialize the given state with the source string and maximum Levenshtein
6216+
* distance to consider.
6217+
*/
6218+
void
6219+
initClosestMatch(ClosestMatchState *state, const char *source, int max_d)
6220+
{
6221+
Assert(state);
6222+
Assert(max_d >= 0);
6223+
6224+
state->source = source;
6225+
state->min_d = -1;
6226+
state->max_d = max_d;
6227+
state->match = NULL;
6228+
}
6229+
6230+
/*
6231+
* If the candidate string is a closer match than the current one saved (or
6232+
* there is no match saved), save it as the closest match.
6233+
*
6234+
* If the source or candidate string is NULL, empty, or too long, this function
6235+
* takes no action. Likewise, if the Levenshtein distance exceeds the maximum
6236+
* allowed or more than half the characters are different, no action is taken.
6237+
*/
6238+
void
6239+
updateClosestMatch(ClosestMatchState *state, const char *candidate)
6240+
{
6241+
int dist;
6242+
6243+
Assert(state);
6244+
6245+
if (state->source == NULL || state->source[0] == '\0' ||
6246+
candidate == NULL || candidate[0] == '\0')
6247+
return;
6248+
6249+
/*
6250+
* To avoid ERROR-ing, we check the lengths here instead of setting
6251+
* 'trusted' to false in the call to varstr_levenshtein_less_equal().
6252+
*/
6253+
if (strlen(state->source) > MAX_LEVENSHTEIN_STRLEN ||
6254+
strlen(candidate) > MAX_LEVENSHTEIN_STRLEN)
6255+
return;
6256+
6257+
dist = varstr_levenshtein_less_equal(state->source, strlen(state->source),
6258+
candidate, strlen(candidate), 1, 1, 1,
6259+
state->max_d, true);
6260+
if (dist <= state->max_d &&
6261+
dist <= strlen(state->source) / 2 &&
6262+
(state->min_d == -1 || dist < state->min_d))
6263+
{
6264+
state->min_d = dist;
6265+
state->match = candidate;
6266+
}
6267+
}
6268+
6269+
/*
6270+
* Return the closest match. If no suitable candidates were provided via
6271+
* updateClosestMatch(), return NULL.
6272+
*/
6273+
const char *
6274+
getClosestMatch(ClosestMatchState *state)
6275+
{
6276+
Assert(state);
6277+
6278+
return state->match;
6279+
}
6280+
6281+
62006282
/*
62016283
* Unicode support
62026284
*/

src/include/utils/varlena.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,16 @@ extern text *replace_text_regexp(text *src_text, text *pattern_text,
3838
int cflags, Oid collation,
3939
int search_start, int n);
4040

41+
typedef struct ClosestMatchState
42+
{
43+
const char *source;
44+
int min_d;
45+
int max_d;
46+
const char *match;
47+
} ClosestMatchState;
48+
49+
extern void initClosestMatch(ClosestMatchState *state, const char *source, int max_d);
50+
extern void updateClosestMatch(ClosestMatchState *state, const char *candidate);
51+
extern const char *getClosestMatch(ClosestMatchState *state);
52+
4153
#endif

src/test/regress/expected/foreign_data.out

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,6 @@ CREATE SERVER s6 VERSION '16.0' FOREIGN DATA WRAPPER foo OPTIONS (host 'a', dbna
329329
CREATE SERVER s7 TYPE 'oracle' VERSION '17.0' FOREIGN DATA WRAPPER foo OPTIONS (host 'a', dbname 'b');
330330
CREATE SERVER s8 FOREIGN DATA WRAPPER postgresql OPTIONS (foo '1'); -- ERROR
331331
ERROR: invalid option "foo"
332-
HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, requiressl, sslmode, gsslib
333332
CREATE SERVER s8 FOREIGN DATA WRAPPER postgresql OPTIONS (host 'localhost', dbname 's8db');
334333
\des+
335334
List of foreign servers
@@ -440,7 +439,6 @@ ERROR: permission denied for foreign-data wrapper foo
440439
RESET ROLE;
441440
ALTER SERVER s8 OPTIONS (foo '1'); -- ERROR option validation
442441
ERROR: invalid option "foo"
443-
HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, requiressl, sslmode, gsslib
444442
ALTER SERVER s8 OPTIONS (connect_timeout '30', SET dbname 'db1', DROP host);
445443
SET ROLE regress_test_role;
446444
ALTER SERVER s1 OWNER TO regress_test_indirect; -- ERROR
@@ -597,7 +595,7 @@ ERROR: user mapping for "regress_foreign_data_user" already exists for server "
597595
CREATE USER MAPPING FOR public SERVER s4 OPTIONS ("this mapping" 'is public');
598596
CREATE USER MAPPING FOR user SERVER s8 OPTIONS (username 'test', password 'secret'); -- ERROR
599597
ERROR: invalid option "username"
600-
HINT: Valid options in this context are: user, password
598+
HINT: Perhaps you meant the option "user".
601599
CREATE USER MAPPING FOR user SERVER s8 OPTIONS (user 'test', password 'secret');
602600
ALTER SERVER s5 OWNER TO regress_test_role;
603601
ALTER SERVER s6 OWNER TO regress_test_indirect;
@@ -636,7 +634,7 @@ ALTER USER MAPPING FOR public SERVER s5 OPTIONS (gotcha 'true'); -- E
636634
ERROR: user mapping for "public" does not exist for server "s5"
637635
ALTER USER MAPPING FOR current_user SERVER s8 OPTIONS (username 'test'); -- ERROR
638636
ERROR: invalid option "username"
639-
HINT: Valid options in this context are: user, password
637+
HINT: Perhaps you meant the option "user".
640638
ALTER USER MAPPING FOR current_user SERVER s8 OPTIONS (DROP user, SET password 'public');
641639
SET ROLE regress_test_role;
642640
ALTER USER MAPPING FOR current_user SERVER s5 OPTIONS (ADD modified '1');

0 commit comments

Comments
 (0)