diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 9915731..20091e5 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2007,6 +2007,78 @@ hello 10
+
+ \if expr
+ \elif expr
+ \else
+ \endif
+
+
+ This group of commands implements nestable conditional blocks, like
+ this:
+
+
+SELECT
+ EXISTS(SELECT 1 FROM customer) as has_customers,
+ EXISTS(SELECT 1 FROM employee) as has_employees
+\gset
+\if :has_users
+ SELECT * FROM customer ORDER BY creation_date LIMIT 5;
+\elif :has_employees
+ \echo 'no customers found'
+ SELECT * FROM employee ORDER BY creation_date LIMIT 5;
+\else
+ \if yes
+ \echo 'No customers or employees'
+ \else
+ \echo 'this should never print'
+ \endif
+\endif
+
+
+ Conditional blocks must begin with a \if and end
+ with an \endif, and the pairs must be found in
+ the same source file. If an EOF is reached on the main file or an
+ \include-ed file before all
+ \if-\endif are matched, then
+ psql will raise an error.
+
+
+ The \if and \elif commands
+ read the rest of the line and evaluate it as a boolean expression.
+ Currently, expressions are limited to a single unquoted string
+ which is evaluated like other options booleans, so the valid values
+ are any unabiguous case insensitive matches for one of:
+ true, false, 1,
+ 0, on, off,
+ yes, no. So
+ t, T, and tR
+ will all match true.
+
+
+ Queries within a false branch of a conditional block will not be
+ sent to the server.
+
+
+ Non-conditional \-commands within a false branch
+ of a conditional block will not be evaluated for correctness. The
+ command will be ignored along with all remaining input to the end
+ of the line.
+
+
+ Expressions on \if and \elif
+ commands within a false branch of a conditional block will not be
+ evaluated.
+
+
+ A conditional block can at most one \else command.
+
+
+ The \elif command cannot follow the
+ \else command.
+
+
+
\ir or \include_relative filename
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 4139b77..feb9ddc 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -49,6 +49,7 @@
#include "psqlscanslash.h"
#include "settings.h"
#include "variables.h"
+#include "fe_utils/psqlscan_int.h"
/*
* Editable database object types.
@@ -132,7 +133,7 @@ HandleSlashCmds(PsqlScanState scan_state,
status = PSQL_CMD_ERROR;
}
- if (status != PSQL_CMD_ERROR)
+ if (status != PSQL_CMD_ERROR && psqlscan_branch_active(scan_state))
{
/* eat any remaining arguments after a valid command */
/* note we suppress evaluation of backticks here */
@@ -194,6 +195,68 @@ read_connect_arg(PsqlScanState scan_state)
return result;
}
+/*
+ * Read and interpret argument as a boolean expression.
+ * Return true if a boolean value was successfully parsed.
+ */
+static bool
+read_boolean_expression(PsqlScanState scan_state, char *action,
+ bool *result)
+{
+ bool success = false;
+ char *value = psql_scan_slash_option(scan_state,
+ OT_NORMAL, NULL, false);
+ /*
+ * placeholder code until ParseVariableBool() ads error detection
+ * once that patch is in place, use that instead
+ */
+ if (value)
+ {
+ size_t len;
+
+ if (value == NULL)
+ return false; /* not set -> assume "off" */
+
+ len = strlen(value);
+
+ if ((pg_strncasecmp(value, "true", len) == 0) ||
+ (pg_strncasecmp(value, "yes", len) == 0) ||
+ (pg_strncasecmp(value, "on", (len > 2 ? len : 2)) == 0) ||
+ (pg_strcasecmp(value, "1") == 0))
+ {
+ success = true;
+ *result = true;
+ }
+ else if ((pg_strncasecmp(value, "false", len) == 0) ||
+ (pg_strncasecmp(value, "no", len) == 0) ||
+ (pg_strncasecmp(value, "off", (len > 2 ? len : 2)) == 0) ||
+ (pg_strcasecmp(value, "0") == 0))
+ {
+ success = true;
+ *result = false;
+ }
+ else
+ {
+ psql_error("\\%s: invalid boolean expression: %s\n",
+ action, value);
+ }
+ free(value);
+ }
+ else
+ {
+ psql_error("\\%s: no expression given\n",action);
+ }
+ return success;
+}
+
+static bool
+is_branching_command(const char *cmd)
+{
+ return ((strcmp(cmd, "if") == 0 || \
+ strcmp(cmd, "elif") == 0 || \
+ strcmp(cmd, "else") == 0 || \
+ strcmp(cmd, "endif") == 0));
+}
/*
* Subroutine to actually try to execute a backslash command.
@@ -207,6 +270,14 @@ exec_command(const char *cmd,
* failed */
backslashResult status = PSQL_CMD_SKIP_LINE;
+ if (!psqlscan_branch_active(scan_state) && !is_branching_command(cmd) )
+ {
+ /* Continue with an empty buffer as if the command were never read */
+ resetPQExpBuffer(query_buf);
+ psql_scan_reset(scan_state);
+ return status;
+ }
+
/*
* \a -- toggle field alignment This makes little sense but we keep it
* around.
@@ -984,6 +1055,114 @@ exec_command(const char *cmd,
}
}
+ else if (strcmp(cmd, "if") == 0)
+ {
+ ifState new_if_state = IFSTATE_IGNORED;
+ if (psqlscan_branch_active(scan_state))
+ {
+ bool if_true = false;
+ success = read_boolean_expression(scan_state, "if", &if_true);
+ if (success)
+ {
+ if (if_true)
+ new_if_state = IFSTATE_TRUE;
+ else
+ new_if_state = IFSTATE_FALSE;
+ }
+ }
+ if (success)
+ psqlscan_branch_push(scan_state,new_if_state);
+ psql_scan_reset(scan_state);
+ }
+
+ else if (strcmp(cmd, "elif") == 0)
+ {
+ if (psqlscan_branch_empty(scan_state))
+ {
+ psql_error("encountered un-matched \\elif\n");
+ success = false;
+ }
+ else
+ {
+ bool elif_true = false;
+ switch (psqlscan_branch_get_state(scan_state))
+ {
+ case IFSTATE_IGNORED:
+ /* inactive branch, do nothing */
+ break;
+ case IFSTATE_TRUE:
+ /* just finished true section of active branch */
+ psqlscan_branch_set_state(scan_state, IFSTATE_IGNORED);
+ break;
+ case IFSTATE_FALSE:
+ /* determine if this section is true or not */
+ success = read_boolean_expression(scan_state, "elif",
+ &elif_true);
+ if (success)
+ {
+ if (elif_true)
+ psqlscan_branch_set_state(scan_state, IFSTATE_TRUE);
+ }
+ break;
+ case IFSTATE_ELSE_TRUE:
+ case IFSTATE_ELSE_FALSE:
+ psql_error("encountered \\elif after \\else\n");
+ success = false;
+ break;
+ default:
+ break;
+ }
+ }
+ psql_scan_reset(scan_state);
+ }
+
+ else if (strcmp(cmd, "else") == 0)
+ {
+ if (psqlscan_branch_empty(scan_state))
+ {
+ psql_error("encountered un-matched \\else\n");
+ success = false;
+ }
+ else
+ {
+ switch (psqlscan_branch_get_state(scan_state))
+ {
+ case IFSTATE_TRUE:
+ /* just finished true section of active branch */
+ case IFSTATE_IGNORED:
+ /* whole branch was inactive */
+ psqlscan_branch_set_state(scan_state, IFSTATE_ELSE_FALSE);
+ break;
+ case IFSTATE_FALSE:
+ /* just finished true section of active branch */
+ psqlscan_branch_set_state(scan_state, IFSTATE_ELSE_TRUE);
+ break;
+ case IFSTATE_ELSE_TRUE:
+ case IFSTATE_ELSE_FALSE:
+ psql_error("encountered \\else after \\else\n");
+ success = false;
+ break;
+ default:
+ break;
+ }
+ }
+ psql_scan_reset(scan_state);
+ }
+
+ else if (strcmp(cmd, "endif") == 0)
+ {
+ if (psqlscan_branch_empty(scan_state))
+ {
+ psql_error("encountered un-matched \\endif\n");
+ success = false;
+ }
+ else
+ {
+ psqlscan_branch_end_state(scan_state);
+ }
+ psql_scan_reset(scan_state);
+ }
+
/* \l is list databases */
else if (strcmp(cmd, "l") == 0 || strcmp(cmd, "list") == 0 ||
strcmp(cmd, "l+") == 0 || strcmp(cmd, "list+") == 0)
diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c
index bb306a4..7252824 100644
--- a/src/bin/psql/mainloop.c
+++ b/src/bin/psql/mainloop.c
@@ -15,7 +15,7 @@
#include "settings.h"
#include "mb/pg_wchar.h"
-
+#include "fe_utils/psqlscan_int.h"
/* callback functions for our flex lexer */
const PsqlScanCallbacks psqlscan_callbacks = {
@@ -23,7 +23,6 @@ const PsqlScanCallbacks psqlscan_callbacks = {
psql_error
};
-
/*
* Main processing loop for reading lines of input
* and sending them to the backend.
@@ -51,6 +50,9 @@ MainLoop(FILE *source)
volatile int count_eof = 0;
volatile bool die_on_error = false;
+ /* only needed at the end to detect unbalanced ifs in scan_state */
+ bool if_endifs_balanced = true;
+
/* Save the prior command source */
FILE *prev_cmd_source;
bool prev_cmd_interactive;
@@ -285,21 +287,28 @@ MainLoop(FILE *source)
if (scan_result == PSCAN_SEMICOLON ||
(scan_result == PSCAN_EOL && pset.singleline))
{
- /*
- * Save query in history. We use history_buf to accumulate
- * multi-line queries into a single history entry.
- */
- if (pset.cur_cmd_interactive && !line_saved_in_history)
+ if (psqlscan_branch_active(scan_state))
{
- pg_append_history(line, history_buf);
- pg_send_history(history_buf);
- line_saved_in_history = true;
+ /*
+ * Save query in history. We use history_buf to accumulate
+ * multi-line queries into a single history entry.
+ */
+ if (pset.cur_cmd_interactive && !line_saved_in_history)
+ {
+ pg_append_history(line, history_buf);
+ pg_send_history(history_buf);
+ line_saved_in_history = true;
+ }
+
+ /* execute query */
+ success = SendQuery(query_buf->data);
}
+ else
+ success = true;
- /* execute query */
- success = SendQuery(query_buf->data);
slashCmdStatus = success ? PSQL_CMD_SEND : PSQL_CMD_ERROR;
pset.stmt_lineno = 1;
+ slashCmdStatus = success ? PSQL_CMD_SEND : PSQL_CMD_ERROR;
/* transfer query to previous_buf by pointer-swapping */
{
@@ -358,15 +367,21 @@ MainLoop(FILE *source)
if (slashCmdStatus == PSQL_CMD_SEND)
{
- success = SendQuery(query_buf->data);
-
- /* transfer query to previous_buf by pointer-swapping */
+ if (psqlscan_branch_active(scan_state))
{
- PQExpBuffer swap_buf = previous_buf;
+ success = SendQuery(query_buf->data);
- previous_buf = query_buf;
- query_buf = swap_buf;
+ /* transfer query to previous_buf by pointer-swapping */
+ {
+ PQExpBuffer swap_buf = previous_buf;
+
+ previous_buf = query_buf;
+ query_buf = swap_buf;
+ }
}
+ else
+ success = true;
+
resetPQExpBuffer(query_buf);
/* flush any paren nesting info after forced send */
@@ -425,12 +440,17 @@ MainLoop(FILE *source)
if (query_buf->len > 0 && !pset.cur_cmd_interactive &&
successResult == EXIT_SUCCESS)
{
- /* save query in history */
- if (pset.cur_cmd_interactive)
- pg_send_history(history_buf);
+ if (psqlscan_branch_active(scan_state))
+ {
+ /* save query in history */
+ if (pset.cur_cmd_interactive)
+ pg_send_history(history_buf);
- /* execute query */
- success = SendQuery(query_buf->data);
+ /* execute query */
+ success = SendQuery(query_buf->data);
+ }
+ else
+ success = true;
if (!success && die_on_error)
successResult = EXIT_USER;
@@ -451,11 +471,17 @@ MainLoop(FILE *source)
destroyPQExpBuffer(previous_buf);
destroyPQExpBuffer(history_buf);
+ if (slashCmdStatus != PSQL_CMD_TERMINATE)
+ if_endifs_balanced = psqlscan_branch_empty(scan_state);
+
psql_scan_destroy(scan_state);
pset.cur_cmd_source = prev_cmd_source;
pset.cur_cmd_interactive = prev_cmd_interactive;
pset.lineno = prev_lineno;
+ if (! if_endifs_balanced )
+ psql_error("found EOF before closing \\endif(s)\n");
+
return successResult;
} /* MainLoop() */
diff --git a/src/bin/psql/mainloop.h b/src/bin/psql/mainloop.h
index 228a5e0..47f4c32 100644
--- a/src/bin/psql/mainloop.h
+++ b/src/bin/psql/mainloop.h
@@ -14,4 +14,5 @@ extern const PsqlScanCallbacks psqlscan_callbacks;
extern int MainLoop(FILE *source);
+
#endif /* MAINLOOP_H */
diff --git a/src/fe_utils/psqlscan.l b/src/fe_utils/psqlscan.l
index 1b29341..f70841c 100644
--- a/src/fe_utils/psqlscan.l
+++ b/src/fe_utils/psqlscan.l
@@ -904,6 +904,9 @@ psql_scan_create(const PsqlScanCallbacks *callbacks)
psql_scan_reset(state);
+ state->branch_stack = NULL;
+ state->branch_block_active = true;
+
return state;
}
@@ -919,6 +922,13 @@ psql_scan_destroy(PsqlScanState state)
yylex_destroy(state->scanner);
+ while (state->branch_stack != NULL)
+ {
+ IfStackElem *p = state->branch_stack;
+ state->branch_stack = state->branch_stack->next;
+ free(p);
+ }
+
free(state);
}
@@ -1426,3 +1436,103 @@ psqlscan_escape_variable(PsqlScanState state, const char *txt, int len,
psqlscan_emit(state, txt, len);
}
}
+
+/*
+ * psqlscan_branch_empty
+ *
+ * True if there are no active \if-structures
+ */
+bool
+psqlscan_branch_empty(PsqlScanState state)
+{
+ return (state->branch_stack == NULL);
+}
+
+/*
+ * psqlscan_branch_active
+ *
+ * True if the current \if-block (if any) is true and queries/commands
+ * should be executed.
+ */
+bool
+psqlscan_branch_active(PsqlScanState state)
+{
+ return state->branch_block_active;
+}
+
+/*
+ * Fetch the current state of the top of the stack
+ */
+ifState
+psqlscan_branch_get_state(PsqlScanState state)
+{
+ if (psqlscan_branch_empty(state))
+ return IFSTATE_NONE;
+ return state->branch_stack->if_state;
+}
+
+/*
+ * psqlscan_branch_update_active
+ *
+ * Scan the branch_stack to determine whether the next statements
+ * can execute or should be skipped. Cache this result in
+ * branch_block_active.
+ */
+static void
+psqlscan_branch_update_active(PsqlScanState state)
+{
+ ifState s = psqlscan_branch_get_state(state);
+ state->branch_block_active = ( (s == IFSTATE_NONE) ||
+ (s == IFSTATE_TRUE) ||
+ (s == IFSTATE_ELSE_TRUE));
+}
+
+/*
+ * psqlscan_branch_push
+ *
+ * Create a new \if branch.
+ */
+bool
+psqlscan_branch_push(PsqlScanState state, ifState new_state)
+{
+ IfStackElem *p = pg_malloc0(sizeof(IfStackElem));
+ p->if_state = new_state;
+ p->next = state->branch_stack;
+ state->branch_stack = p;
+ psqlscan_branch_update_active(state);
+ return true;
+}
+
+/*
+ * psqlscan_branch_set_state
+ *
+ * Change the state of the topmost branch.
+ * Returns false if there was branch state to set.
+ */
+bool
+psqlscan_branch_set_state(PsqlScanState state, ifState new_state)
+{
+ if (psqlscan_branch_empty(state))
+ return false;
+ state->branch_stack->if_state = new_state;
+ psqlscan_branch_update_active(state);
+ return true;
+}
+
+/*
+ * psqlscan_branch_end_state
+ *
+ * Destroy the topmost branch because and \endif was encountered.
+ * Returns false if there was no branch to end.
+ */
+bool
+psqlscan_branch_end_state(PsqlScanState state)
+{
+ IfStackElem *p = state->branch_stack;
+ if (!p)
+ return false;
+ state->branch_stack = state->branch_stack->next;
+ free(p);
+ psqlscan_branch_update_active(state);
+ return true;
+}
diff --git a/src/include/fe_utils/psqlscan_int.h b/src/include/fe_utils/psqlscan_int.h
index 0fddc7a..734e719 100644
--- a/src/include/fe_utils/psqlscan_int.h
+++ b/src/include/fe_utils/psqlscan_int.h
@@ -75,6 +75,30 @@ typedef struct StackElem
struct StackElem *next;
} StackElem;
+typedef enum _ifState
+{
+ IFSTATE_NONE = 0, /* Not currently in an \if block */
+ IFSTATE_TRUE, /* currently in an \if or \elif which is true
+ * and all parent branches (if any) are true */
+ IFSTATE_FALSE, /* currently in an \if or \elif which is false
+ * but no true branch has yet been seen,
+ * and all parent branches (if any) are true */
+ IFSTATE_IGNORED, /* currently in an \elif which follows a true \if
+ * or the whole \if is a child of a false parent */
+ IFSTATE_ELSE_TRUE, /* currently in an \else which is true
+ * and all parent branches (if any) are true */
+ IFSTATE_ELSE_FALSE /* currently in an \else which is false or ignored */
+} ifState;
+
+/*
+ * The state of nested ifs is stored in a stack.
+ */
+typedef struct IfStackElem
+{
+ ifState if_state;
+ struct IfStackElem *next;
+} IfStackElem;
+
/*
* All working state of the lexer must be stored in PsqlScanStateData
* between calls. This allows us to have multiple open lexer operations,
@@ -118,6 +142,12 @@ typedef struct PsqlScanStateData
* Callback functions provided by the program making use of the lexer.
*/
const PsqlScanCallbacks *callbacks;
+
+ /*
+ * \if branch state variables
+ */
+ IfStackElem *branch_stack;
+ bool branch_block_active;
} PsqlScanStateData;
@@ -141,4 +171,21 @@ extern void psqlscan_escape_variable(PsqlScanState state,
const char *txt, int len,
bool as_ident);
+/*
+ * branching commands
+ */
+extern bool psqlscan_branch_empty(PsqlScanState state);
+
+extern bool psqlscan_branch_active(PsqlScanState state);
+
+extern ifState psqlscan_branch_get_state(PsqlScanState state);
+
+extern bool psqlscan_branch_push(PsqlScanState state,
+ ifState new_state);
+
+extern bool psqlscan_branch_set_state(PsqlScanState state,
+ ifState new_state);
+
+extern bool psqlscan_branch_end_state(PsqlScanState state);
+
#endif /* PSQLSCAN_INT_H */
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 464436a..1dcaa46 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -2686,6 +2686,66 @@ deallocate q;
\pset format aligned
\pset expanded off
\pset border 1
+\if true
+ \if 1
+ \if yes
+ \if on
+ \echo 'all true'
+all true
+ \else
+ \echo 'should not print #1-1'
+ \endif
+ \else
+ \echo 'should not print #1-2'
+ \endif
+ \else
+ \echo 'should not print #1-3'
+ \endif
+\else
+ \echo 'should not print #1-4'
+\endif
+\if false
+ \echo 'should not print #2-1'
+\elif 0
+ \echo 'should not print #2-2'
+\elif no
+ \echo 'should not print #2-3'
+\elif off
+ \echo 'should not print #2-4'
+\else
+ \echo 'all false'
+all false
+\endif
+\if true
+ \echo 'first thing true'
+first thing true
+\else
+ \echo 'should not print #3-1'
+\endif
+\if false
+ \echo 'should not print #4-1'
+\elif true
+ \echo 'second thing true'
+second thing true
+\else
+ \echo 'should not print #5-1'
+\endif
+\endif
+encountered un-matched \endif
+\else
+encountered un-matched \else
+\elif
+encountered un-matched \elif
+\if true
+\else
+\else
+encountered \else after \else
+\endif
+\if false
+\else
+\elif
+encountered \elif after \else
+\endif
-- SHOW_CONTEXT
\set SHOW_CONTEXT never
do $$
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index 900aa7e..4f41894 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -357,6 +357,66 @@ deallocate q;
\pset expanded off
\pset border 1
+\if true
+ \if 1
+ \if yes
+ \if on
+ \echo 'all true'
+ \else
+ \echo 'should not print #1-1'
+ \endif
+ \else
+ \echo 'should not print #1-2'
+ \endif
+ \else
+ \echo 'should not print #1-3'
+ \endif
+\else
+ \echo 'should not print #1-4'
+\endif
+
+\if false
+ \echo 'should not print #2-1'
+\elif 0
+ \echo 'should not print #2-2'
+\elif no
+ \echo 'should not print #2-3'
+\elif off
+ \echo 'should not print #2-4'
+\else
+ \echo 'all false'
+\endif
+
+\if true
+ \echo 'first thing true'
+\else
+ \echo 'should not print #3-1'
+\endif
+
+\if false
+ \echo 'should not print #4-1'
+\elif true
+ \echo 'second thing true'
+\else
+ \echo 'should not print #5-1'
+\endif
+
+\endif
+
+\else
+
+\elif
+
+\if true
+\else
+\else
+\endif
+
+\if false
+\else
+\elif
+\endif
+
-- SHOW_CONTEXT
\set SHOW_CONTEXT never