diff --git a/doc/README.isc_info_xxx b/doc/README.isc_info_xxx index 9a487be8b88..77909a295d2 100644 --- a/doc/README.isc_info_xxx +++ b/doc/README.isc_info_xxx @@ -96,3 +96,10 @@ New items for isc_transaction_info: 6. isc_info_tra_lock_timeout return lock timeout of current transaction + +New items for isc_dsql_sql_info: + +1. isc_info_sql_exec_path_nodes + return execution nodes tree formatted as XML. + The response can be span over multiple chunks to exceed the response + length limit. diff --git a/doc/README.isql_enhancements.txt b/doc/README.isql_enhancements.txt index b9a81f49a17..0a3a5b79a35 100644 --- a/doc/README.isql_enhancements.txt +++ b/doc/README.isql_enhancements.txt @@ -540,3 +540,12 @@ CON> where true; CONSTANT ============ 1 + +16) SET EXEC_PATH_DISPLAY NODES + +Retrieves the execution nodes tree of a DML statement formatted as XML. + +It requires server v6 or greater to work. + +SET EXEC_PATH_DISPLAY OFF disables both output of BLR and nodes tree. +SET EXEC_PATH_DISPLAY LIMIT set limit of output to bytes. diff --git a/src/dsql/ExprNodes.cpp b/src/dsql/ExprNodes.cpp index 7d74b33b55f..1091c104739 100644 --- a/src/dsql/ExprNodes.cpp +++ b/src/dsql/ExprNodes.cpp @@ -306,7 +306,11 @@ void Printable::print(NodePrinter& printer) const { NodePrinter subPrinter(printer.getIndent() + 1); Firebird::string tag(internalPrint(subPrinter)); - printer.begin(tag); + Firebird::string attr; +#ifdef DEV_BUILD + attr.printf("addr=\"%p\"", this); +#endif + printer.begin(tag, attr.c_str()); printer.append(subPrinter); printer.end(); } diff --git a/src/dsql/NodePrinter.h b/src/dsql/NodePrinter.h index 0f042b6b79d..6eb28da5d7e 100644 --- a/src/dsql/NodePrinter.h +++ b/src/dsql/NodePrinter.h @@ -42,11 +42,16 @@ class NodePrinter } public: - void begin(const Firebird::string& s) + void begin(const Firebird::string& s, const char* attributes = nullptr) { printIndent(); text += "<"; text += s; + if (attributes != nullptr) + { + text += ' '; + text += attributes; + } text += ">\n"; ++indent; diff --git a/src/dsql/dsql.cpp b/src/dsql/dsql.cpp index 198c47e6fb4..40e5fb670a6 100644 --- a/src/dsql/dsql.cpp +++ b/src/dsql/dsql.cpp @@ -90,6 +90,7 @@ static DsqlRequest* safePrepareRequest(thread_db*, dsql_dbb*, jrd_tra*, ULONG, c static RefPtr prepareStatement(thread_db*, dsql_dbb*, jrd_tra*, ULONG, const TEXT*, USHORT, unsigned, bool, ntrace_result_t* traceResult); static UCHAR* put_item(UCHAR, const USHORT, const UCHAR*, UCHAR*, const UCHAR* const); +static UCHAR* putLongItem(UCHAR tag, ULONG length, const UCHAR* item, UCHAR* buffer, const UCHAR* endOfBuffer); 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*, const UCHAR* const, USHORT, bool); @@ -717,6 +718,40 @@ static UCHAR* put_item( UCHAR item, return ptr + length; } +/** + + putLongItem + + @brief Put long information item in output buffer, splitting it into + several chunks if nesessary and return an updated pointer. + If there isn't room for the whole item, indicate truncation and return NULL. + + + @param item Tag to prepend data + @param length Full length of data + @param string Data + @param ptr Target buffer + @param end End of target buffer + + **/ +static UCHAR* putLongItem(UCHAR item, ULONG length, const UCHAR* string, UCHAR* buffer, const UCHAR* endOfBuffer) +{ + while (length) + { + // 1-byte item + 2-byte length + isc_info_end/isc_info_truncated == 4 + ULONG bufferLength = endOfBuffer - buffer - 4; + ULONG maxLength = MIN(bufferLength, MAX_USHORT); + ULONG segmentLength = MIN(length, maxLength); + + buffer = put_item(item, segmentLength, string, buffer, endOfBuffer); + if (!buffer) + return nullptr; + + string += segmentLength; + length -= segmentLength; + } + return buffer; +} void IntlString::dsqlPass(DsqlCompilerScratch* dsqlScratch) { @@ -1054,17 +1089,7 @@ static void sql_info(thread_db* tdbb, if (path.hasData()) { - // 1-byte item + 2-byte length + isc_info_end/isc_info_truncated == 4 - const ULONG bufferLength = end_info - info - 4; - const ULONG maxLength = MIN(bufferLength, MAX_USHORT); - - if (path.getCount() > maxLength) - { - *info = isc_info_truncated; - info = NULL; - } - else - info = put_item(item, path.getCount(), path.begin(), info, end_info); + info = putLongItem(item, path.getCount(), path.begin(), info, end_info); } if (!info) @@ -1072,6 +1097,22 @@ static void sql_info(thread_db* tdbb, } break; + case isc_info_sql_exec_path_nodes: + if (const Statement* stmt = dsqlRequest->getStatement()) + { + if (stmt->topNode) + { + NodePrinter printer; + stmt->topNode->print(printer); + + const string& data = printer.getText(); + info = putLongItem(item, data.size(), reinterpret_cast(data.c_str()), info, end_info); + if (!info) + return; + } + } + break; + case isc_info_sql_num_variables: case isc_info_sql_describe_vars: if (messageFound) diff --git a/src/include/firebird/impl/inf_pub.h b/src/include/firebird/impl/inf_pub.h index fcc46468873..4cdfea9d9f8 100644 --- a/src/include/firebird/impl/inf_pub.h +++ b/src/include/firebird/impl/inf_pub.h @@ -507,6 +507,7 @@ enum info_db_provider #define isc_info_sql_exec_path_blr_bytes 31 #define isc_info_sql_exec_path_blr_text 32 #define isc_info_sql_relation_schema 33 +#define isc_info_sql_exec_path_nodes 34 /*********************************/ /* SQL information return values */ diff --git a/src/include/firebird/impl/msg/isql.h b/src/include/firebird/impl/msg/isql.h index fc4a9619737..fe5b958864f 100644 --- a/src/include/firebird/impl/msg/isql.h +++ b/src/include/firebird/impl/msg/isql.h @@ -208,3 +208,4 @@ FB_IMPL_MSG_SYMBOL(ISQL, 208, HLP_SETAUTOTERM, " SET AUTOTERM -- to FB_IMPL_MSG_SYMBOL(ISQL, 209, HLP_SETWIRESTATS, " SET WIRE_stats -- toggle display of wire (network) statistics") FB_IMPL_MSG_SYMBOL(ISQL, 210, USAGE_SEARCH_PATH, " -(se)arch_path set schema search path") FB_IMPL_MSG_SYMBOL(ISQL, 211, MSG_SCHEMAS, "Schemas:") +FB_IMPL_MSG_SYMBOL(ISQL, 212, HLP_SETEXECPATHDISPLAY, " SET EXEC_PATH_DISPLAY > -- toggle display of query execution path") diff --git a/src/include/gen/Firebird.pas b/src/include/gen/Firebird.pas index eb0cfeae5a0..95d18f784f9 100644 --- a/src/include/gen/Firebird.pas +++ b/src/include/gen/Firebird.pas @@ -4928,6 +4928,7 @@ IPerformanceStatsImpl = class(IPerformanceStats) isc_info_sql_exec_path_blr_bytes = byte(31); isc_info_sql_exec_path_blr_text = byte(32); isc_info_sql_relation_schema = byte(33); + isc_info_sql_exec_path_nodes = byte(34); isc_info_sql_stmt_select = byte(1); isc_info_sql_stmt_insert = byte(2); isc_info_sql_stmt_update = byte(3); diff --git a/src/isql/FrontendParser.cpp b/src/isql/FrontendParser.cpp index 64218057132..316edd16b99 100644 --- a/src/isql/FrontendParser.cpp +++ b/src/isql/FrontendParser.cpp @@ -327,8 +327,27 @@ FrontendParser::AnySetNode FrontendParser::parseSet() return parsed.value(); else if (const auto parsed = parseSet(text, TOKEN_ECHO)) return parsed.value(); - else if (const auto parsed = parseSet(text, TOKEN_EXEC_PATH_DISPLAY)) - return parsed.value(); + else if (text == TOKEN_EXEC_PATH_DISPLAY) + { + if (const auto arg = lexer.getToken(); arg.type != Token::TYPE_EOF) + { + SetExecPathDisplayNode node; + node.arg = arg.processedText; + + if (node.arg == "LIMIT") + { + const auto value = lexer.getToken(); + if (value.type != Token::TYPE_OTHER) + { + return InvalidNode(); + } + node.value = atoi(value.rawText.c_str()); + } + + if (parseEof()) + return node; + } + } else if (const auto parsed = parseSet(text, TOKEN_EXPLAIN)) return parsed.value(); else if (const auto parsed = parseSet(text, TOKEN_HEADING)) diff --git a/src/isql/FrontendParser.h b/src/isql/FrontendParser.h index 28a1b9cc3ba..329e605c652 100644 --- a/src/isql/FrontendParser.h +++ b/src/isql/FrontendParser.h @@ -70,7 +70,7 @@ class FrontendParser struct SetBulkInsertNode { std::string statement; }; struct SetCountNode { std::string arg; }; struct SetEchoNode { std::string arg; }; - struct SetExecPathDisplayNode { std::string arg; }; + struct SetExecPathDisplayNode { std::string arg; unsigned value; }; struct SetExplainNode { std::string arg; }; struct SetHeadingNode { std::string arg; }; struct SetKeepTranParamsNode { std::string arg; }; diff --git a/src/isql/isql.epp b/src/isql/isql.epp index daab2b9d08f..12f69e2f12c 100644 --- a/src/isql/isql.epp +++ b/src/isql/isql.epp @@ -503,6 +503,13 @@ static Firebird::GlobalPtr TranParams; class SetValues { public: + enum ExecPathOptions : unsigned + { + OFF = 0x00, + BLR = 0x01, + NODES = 0x02 + }; + SetValues() { //ColList global_Cols; @@ -510,7 +517,8 @@ public: Echo = false; Time_display = false; Sqlda_display = false; - ExecPathDisplay[0] = 0; + ExecPathDisplay = ExecPathOptions::OFF; + ExecPathDisplayLimit = 1024 * 1024; Stats = false; Autocommit = true; // Commit ddl Warnings = true; // Print warnings @@ -537,7 +545,8 @@ public: bool Echo; bool Time_display; bool Sqlda_display; - UCHAR ExecPathDisplay[10]; + unsigned ExecPathDisplay; + unsigned ExecPathDisplayLimit; bool Stats; bool Autocommit; // Commit ddl bool Warnings; // Print warnings @@ -4750,13 +4759,13 @@ static processing_state execSetDebugCommand() if (!DB) return SKIP; - const char* stmt = setValues.ExecPathDisplay[0] ? + const char* stmt = setValues.ExecPathDisplay ? "set debug option dsql_keep_blr = true" : "set debug option dsql_keep_blr = false"; DB->execute(fbStatus, nullptr, 0, stmt, isqlGlob.SQL_dialect, nullptr, nullptr, nullptr, nullptr); - if (setValues.ExecPathDisplay[0] && (fbStatus->getState() & Firebird::IStatus::STATE_ERRORS)) + if (setValues.ExecPathDisplay && (fbStatus->getState() & Firebird::IStatus::STATE_ERRORS)) { STDERROUT("SET EXEC_PATH_DISPLAY is not supported in this connection."); return FAIL; @@ -5015,17 +5024,21 @@ static processing_state frontend(const std::string& statement) if (node.arg.empty()) return ps_ERR; else if (node.arg == "OFF") - setValues.ExecPathDisplay[0] = 0; - else + setValues.ExecPathDisplay = SetValues::ExecPathOptions::OFF; + else if (node.arg == "BLR") { - static constexpr UCHAR execPath[] = {isc_info_sql_exec_path_blr_text}; - - if (node.arg != "BLR") - return ps_ERR; - - memcpy(setValues.ExecPathDisplay, execPath, FB_NELEM(execPath)); - setValues.ExecPathDisplay[FB_NELEM(execPath)] = 0; + setValues.ExecPathDisplay |= SetValues::ExecPathOptions::BLR; } + else if (node.arg == "NODES") + { + setValues.ExecPathDisplay |= SetValues::ExecPathOptions::NODES; + } + else if (node.arg == "LIMIT" && node.value > 0) + { + setValues.ExecPathDisplayLimit = node.value + 4; // item + length + overflow tag + } + else + return ps_ERR; return execSetDebugCommand(); }, @@ -5673,7 +5686,7 @@ void ISQL_get_version(bool call_by_create_db) else isqlGlob.db_SQL_dialect = SQL_DIALECT_V5; - if (setValues.ExecPathDisplay[0]) + if (setValues.ExecPathDisplay) execSetDebugCommand(); } @@ -5825,6 +5838,24 @@ static processing_state print_sets() print_set("Access Plan only:", setValues.Planonly); print_set("Explain Access Plan:", setValues.ExplainPlan); + isqlGlob.printf("%-25s", "Execution path display:"); + if (setValues.ExecPathDisplay == SetValues::ExecPathOptions::OFF) + { + isqlGlob.printf("OFF"); + } + else + { + if (setValues.ExecPathDisplay & SetValues::ExecPathOptions::BLR) + { + isqlGlob.printf("BLR "); + } + if (setValues.ExecPathDisplay & SetValues::ExecPathOptions::NODES) + { + isqlGlob.printf("NODES"); + } + } + isqlGlob.printf(NEWLINE); + isqlGlob.printf("%-25s", "Display BLOB type:"); switch (setValues.Doblob) { @@ -5942,6 +5973,7 @@ static processing_state help(const TEXT* what) HLP_SETCOUNT, // SET COUNT -- toggle count of selected rows on/off HLP_SETMAXROWS, // SET MAXROWS [] -- toggle limit of selected rows to , zero is no limit HLP_SETECHO, // SET ECHO -- toggle command echo on/off + HLP_SETEXECPATHDISPLAY, // SET EXEC_PATH_DISPLAY -- toggle display of query execution path HLP_SETEXPLAIN, // SET EXPLAIN -- toggle display of query plan in the explained form HLP_SETHEADING, // SET HEADING -- toggle column titles display on/off HLP_SETKEEPTRAN, // SET KEEP_TRAN_params -- toggle to keep or not to keep text of following successful SET TRANSACTION statement @@ -6377,7 +6409,7 @@ static processing_state newdb(const TEXT* dbname, } } - if (setValues.ExecPathDisplay[0]) + if (setValues.ExecPathDisplay) execSetDebugCommand(); global_Stmt = NULL; @@ -8057,10 +8089,16 @@ static void process_exec_path() return; Firebird::Array pathBuffer; - pathBuffer.getBuffer(MAX_USHORT, false); + pathBuffer.getBuffer(setValues.ExecPathDisplayLimit, false); - for (const UCHAR* code = setValues.ExecPathDisplay; *code; ++code) + static constexpr UCHAR execPath[] = { isc_info_sql_exec_path_blr_text, isc_info_sql_exec_path_nodes }; + + for (unsigned i = 0; i < std::size(execPath); ++i) { + if ((setValues.ExecPathDisplay & (1 << i)) == 0) + continue; + + const UCHAR* code = &execPath[i]; global_Stmt->getInfo(fbStatus, 1, code, pathBuffer.getCount(), pathBuffer.begin()); if (ISQL_errmsg(fbStatus)) @@ -8076,24 +8114,27 @@ static void process_exec_path() { const USHORT len = (USHORT) gds__vax_integer(ptr, sizeof(USHORT)); ptr += sizeof(USHORT); - pathString.assign((const char*) ptr, len); + pathString.append((const char*) ptr, len); ptr += len; } else if (tag == isc_info_end) break; else if (tag == isc_info_truncated) { - pathString = "* error: overflow *\n"; + if (!pathString.empty()) + pathString += '\n'; + pathString += "* error: overflow *\n"; break; } else - pathString = "* unknown error *\n"; + pathString += "* unknown error *\n"; } if (pathString.hasData()) { IUTILS_printf2(Diag, "%sExecution path (%s):%s%s%s", NEWLINE, (*code == isc_info_sql_exec_path_blr_text ? "BLR" : + *code == isc_info_sql_exec_path_nodes ? "Nodes" : "* unknown *" ), NEWLINE, NEWLINE, @@ -8538,7 +8579,7 @@ static processing_state process_statement(const std::string& str) return ret; // do not execute } - if (setValues.ExecPathDisplay[0]) + if (setValues.ExecPathDisplay) process_exec_path(); // If the statement isn't a select, execute it and be done diff --git a/src/isql/tests/FrontendParserTest.cpp b/src/isql/tests/FrontendParserTest.cpp index d4f62903351..78548a4b7bb 100644 --- a/src/isql/tests/FrontendParserTest.cpp +++ b/src/isql/tests/FrontendParserTest.cpp @@ -241,12 +241,17 @@ BOOST_AUTO_TEST_CASE(ParseSetTest) BOOST_TEST(std::holds_alternative(FrontendParser::parse( "set echo off x", parserOptions))); - BOOST_TEST(std::get(parseSet( - "set exec_path_display")).arg.empty()); + BOOST_TEST(std::holds_alternative(FrontendParser::parse( + "set exec_path_display", parserOptions))); BOOST_TEST((std::get(parseSet( "set exec_path_display blr")).arg == "BLR")); BOOST_TEST(std::holds_alternative(FrontendParser::parse( "set exec_path_display off x", parserOptions))); + BOOST_TEST(std::holds_alternative(FrontendParser::parse( + "set exec_path_display limit", parserOptions))); + auto node = std::get(parseSet( + "set exec_path_display limit 100")); + BOOST_TEST((node.arg == "LIMIT" && node.value == 100)); BOOST_TEST(std::get(parseSet( "set explain")).arg.empty());