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)