diff --git a/builds/win32/msvc15/isql_static.vcxproj b/builds/win32/msvc15/isql_static.vcxproj
index 960e883c64..2fe7388b96 100644
--- a/builds/win32/msvc15/isql_static.vcxproj
+++ b/builds/win32/msvc15/isql_static.vcxproj
@@ -23,6 +23,7 @@
+
@@ -38,6 +39,7 @@
+
diff --git a/builds/win32/msvc15/isql_static.vcxproj.filters b/builds/win32/msvc15/isql_static.vcxproj.filters
index 6377230369..5605a4551f 100644
--- a/builds/win32/msvc15/isql_static.vcxproj.filters
+++ b/builds/win32/msvc15/isql_static.vcxproj.filters
@@ -27,6 +27,9 @@
ISQL files
+
+ ISQL files
+
ISQL files
@@ -67,6 +70,9 @@
Header files
+
+ Header files
+
Header files
diff --git a/builds/win32/msvc15/isql_test.vcxproj b/builds/win32/msvc15/isql_test.vcxproj
index 222c1963eb..dabd1312a7 100644
--- a/builds/win32/msvc15/isql_test.vcxproj
+++ b/builds/win32/msvc15/isql_test.vcxproj
@@ -176,6 +176,7 @@
+
diff --git a/builds/win32/msvc15/isql_test.vcxproj.filters b/builds/win32/msvc15/isql_test.vcxproj.filters
index 5ebbc36eba..331157ba2b 100644
--- a/builds/win32/msvc15/isql_test.vcxproj.filters
+++ b/builds/win32/msvc15/isql_test.vcxproj.filters
@@ -7,6 +7,9 @@
+
+ source
+
source
diff --git a/doc/README.isql_enhancements.txt b/doc/README.isql_enhancements.txt
index 33d70a8720..181f43f5ca 100644
--- a/doc/README.isql_enhancements.txt
+++ b/doc/README.isql_enhancements.txt
@@ -330,7 +330,7 @@ SQL> SET PER_TAB OFF;
Isql enhancements in Firebird v6.
---------------------------------
-EXPLAIN statement.
+12) EXPLAIN statement.
Author: Adriano dos Santos Fernandes
@@ -353,3 +353,69 @@ CON> select id from employees where id = ? into id;
CON> end!
SQL>
SQL> set term ;!
+
+
+13) SET AUTOTERM ON/OFF
+
+Author: Adriano dos Santos Fernandes
+
+When set to ON, terminator defined with SET TERM is changed to semicolon and a new logic
+for TERM detection is used, where engine helps ISQL to detect valid usage of semicolons
+inside statements.
+
+At each semicolon (outside quotes or comments), ISQL prepares the query buffer with
+engine using flag IStatement::PREPARE_REQUIRE_SEMICOLON.
+
+If engine prepares the statement correctly, it's run and ISQL is put in new statement
+mode.
+
+If engine returns error isc_command_end_err2, then ISQL is put in statement
+continuation mode and asks for another line, repeating the process.
+
+If engine returns a different error, the error is shown and ISQL is put in new statement
+mode.
+
+Notes:
+- This option can also be activated with command line parameter -autot(erm)
+- It can only be used with Firebird engine/server v6 or later
+- SET TERM command automatically sets AUTOTERM to OFF
+- SET AUTOTERM ON command automatically sets TERM to semicolon
+- While AUTOTERM ON can be used in non-interactive scripts, at each semicolon,
+ statement may be tried to be compiled using the server/engine.
+ That may be slow for big scripts with PSQL statements spanning many lines.
+
+Examples:
+
+SQL> SET AUTOTERM ON;
+
+SQL> execute block returns (o1 integer)
+CON> as
+CON> begin
+CON> o1 = 1;
+CON> suspend;
+CON> end;
+
+ O1
+============
+ 1
+
+SQL> select 1 from rdb$database;
+
+ CONSTANT
+============
+ 1
+
+SQL> select 1
+CON> from rdb$database;
+
+ CONSTANT
+============
+ 1
+
+SQL> select 1
+CON> from rdb$database
+CON> where true;
+
+ CONSTANT
+============
+ 1
diff --git a/src/dsql/Parser.cpp b/src/dsql/Parser.cpp
index ba089ad88e..2a29abd2ba 100644
--- a/src/dsql/Parser.cpp
+++ b/src/dsql/Parser.cpp
@@ -41,12 +41,14 @@ using namespace Jrd;
Parser::Parser(thread_db* tdbb, MemoryPool& pool, MemoryPool* aStatementPool, DsqlCompilerScratch* aScratch,
- USHORT aClientDialect, USHORT aDbDialect, const TEXT* string, size_t length, SSHORT charSetId)
+ USHORT aClientDialect, USHORT aDbDialect, bool aRequireSemicolon,
+ const TEXT* string, size_t length, SSHORT charSetId)
: PermanentStorage(pool),
statementPool(aStatementPool),
scratch(aScratch),
client_dialect(aClientDialect),
db_dialect(aDbDialect),
+ requireSemicolon(aRequireSemicolon),
transformedString(pool),
strMarks(pool),
stmt_ambiguous(false)
diff --git a/src/dsql/Parser.h b/src/dsql/Parser.h
index 77e0b2ac04..4607f50632 100644
--- a/src/dsql/Parser.h
+++ b/src/dsql/Parser.h
@@ -132,7 +132,8 @@ public:
public:
Parser(thread_db* tdbb, MemoryPool& pool, MemoryPool* aStatementPool, DsqlCompilerScratch* aScratch,
- USHORT aClientDialect, USHORT aDbDialect, const TEXT* string, size_t length, SSHORT charSetId);
+ USHORT aClientDialect, USHORT aDbDialect, bool aRequireSemicolon,
+ const TEXT* string, size_t length, SSHORT charSetId);
~Parser();
public:
@@ -363,6 +364,7 @@ private:
DsqlCompilerScratch* scratch;
USHORT client_dialect;
USHORT db_dialect;
+ const bool requireSemicolon;
USHORT parser_version;
CharSet* charSet;
diff --git a/src/dsql/dsql.cpp b/src/dsql/dsql.cpp
index 11717dfd98..18ad503847 100644
--- a/src/dsql/dsql.cpp
+++ b/src/dsql/dsql.cpp
@@ -84,9 +84,9 @@ using namespace Firebird;
static ULONG get_request_info(thread_db*, DsqlRequest*, ULONG, UCHAR*);
static dsql_dbb* init(Jrd::thread_db*, Jrd::Attachment*);
-static DsqlRequest* prepareRequest(thread_db*, dsql_dbb*, jrd_tra*, ULONG, const TEXT*, USHORT, bool);
+static DsqlRequest* prepareRequest(thread_db*, dsql_dbb*, jrd_tra*, ULONG, const TEXT*, USHORT, unsigned, bool);
static RefPtr prepareStatement(thread_db*, dsql_dbb*, jrd_tra*, ULONG, const TEXT*, USHORT,
- bool, ntrace_result_t* traceResult);
+ unsigned, bool, ntrace_result_t* traceResult);
static UCHAR* put_item(UCHAR, const USHORT, const UCHAR*, UCHAR*, const UCHAR* const);
static void sql_info(thread_db*, DsqlRequest*, ULONG, const UCHAR*, ULONG, UCHAR*);
static UCHAR* var_info(const dsql_msg*, const UCHAR*, const UCHAR* const, UCHAR*,
@@ -261,7 +261,7 @@ DsqlRequest* DSQL_prepare(thread_db* tdbb,
// Allocate a new request block and then prepare the request.
dsqlRequest = prepareRequest(tdbb, database, transaction, length, string, dialect,
- isInternalRequest);
+ prepareFlags, isInternalRequest);
// Can not prepare a CREATE DATABASE/SCHEMA statement
@@ -336,7 +336,7 @@ void DSQL_execute_immediate(thread_db* tdbb, Jrd::Attachment* attachment, jrd_tr
try
{
dsqlRequest = prepareRequest(tdbb, database, *tra_handle, length, string, dialect,
- isInternalRequest);
+ 0, isInternalRequest);
const auto dsqlStatement = dsqlRequest->getDsqlStatement();
@@ -443,7 +443,7 @@ static dsql_dbb* init(thread_db* tdbb, Jrd::Attachment* attachment)
// Prepare a request for execution.
// Note: caller is responsible for pool handling.
static DsqlRequest* prepareRequest(thread_db* tdbb, dsql_dbb* database, jrd_tra* transaction,
- ULONG textLength, const TEXT* text, USHORT clientDialect, bool isInternalRequest)
+ ULONG textLength, const TEXT* text, USHORT clientDialect, unsigned prepareFlags, bool isInternalRequest)
{
TraceDSQLPrepare trace(database->dbb_attachment, transaction, textLength, text, isInternalRequest);
@@ -451,7 +451,7 @@ static DsqlRequest* prepareRequest(thread_db* tdbb, dsql_dbb* database, jrd_tra*
try
{
auto statement = prepareStatement(tdbb, database, transaction, textLength, text,
- clientDialect, isInternalRequest, &traceResult);
+ clientDialect, prepareFlags, isInternalRequest, &traceResult);
auto dsqlRequest = statement->createRequest(tdbb, database);
@@ -472,7 +472,8 @@ static DsqlRequest* prepareRequest(thread_db* tdbb, dsql_dbb* database, jrd_tra*
// Prepare a statement for execution.
// Note: caller is responsible for pool handling.
static RefPtr prepareStatement(thread_db* tdbb, dsql_dbb* database, jrd_tra* transaction,
- ULONG textLength, const TEXT* text, USHORT clientDialect, bool isInternalRequest, ntrace_result_t* traceResult)
+ ULONG textLength, const TEXT* text, USHORT clientDialect, unsigned prepareFlags, bool isInternalRequest,
+ ntrace_result_t* traceResult)
{
Database* const dbb = tdbb->getDatabase();
@@ -493,15 +494,18 @@ static RefPtr prepareStatement(thread_db* tdbb, dsql_dbb* databas
Arg::Gds(isc_command_end_err2) << Arg::Num(1) << Arg::Num(1));
}
- // Get rid of the trailing ";" if there is one.
-
- for (const TEXT* p = text + textLength; p-- > text;)
+ if (!(prepareFlags & IStatement::PREPARE_REQUIRE_SEMICOLON))
{
- if (*p != ' ')
+ // Get rid of the trailing ";" if there is one.
+
+ for (const TEXT* p = text + textLength; p-- > text;)
{
- if (*p == ';')
- textLength = p - text;
- break;
+ if (*p != ' ')
+ {
+ if (*p == ';')
+ textLength = p - text;
+ break;
+ }
}
}
@@ -556,7 +560,9 @@ static RefPtr prepareStatement(thread_db* tdbb, dsql_dbb* databas
scratch->flags |= DsqlCompilerScratch::FLAG_INTERNAL_REQUEST;
Parser parser(tdbb, *scratchPool, statementPool, scratch, clientDialect,
- dbDialect, text, textLength, charSetId);
+ dbDialect,
+ (prepareFlags & IStatement::PREPARE_REQUIRE_SEMICOLON),
+ text, textLength, charSetId);
// Parse the SQL statement. If it croaks, return
dsqlStatement = parser.parse();
diff --git a/src/dsql/parse.y b/src/dsql/parse.y
index f0e18947a0..aaf8c05b1b 100644
--- a/src/dsql/parse.y
+++ b/src/dsql/parse.y
@@ -868,8 +868,15 @@ using namespace Firebird;
// list of possible statements
top
- : statement { parsedStatement = $1; }
- | statement ';' { parsedStatement = $1; }
+ : statement
+ {
+ if (requireSemicolon)
+ yyerrorIncompleteCmd(YYPOSNARG(1));
+
+ parsedStatement = $1;
+ }
+ | statement ';'
+ { parsedStatement = $1; }
;
%type statement
diff --git a/src/include/firebird/FirebirdInterface.idl b/src/include/firebird/FirebirdInterface.idl
index eabac62a9c..344fd24b0c 100644
--- a/src/include/firebird/FirebirdInterface.idl
+++ b/src/include/firebird/FirebirdInterface.idl
@@ -474,6 +474,7 @@ interface Statement : ReferenceCounted
const uint PREPARE_PREFETCH_DETAILED_PLAN = 0x10;
const uint PREPARE_PREFETCH_AFFECTED_RECORDS = 0x20; // not used yet
const uint PREPARE_PREFETCH_FLAGS = 0x40;
+ const uint PREPARE_REQUIRE_SEMICOLON = 0x80;
const uint PREPARE_PREFETCH_METADATA =
PREPARE_PREFETCH_TYPE | PREPARE_PREFETCH_FLAGS |
PREPARE_PREFETCH_INPUT_PARAMETERS | PREPARE_PREFETCH_OUTPUT_PARAMETERS;
diff --git a/src/include/firebird/IdlFbInterfaces.h b/src/include/firebird/IdlFbInterfaces.h
index e52e2a133b..83f9f31a57 100644
--- a/src/include/firebird/IdlFbInterfaces.h
+++ b/src/include/firebird/IdlFbInterfaces.h
@@ -1914,6 +1914,7 @@ namespace Firebird
static CLOOP_CONSTEXPR unsigned PREPARE_PREFETCH_DETAILED_PLAN = 0x10;
static CLOOP_CONSTEXPR unsigned PREPARE_PREFETCH_AFFECTED_RECORDS = 0x20;
static CLOOP_CONSTEXPR unsigned PREPARE_PREFETCH_FLAGS = 0x40;
+ static CLOOP_CONSTEXPR unsigned PREPARE_REQUIRE_SEMICOLON = 0x80;
static CLOOP_CONSTEXPR unsigned PREPARE_PREFETCH_METADATA = IStatement::PREPARE_PREFETCH_TYPE | IStatement::PREPARE_PREFETCH_FLAGS | IStatement::PREPARE_PREFETCH_INPUT_PARAMETERS | IStatement::PREPARE_PREFETCH_OUTPUT_PARAMETERS;
static CLOOP_CONSTEXPR unsigned PREPARE_PREFETCH_ALL = IStatement::PREPARE_PREFETCH_METADATA | IStatement::PREPARE_PREFETCH_LEGACY_PLAN | IStatement::PREPARE_PREFETCH_DETAILED_PLAN | IStatement::PREPARE_PREFETCH_AFFECTED_RECORDS;
static CLOOP_CONSTEXPR unsigned FLAG_HAS_CURSOR = 0x1;
diff --git a/src/include/firebird/impl/msg/isql.h b/src/include/firebird/impl/msg/isql.h
index 9dda2f1003..e6d73afb25 100644
--- a/src/include/firebird/impl/msg/isql.h
+++ b/src/include/firebird/impl/msg/isql.h
@@ -202,3 +202,5 @@ FB_IMPL_MSG_SYMBOL(ISQL, 202, NO_PUBLICATIONS, "There is no publications in this
FB_IMPL_MSG_SYMBOL(ISQL, 203, MSG_PUBLICATIONS, "Publications:")
FB_IMPL_MSG_SYMBOL(ISQL, 204, MSG_PROCEDURES, "Procedures:")
FB_IMPL_MSG_SYMBOL(ISQL, 205, HLP_EXPLAIN, "EXPLAIN -- explain a query access plan")
+FB_IMPL_MSG_SYMBOL(ISQL, 206, USAGE_AUTOTERM, " -autot(erm) use auto statement terminator (set autoterm on)")
+FB_IMPL_MSG_SYMBOL(ISQL, 207, AUTOTERM_NOT_SUPPORTED, "SET AUTOTERM ON is not supported in engine/server and has been disabled")
diff --git a/src/include/gen/Firebird.pas b/src/include/gen/Firebird.pas
index d755096694..1c39bd9124 100644
--- a/src/include/gen/Firebird.pas
+++ b/src/include/gen/Firebird.pas
@@ -1533,6 +1533,7 @@ type
const PREPARE_PREFETCH_DETAILED_PLAN = Cardinal($10);
const PREPARE_PREFETCH_AFFECTED_RECORDS = Cardinal($20);
const PREPARE_PREFETCH_FLAGS = Cardinal($40);
+ const PREPARE_REQUIRE_SEMICOLON = Cardinal($80);
const PREPARE_PREFETCH_METADATA = Cardinal(IStatement.PREPARE_PREFETCH_TYPE or IStatement.PREPARE_PREFETCH_FLAGS or IStatement.PREPARE_PREFETCH_INPUT_PARAMETERS or IStatement.PREPARE_PREFETCH_OUTPUT_PARAMETERS);
const PREPARE_PREFETCH_ALL = Cardinal(IStatement.PREPARE_PREFETCH_METADATA or IStatement.PREPARE_PREFETCH_LEGACY_PLAN or IStatement.PREPARE_PREFETCH_DETAILED_PLAN or IStatement.PREPARE_PREFETCH_AFFECTED_RECORDS);
const FLAG_HAS_CURSOR = Cardinal($1);
diff --git a/src/isql/FrontendLexer.cpp b/src/isql/FrontendLexer.cpp
new file mode 100644
index 0000000000..528488c53a
--- /dev/null
+++ b/src/isql/FrontendLexer.cpp
@@ -0,0 +1,386 @@
+/*
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Adriano dos Santos Fernandes
+ * for the Firebird Open Source RDBMS project.
+ *
+ * Copyright (c) 2023 Adriano dos Santos Fernandes
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ *
+ */
+
+#include "firebird.h"
+#include "../isql/FrontendLexer.h"
+#include
+#include
+
+
+static std::string trim(std::string_view str);
+
+
+static std::string trim(std::string_view str)
+{
+ auto finish = str.end();
+ auto start = str.begin();
+
+ while (start != finish && isspace(*start))
+ ++start;
+
+ --finish;
+
+ while (finish > start && isspace(*finish))
+ --finish;
+
+ return std::string(start, finish + 1);
+}
+
+
+std::string FrontendLexer::stripComments(std::string_view statement)
+{
+ FrontendLexer lexer(statement);
+ std::string processedStatement;
+
+ while (lexer.pos < lexer.end)
+ {
+ auto oldPos = lexer.pos;
+
+ lexer.skipSpacesAndComments();
+
+ if (lexer.pos > oldPos)
+ processedStatement += ' ';
+
+ oldPos = lexer.pos;
+
+ if (!lexer.getStringToken().has_value() && lexer.pos < lexer.end)
+ ++lexer.pos;
+
+ processedStatement += std::string(oldPos, lexer.pos);
+ }
+
+ return trim(processedStatement);
+}
+
+bool FrontendLexer::isBufferEmpty() const
+{
+ return trim(std::string(deletePos, end)).empty();
+}
+
+void FrontendLexer::appendBuffer(std::string_view newBuffer)
+{
+ const auto posIndex = pos - buffer.begin();
+ const auto deletePosIndex = deletePos - buffer.begin();
+ buffer.append(newBuffer);
+ pos = buffer.begin() + posIndex;
+ end = buffer.end();
+ deletePos = buffer.begin() + deletePosIndex;
+}
+
+void FrontendLexer::reset()
+{
+ buffer.clear();
+ pos = buffer.begin();
+ end = buffer.end();
+ deletePos = buffer.begin();
+}
+
+std::variant FrontendLexer::getSingleStatement(
+ std::string_view term)
+{
+ const auto posIndex = pos - deletePos;
+ buffer.erase(buffer.begin(), deletePos);
+ pos = buffer.begin() + posIndex;
+ end = buffer.end();
+ deletePos = buffer.begin();
+
+ try
+ {
+ if (pos < end)
+ {
+ skipSpacesAndComments();
+
+ const auto savePos = pos;
+
+ if (pos + 1 < end && *pos == '?')
+ {
+ if (*++pos == '\r')
+ ++pos;
+
+ if (pos < end && *pos == '\n')
+ {
+ deletePos = ++pos;
+ const auto statement = trim(std::string(buffer.cbegin(), pos));
+ return SingleStatement{statement, statement};
+ }
+ }
+
+ pos = savePos;
+ }
+
+ while (pos < end)
+ {
+ if (end - pos >= term.length() && std::equal(term.begin(), term.end(), pos))
+ {
+ const auto initialStatement = std::string(buffer.cbegin(), pos);
+ pos += term.length();
+ const auto trailingPos = pos;
+ skipSpacesAndComments();
+ deletePos = pos;
+
+ const auto statement1 = initialStatement + std::string(trailingPos, pos);
+ const auto statement2 = initialStatement + ";" + std::string(trailingPos, pos);
+
+ return SingleStatement{trim(statement1), trim(statement2)};
+ }
+
+ if (!getStringToken().has_value() && pos < end)
+ ++pos;
+
+ skipSpacesAndComments();
+ }
+ }
+ catch (const IncompleteTokenError& error)
+ {
+ return error;
+ }
+
+ return IncompleteTokenError{false};
+}
+
+FrontendLexer::Token FrontendLexer::getToken()
+{
+ skipSpacesAndComments();
+
+ Token token;
+
+ if (pos >= end)
+ {
+ token.type = Token::TYPE_EOF;
+ return token;
+ }
+
+ if (const auto optStringToken = getStringToken(); optStringToken.has_value())
+ return optStringToken.value();
+
+ const auto start = pos;
+
+ switch (toupper(*pos))
+ {
+ case '(':
+ token.type = Token::TYPE_OPEN_PAREN;
+ token.processedText = *pos++;
+ break;
+
+ case ')':
+ token.type = Token::TYPE_CLOSE_PAREN;
+ token.processedText = *pos++;
+ break;
+
+ case ',':
+ token.type = Token::TYPE_COMMA;
+ token.processedText = *pos++;
+ break;
+
+ case ';':
+ token.type = Token::TYPE_OTHER;
+ token.processedText = *pos++;
+ break;
+
+ default:
+ while (pos != end && !isspace(*pos))
+ ++pos;
+
+ token.processedText = std::string(start, pos);
+ std::transform(token.processedText.begin(), token.processedText.end(),
+ token.processedText.begin(), toupper);
+ break;
+ }
+
+ token.rawText = std::string(start, pos);
+
+ return token;
+}
+
+std::optional FrontendLexer::getStringToken()
+{
+ Token token;
+
+ if (pos >= end)
+ return std::nullopt;
+
+ const auto start = pos;
+
+ switch (toupper(*pos))
+ {
+ case '\'':
+ case '"':
+ {
+ const auto quote = *pos++;
+
+ while (pos != end)
+ {
+ if (*pos == quote)
+ {
+ if ((pos + 1) < end && *(pos + 1) == quote)
+ ++pos;
+ else
+ break;
+ }
+
+ token.processedText += *pos++;
+ }
+
+ if (pos == end)
+ {
+ pos = start;
+ throw IncompleteTokenError{false};
+ }
+ else
+ {
+ ++pos;
+ token.type = quote == '\'' ? Token::TYPE_STRING : Token::TYPE_META_STRING;
+ }
+
+ break;
+ }
+
+ case 'Q':
+ if (pos + 1 != end && pos[1] == '\'')
+ {
+ if (pos + 4 < end)
+ {
+ char endChar;
+
+ switch (pos[2])
+ {
+ case '{':
+ endChar = '}';
+ break;
+
+ case '[':
+ endChar = ']';
+ break;
+
+ case '(':
+ endChar = ')';
+ break;
+
+ case '<':
+ endChar = '>';
+ break;
+
+ default:
+ endChar = pos[2];
+ break;
+ }
+
+ pos += 3;
+
+ while (pos + 1 < end)
+ {
+ if (*pos == endChar && pos[1] == '\'')
+ {
+ pos += 2;
+ token.type = Token::TYPE_STRING;
+ break;
+ }
+
+ token.processedText += *pos++;
+ }
+ }
+
+ if (token.type != Token::TYPE_STRING)
+ {
+ pos = start;
+ throw IncompleteTokenError{false};
+ }
+
+ break;
+ }
+ [[fallthrough]];
+
+ default:
+ return std::nullopt;
+ }
+
+ token.rawText = std::string(start, pos);
+
+ return token;
+}
+
+void FrontendLexer::skipSpacesAndComments()
+{
+ while (pos != end && (isspace(*pos) || *pos == '-' || *pos == '/'))
+ {
+ while (pos != end && isspace(*pos))
+ ++pos;
+
+ if (pos == end)
+ break;
+
+ if (*pos == '-')
+ {
+ if (pos + 1 != end && pos[1] == '-')
+ {
+ pos += 2;
+
+ while (pos != end)
+ {
+ const auto c = *pos++;
+
+ if (c == '\r')
+ {
+ if (pos != end && *pos == '\n')
+ ++pos;
+ break;
+ }
+ else if (c == '\n')
+ break;
+ }
+ }
+ else
+ break;
+ }
+ else if (*pos == '/')
+ {
+ const auto start = pos;
+
+ if (pos + 1 != end && pos[1] == '*')
+ {
+ bool finished = false;
+ pos += 2;
+
+ while (pos != end)
+ {
+ const auto c = *pos++;
+
+ if (c == '*' && pos != end && *pos == '/')
+ {
+ ++pos;
+ finished = true;
+ break;
+ }
+ }
+
+ if (!finished)
+ {
+ pos = start;
+ throw IncompleteTokenError{true};
+ }
+ }
+ else
+ break;
+ }
+ }
+}
diff --git a/src/isql/FrontendLexer.h b/src/isql/FrontendLexer.h
new file mode 100644
index 0000000000..d2ac1f88f2
--- /dev/null
+++ b/src/isql/FrontendLexer.h
@@ -0,0 +1,113 @@
+/*
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Adriano dos Santos Fernandes
+ * for the Firebird Open Source RDBMS project.
+ *
+ * Copyright (c) 2023 Adriano dos Santos Fernandes
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ *
+ */
+
+#ifndef FB_ISQL_FRONTEND_LEXER_H
+#define FB_ISQL_FRONTEND_LEXER_H
+
+#include
+#include
+#include
+#include
+
+class FrontendLexer
+{
+public:
+ struct Token
+ {
+ enum Type
+ {
+ TYPE_EOF,
+ TYPE_STRING,
+ TYPE_META_STRING,
+ TYPE_OPEN_PAREN,
+ TYPE_CLOSE_PAREN,
+ TYPE_COMMA,
+ TYPE_OTHER
+ };
+
+ Type type = TYPE_OTHER;
+ std::string rawText;
+ std::string processedText;
+ };
+
+ struct SingleStatement
+ {
+ std::string withoutSemicolon;
+ std::string withSemicolon;
+ };
+
+ struct IncompleteTokenError
+ {
+ bool insideComment;
+ };
+
+public:
+ FrontendLexer(std::string_view aBuffer = {})
+ : buffer(aBuffer),
+ pos(buffer.begin()),
+ end(buffer.end()),
+ deletePos(buffer.begin())
+ {
+ }
+
+ FrontendLexer(const FrontendLexer&) = delete;
+ FrontendLexer& operator=(const FrontendLexer&) = delete;
+
+public:
+ static std::string stripComments(std::string_view statement);
+
+public:
+ auto getBuffer() const
+ {
+ return buffer;
+ }
+
+ auto getPos() const
+ {
+ return pos;
+ }
+
+ void rewind()
+ {
+ deletePos = buffer.begin();
+ }
+
+ bool isBufferEmpty() const;
+
+ void appendBuffer(std::string_view newBuffer);
+ void reset();
+ std::variant getSingleStatement(std::string_view term);
+ Token getToken();
+
+private:
+ std::optional getStringToken();
+ void skipSpacesAndComments();
+
+private:
+ std::string buffer;
+ std::string::const_iterator pos;
+ std::string::const_iterator end;
+ std::string::const_iterator deletePos;
+};
+
+#endif // FB_ISQL_FRONTEND_LEXER_H
diff --git a/src/isql/InputDevices.cpp b/src/isql/InputDevices.cpp
index faae0a84e7..10e8f84bdd 100644
--- a/src/isql/InputDevices.cpp
+++ b/src/isql/InputDevices.cpp
@@ -251,7 +251,8 @@ void InputDevices::saveCommand(const char* statement, const char* term)
if (f)
{
fputs(statement, f);
- fputs(term, f);
+ if (*term)
+ fputs(term, f);
// Add newline to make the file more readable.
fputc('\n', f);
}
diff --git a/src/isql/isql.epp b/src/isql/isql.epp
index c7f922d1a1..b8b0a9e308 100644
--- a/src/isql/isql.epp
+++ b/src/isql/isql.epp
@@ -54,6 +54,7 @@
#include
#include
#include
+#include "../isql/FrontendLexer.h"
#include "../common/utils_proto.h"
#include "../common/classes/array.h"
#include "../common/classes/init.h"
@@ -123,10 +124,7 @@ enum literal_string_type
#include "../common/classes/MsgPrint.h"
#include "../common/classes/array.h"
-using Firebird::string;
-using Firebird::PathName;
-using Firebird::TempFile;
-using Firebird::TimeZoneUtil;
+using namespace Firebird;
using MsgFormat::SafeArg;
#include "../isql/ColList.h"
@@ -304,11 +302,6 @@ static inline int fb_isspace(const char c)
return isspace((int)(UCHAR)c);
}
-static inline int fb_isspace(const SSHORT c)
-{
- return isspace((int)(UCHAR)c);
-}
-
static inline int fb_isdigit(const char c)
{
return isdigit((int)(UCHAR)c);
@@ -466,6 +459,7 @@ static processing_state add_row(TEXT*);
static processing_state blobedit(const TEXT*, const TEXT* const*);
static processing_state bulk_insert_hack(const char* command);
static bool bulk_insert_retriever(const char* prompt);
+static void check_autoterm();
static bool check_date(const tm& times);
static bool check_time(const tm& times);
static bool check_timestamp(const tm& times, const int msec);
@@ -488,7 +482,6 @@ static void frontend_load_parms(const TEXT* p, TEXT* parms[], TEXT* lparms[],
static processing_state do_set_command(const TEXT*, bool*);
static processing_state get_dialect(const char* const dialect_str,
char* const bad_dialect_buf, bool& bad_dialect);
-static processing_state get_statement(string&, const TEXT*);
static bool get_numeric(const UCHAR*, USHORT, SSHORT*, SINT64*);
static void print_set(const char* str, bool v);
static processing_state print_sets();
@@ -515,7 +508,7 @@ static void process_plan();
static void process_exec_path();
static SINT64 process_record_count(const unsigned statement_type);
static unsigned process_message_display(Firebird::IMessageMetadata* msg, unsigned pad[]);
-static processing_state process_statement(const TEXT*);
+static processing_state process_statement(const std::string&);
#ifdef WIN_NT
static BOOL CALLBACK query_abort(DWORD);
#else
@@ -581,6 +574,7 @@ public:
Plan = false;
Planonly = false;
ExplainPlan = false;
+ AutoTerm = false;
Heading = true;
BailOnError = false;
StmtTimeout = 0;
@@ -607,6 +601,7 @@ public:
bool Plan;
bool Planonly;
bool ExplainPlan;
+ bool AutoTerm;
bool Heading;
bool BailOnError;
unsigned int StmtTimeout;
@@ -625,7 +620,7 @@ static FILE* Help;
static const TEXT* const sql_prompt = "SQL> ";
-// Keep in sync with the chars that have their own "case" in get_statement(...).
+// Keep in sync with the chars that have their own "case" in the frontend lexer.
static const char FORBIDDEN_TERM_CHARS[] = { '\n', '-', '*', '/', SINGLE_QUOTE, DBL_QUOTE };
static const char FORBIDDEN_TERM_CHARS_DISPLAY[] = ", -, *, /, SINGLE_QUOTE, DOUBLE_QUOTE";
@@ -721,6 +716,32 @@ private:
static Firebird::GlobalPtr perTableStats;
+class StatementGetter
+{
+public:
+ StatementGetter()
+ {
+ // Lookup the continuation prompt once
+ if (!*conPrompt)
+ IUTILS_msg_get(CON_PROMPT, conPrompt);
+ }
+
+public:
+ std::pair getStatement();
+
+ void rewind()
+ {
+ lexer.rewind();
+ }
+
+private:
+ static TEXT conPrompt[MSG_LENGTH];
+ FrontendLexer lexer;
+};
+
+TEXT StatementGetter::conPrompt[MSG_LENGTH] = "";
+
+
static UCHAR predefined_blob_subtype_bpb[] =
{
isc_bpb_version1,
@@ -3984,6 +4005,51 @@ static bool bulk_insert_retriever(const char* prompt)
}
+// Check if SET AUTOTERM is allowed. If not, disable it.
+static void check_autoterm()
+{
+ if (!DB || !setValues.AutoTerm)
+ return;
+
+ static const UCHAR protocolInfo[] =
+ {
+ fb_info_protocol_version,
+ isc_info_end
+ };
+
+ UCHAR buffer[BUFFER_LENGTH128];
+
+ DB->getInfo(fbStatus, sizeof(protocolInfo), protocolInfo, sizeof(buffer), buffer);
+ if (ISQL_errmsg(fbStatus))
+ return;
+
+ SLONG protocolVersion = -1;
+
+ for (ClumpletReader p(ClumpletReader::InfoResponse, buffer, sizeof(buffer)); !p.isEof(); p.moveNext())
+ {
+ switch (p.getClumpTag())
+ {
+ case isc_info_end:
+ break;
+
+ case fb_info_protocol_version:
+ protocolVersion = p.getInt();
+ break;
+ }
+ }
+
+ if (!(protocolVersion == 0 || protocolVersion >= 19) && // PROTOCOL_VERSION19
+ ENCODE_ODS(isqlGlob.major_ods, isqlGlob.minor_ods) >= ODS_13_2)
+ {
+ setValues.AutoTerm = false;
+
+ TEXT errbuf[MSG_LENGTH];
+ IUTILS_msg_get(AUTOTERM_NOT_SUPPORTED, errbuf);
+ STDERROUT(errbuf);
+ }
+}
+
+
// *******************
// c h e c k _ d a t e
// *******************
@@ -4432,13 +4498,11 @@ static void do_isql()
// Read statements and process them from Ifp until the ret
// code tells us we are done
- string stmt;
- processing_state ret;
-
+ StatementGetter statementGetter;
bool done = false;
+
while (!done)
{
-
if (Abort_flag)
{
if (D__trans)
@@ -4498,7 +4562,10 @@ static void do_isql()
}
}
- ret = get_statement(stmt, sql_prompt);
+ auto [statement, ret] = statementGetter.getStatement();
+
+ if (!statement.withoutSemicolon.empty())
+ ret = frontend(FrontendLexer::stripComments(statement.withoutSemicolon).c_str());
// If there is no database yet, remind us of the need to connect
@@ -4511,6 +4578,7 @@ static void do_isql()
IUTILS_msg_get(NO_DB, errbuf);
STDERROUT(errbuf);
}
+
if (!Interactive && setValues.BailOnError)
ret = FAIL;
else
@@ -4520,16 +4588,29 @@ static void do_isql()
switch (ret)
{
case CONT:
- if (process_statement(stmt.c_str()) == ps_ERR)
+ switch (process_statement(setValues.AutoTerm ? statement.withSemicolon : statement.withoutSemicolon))
{
- Exit_value = FINI_ERROR;
- if (!Interactive && setValues.BailOnError)
- Abort_flag = true;
+ case TRUNCATED:
+ statementGetter.rewind();
+ break;
+
+ case ps_ERR:
+ Exit_value = FINI_ERROR;
+ if (!Interactive && setValues.BailOnError)
+ Abort_flag = true;
+ [[fallthrough]];
+
+ default:
+ // Place each non frontend statement in the temp file if we are reading from stdin.
+ Filelist->saveCommand(
+ (setValues.AutoTerm ? statement.withSemicolon : statement.withoutSemicolon).c_str(),
+ (setValues.AutoTerm ? "" : isqlGlob.global_Term));
+ break;
}
break;
case END:
- case EOF:
+ case FOUND_EOF:
case EXIT:
if (Abort_flag)
{
@@ -4623,10 +4704,6 @@ static void do_isql()
done = true;
break;
- case ERR_BUFFER_OVERFLOW:
- IUTILS_msg_get(BUFFER_OVERFLOW, errbuf);
- STDERROUT(errbuf);
-
case EXTRACT:
case EXTRACTALL:
default:
@@ -5368,7 +5445,7 @@ static processing_state frontend_set(const char* cmd, const char* const* parms,
enum set_commands
{
stat, count, list, plan, planonly, explain, blobdisplay, echo, autoddl,
- width, transaction, terminator, names, time,
+ autoterm, width, transaction, terminator, names, time,
sqlda_display,
exec_path_display,
sql, warning, sqlCont, heading, bail,
@@ -5392,6 +5469,7 @@ static processing_state frontend_set(const char* cmd, const char* const* parms,
{SetOptions::blobdisplay, "BLOBDISPLAY", 4},
{SetOptions::echo, "ECHO", 0},
{SetOptions::autoddl, "AUTODDL", 4},
+ {SetOptions::autoterm, "AUTOTERM", 5},
{SetOptions::width, "WIDTH", 0},
{SetOptions::transaction, "TRANSACTION", 5},
{SetOptions::terminator, "TERMINATOR", 4},
@@ -5482,6 +5560,15 @@ static processing_state frontend_set(const char* cmd, const char* const* parms,
ret = do_set_command(parms[2], &setValues.Autocommit);
break;
+ case SetOptions::autoterm:
+ ret = do_set_command(parms[2], &setValues.AutoTerm);
+ if (setValues.AutoTerm)
+ {
+ isqlGlob.Termlen = 1;
+ strcpy(isqlGlob.global_Term, ";");
+ }
+ break;
+
case SetOptions::width:
ret = newsize(parms[2][0] == '"' ? lparms[2] : parms[2], parms[3]);
break;
@@ -5504,6 +5591,8 @@ static processing_state frontend_set(const char* cmd, const char* const* parms,
}
}
+ setValues.AutoTerm = false;
+
isqlGlob.Termlen = strlen(a);
if (isqlGlob.Termlen < MAXTERM_SIZE)
{
@@ -5808,85 +5897,59 @@ static processing_state get_dialect(const char* const dialect_str,
}
-static processing_state get_statement(string& statement,
- const TEXT* statement_prompt)
+std::pair StatementGetter::getStatement()
{
-/**************************************
- *
- * g e t _ s t a t e m e n t
- *
- **************************************
- *
- * Functional description
- * Get an SQL statement, or QUIT/EXIT command to process
- *
- * Arguments: Pointer to statement, size of statement_buffer and prompt msg.
- *
- **************************************/
- processing_state ret = CONT;
-
- // Lookup the continuation prompt once
- TEXT con_prompt[MSG_LENGTH];
- IUTILS_msg_get(CON_PROMPT, con_prompt);
-
- if ((Interactive && !Input_file) || setValues.Echo) {
- ISQL_prompt(statement_prompt);
- }
-
- // Clear out statement buffer
- statement.resize(0);
-
- // Set count of characters to zero
-
- size_t valuable_count = 0; // counter of valuable (non-space) chars
- size_t comment_pos = 0; // position of block comment start
- size_t non_comment_pos = 0; // position of char after block comment
- const size_t term_length = isqlGlob.Termlen - 1; // additional variable for decreasing calculation
-
Filelist->Ifp().indev_line = Filelist->Ifp().indev_aux;
- bool done = false;
- enum
+ const auto* prompt = lexer.isBufferEmpty() ? sql_prompt : conPrompt;
+ std::string_view term(isqlGlob.global_Term, isqlGlob.Termlen);
+ std::string buffer;
+
+ while (true)
{
- normal,
- in_single_line_comment,
- in_block_comment,
- in_single_quoted_string,
- in_double_quoted_string
- } state = normal;
+ if ((Interactive && !Input_file) || setValues.Echo)
+ ISQL_prompt(prompt);
- char lastChar = '\0';
- char altQuoteChar = '\0';
- unsigned altQuoteStringLength = 0;
-
- while (!done)
- {
SSHORT c = getNextInputChar();
- switch (c)
+
+ if (c == EOF)
{
- case EOF:
// Go back to getc if we get interrupted by a signal.
if (SYSCALL_INTERRUPTED(errno))
{
errno = 0;
- break;
+ continue;
}
+ lexer.appendBuffer(buffer);
+ buffer.clear();
+
// If there was something valuable before EOF - error
- if (valuable_count > 0)
+ if (!lexer.isBufferEmpty())
{
- TEXT errbuf[MSG_LENGTH];
- IUTILS_msg_get(UNEXPECTED_EOF, errbuf);
- STDERROUT(errbuf);
- Exit_value = FINI_ERROR;
- ret = FAIL;
+ bool isEmpty = false;
+
+ try
+ {
+ isEmpty = FrontendLexer::stripComments(lexer.getBuffer()).empty();
+ }
+ catch (const FrontendLexer::IncompleteTokenError&)
+ {
+ }
+
+ if (!isEmpty)
+ {
+ TEXT errbuf[MSG_LENGTH];
+ IUTILS_msg_get(UNEXPECTED_EOF, errbuf);
+ STDERROUT(errbuf);
+ }
}
// If we hit EOF at the top of the flist, exit time
if (Filelist->count() == 1)
- return FOUND_EOF;
+ return {{}, FOUND_EOF};
// If this is not tmpfile, close it
@@ -5898,7 +5961,7 @@ static processing_state get_statement(string& statement,
Filelist->removeIntoIfp();
if ((Interactive && !Input_file) || setValues.Echo)
- ISQL_prompt(statement_prompt);
+ prompt = sql_prompt;
// CVC: Let's detect if we went back to the first level.
if (Filelist->readingStdin())
@@ -5910,182 +5973,43 @@ static processing_state get_statement(string& statement,
// Try to convince the new routines to go back to previous file(s)
// This should fix the INPUT bug introduced with editline.
getColumn = -1;
- break;
- case '\n':
-// case '\0': // In particular with readline the \n is removed
- if (state == in_single_line_comment)
+ if (!lexer.isBufferEmpty())
{
- state = normal;
- }
-
- // Catch the help ? without a terminator
- if (statement.length() == 1 && statement[0] == '?')
- {
- c = 0;
- done = true;
- break;
- }
-
- // If in a comment, keep reading
- if ((Interactive && !Input_file) || setValues.Echo)
- {
- if (state == in_block_comment)
- {
- // Block comment prompt.
- ISQL_prompt("--> ");
- }
- else if (valuable_count == 0)
- {
- // Ignore a series of nothing at the beginning
- ISQL_prompt(statement_prompt);
- }
- else
- {
- ISQL_prompt(con_prompt);
- }
- }
-
- break;
-
- case '-':
- // Could this the be start of a single-line comment.
- if (state == normal && statement.length() > 0 &&
- statement[statement.length() - 1] == '-')
- {
- state = in_single_line_comment;
- if (valuable_count == 1)
- valuable_count = 0;
- }
- break;
-
- case '*':
- // Could this the be start of a comment. We can only look back,
- // not forward.
- // Ignore possibilities of a comment beginning inside
- // quoted strings.
- if (state == normal && statement.length() > 0 &&
- statement[statement.length() - 1] == '/' && statement.length() != non_comment_pos)
- {
- state = in_block_comment;
- comment_pos = statement.length() - 1;
- if (valuable_count == 1)
- valuable_count = 0;
- }
- break;
-
- case '/':
- // Perhaps this is the end of a comment.
- // Ignore possibilities of a comment ending inside
- // quoted strings.
- // Ignore things like /*/ since it isn't a block comment; only the start of it. Or end.
- if (state == in_block_comment && statement.length() > 2 &&
- statement[statement.length() - 1] == '*' && statement.length() > comment_pos + 2)
- {
- state = normal;
- non_comment_pos = statement.length() + 1; // mark start of non-comment to track this: /**/*
- valuable_count--; // This char is not valuable
- }
- break;
-
- case SINGLE_QUOTE:
- switch (state)
- {
- case normal:
- if (lastChar == 'q' || lastChar == 'Q')
- {
- statement += (lastChar = c);
- altQuoteChar = c = getNextInputChar();
- altQuoteStringLength = statement.length();
-
- switch (altQuoteChar)
- {
- case '{':
- altQuoteChar = '}';
- break;
- case '(':
- altQuoteChar = ')';
- break;
- case '[':
- altQuoteChar = ']';
- break;
- case '<':
- altQuoteChar = '>';
- break;
- }
- }
- else
- altQuoteChar = '\0';
-
- state = in_single_quoted_string;
- break;
- case in_single_quoted_string:
- if (!altQuoteChar || (statement.length() != altQuoteStringLength + 1 && lastChar == altQuoteChar))
- state = normal;
- break;
- }
- break;
-
- case DBL_QUOTE:
- switch (state)
- {
- case normal:
- state = in_double_quoted_string;
- break;
- case in_double_quoted_string:
- state = normal;
- break;
- }
- break;
-
-
- default:
- if (state == normal && c == isqlGlob.global_Term[term_length] &&
- // one-char terminator or the beginning also match
- (isqlGlob.Termlen == 1u ||
- (valuable_count >= term_length &&
- strncmp(&statement[statement.length() - term_length],
- isqlGlob.global_Term, term_length) == 0)))
- {
- c = 0;
- done = true;
- statement.resize(statement.length() - term_length);
+ lexer.reset();
+ Exit_value = FINI_ERROR;
+ return {{}, FAIL};
}
}
-
- // Any non-space character is significant if not in comment
- if (state != in_block_comment &&
- state != in_single_line_comment &&
- !fb_isspace(c) && c != EOF)
+ else
{
- valuable_count++;
- if (valuable_count == 1) // this is the first valuable char in stream
+ buffer += (char) c;
+
+ if (c == '\n' ||
+ (buffer.length() >= isqlGlob.Termlen &&
+ std::equal(buffer.end() - isqlGlob.Termlen, buffer.end(), term.begin())))
{
- // ignore all previous crap
- statement.resize(0);
- non_comment_pos = 0;
+ lexer.appendBuffer(buffer);
+ buffer.clear();
+
+ const auto singleStatementVar = lexer.getSingleStatement(term);
+
+ if (const auto singleStatement = std::get_if(&singleStatementVar))
+ return {*singleStatement, CONT};
+ else if (const auto incompleteTokenError =
+ std::get_if(&singleStatementVar))
+ {
+ prompt =
+ incompleteTokenError->insideComment ? "---> " :
+ lexer.isBufferEmpty() ? sql_prompt :
+ conPrompt;
+ }
}
}
-
- statement += (lastChar = c);
}
- // If this was a null statement, skip it
- if (ret == CONT && statement.isEmpty())
- ret = SKIP;
-
- if (ret == CONT)
- ret = frontend(statement.c_str());
-
- if (ret == CONT)
- {
- // Place each non frontend statement in the temp file if we are reading
- // from stdin.
-
- Filelist->saveCommand(statement.c_str(), isqlGlob.global_Term);
- }
-
- return ret;
+ fb_assert(false);
+ return {{}, FOUND_EOF};
}
@@ -6484,6 +6408,9 @@ static processing_state print_sets()
p = p->next;
}
}
+
+ print_set("Auto Term:", setValues.AutoTerm);
+
isqlGlob.printf("%-25s%s%s", "Terminator:", isqlGlob.global_Term, NEWLINE);
print_set("Time:", setValues.Time_display);
@@ -6991,6 +6918,8 @@ static processing_state newdb(TEXT* dbname,
global_Stmt = NULL;
+ check_autoterm();
+
return SKIP;
}
@@ -7463,6 +7392,7 @@ static processing_state parse_arg(int argc, SCHAR** argv, SCHAR* tabname)
break;
case IN_SW_ISQL_TERM:
+ setValues.AutoTerm = false;
isqlGlob.Termlen = strlen(swarg_str);
if (isqlGlob.Termlen >= MAXTERM_SIZE) {
isqlGlob.Termlen = MAXTERM_SIZE - 1;
@@ -7604,6 +7534,10 @@ static processing_state parse_arg(int argc, SCHAR** argv, SCHAR* tabname)
}
break;
+ case IN_SW_ISQL_AUTOTERM:
+ setValues.AutoTerm = true;
+ break;
+
case IN_SW_ISQL_HELP:
ret = ps_ERR;
break;
@@ -8942,7 +8876,7 @@ static unsigned process_message_display(Firebird::IMessageMetadata* message, uns
}
-static processing_state process_statement(const TEXT* str2)
+static processing_state process_statement(const std::string& str)
{
/**************************************
*
@@ -9033,9 +8967,20 @@ static processing_state process_statement(const TEXT* str2)
flags |= Firebird::IStatement::PREPARE_PREFETCH_LEGACY_PLAN;
}
- global_Stmt = DB->prepare(fbStatus, prepare_trans, 0, str2, isqlGlob.SQL_dialect, flags);
+ if (setValues.AutoTerm)
+ flags |= IStatement::PREPARE_REQUIRE_SEMICOLON;
+
+ global_Stmt = DB->prepare(fbStatus, prepare_trans,
+ 0, str.c_str(), isqlGlob.SQL_dialect, flags);
+
if (failed())
{
+ if (setValues.AutoTerm &&
+ fb_utils::containsErrorCode(fbStatus->getErrors(), isc_command_end_err2))
+ {
+ return TRUNCATED;
+ }
+
if (isqlGlob.SQL_dialect == SQL_DIALECT_V6_TRANSITION && Input_file)
{
isqlGlob.printf("%s%s%s%s%s%s",
@@ -9043,7 +8988,7 @@ static processing_state process_statement(const TEXT* str2)
"**** Error preparing statement:",
NEWLINE,
NEWLINE,
- str2,
+ str.c_str(),
NEWLINE);
}
ISQL_errmsg(fbStatus);
@@ -9160,7 +9105,7 @@ static processing_state process_statement(const TEXT* str2)
(statement_type == isc_info_sql_stmt_ddl ||
statement_type == isc_info_sql_stmt_set_generator))
{
- DB->execute(fbStatus, D__trans, 0, str2, isqlGlob.SQL_dialect, NULL, NULL, NULL, NULL);
+ DB->execute(fbStatus, D__trans, 0, str.c_str(), isqlGlob.SQL_dialect, NULL, NULL, NULL, NULL);
setValues.StmtTimeout = 0;
if (ISQL_errmsg(fbStatus))
{
@@ -9195,9 +9140,9 @@ static processing_state process_statement(const TEXT* str2)
if (statement_type == isc_info_sql_stmt_start_trans)
{
// CVC: Starting a txn can fail, too. Let's check it, although I
- // suspect isql will catch it in frontend_set() through get_statement(),
+ // suspect isql will catch it in frontend_set() through StatementGetter,
// so this place has little chance to be reached.
- if (newtrans(str2) == FAIL)
+ if (newtrans(str.c_str()) == FAIL)
return ps_ERR;
if (setValues.Stats && (print_performance(perf_before) == ps_ERR))
diff --git a/src/isql/isql.h b/src/isql/isql.h
index 3fb758d5b1..6051d968a0 100644
--- a/src/isql/isql.h
+++ b/src/isql/isql.h
@@ -73,7 +73,7 @@ enum processing_state {
EXTRACTALL = 8,
FETCH = 9,
OBJECT_NOT_FOUND = 10,
- ERR_BUFFER_OVERFLOW = 11
+ TRUNCATED = 11
};
// Which blob subtypes to print
diff --git a/src/isql/isqlswi.h b/src/isql/isqlswi.h
index 6d0654ae75..0bb6064403 100644
--- a/src/isql/isqlswi.h
+++ b/src/isql/isqlswi.h
@@ -32,36 +32,37 @@ enum isql_switches
{
IN_SW_ISQL_0 = 0,
IN_SW_ISQL_EXTRACTALL = 1,
- IN_SW_ISQL_BAIL = 2,
- IN_SW_ISQL_CACHE = 3,
- IN_SW_ISQL_CHARSET = 4,
- IN_SW_ISQL_DATABASE = 5,
- IN_SW_ISQL_ECHO = 6,
- IN_SW_ISQL_EXTRACT = 7,
- IN_SW_ISQL_FETCHPASS = 8,
- IN_SW_ISQL_INPUT = 9,
- IN_SW_ISQL_MERGE = 10,
- IN_SW_ISQL_MERGE2 = 11,
- IN_SW_ISQL_NOAUTOCOMMIT = 12,
- IN_SW_ISQL_NODBTRIGGERS = 13,
- IN_SW_ISQL_NOWARN = 14,
- IN_SW_ISQL_OUTPUT = 15,
- IN_SW_ISQL_PAGE = 16,
- IN_SW_ISQL_PASSWORD = 17,
- IN_SW_ISQL_QUIET = 18,
- IN_SW_ISQL_ROLE = 19,
- IN_SW_ISQL_ROLE2 = 20,
- IN_SW_ISQL_SQLDIALECT = 21,
- IN_SW_ISQL_TERM = 22,
+ IN_SW_ISQL_AUTOTERM = 2,
+ IN_SW_ISQL_BAIL = 3,
+ IN_SW_ISQL_CACHE = 4,
+ IN_SW_ISQL_CHARSET = 5,
+ IN_SW_ISQL_DATABASE = 6,
+ IN_SW_ISQL_ECHO = 7,
+ IN_SW_ISQL_EXTRACT = 8,
+ IN_SW_ISQL_FETCHPASS = 9,
+ IN_SW_ISQL_INPUT = 10,
+ IN_SW_ISQL_MERGE = 11,
+ IN_SW_ISQL_MERGE2 = 12,
+ IN_SW_ISQL_NOAUTOCOMMIT = 13,
+ IN_SW_ISQL_NODBTRIGGERS = 14,
+ IN_SW_ISQL_NOWARN = 15,
+ IN_SW_ISQL_OUTPUT = 16,
+ IN_SW_ISQL_PAGE = 17,
+ IN_SW_ISQL_PASSWORD = 18,
+ IN_SW_ISQL_QUIET = 19,
+ IN_SW_ISQL_ROLE = 20,
+ IN_SW_ISQL_ROLE2 = 21,
+ IN_SW_ISQL_SQLDIALECT = 22,
+ IN_SW_ISQL_TERM = 23,
#ifdef TRUSTED_AUTH
- IN_SW_ISQL_TRUSTED = 23,
+ IN_SW_ISQL_TRUSTED = 24,
#endif
- IN_SW_ISQL_USER = 24,
- IN_SW_ISQL_VERSION = 25,
+ IN_SW_ISQL_USER = 25,
+ IN_SW_ISQL_VERSION = 26,
#ifdef DEV_BUILD
- IN_SW_ISQL_EXTRACTTBL = 26,
+ IN_SW_ISQL_EXTRACTTBL = 27,
#endif
- IN_SW_ISQL_HELP = 27
+ IN_SW_ISQL_HELP = 28
};
@@ -69,7 +70,8 @@ enum IsqlOptionType { iqoArgNone, iqoArgInteger, iqoArgString };
static const Switches::in_sw_tab_t isql_in_sw_table[] =
{
- {IN_SW_ISQL_EXTRACTALL , 0, "ALL" , 0, 0, 0, false, false, 11 , 1, NULL, iqoArgNone},
+ {IN_SW_ISQL_EXTRACTALL , 0, "ALL" , 0, 0, 0, false, false, 11 , 1, NULL, iqoArgNone},
+ {IN_SW_ISQL_AUTOTERM , 0, "AUTOTERM" , 0, 0, 0, false, false, 205 , 5, NULL, iqoArgNone},
{IN_SW_ISQL_BAIL , 0, "BAIL" , 0, 0, 0, false, false, 104 , 1, NULL, iqoArgNone},
{IN_SW_ISQL_CACHE , 0, "CACHE" , 0, 0, 0, false, false, 111 , 1, NULL, iqoArgInteger},
{IN_SW_ISQL_CHARSET , 0, "CHARSET" , 0, 0, 0, false, false, 122 , 2, NULL, iqoArgString},
diff --git a/src/isql/tests/FrontendLexerTest.cpp b/src/isql/tests/FrontendLexerTest.cpp
new file mode 100644
index 0000000000..d82f52e6b4
--- /dev/null
+++ b/src/isql/tests/FrontendLexerTest.cpp
@@ -0,0 +1,148 @@
+/*
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Adriano dos Santos Fernandes
+ * for the Firebird Open Source RDBMS project.
+ *
+ * Copyright (c) 2023 Adriano dos Santos Fernandes
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ *
+ */
+
+#include "firebird.h"
+#include "boost/test/unit_test.hpp"
+#include "../FrontendLexer.h"
+#include
+
+using namespace Firebird;
+
+BOOST_AUTO_TEST_SUITE(ISqlSuite)
+BOOST_AUTO_TEST_SUITE(FrontendLexerSuite)
+
+
+BOOST_AUTO_TEST_SUITE(FrontendLexerTests)
+
+BOOST_AUTO_TEST_CASE(StripCommentsTest)
+{
+ BOOST_TEST(FrontendLexer::stripComments(
+ "/* comment */ select 1 from rdb$database /* comment */") == "select 1 from rdb$database");
+
+ BOOST_TEST(FrontendLexer::stripComments(
+ "-- comment\nselect '123' /* comment */ from rdb$database -- comment") == "select '123' from rdb$database");
+}
+
+BOOST_AUTO_TEST_CASE(GetSingleStatementTest)
+{
+ { // scope
+ const std::string s1 =
+ "select /* ; */ -- ;\n"
+ " ';' || q'{;}'\n"
+ " from rdb$database;";
+
+ BOOST_TEST(std::get(FrontendLexer(
+ s1 + "\nselect ...").getSingleStatement(";")).withSemicolon == s1);
+ }
+
+ { // scope
+ const std::string s1 = "select 1 from rdb$database; -- comment";
+ const std::string s2 = "select 2 from rdb$database;";
+ const std::string s3 = "select 3 from rdb$database;";
+ const std::string s4 = "execute block returns (o1 integer) as begin o1 = 1;";
+ const std::string s5 = "end;";
+ const std::string s6 = "?";
+ const std::string s7 = "? set;";
+
+ FrontendLexer lexer(s1 + "\n" + s2);
+
+ BOOST_TEST(std::get(lexer.getSingleStatement(";")).withSemicolon == s1);
+
+ lexer.appendBuffer(s3 + "\n" + s4 + s5);
+
+ BOOST_TEST(std::get(lexer.getSingleStatement(";")).withSemicolon == s2);
+ BOOST_TEST(std::get(lexer.getSingleStatement(";")).withSemicolon == s3);
+
+ BOOST_TEST(std::get(lexer.getSingleStatement(";")).withSemicolon == s4);
+ lexer.rewind();
+ BOOST_TEST(std::get(lexer.getSingleStatement(";")).withSemicolon == s4 + s5);
+
+ lexer.appendBuffer(s6 + "\n" + s7);
+ BOOST_TEST(std::get(lexer.getSingleStatement(";")).withSemicolon == s6);
+ BOOST_TEST(std::get(lexer.getSingleStatement(";")).withSemicolon == s7);
+ }
+
+ BOOST_TEST(!std::get(FrontendLexer(
+ "select 1 from rdb$database").getSingleStatement(";")).insideComment);
+ BOOST_TEST(std::get(FrontendLexer(
+ "select 1 from rdb$database; /*").getSingleStatement(";")).insideComment);
+}
+
+BOOST_AUTO_TEST_CASE(SkipSingleLineCommentsTest)
+{
+ FrontendLexer lexer(
+ "-- comment 0\r\n"
+ "set -- comment 1\n"
+ "stats -- comment 2\r\n"
+ "- -- comment 3\n"
+ "-- comment 4"
+ );
+
+ BOOST_TEST(lexer.getToken().processedText == "SET");
+ BOOST_TEST(lexer.getToken().processedText == "STATS");
+ BOOST_TEST(lexer.getToken().processedText == "-");
+ BOOST_TEST(lexer.getToken().type == FrontendLexer::Token::TYPE_EOF);
+}
+
+BOOST_AUTO_TEST_CASE(SkipMultiLineCommentsTest)
+{
+ FrontendLexer lexer(
+ "/* comment 0 */\r\n"
+ "set /* comment 1\n"
+ "comment 1 continuation\n"
+ "*/ stats /* comment 2\r\n"
+ "* */ / /* comment 3*/ /* comment 4*/"
+ );
+
+ BOOST_TEST(lexer.getToken().processedText == "SET");
+ BOOST_TEST(lexer.getToken().processedText == "STATS");
+ BOOST_TEST(lexer.getToken().processedText == "/");
+ BOOST_TEST(lexer.getToken().type == FrontendLexer::Token::TYPE_EOF);
+}
+
+BOOST_AUTO_TEST_CASE(ParseStringsTest)
+{
+ FrontendLexer lexer(
+ "'ab''c\"d' "
+ "\"ab''c\"\"d\" "
+ "q'{ab'c\"d}' "
+ "q'(ab'c\"d)' "
+ "q'[ab'c\"d]' "
+ "q'' "
+ "q'!ab'c\"d!' "
+ );
+
+ BOOST_TEST(lexer.getToken().processedText == "ab'c\"d");
+ BOOST_TEST(lexer.getToken().processedText == "ab''c\"d");
+ BOOST_TEST(lexer.getToken().processedText == "ab'c\"d");
+ BOOST_TEST(lexer.getToken().processedText == "ab'c\"d");
+ BOOST_TEST(lexer.getToken().processedText == "ab'c\"d");
+ BOOST_TEST(lexer.getToken().processedText == "ab'c\"d");
+ BOOST_TEST(lexer.getToken().processedText == "ab'c\"d");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // FrontendLexerTests
+
+
+BOOST_AUTO_TEST_SUITE_END() // FrontendLexerSuite
+BOOST_AUTO_TEST_SUITE_END() // ISqlSuite
diff --git a/src/remote/client/interface.cpp b/src/remote/client/interface.cpp
index ac34c7c738..a6d1d9ca18 100644
--- a/src/remote/client/interface.cpp
+++ b/src/remote/client/interface.cpp
@@ -4131,6 +4131,7 @@ Statement* Attachment::prepare(CheckStatusWrapper* status, ITransaction* apiTra,
prepare->p_sqlst_items.cstr_length = (ULONG) items.getCount();
prepare->p_sqlst_items.cstr_address = items.begin();
prepare->p_sqlst_buffer_length = (ULONG) buffer.getCount();
+ prepare->p_sqlst_flags = flags;
send_packet(rdb->rdb_port, packet);
diff --git a/src/remote/inet.cpp b/src/remote/inet.cpp
index 01bd67d653..ba94da906e 100644
--- a/src/remote/inet.cpp
+++ b/src/remote/inet.cpp
@@ -716,7 +716,8 @@ rem_port* INET_analyze(ClntAuthBlock* cBlock,
REMOTE_PROTOCOL(PROTOCOL_VERSION15, ptype_lazy_send, 6),
REMOTE_PROTOCOL(PROTOCOL_VERSION16, ptype_lazy_send, 7),
REMOTE_PROTOCOL(PROTOCOL_VERSION17, ptype_lazy_send, 8),
- REMOTE_PROTOCOL(PROTOCOL_VERSION18, ptype_lazy_send, 9)
+ REMOTE_PROTOCOL(PROTOCOL_VERSION18, ptype_lazy_send, 9),
+ REMOTE_PROTOCOL(PROTOCOL_VERSION19, ptype_lazy_send, 10)
};
fb_assert(FB_NELEM(protocols_to_try) <= FB_NELEM(cnct->p_cnct_versions));
cnct->p_cnct_count = FB_NELEM(protocols_to_try);
diff --git a/src/remote/os/win32/xnet.cpp b/src/remote/os/win32/xnet.cpp
index 9dc8e34557..e24a4d13bb 100644
--- a/src/remote/os/win32/xnet.cpp
+++ b/src/remote/os/win32/xnet.cpp
@@ -307,7 +307,8 @@ rem_port* XNET_analyze(ClntAuthBlock* cBlock,
REMOTE_PROTOCOL(PROTOCOL_VERSION15, ptype_batch_send, 6),
REMOTE_PROTOCOL(PROTOCOL_VERSION16, ptype_batch_send, 7),
REMOTE_PROTOCOL(PROTOCOL_VERSION17, ptype_batch_send, 8),
- REMOTE_PROTOCOL(PROTOCOL_VERSION18, ptype_batch_send, 9)
+ REMOTE_PROTOCOL(PROTOCOL_VERSION18, ptype_batch_send, 9),
+ REMOTE_PROTOCOL(PROTOCOL_VERSION19, ptype_lazy_send, 10)
};
fb_assert(FB_NELEM(protocols_to_try) <= FB_NELEM(cnct->p_cnct_versions));
cnct->p_cnct_count = FB_NELEM(protocols_to_try);
diff --git a/src/remote/protocol.cpp b/src/remote/protocol.cpp
index ef466b8f34..353e34cb50 100644
--- a/src/remote/protocol.cpp
+++ b/src/remote/protocol.cpp
@@ -700,6 +700,10 @@ bool_t xdr_protocol(RemoteXdr* xdrs, PACKET* p)
MAP(xdr_long, reinterpret_cast(prep_stmt->p_sqlst_buffer_length));
// p_sqlst_buffer_length was USHORT in older versions
fixupLength(xdrs, prep_stmt->p_sqlst_buffer_length);
+
+ if (port->port_protocol >= PROTOCOL_VERSION19)
+ MAP(xdr_short, reinterpret_cast(prep_stmt->p_sqlst_flags));
+
DEBUG_PRINTSIZE(xdrs, p->p_operation);
return P_TRUE(xdrs, p);
diff --git a/src/remote/protocol.h b/src/remote/protocol.h
index 992ff9bfb0..e2db211572 100644
--- a/src/remote/protocol.h
+++ b/src/remote/protocol.h
@@ -104,6 +104,11 @@ const USHORT PROTOCOL_VERSION17 = (FB_PROTOCOL_FLAG | 17);
const USHORT PROTOCOL_VERSION18 = (FB_PROTOCOL_FLAG | 18);
const USHORT PROTOCOL_FETCH_SCROLL = PROTOCOL_VERSION18;
+// Protocol 19:
+// - supports passing flags to IStatement::prepare
+
+const USHORT PROTOCOL_VERSION19 = (FB_PROTOCOL_FLAG | 19);
+
// Architecture types
enum P_ARCH
@@ -612,6 +617,7 @@ typedef struct p_sqlst
USHORT p_sqlst_messages; // Number of messages
CSTRING p_sqlst_out_blr; // blr describing output message
USHORT p_sqlst_out_message_number;
+ USHORT p_sqlst_flags; // prepare flags
} P_SQLST;
typedef struct p_sqldata
diff --git a/src/remote/server/server.cpp b/src/remote/server/server.cpp
index 6c60f991b9..faaf29fcc4 100644
--- a/src/remote/server/server.cpp
+++ b/src/remote/server/server.cpp
@@ -1925,7 +1925,7 @@ static bool accept_connection(rem_port* port, P_CNCT* connect, PACKET* send)
{
if ((protocol->p_cnct_version == PROTOCOL_VERSION10 ||
(protocol->p_cnct_version >= PROTOCOL_VERSION11 &&
- protocol->p_cnct_version <= PROTOCOL_VERSION18)) &&
+ protocol->p_cnct_version <= PROTOCOL_VERSION19)) &&
(protocol->p_cnct_architecture == arch_generic ||
protocol->p_cnct_architecture == ARCHITECTURE) &&
protocol->p_cnct_weight >= weight)
@@ -4840,7 +4840,8 @@ ISC_STATUS rem_port::prepare_statement(P_SQLST * prepareL, PACKET* sendL)
// stuff isc_info_length in front of info items buffer
*info = isc_info_length;
memmove(info + 1, prepareL->p_sqlst_items.cstr_address, infoLength++);
- const unsigned int flags = StatementMetadata::buildInfoFlags(infoLength, info);
+ unsigned flags = StatementMetadata::buildInfoFlags(infoLength, info) |
+ prepareL->p_sqlst_flags;
ITransaction* iface = NULL;
if (transaction)