diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index ae58708..c0ba4c4 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2035,6 +2035,67 @@ hello 10
+
+ \if expr
+ \elif expr
+ \else
+ \endif
+
+
+ This group of commands implements nestable conditional blocks, like
+ this:
+
+
+SELECT
+ EXISTS(SELECT 1 FROM customer WHERE customer_id = 123) as is_customer,
+ EXISTS(SELECT 1 FROM employee WHERE employee_id = 456) as is_employee
+\gset
+\if :is_customer
+ SELECT * FROM customer WHERE customer_id = 123;
+\elif :is_employee
+ \echo 'is not a customer but is an employee'
+ SELECT * FROM employee WHERE employee_id = 456;
+\else
+ \if yes
+ \echo 'not a customer or employee'
+ \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 local
+ \if-\endif are matched, then
+ psql will raise an error.
+
+
+ A conditional block can have any number of
+ \elif clauses, which may optionally be followed by a
+ single \else clause.
+
+
+ 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 are evaluated like other on/off options, so the valid values
+ are any unambiguous case insensitive matches for one of:
+ true, false, 1,
+ 0, on, off,
+ yes, no. So
+ t, T, and tR
+ will all match true.
+
+
+ Lines within false branches are not evaluated in any way: queries are
+ not sent to the server, non-conditional commands are not evaluated but
+ bluntly ignored, nested if-expressions in such branches are also not
+ evaluated but are tallied to check for proper nesting.
+
+
+
\ir or \include_relative filename
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index c53733f..7a418c6 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -61,8 +61,16 @@ uninstall:
clean distclean:
rm -f psql$(X) $(OBJS) lex.backup
+ rm -rf tmp_check
# files removed here are supposed to be in the distribution tarball,
# so do not clean them in the clean/distclean rules
maintainer-clean: distclean
rm -f sql_help.h sql_help.c psqlscanslash.c
+
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index f17f610..f10d7ac 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 && pset.active_branch)
{
/* eat any remaining arguments after a valid command */
/* note we suppress evaluation of backticks here */
@@ -194,6 +195,30 @@ 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)
+{
+ char *value = psql_scan_slash_option(scan_state,
+ OT_NORMAL, NULL, false);
+ bool success = ParseVariableBool(value, action, result);
+ free(value);
+ return success;
+}
+
+/*
+ * Return true if the command given is a branching command.
+ */
+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 +232,17 @@ exec_command(const char *cmd,
* failed */
backslashResult status = PSQL_CMD_SKIP_LINE;
+ if (!pset.active_branch && !is_branching_command(cmd))
+ {
+ if (pset.cur_cmd_interactive)
+ psql_error("inside inactive branch, command ignored.\n");
+
+ /* 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.
@@ -1006,6 +1042,259 @@ exec_command(const char *cmd,
}
}
+ /*
+ * \if is the beginning of an \if..\endif block.
+ * must be a valid boolean expression, or the whole block will be
+ * ignored.
+ * If this \if is itself a part of a branch that is false/ignored, it too
+ * will automatically be ignored.
+ */
+ else if (strcmp(cmd, "if") == 0)
+ {
+ ifState new_if_state = IFSTATE_IGNORED;
+ /*
+ * only evaluate the expression for truth if the underlying
+ * branch is active
+ */
+ if (pset.active_branch)
+ {
+ bool if_true = false;
+ success = read_boolean_expression(scan_state, "\\if ", &if_true);
+ if (success)
+ {
+ if (if_true)
+ {
+ new_if_state = IFSTATE_TRUE;
+ if (pset.cur_cmd_interactive)
+ psql_error("new \\if is true, executing commands\n");
+ pset.active_branch = true;
+ }
+ else
+ {
+ new_if_state = IFSTATE_FALSE;
+ if (pset.cur_cmd_interactive)
+ psql_error("new \\if is false, ignoring commands "
+ "until next \\elif, \\else, or \\endif\n");
+ pset.active_branch = false;
+ }
+ }
+ else
+ {
+ /*
+ * show this error in both interactive and script, because the
+ * session is now in a state where all blocks will be ignored
+ * regardless of expression truth. suppress this error in
+ * cases where the script is already going to terminate.
+ */
+ if (pset.on_error_stop)
+ psql_error("new \\if is invalid.\n");
+ else
+ psql_error("new \\if is invalid, "
+ "ignoring commands until next \\endif\n");
+ pset.active_branch = false;
+ }
+ }
+ else
+ {
+ if (pset.cur_cmd_interactive)
+ psql_error("new \\if is inside ignored block, "
+ "ignoring commands until next \\endif\n");
+ pset.active_branch = false;
+ }
+
+ psqlscan_branch_push(scan_state,new_if_state);
+ psql_scan_reset(scan_state);
+ }
+
+ /*
+ * \elif is part of an \if..\endif block
+ * will only be evalated for boolean truth if no previous
+ * \if or \endif in the block has evaluated to true and the \if..\endif
+ * block is not itself being ignored.
+ * in the event that does not conform to a proper boolean expression,
+ * all following statements in the block will be ignored until \endif is
+ * encountered.
+ *
+ */
+ else if (strcmp(cmd, "elif") == 0)
+ {
+ bool elif_true = false;
+ switch (psqlscan_branch_get_state(scan_state))
+ {
+ case IFSTATE_IGNORED:
+ /*
+ * inactive branch, do nothing:
+ * either if-endif already had a true block,
+ * or whole parent block is false.
+ */
+ if (pset.cur_cmd_interactive)
+ psql_error("\\elif is inside ignored block, "
+ "ignoring commands until next \\endif\n");
+ pset.active_branch = false;
+ break;
+ case IFSTATE_TRUE:
+ /*
+ * just finished true section of active branch
+ * do not evaluate expression, just skip
+ */
+ if (pset.cur_cmd_interactive)
+ psql_error("\\elif is after true block, "
+ "ignoring commands until next \\endif\n");
+ psqlscan_branch_set_state(scan_state, IFSTATE_IGNORED);
+ pset.active_branch = false;
+ break;
+ case IFSTATE_FALSE:
+ /*
+ * have not yet found a true block in this if-endif,
+ * determine if this section is true or not.
+ * variable expansion must be temporarily turned back
+ * on to read the boolean expression.
+ */
+ pset.active_branch = true;
+ success = read_boolean_expression(scan_state, "\\elif ",
+ &elif_true);
+ if (success)
+ {
+ if (elif_true)
+ {
+ psqlscan_branch_set_state(scan_state, IFSTATE_TRUE);
+ if (pset.cur_cmd_interactive)
+ psql_error("\\elif is true, executing commands\n");
+ pset.active_branch = false;
+ }
+ else
+ {
+ if (pset.cur_cmd_interactive)
+ psql_error("\\elif is false, ignoring commands "
+ "until next \\elif, \\else, or \\endif\n");
+ pset.active_branch = false;
+ }
+ }
+ else
+ {
+ /*
+ * show this error in both interactive and script, because the
+ * session is now in a state where all blocks will be ignored
+ * regardless of expression truth. suppress this error in
+ * cases where the script is already going to terminate.
+ */
+ if (pset.on_error_stop)
+ psql_error("\\elif is invalid.\n");
+ else
+ psql_error("\\elif is invalid, "
+ "ignoring commands until next \\endif\n");
+ psqlscan_branch_set_state(scan_state, IFSTATE_IGNORED);
+ pset.active_branch = false;
+ }
+ pset.active_branch = psqlscan_branch_active(scan_state);
+ break;
+ case IFSTATE_ELSE_TRUE:
+ case IFSTATE_ELSE_FALSE:
+ psql_error("encountered \\elif after \\else\n");
+ success = false;
+ pset.active_branch = false;
+ break;
+ case IFSTATE_NONE:
+ /* no if to elif from */
+ psql_error("encountered un-matched \\elif\n");
+ success = false;
+ break;
+ default:
+ break;
+ }
+ psql_scan_reset(scan_state);
+ }
+
+ /*
+ * \else is part of an \if..\endif block
+ * the statements within an \else branch will only be executed if
+ * all previous \if and \endif expressions evaluated to false
+ * and the block was not itself being ignored.
+ */
+ else if (strcmp(cmd, "else") == 0)
+ {
+ switch (psqlscan_branch_get_state(scan_state))
+ {
+ case IFSTATE_TRUE:
+ case IFSTATE_IGNORED:
+ /*
+ * either just finished true section of an active branch,
+ * or whole branch was inactive. either way, be on the
+ * lookout for any invalid \endif or \else commands
+ */
+ if (pset.cur_cmd_interactive)
+ psql_error("\\else after true condition or in ignored block, "
+ "ignoring commands until next \\endif\n");
+ pset.active_branch = false;
+ psqlscan_branch_set_state(scan_state, IFSTATE_ELSE_FALSE);
+ break;
+ case IFSTATE_FALSE:
+ /* just finished false section of an active branch */
+ if (pset.cur_cmd_interactive)
+ psql_error("\\else after all previous conditions false, "
+ "executing commands\n");
+ psqlscan_branch_set_state(scan_state, IFSTATE_ELSE_TRUE);
+ pset.active_branch = true;
+ break;
+ case IFSTATE_NONE:
+ /* no if to else from */
+ psql_error("encountered un-matched \\else\n");
+ success = false;
+ break;
+ case IFSTATE_ELSE_TRUE:
+ case IFSTATE_ELSE_FALSE:
+ psql_error("encountered \\else after \\else\n");
+ success = false;
+ pset.active_branch = false;
+ break;
+ default:
+ break;
+ }
+ psql_scan_reset(scan_state);
+ }
+
+ /*
+ * \endif - closing statment of an \if...\endif block
+ */
+ else if (strcmp(cmd, "endif") == 0)
+ {
+ /*
+ * get rid of this ifstate element and look at the previous
+ * one, if any
+ */
+ if (psqlscan_branch_pop(scan_state))
+ {
+ bool was_active = pset.active_branch;
+ pset.active_branch = psqlscan_branch_active(scan_state);
+
+ if (pset.cur_cmd_interactive)
+ {
+ if (psqlscan_branch_stack_empty(scan_state))
+ /* no more branches */
+ psql_error("exited \\if, executing commands\n");
+ else if (!pset.active_branch)
+ /* was ignoring, still ignoring */
+ psql_error("exited \\if to false parent branch, "
+ "ignoring commands until next \\endif\n");
+ else if (was_active)
+ /* was true, still true */
+ psql_error("exited \\if to true parent branch, "
+ "continuing executing commands\n");
+ else
+ /* was false, is true again */
+ psql_error("exited \\if to true parent branch, "
+ "resuming executing commands\n");
+ }
+ }
+ else
+ {
+ psql_error("encountered un-matched \\endif\n");
+ success = false;
+ }
+
+ 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/common.c b/src/bin/psql/common.c
index 6e3acdc..9f1a072 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -119,6 +119,11 @@ setQFout(const char *fname)
* If "escape" is true, return the value suitably quoted and escaped,
* as an identifier or string literal depending on "as_ident".
* (Failure in escaping should lead to returning NULL.)
+ *
+ * Variables are not expanded if the current branch is inactive
+ * (part of an \if..\endif section which is false). \elif branches
+ * will need temporarily mark the branch active in order to
+ * properly evaluate conditionals.
*/
char *
psql_get_variable(const char *varname, bool escape, bool as_ident)
@@ -126,6 +131,10 @@ psql_get_variable(const char *varname, bool escape, bool as_ident)
char *result;
const char *value;
+ /* do not expand variables if the branch is inactive */
+ if (!pset.active_branch)
+ return NULL;
+
value = GetVariable(pset.vars, varname);
if (!value)
return NULL;
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 3e3cab4..9f9e1a6 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -210,6 +210,13 @@ slashUsage(unsigned short int pager)
fprintf(output, _(" \\qecho [STRING] write string to query output stream (see \\o)\n"));
fprintf(output, "\n");
+ fprintf(output, _("Conditionals\n"));
+ fprintf(output, _(" \\if begin a conditional block\n"));
+ fprintf(output, _(" \\elif else if in the current conditional block\n"));
+ fprintf(output, _(" \\else else in the current conditional block\n"));
+ fprintf(output, _(" \\endif end current conditional block\n"));
+ fprintf(output, "\n");
+
fprintf(output, _("Informational\n"));
fprintf(output, _(" (options: S = show system objects, + = additional detail)\n"));
fprintf(output, _(" \\d[S+] list tables, views, and sequences\n"));
diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c
index 6e358e2..b599581 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,6 +23,23 @@ const PsqlScanCallbacks psqlscan_callbacks = {
psql_error
};
+/*
+ * execute query if branch is active.
+ * warn interactive users about ignored queries.
+ */
+static
+bool send_query(const char *query)
+{
+ /* execute query if branch is active */
+ if (pset.active_branch)
+ return SendQuery(query);
+
+ if (pset.cur_cmd_interactive)
+ psql_error("inside inactive branch, query ignored. "
+ "use \\endif to exit current branch.\n");
+
+ return true;
+}
/*
* Main processing loop for reading lines of input
@@ -122,7 +139,35 @@ MainLoop(FILE *source)
cancel_pressed = false;
if (pset.cur_cmd_interactive)
+ {
putc('\n', stdout);
+ /*
+ * if interactive user is in a branch, then Ctrl-C will exit
+ * from the inner-most branch
+ */
+ if (!psqlscan_branch_stack_empty(scan_state))
+ {
+ bool was_active = pset.active_branch;
+ psqlscan_branch_pop(scan_state);
+ pset.active_branch = psqlscan_branch_active(scan_state);
+
+ if (psqlscan_branch_stack_empty(scan_state))
+ /* no more branches */
+ psql_error("escaped \\if, executing commands\n");
+ else if (!pset.active_branch)
+ /* was ignoring, still ignoring */
+ psql_error("escaped \\if to false parent branch, "
+ "ignoring commands until next \\endif\n");
+ else if (was_active)
+ /* was true, still true */
+ psql_error("escaped \\if to true parent branch, "
+ "continuing executing commands\n");
+ else
+ /* was false, is true again */
+ psql_error("escaped \\if to true parent branch, "
+ "resuming executing commands\n");
+ }
+ }
else
{
successResult = EXIT_USER;
@@ -296,8 +341,8 @@ MainLoop(FILE *source)
line_saved_in_history = true;
}
- /* execute query */
- success = SendQuery(query_buf->data);
+ success = send_query(query_buf->data);
+
slashCmdStatus = success ? PSQL_CMD_SEND : PSQL_CMD_ERROR;
pset.stmt_lineno = 1;
@@ -358,7 +403,7 @@ MainLoop(FILE *source)
if (slashCmdStatus == PSQL_CMD_SEND)
{
- success = SendQuery(query_buf->data);
+ success = send_query(query_buf->data);
/* transfer query to previous_buf by pointer-swapping */
{
@@ -367,6 +412,7 @@ MainLoop(FILE *source)
previous_buf = query_buf;
query_buf = swap_buf;
}
+
resetPQExpBuffer(query_buf);
/* flush any paren nesting info after forced send */
@@ -429,8 +475,7 @@ MainLoop(FILE *source)
if (pset.cur_cmd_interactive)
pg_send_history(history_buf);
- /* execute query */
- success = SendQuery(query_buf->data);
+ success = send_query(query_buf->data);
if (!success && die_on_error)
successResult = EXIT_USER;
@@ -451,6 +496,19 @@ MainLoop(FILE *source)
destroyPQExpBuffer(previous_buf);
destroyPQExpBuffer(history_buf);
+ /*
+ * check for unbalanced \if-\endifs unless user explicitly quit,
+ * or the script is erroring out
+ */
+ if (slashCmdStatus != PSQL_CMD_TERMINATE
+ && successResult != EXIT_USER
+ && !psqlscan_branch_stack_empty(scan_state))
+ {
+ psql_error("found EOF before closing \\endif(s)\n");
+ if (die_on_error)
+ successResult = EXIT_USER;
+ }
+
psql_scan_destroy(scan_state);
pset.cur_cmd_source = prev_cmd_source;
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 195f5a1..c8aeab6 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -114,6 +114,9 @@ typedef struct _psqlSettings
VariableSpace vars; /* "shell variable" repository */
+ bool active_branch; /* true if session is not in an \if branch
+ * or the current branch is true */
+
/*
* The remaining fields are set by assign hooks associated with entries in
* "vars". They should not be set directly except by those hook
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 88d686a..5c8760a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -128,6 +128,8 @@ main(int argc, char *argv[])
setvbuf(stderr, NULL, _IONBF, 0);
#endif
+ pset.active_branch = true;
+
pset.progname = get_progname(argv[0]);
pset.db = NULL;
diff --git a/src/bin/psql/t/001_if.pl b/src/bin/psql/t/001_if.pl
new file mode 100644
index 0000000..88f3013
--- /dev/null
+++ b/src/bin/psql/t/001_if.pl
@@ -0,0 +1,41 @@
+use strict;
+use warnings;
+
+use Config;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 21;
+
+#
+# test invalid \if respects ON_ERROR_STOP
+#
+my $node = get_new_node('master');
+$node->init;
+$node->start;
+
+my $tests = [
+ # syntax errors
+ [ "\\if invalid\n\\echo NO\n\\endif\n\\echo NO\n", '', 'boolean expected', 'syntax error' ],
+ [ "\\if false\n\\elif invalid\n\\echo NO\n\\endif\n\\echo NO\n", '', 'boolean expected', 'syntax error' ],
+ # unmatched checks
+ [ "\\if true\n", '', 'found EOF before closing.*endif', 'unmatched \if' ],
+ [ "\\elif true\n\\echo NO\n", '', 'encountered un-matched.*elif', 'unmatched \elif' ],
+ [ "\\else\n\\echo NO\n", '', 'encountered un-matched.*else', 'unmatched \else' ],
+ [ "\\endif\n\\echo NO\n", '', 'encountered un-matched.*endif', 'unmatched \endif' ],
+ # error stop messages
+ [ "\\set ON_ERROR_STOP on\n\\if error\n\\echo NO\n\\endif\n\\echo NO\n", '', 'new.*if is invalid.', 'invalid if'],
+];
+
+# 3 checks per tests
+for my $test (@$tests) {
+ my ($script, $stdout_expect, $stderr_re, $name) = @$test;
+ my ($stdout, $stderr);
+ my $retcode = $node->psql('postgres', $script,
+ stdout => \$stdout, stderr => \$stderr,
+ on_error_stop => 1);
+ is($retcode,'3',"$name test ON_ERROR_STOP");
+ is($stdout, $stdout_expect, "$name test STDOUT");
+ like($stderr, qr/$stderr_re/, "$name test STDERR");
+}
+
+$node->teardown_node;
diff --git a/src/fe_utils/psqlscan.l b/src/fe_utils/psqlscan.l
index 1b29341..998630a 100644
--- a/src/fe_utils/psqlscan.l
+++ b/src/fe_utils/psqlscan.l
@@ -904,6 +904,8 @@ psql_scan_create(const PsqlScanCallbacks *callbacks)
psql_scan_reset(state);
+ state->branch_stack = NULL;
+
return state;
}
@@ -919,6 +921,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 +1435,86 @@ psqlscan_escape_variable(PsqlScanState state, const char *txt, int len,
psqlscan_emit(state, txt, len);
}
}
+
+/*
+ * psqlscan_branch_stack_empty
+ *
+ * True if there are no active \if-structures
+ */
+bool
+psqlscan_branch_stack_empty(PsqlScanState state)
+{
+ return state->branch_stack == NULL;
+}
+
+/*
+ * Fetch the current state of the top of the stack
+ */
+ifState
+psqlscan_branch_get_state(PsqlScanState state)
+{
+ if (psqlscan_branch_stack_empty(state))
+ return IFSTATE_NONE;
+ return state->branch_stack->if_state;
+}
+
+/*
+ * 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)
+{
+ ifState s = psqlscan_branch_get_state(state);
+ return 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;
+ 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_stack_empty(state))
+ return false;
+ state->branch_stack->if_state = new_state;
+ return true;
+}
+
+/*
+ * psqlscan_branch_pop
+ *
+ * Destroy the topmost branch because and \endif was encountered.
+ * Returns false if there was no branch to end.
+ */
+bool
+psqlscan_branch_pop(PsqlScanState state)
+{
+ IfStackElem *p = state->branch_stack;
+ if (!p)
+ return false;
+ state->branch_stack = state->branch_stack->next;
+ free(p);
+ return true;
+}
diff --git a/src/include/fe_utils/psqlscan_int.h b/src/include/fe_utils/psqlscan_int.h
index 0fddc7a..321d455 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,11 @@ typedef struct PsqlScanStateData
* Callback functions provided by the program making use of the lexer.
*/
const PsqlScanCallbacks *callbacks;
+
+ /*
+ * \if branch state variables
+ */
+ IfStackElem *branch_stack;
} PsqlScanStateData;
@@ -141,4 +170,21 @@ extern void psqlscan_escape_variable(PsqlScanState state,
const char *txt, int len,
bool as_ident);
+/*
+ * branching commands
+ */
+extern bool psqlscan_branch_stack_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_pop(PsqlScanState state);
+
#endif /* PSQLSCAN_INT_H */
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 026a4f0..d4f1849 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -2712,6 +2712,96 @@ deallocate q;
\pset format aligned
\pset expanded off
\pset border 1
+-- test a large nested if using a variety of true-equivalents
+\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
+-- test a variety of false-equivalents in an if/elif/else structure
+\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
+-- test simple true-then-else
+\if true
+ \echo 'first thing true'
+first thing true
+\else
+ \echo 'should not print #3-1'
+\endif
+-- test simple false-true-else
+\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
+-- test invalidation of whole if-endif when invalid boolean is given
+\if invalid_boolean_expression
+unrecognized value "invalid_boolean_expression" for "\if ": boolean expected
+new \if is invalid, ignoring commands until next \endif
+ \echo 'should not print #6-1'
+\else
+ \echo 'should not print because whole if-then is wrecked #6-2'
+\endif
+-- test non-evaluation of variables in false blocks
+\set var 'ab''cd'
+-- select :var;
+\if false
+ select :var ;
+\else
+ select 1;
+ ?column?
+----------
+ 1
+(1 row)
+
+\endif
+-- test un-matched endif
+\endif
+encountered un-matched \endif
+-- test un-matched else
+\else
+encountered un-matched \else
+-- test un-matched elif
+\elif
+encountered un-matched \elif
+-- test double-else error
+\if true
+\else
+\else
+encountered \else after \else
+\endif
+-- test elif out-of-order
+\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 d823d11..ba41d9e 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -375,6 +375,91 @@ deallocate q;
\pset expanded off
\pset border 1
+-- test a large nested if using a variety of true-equivalents
+\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
+
+-- test a variety of false-equivalents in an if/elif/else structure
+\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
+
+-- test simple true-then-else
+\if true
+ \echo 'first thing true'
+\else
+ \echo 'should not print #3-1'
+\endif
+
+-- test simple false-true-else
+\if false
+ \echo 'should not print #4-1'
+\elif true
+ \echo 'second thing true'
+\else
+ \echo 'should not print #5-1'
+\endif
+
+-- test invalidation of whole if-endif when invalid boolean is given
+\if invalid_boolean_expression
+ \echo 'should not print #6-1'
+\else
+ \echo 'should not print because whole if-then is wrecked #6-2'
+\endif
+
+-- test non-evaluation of variables in false blocks
+\set var 'ab''cd'
+-- select :var;
+\if false
+ select :var ;
+\else
+ select 1;
+\endif
+
+-- test un-matched endif
+\endif
+
+-- test un-matched else
+\else
+
+-- test un-matched elif
+\elif
+
+-- test double-else error
+\if true
+\else
+\else
+\endif
+
+-- test elif out-of-order
+\if false
+\else
+\elif
+\endif
+
-- SHOW_CONTEXT
\set SHOW_CONTEXT never