v2-0001-psql-Fix-CREATE-SCHEMA-scanning-with-object-names.patch
application/octet-stream
Filename: v2-0001-psql-Fix-CREATE-SCHEMA-scanning-with-object-names.patch
Type: application/octet-stream
Part: 0
Patch
Format: format-patch
Series: patch v2-0001
Subject: psql: Fix CREATE SCHEMA scanning with object names like "begin"
| File | + | − |
|---|---|---|
| src/bin/psql/t/001_basic.pl | 76 | 0 |
| src/fe_utils/psqlscan.l | 118 | 55 |
| src/include/fe_utils/psqlscan_int.h | 5 | 0 |
From 8a4fc0a11b63cf10b5b5f0784bf3cbff5c4aa97d Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Mon, 22 Jun 2026 15:33:04 +0800
Subject: [PATCH v2] psql: Fix CREATE SCHEMA scanning with object names like
"begin"
Since d51697484, psql's scanner treats CREATE SCHEMA as a command that
may contain SQL-standard routine bodies, so that semicolons inside
BEGIN ATOMIC ... END blocks do not terminate the command too early.
However, the heuristic enabled BEGIN/END tracking for every CREATE SCHEMA
statement. As a result, an object name such as "begin" could make
interactive psql think the statement was still incomplete, for example:
CREATE SCHEMA s CREATE VIEW begin AS SELECT 1;
Fix this by tracking identifiers from each top-level CREATE schema
element separately while scanning CREATE SCHEMA, and applying the existing
CREATE [OR REPLACE] FUNCTION/PROCEDURE heuristic to the current schema
element rather than to the whole CREATE SCHEMA statement.
This intentionally leaves the existing top-level CREATE FUNCTION/PROCEDURE
heuristic unchanged.
Add interactive psql tests for false-positive cases and for nested SQL
routine bodies inside CREATE SCHEMA.
Author: Chao Li <lic@highgo.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/8E03BB8D-003D-4850-9772-5F8015A5A0C7@gmail.com
---
src/bin/psql/t/001_basic.pl | 76 ++++++++++++
src/fe_utils/psqlscan.l | 173 +++++++++++++++++++---------
src/include/fe_utils/psqlscan_int.h | 5 +
3 files changed, 199 insertions(+), 55 deletions(-)
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index bbd330216ae..c7dd50bafd6 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -72,6 +72,82 @@ max_wal_senders = 4
});
$node->start;
+{
+ my $h = $node->interactive_psql('postgres');
+ $h->set_query_timer_restart();
+
+ my $out = $h->query_until(
+ qr/^CREATE SCHEMA\r?$/m,
+ "DROP SCHEMA IF EXISTS psql_scan_schema CASCADE;\n"
+ . "CREATE SCHEMA psql_scan_schema CREATE VIEW begin AS SELECT 1;\n");
+ like(
+ $out,
+ qr/^CREATE SCHEMA\r?$/m,
+ 'CREATE SCHEMA with object named begin ends at semicolon');
+
+ $out = $h->query_until(
+ qr/^CREATE SCHEMA\r?$/m,
+ "DROP SCHEMA psql_scan_schema CASCADE;\n"
+ . "CREATE SCHEMA psql_scan_schema\n"
+ . " CREATE VIEW v AS SELECT 1 AS function, 2 AS begin;\n");
+ like(
+ $out,
+ qr/^CREATE SCHEMA\r?$/m,
+ 'CREATE SCHEMA with column alias function and begin ends at semicolon');
+
+ $out = $h->query_until(
+ qr/^CREATE SCHEMA\r?$/m,
+ "DROP SCHEMA psql_scan_schema CASCADE;\n"
+ . "CREATE SCHEMA psql_scan_schema\n"
+ . " CREATE FUNCTION f() RETURNS int LANGUAGE sql AS \$\$ SELECT 1; \$\$\n"
+ . " CREATE VIEW begin AS SELECT 1;\n");
+ like(
+ $out,
+ qr/^CREATE SCHEMA\r?$/m,
+ 'CREATE SCHEMA with SQL function and later object named begin ends at semicolon');
+
+ $out = $h->query_until(
+ qr/^CREATE SCHEMA\r?$/m,
+ "DROP SCHEMA psql_scan_schema CASCADE;\n"
+ . "CREATE SCHEMA psql_scan_schema\n"
+ . " CREATE PROCEDURE p()\n"
+ . " BEGIN ATOMIC\n"
+ . " SELECT 1;\n"
+ . " END;\n");
+ like(
+ $out,
+ qr/^CREATE SCHEMA\r?$/m,
+ 'CREATE SCHEMA with SQL procedure body waits for END');
+
+ $out = $h->query_until(
+ qr/^CREATE SCHEMA\r?$/m,
+ "DROP SCHEMA psql_scan_schema CASCADE;\n"
+ . "CREATE SCHEMA psql_scan_schema\n"
+ . " CREATE FUNCTION f_case() RETURNS int\n"
+ . " BEGIN ATOMIC\n"
+ . " RETURN CASE WHEN true THEN 1 ELSE 0 END;\n"
+ . " END;\n");
+ like(
+ $out,
+ qr/^CREATE SCHEMA\r?$/m,
+ 'CREATE SCHEMA with SQL function body containing CASE waits for routine END');
+
+ $out = $h->query_until(
+ qr/^CREATE SCHEMA\r?$/m,
+ "DROP SCHEMA psql_scan_schema CASCADE;\n"
+ . "CREATE SCHEMA psql_scan_schema\n"
+ . " CREATE OR REPLACE FUNCTION f() RETURNS int\n"
+ . " BEGIN ATOMIC\n"
+ . " SELECT 1;\n"
+ . " END;\n");
+ like(
+ $out,
+ qr/^CREATE SCHEMA\r?$/m,
+ 'CREATE SCHEMA with OR REPLACE SQL function body waits for END');
+
+ $h->quit or die "psql returned $?";
+}
+
psql_like($node, '\copyright', qr/Copyright/, '\copyright');
psql_like($node, '\help', qr/ALTER/, '\help without arguments');
psql_like($node, '\help SELECT', qr/SELECT/, '\help with argument');
diff --git a/src/fe_utils/psqlscan.l b/src/fe_utils/psqlscan.l
index d29dda4d8e1..722924688c6 100644
--- a/src/fe_utils/psqlscan.l
+++ b/src/fe_utils/psqlscan.l
@@ -61,6 +61,122 @@ typedef int YYSTYPE;
#define ECHO psqlscan_emit(cur_state, yytext, yyleng)
+static void
+psqlscan_reset_create_schema_identifiers(PsqlScanState state)
+{
+ memset(state->create_schema_identifiers, 0,
+ sizeof(state->create_schema_identifiers));
+ state->create_schema_identifier_count = 0;
+}
+
+static void
+psqlscan_record_create_keyword(char *identifiers, int *identifier_count,
+ int identifiers_size, const char *identifier)
+{
+ if (*identifier_count < identifiers_size)
+ {
+ /*
+ * "schema" is needed for recognizing top-level CREATE SCHEMA. It is
+ * harmless in the CREATE SCHEMA subcommand buffer.
+ */
+ if (pg_strcasecmp(identifier, "create") == 0 ||
+ pg_strcasecmp(identifier, "function") == 0 ||
+ pg_strcasecmp(identifier, "procedure") == 0 ||
+ pg_strcasecmp(identifier, "or") == 0 ||
+ pg_strcasecmp(identifier, "replace") == 0 ||
+ pg_strcasecmp(identifier, "schema") == 0)
+ identifiers[*identifier_count] =
+ pg_tolower((unsigned char) identifier[0]);
+ }
+
+ (*identifier_count)++;
+}
+
+static bool
+psqlscan_is_create_routine(const char *identifiers)
+{
+ return identifiers[0] == 'c' &&
+ (identifiers[1] == 'f' || identifiers[1] == 'p' ||
+ (identifiers[1] == 'o' && identifiers[2] == 'r' &&
+ (identifiers[3] == 'f' || identifiers[3] == 'p')));
+}
+
+/*
+ * Track whether we are inside a BEGIN .. END block in a function definition,
+ * so that semicolons contained therein don't terminate the whole statement.
+ * Short of writing a full parser here, the following heuristic should work.
+ * We track whether the beginning of the statement matches CREATE [OR REPLACE]
+ * {FUNCTION|PROCEDURE}. For CREATE SCHEMA, track BEGIN .. END blocks only
+ * after recognizing an embedded CREATE [OR REPLACE] FUNCTION/PROCEDURE
+ * subcommand.
+ */
+static void
+psqlscan_track_identifier(PsqlScanState state, const char *identifier)
+{
+ bool is_create_schema;
+ bool track_begin_depth;
+
+ if (state->identifier_count == 0)
+ {
+ memset(state->identifiers, 0, sizeof(state->identifiers));
+ psqlscan_reset_create_schema_identifiers(state);
+ }
+
+ psqlscan_record_create_keyword(state->identifiers,
+ &state->identifier_count,
+ sizeof(state->identifiers),
+ identifier);
+
+ is_create_schema =
+ state->identifiers[0] == 'c' &&
+ state->identifiers[1] == 's';
+
+ /*
+ * In CREATE SCHEMA, track identifiers from each top-level CREATE schema
+ * element separately, so that BEGIN/END tracking is enabled only for nested
+ * CREATE [OR REPLACE] FUNCTION/PROCEDURE elements. This avoids treating
+ * identifiers appearing in expressions as schema elements.
+ */
+ if (is_create_schema &&
+ state->paren_depth == 0 &&
+ state->begin_depth == 0)
+ {
+ if (pg_strcasecmp(identifier, "create") == 0)
+ psqlscan_reset_create_schema_identifiers(state);
+
+ psqlscan_record_create_keyword(state->create_schema_identifiers,
+ &state->create_schema_identifier_count,
+ sizeof(state->create_schema_identifiers),
+ identifier);
+ }
+
+ track_begin_depth =
+ psqlscan_is_create_routine(state->identifiers) ||
+ (is_create_schema &&
+ psqlscan_is_create_routine(state->create_schema_identifiers));
+
+ if (track_begin_depth &&
+ state->paren_depth == 0)
+ {
+ if (pg_strcasecmp(identifier, "begin") == 0)
+ state->begin_depth++;
+ else if (pg_strcasecmp(identifier, "case") == 0)
+ {
+ /*
+ * CASE also ends with END. We only need to track this if we are
+ * already inside a BEGIN.
+ */
+ if (state->begin_depth >= 1)
+ state->begin_depth++;
+ }
+ else if (pg_strcasecmp(identifier, "end") == 0)
+ {
+ if (state->begin_depth > 0)
+ state->begin_depth--;
+ }
+ }
+}
+
%}
%option reentrant
@@ -921,61 +1037,7 @@ other .
{identifier} {
- /*
- * We need to track if we are inside a BEGIN .. END block
- * in a function definition, so that semicolons contained
- * therein don't terminate the whole statement. Short of
- * writing a full parser here, the following heuristic
- * should work. First, we track whether the beginning of
- * the statement matches CREATE [OR REPLACE]
- * {FUNCTION|PROCEDURE|SCHEMA}. (Allowing this in
- * CREATE SCHEMA, without tracking whether we're within a
- * CREATE FUNCTION/PROCEDURE subcommand, is a bit shaky
- * but should be okay with the present set of valid
- * subcommands.)
- */
-
- if (cur_state->identifier_count == 0)
- memset(cur_state->identifiers, 0, sizeof(cur_state->identifiers));
-
- if (cur_state->identifier_count < sizeof(cur_state->identifiers))
- {
- if (pg_strcasecmp(yytext, "create") == 0 ||
- pg_strcasecmp(yytext, "function") == 0 ||
- pg_strcasecmp(yytext, "procedure") == 0 ||
- pg_strcasecmp(yytext, "or") == 0 ||
- pg_strcasecmp(yytext, "replace") == 0 ||
- pg_strcasecmp(yytext, "schema") == 0)
- cur_state->identifiers[cur_state->identifier_count] = pg_tolower((unsigned char) yytext[0]);
- }
-
- cur_state->identifier_count++;
-
- if (cur_state->identifiers[0] == 'c' &&
- (cur_state->identifiers[1] == 'f' || cur_state->identifiers[1] == 'p' ||
- (cur_state->identifiers[1] == 'o' && cur_state->identifiers[2] == 'r' &&
- (cur_state->identifiers[3] == 'f' || cur_state->identifiers[3] == 'p')) ||
- cur_state->identifiers[1] == 's') &&
- cur_state->paren_depth == 0)
- {
- if (pg_strcasecmp(yytext, "begin") == 0)
- cur_state->begin_depth++;
- else if (pg_strcasecmp(yytext, "case") == 0)
- {
- /*
- * CASE also ends with END. We only need to track
- * this if we are already inside a BEGIN.
- */
- if (cur_state->begin_depth >= 1)
- cur_state->begin_depth++;
- }
- else if (pg_strcasecmp(yytext, "end") == 0)
- {
- if (cur_state->begin_depth > 0)
- cur_state->begin_depth--;
- }
- }
-
+ psqlscan_track_identifier(cur_state, yytext);
ECHO;
}
@@ -1294,6 +1356,7 @@ psql_scan_reset(PsqlScanState state)
state->dolqstart = NULL;
state->identifier_count = 0;
state->begin_depth = 0;
+ psqlscan_reset_create_schema_identifiers(state);
}
/*
diff --git a/src/include/fe_utils/psqlscan_int.h b/src/include/fe_utils/psqlscan_int.h
index 488f416f0e5..0d8b9034028 100644
--- a/src/include/fe_utils/psqlscan_int.h
+++ b/src/include/fe_utils/psqlscan_int.h
@@ -121,6 +121,11 @@ typedef struct PsqlScanStateData
char identifiers[4]; /* records the first few identifiers */
int begin_depth; /* depth of begin/end pairs */
+ int create_schema_identifier_count; /* first few identifiers of a
+ * CREATE SCHEMA element */
+ char create_schema_identifiers[4]; /* count for
+ * create_schema_identifiers */
+
/*
* Callback functions provided by the program making use of the lexer,
* plus a void* callback passthrough argument.
--
2.50.1 (Apple Git-155)