From 95d338e22f58405ebc255286f8f6594c01e9f0b0 Mon Sep 17 00:00:00 2001 From: Mattias <81063080+MSebanc@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:55:12 -0700 Subject: [PATCH] Added new output modes for shell (#4053) Co-authored-by: CI Bot --- tools/shell/embedded_shell.cpp | 550 ++++++++++++++++++------ tools/shell/include/embedded_shell.h | 29 +- tools/shell/include/output.h | 231 ++++++++++ tools/shell/shell_runner.cpp | 52 ++- tools/shell/test/test_shell_commands.py | 256 +++++++++++ tools/shell/test/test_shell_flags.py | 239 +++++++++- 6 files changed, 1224 insertions(+), 133 deletions(-) create mode 100644 tools/shell/include/output.h diff --git a/tools/shell/embedded_shell.cpp b/tools/shell/embedded_shell.cpp index aa927a0953..7939cf6f74 100644 --- a/tools/shell/embedded_shell.cpp +++ b/tools/shell/embedded_shell.cpp @@ -48,7 +48,10 @@ struct ShellCommand { const char* QUIT = ":quit"; const char* MAX_ROWS = ":max_rows"; const char* MAX_WIDTH = ":max_width"; - const std::array commandList = {HELP, CLEAR, QUIT, MAX_ROWS, MAX_WIDTH}; + const char* MODE = ":mode"; + const char* STATS = ":stats"; + const std::array commandList = {HELP, CLEAR, QUIT, MAX_ROWS, MAX_WIDTH, MODE, + STATS}; } shellCommand; const char* TAB = " "; @@ -67,8 +70,6 @@ std::vector relTableNames; bool continueLine = false; std::string currLine; -const int defaultMaxRows = 20; - static Connection* globalConnection; #ifndef _WIN32 @@ -262,16 +263,23 @@ std::string findClosestCommand(std::string lineStr) { } int EmbeddedShell::processShellCommands(std::string lineStr) { - if (lineStr == shellCommand.HELP) { + std::istringstream iss(lineStr); + std::string command, arg; + iss >> command >> arg; + if (command == shellCommand.HELP) { printHelp(); - } else if (lineStr == shellCommand.CLEAR) { + } else if (command == shellCommand.CLEAR) { linenoiseClearScreen(); - } else if (lineStr == shellCommand.QUIT) { + } else if (command == shellCommand.QUIT) { return -1; - } else if (lineStr.rfind(shellCommand.MAX_ROWS) == 0) { - setMaxRows(lineStr.substr(strlen(shellCommand.MAX_ROWS))); - } else if (lineStr.rfind(shellCommand.MAX_WIDTH) == 0) { - setMaxWidth(lineStr.substr(strlen(shellCommand.MAX_WIDTH))); + } else if (command == shellCommand.MAX_ROWS) { + setMaxRows(arg); + } else if (command == shellCommand.MAX_WIDTH) { + setMaxWidth(arg); + } else if (command == shellCommand.MODE) { + setMode(arg); + } else if (command == shellCommand.STATS) { + setStats(arg); } else { printf("Error: Unknown command: \"%s\". Enter \":help\" for help\n", lineStr.c_str()); printf("Did you mean: \"%s\"?\n", findClosestCommand(lineStr).c_str()); @@ -280,16 +288,18 @@ int EmbeddedShell::processShellCommands(std::string lineStr) { } EmbeddedShell::EmbeddedShell(std::shared_ptr database, std::shared_ptr conn, - const char* pathToHistory) { - path_to_history = pathToHistory; + ShellConfig& shellConfig) { + path_to_history = shellConfig.path_to_history; linenoiseHistoryLoad(path_to_history); linenoiseSetCompletionCallback(completion); linenoiseSetHighlightCallback(highlight); this->database = database; this->conn = conn; globalConnection = conn.get(); - maxRowSize = defaultMaxRows; - maxPrintWidth = 0; // Will be determined when printing + maxRowSize = shellConfig.maxRowSize; + maxPrintWidth = shellConfig.maxPrintWidth; + drawingCharacters = std::move(shellConfig.drawingCharacters); + stats = shellConfig.stats; updateTableNames(); KU_ASSERT(signal(SIGINT, interruptHandler) != SIG_ERR); } @@ -425,6 +435,91 @@ void EmbeddedShell::setMaxWidth(const std::string& maxWidthString) { printf("maxWidth set as %d\n", parsedMaxWidth); } +void printModeInfo() { + printf("Available output modes:\n"); + printf("%sbox (default):%sTables using unicode box-drawing characters\n", TAB, TAB); + printf("%scolumn:%sOutput in columns\n", TAB, TAB); + printf("%scsv:%sComma-separated values\n", TAB, TAB); + printf("%shtml:%sHTML table\n", TAB, TAB); + printf("%sjson:%sResults in a JSON array\n", TAB, TAB); + printf("%sjsonlines:%sResults in a NDJSON format\n", TAB, TAB); + printf("%slatex:%sLaTeX tabular environment code\n", TAB, TAB); + printf("%sline:%sOne value per line\n", TAB, TAB); + printf("%slist:%sValues delimited by \"|\"\n", TAB, TAB); + printf("%smarkdown:%sMarkdown table\n", TAB, TAB); + printf("%stable:%sTables using ASCII characters\n", TAB, TAB); + printf("%stsv:%sTab-separated values\n", TAB, TAB); + printf("%strash:%sNo output\n", TAB, TAB); +} + +void EmbeddedShell::setMode(const std::string& modeString) { + if (modeString.empty()) { + printModeInfo(); + return; + } + switch (PrintTypeUtils::fromString(modeString)) { + case PrintType::BOX: + drawingCharacters = std::make_unique(); + break; + case PrintType::TABLE: + drawingCharacters = std::make_unique(); + break; + case PrintType::CSV: + drawingCharacters = std::make_unique(); + break; + case PrintType::TSV: + drawingCharacters = std::make_unique(); + break; + case PrintType::MARKDOWN: + drawingCharacters = std::make_unique(); + break; + case PrintType::COLUMN: + drawingCharacters = std::make_unique(); + break; + case PrintType::LIST: + drawingCharacters = std::make_unique(); + break; + case PrintType::TRASH: + drawingCharacters = std::make_unique(); + break; + case PrintType::JSON: + drawingCharacters = std::make_unique(); + break; + case PrintType::JSONLINES: + drawingCharacters = std::make_unique(); + break; + case PrintType::HTML: + drawingCharacters = std::make_unique(); + break; + case PrintType::LATEX: + drawingCharacters = std::make_unique(); + break; + case PrintType::LINE: + drawingCharacters = std::make_unique(); + break; + default: + printf("Cannot parse '%s' as output mode.\n\n", modeString.c_str()); + printModeInfo(); + return; + } + printf("mode set as %s\n", modeString.c_str()); +} + +void EmbeddedShell::setStats(const std::string& statsString) { + std::string statsStringLower = statsString; + std::transform(statsStringLower.begin(), statsStringLower.end(), statsStringLower.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (statsStringLower == "on") { + stats = true; + } else if (statsStringLower == "off") { + stats = false; + } else { + printf("Cannot parse '%s' to toggle stats. Expect 'on' or 'off'.\n", statsString.c_str()); + return; + } + printf("stats set as %s\n", stats ? "on" : "off"); +} + void EmbeddedShell::printHelp() { printf("%s%s %sget command list\n", TAB, shellCommand.HELP, TAB); printf("%s%s %sclear shell\n", TAB, shellCommand.CLEAR, TAB); @@ -433,6 +528,8 @@ void EmbeddedShell::printHelp() { shellCommand.MAX_ROWS, TAB); printf("%s%s [max_width] %sset maximum width in characters for display\n", TAB, shellCommand.MAX_WIDTH, TAB); + printf("%s%s [mode] %sset output mode (default: box)\n", TAB, shellCommand.MODE, TAB); + printf("%s%s [on|off] %stoggle query stats on or off\n", TAB, shellCommand.STATS, TAB); printf("\n"); printf("%sNote: you can change and see several system configurations, such as num-threads, \n", TAB); @@ -442,23 +539,195 @@ void EmbeddedShell::printHelp() { printf("%s%s See: \x1B]8;;%s\x1B\\%s\x1B]8;;\x1B\\\n", TAB, TAB, url, url); } -struct BoxDrawingCharacters { - static constexpr const char* DownAndRight = "\u250C"; - static constexpr const char* Horizontal = "\u2500"; - static constexpr const char* DownAndHorizontal = "\u252C"; - static constexpr const char* DownAndLeft = "\u2510"; - static constexpr const char* Vertical = "\u2502"; - static constexpr const char* VerticalAndRight = "\u251C"; - static constexpr const char* VerticalAndHorizontal = "\u253C"; - static constexpr const char* VerticalAndLeft = "\u2524"; - static constexpr const char* UpAndRight = "\u2514"; - static constexpr const char* UpAndHorizontal = "\u2534"; - static constexpr const char* UpAndLeft = "\u2518"; - static constexpr const char* MiddleDot = "\u00B7"; -}; +std::string EmbeddedShell::printJsonExecutionResult(QueryResult& queryResult) const { + auto colNames = queryResult.getColumnNames(); + auto jsonDrawingCharacters = + common::ku_dynamic_cast( + drawingCharacters.get()); + bool jsonLines = jsonDrawingCharacters->printType == PrintType::JSONLINES; + std::string printString = ""; + if (!jsonLines) { + printString = jsonDrawingCharacters->ArrayOpen; + } + while (queryResult.hasNext()) { + auto tuple = queryResult.getNext(); + printString += jsonDrawingCharacters->ObjectOpen; + for (auto i = 0u; i < queryResult.getNumColumns(); i++) { + printString += jsonDrawingCharacters->KeyValue; + printString += colNames[i]; + printString += jsonDrawingCharacters->KeyValue; + printString += jsonDrawingCharacters->KeyDelimiter; + printString += jsonDrawingCharacters->KeyValue; + printString += tuple->getValue(i)->toString(); + printString += jsonDrawingCharacters->KeyValue; + if (i != queryResult.getNumColumns() - 1) { + printString += jsonDrawingCharacters->TupleDelimiter; + } + } + printString += jsonDrawingCharacters->ObjectClose; + if (queryResult.hasNext()) { + if (!jsonLines) { + printString += jsonDrawingCharacters->TupleDelimiter; + } + printString += "\n"; + } + } + if (!jsonLines) { + printString += jsonDrawingCharacters->ArrayClose; + } + printString += "\n"; + return printString; +} + +std::string EmbeddedShell::printHtmlExecutionResult(QueryResult& queryResult) const { + auto colNames = queryResult.getColumnNames(); + auto htmlDrawingCharacters = + common::ku_dynamic_cast( + drawingCharacters.get()); + std::string printString = htmlDrawingCharacters->TableOpen; + printString += "\n"; + printString += htmlDrawingCharacters->RowOpen; + printString += "\n"; + for (auto i = 0u; i < queryResult.getNumColumns(); i++) { + printString += htmlDrawingCharacters->HeaderOpen; + printString += colNames[i]; + printString += htmlDrawingCharacters->HeaderClose; + } + printString += "\n"; + printString += htmlDrawingCharacters->RowClose; + printString += "\n"; + while (queryResult.hasNext()) { + auto tuple = queryResult.getNext(); + printString += htmlDrawingCharacters->RowOpen; + printString += "\n"; + for (auto i = 0u; i < queryResult.getNumColumns(); i++) { + printString += htmlDrawingCharacters->CellOpen; + printString += tuple->getValue(i)->toString(); + printString += htmlDrawingCharacters->CellClose; + } + printString += htmlDrawingCharacters->RowClose; + printString += "\n"; + } + printString += htmlDrawingCharacters->TableClose; + printString += "\n"; + return printString; +} + +std::string EmbeddedShell::printLatexExecutionResult(QueryResult& queryResult) const { + auto colNames = queryResult.getColumnNames(); + auto latexDrawingCharacters = + common::ku_dynamic_cast( + drawingCharacters.get()); + std::string printString = latexDrawingCharacters->TableOpen; + printString += latexDrawingCharacters->AlignOpen; + for (auto i = 0u; i < queryResult.getNumColumns(); i++) { + printString += latexDrawingCharacters->ColumnAlign; + } + printString += latexDrawingCharacters->AlignClose; + printString += "\n"; + printString += latexDrawingCharacters->Line; + printString += "\n"; + for (auto i = 0u; i < queryResult.getNumColumns(); i++) { + printString += colNames[i]; + if (i != queryResult.getNumColumns() - 1) { + printString += latexDrawingCharacters->TupleDelimiter; + } + } + printString += latexDrawingCharacters->EndLine; + printString += "\n"; + printString += latexDrawingCharacters->Line; + printString += "\n"; + while (queryResult.hasNext()) { + auto tuple = queryResult.getNext(); + for (auto i = 0u; i < queryResult.getNumColumns(); i++) { + printString += tuple->getValue(i)->toString(); + if (i != queryResult.getNumColumns() - 1) { + printString += latexDrawingCharacters->TupleDelimiter; + } + } + printString += latexDrawingCharacters->EndLine; + printString += "\n"; + } + printString += latexDrawingCharacters->Line; + printString += "\n"; + printString += latexDrawingCharacters->TableClose; + printString += "\n"; + return printString; +} + +std::string EmbeddedShell::printLineExecutionResult(QueryResult& queryResult) const { + auto colNames = queryResult.getColumnNames(); + std::string printString = ""; + while (queryResult.hasNext()) { + auto tuple = queryResult.getNext(); + for (auto i = 0u; i < queryResult.getNumColumns(); i++) { + printString += colNames[i]; + printString += drawingCharacters->TupleDelimiter; + printString += tuple->getValue(i)->toString(); + printString += "\n"; + } + printString += "\n"; + } + return printString; +} void EmbeddedShell::printExecutionResult(QueryResult& queryResult) const { auto querySummary = queryResult.getQuerySummary(); + if (querySummary->isExplain()) { + printf("%s", queryResult.getNext()->toString().c_str()); + return; + } + if (PrintTypeUtils::isTableType(drawingCharacters->printType)) { + printTruncatedExecutionResult(queryResult); + return; + } + std::string printString = ""; + if (drawingCharacters->printType == PrintType::JSON || + drawingCharacters->printType == PrintType::JSONLINES) { + printString = printJsonExecutionResult(queryResult); + } else if (drawingCharacters->printType == PrintType::HTML) { + printString = printHtmlExecutionResult(queryResult); + } else if (drawingCharacters->printType == PrintType::LATEX) { + printString = printLatexExecutionResult(queryResult); + } else if (drawingCharacters->printType == PrintType::LINE) { + printString = printLineExecutionResult(queryResult); + } else if (drawingCharacters->printType != PrintType::TRASH) { + for (auto i = 0u; i < queryResult.getNumColumns(); i++) { + printString += queryResult.getColumnNames()[i]; + if (i != queryResult.getNumColumns() - 1) { + printString += drawingCharacters->TupleDelimiter; + } + } + printString += "\n"; + while (queryResult.hasNext()) { + auto tuple = queryResult.getNext(); + printString += tuple->toString(std::vector(queryResult.getNumColumns(), 0), + drawingCharacters->TupleDelimiter, UINT32_MAX); + printString += "\n"; + } + } + printf("%s", printString.c_str()); + if (stats) { + if (queryResult.getNumTuples() == 1) { + printf("(1 tuple)\n"); + } else { + printf("(%" PRIu64 " tuples)\n", queryResult.getNumTuples()); + } + if (queryResult.getNumColumns() == 1) { + printf("(1 column)\n"); + } else { + printf("(%" PRIu64 " columns)\n", queryResult.getNumColumns()); + } + printf("Time: %.2fms (compiling), %.2fms (executing)\n", querySummary->getCompilingTime(), + querySummary->getExecutionTime()); + } +} + +void EmbeddedShell::printTruncatedExecutionResult(QueryResult& queryResult) const { + auto tableDrawingCharacters = + common::ku_dynamic_cast( + drawingCharacters.get()); + auto querySummary = queryResult.getQuerySummary(); if (querySummary->isExplain()) { printf("%s", queryResult.getNext()->toString().c_str()); } else { @@ -614,122 +883,135 @@ void EmbeddedShell::printExecutionResult(QueryResult& queryResult) const { } } - if (queryResult.getNumColumns() != 0) { + if (queryResult.getNumColumns() != 0 && tableDrawingCharacters->TopLine) { std::string printString; - printString += BoxDrawingCharacters::DownAndRight; + printString += tableDrawingCharacters->DownAndRight; for (auto i = 0u; i < k; i++) { for (auto l = 0u; l < colsWidth[i]; l++) { - printString += BoxDrawingCharacters::Horizontal; + printString += tableDrawingCharacters->Horizontal; } if (i != colsWidth.size() - 1) { - printString += BoxDrawingCharacters::DownAndHorizontal; + printString += tableDrawingCharacters->DownAndHorizontal; } } if (j >= k) { - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::DownAndHorizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->DownAndHorizontal; } for (auto i = j + 1; i < colsWidth.size(); i++) { for (auto l = 0u; l < colsWidth[i]; l++) { - printString += BoxDrawingCharacters::Horizontal; + printString += tableDrawingCharacters->Horizontal; } if (i != colsWidth.size() - 1) { - printString += BoxDrawingCharacters::DownAndHorizontal; + printString += tableDrawingCharacters->DownAndHorizontal; } } - printString += BoxDrawingCharacters::DownAndLeft; + printString += tableDrawingCharacters->DownAndLeft; printf("%s\n", printString.c_str()); } if (queryResult.getNumColumns() != 0 && !queryResult.getColumnNames()[0].empty()) { std::string printString; + printString += tableDrawingCharacters->Vertical; for (auto i = 0u; i < k; i++) { std::string columnName = queryResult.getColumnNames()[i]; if (columnName.length() > colsWidth[i] - 2) { - columnName = columnName.substr(0, colsWidth[i] - 5) + "..."; + columnName = + columnName.substr(0, colsWidth[i] - 5) + tableDrawingCharacters->Truncation; } - printString += BoxDrawingCharacters::Vertical; printString += " "; printString += columnName; printString += std::string(colsWidth[i] - columnName.length() - 1, ' '); + if (i != k - 1) { + printString += tableDrawingCharacters->Vertical; + } } if (j >= k) { - printString += BoxDrawingCharacters::Vertical; - printString += " ... "; + printString += tableDrawingCharacters->Vertical; + printString += " "; + printString += tableDrawingCharacters->Truncation; + printString += " "; } for (auto i = j + 1; i < colsWidth.size(); i++) { std::string columnName = queryResult.getColumnNames()[i]; if (columnName.length() > colsWidth[i] - 2) { - columnName = columnName.substr(0, colsWidth[i] - 5) + "..."; + columnName = + columnName.substr(0, colsWidth[i] - 5) + tableDrawingCharacters->Truncation; } - printString += BoxDrawingCharacters::Vertical; + printString += tableDrawingCharacters->Vertical; printString += " "; printString += columnName; printString += std::string(colsWidth[i] - columnName.length() - 1, ' '); } - printString += BoxDrawingCharacters::Vertical; - printString += "\n"; - - for (auto i = 0u; i < k; i++) { - std::string columnType = queryResult.getColumnDataTypes()[i].toString(); - if (columnType.length() > colsWidth[i] - 2) { - columnType = columnType.substr(0, colsWidth[i] - 5) + "..."; + printString += tableDrawingCharacters->Vertical; + if (tableDrawingCharacters->Types) { + printString += "\n"; + printString += tableDrawingCharacters->Vertical; + for (auto i = 0u; i < k; i++) { + std::string columnType = queryResult.getColumnDataTypes()[i].toString(); + if (columnType.length() > colsWidth[i] - 2) { + columnType = columnType.substr(0, colsWidth[i] - 5) + + tableDrawingCharacters->Truncation; + } + printString += " "; + printString += columnType; + printString += std::string(colsWidth[i] - columnType.length() - 1, ' '); + if (i != k - 1) { + printString += tableDrawingCharacters->Vertical; + } } - printString += BoxDrawingCharacters::Vertical; - printString += " "; - printString += columnType; - printString += std::string(colsWidth[i] - columnType.length() - 1, ' '); - } - if (j >= k) { - printString += BoxDrawingCharacters::Vertical; - printString += " "; - } - for (auto i = j + 1; i < colsWidth.size(); i++) { - std::string columnType = queryResult.getColumnDataTypes()[i].toString(); - if (columnType.length() > colsWidth[i] - 2) { - columnType = columnType.substr(0, colsWidth[i] - 5) + "..."; + if (j >= k) { + printString += tableDrawingCharacters->Vertical; + printString += " "; } - printString += BoxDrawingCharacters::Vertical; - printString += " "; - printString += columnType; - printString += std::string(colsWidth[i] - columnType.length() - 1, ' '); + for (auto i = j + 1; i < colsWidth.size(); i++) { + std::string columnType = queryResult.getColumnDataTypes()[i].toString(); + if (columnType.length() > colsWidth[i] - 2) { + columnType = columnType.substr(0, colsWidth[i] - 5) + + tableDrawingCharacters->Truncation; + } + printString += tableDrawingCharacters->Vertical; + printString += " "; + printString += columnType; + printString += std::string(colsWidth[i] - columnType.length() - 1, ' '); + } + printString += tableDrawingCharacters->Vertical; } - printString += BoxDrawingCharacters::Vertical; printf("%s\n", printString.c_str()); } if (queryResult.getNumColumns() != 0) { std::string printString; - printString += BoxDrawingCharacters::VerticalAndRight; + printString += tableDrawingCharacters->VerticalAndRight; for (auto i = 0u; i < k; i++) { for (auto l = 0u; l < colsWidth[i]; l++) { - printString += BoxDrawingCharacters::Horizontal; + printString += tableDrawingCharacters->Horizontal; } if (i != colsWidth.size() - 1) { - printString += BoxDrawingCharacters::VerticalAndHorizontal; + printString += tableDrawingCharacters->VerticalAndHorizontal; } } if (j >= k) { - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::VerticalAndHorizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->VerticalAndHorizontal; } for (auto i = j + 1; i < colsWidth.size(); i++) { for (auto l = 0u; l < colsWidth[i]; l++) { - printString += BoxDrawingCharacters::Horizontal; + printString += tableDrawingCharacters->Horizontal; } if (i != colsWidth.size() - 1) { - printString += BoxDrawingCharacters::VerticalAndHorizontal; + printString += tableDrawingCharacters->VerticalAndHorizontal; } } - printString += BoxDrawingCharacters::VerticalAndLeft; + printString += tableDrawingCharacters->VerticalAndLeft; printf("%s\n", printString.c_str()); } @@ -744,34 +1026,36 @@ void EmbeddedShell::printExecutionResult(QueryResult& queryResult) const { rowTruncated = true; for (auto i = 0u; i < 3u; i++) { std::string printString; - printString += BoxDrawingCharacters::Vertical; + printString += tableDrawingCharacters->Vertical; for (auto l = 0u; l < k; l++) { uint32_t spacesToPrint = (colsWidth[l] / 2); printString += std::string(spacesToPrint - 1, ' '); - printString += BoxDrawingCharacters::MiddleDot; - + printString += tableDrawingCharacters->MiddleDot; if (colsWidth[l] % 2 == 1) { printString += " "; } printString += std::string(spacesToPrint, ' '); - printString += BoxDrawingCharacters::Vertical; + if (l != k - 1) { + printString += tableDrawingCharacters->Vertical; + } } if (j >= k) { + printString += tableDrawingCharacters->Vertical; printString += " "; - printString += BoxDrawingCharacters::MiddleDot; + printString += tableDrawingCharacters->MiddleDot; printString += " "; - printString += BoxDrawingCharacters::Vertical; } for (auto l = j + 1; l < colsWidth.size(); l++) { uint32_t spacesToPrint = (colsWidth[l] / 2); + printString += tableDrawingCharacters->Vertical; printString += std::string(spacesToPrint - 1, ' '); - printString += BoxDrawingCharacters::MiddleDot; + printString += tableDrawingCharacters->MiddleDot; if (colsWidth[l] % 2 == 1) { printString += " "; } printString += std::string(spacesToPrint, ' '); - printString += BoxDrawingCharacters::Vertical; } + printString += tableDrawingCharacters->Vertical; printf("%s\n", printString.c_str()); } } @@ -779,7 +1063,9 @@ void EmbeddedShell::printExecutionResult(QueryResult& queryResult) const { continue; } auto tuple = queryResult.getNext(); - auto result = tuple->toString(colsWidth, " ", maxWidth); + auto result = + tuple->toString(colsWidth, tableDrawingCharacters->TupleDelimiter, maxWidth); + std::string printString; uint64_t startPos = 0; std::vector colResults; for (auto i = 0u; i < colsWidth.size(); i++) { @@ -793,76 +1079,82 @@ void EmbeddedShell::printExecutionResult(QueryResult& queryResult) const { // new start position is after the | seperating results startPos = chrIter + 1; } - std::string printString; - printString += BoxDrawingCharacters::Vertical; + printString += tableDrawingCharacters->Vertical; for (auto i = 0u; i < k; i++) { printString += colResults[i]; - printString += BoxDrawingCharacters::Vertical; + if (i != k - 1) { + printString += tableDrawingCharacters->Vertical; + } } if (j >= k) { - printString += " ... "; - printString += BoxDrawingCharacters::Vertical; + printString += tableDrawingCharacters->Vertical; + printString += " "; + printString += tableDrawingCharacters->Truncation; + printString += " "; } for (auto i = j + 1; i < colResults.size(); i++) { + printString += tableDrawingCharacters->Vertical; printString += colResults[i]; - printString += BoxDrawingCharacters::Vertical; } + printString += tableDrawingCharacters->Vertical; printf("%s\n", printString.c_str()); rowCount++; } - if (queryResult.getNumColumns() != 0) { + if (queryResult.getNumColumns() != 0 && tableDrawingCharacters->BottomLine) { std::string printString; - printString += BoxDrawingCharacters::UpAndRight; + printString += tableDrawingCharacters->UpAndRight; for (auto i = 0u; i < k; i++) { for (auto l = 0u; l < colsWidth[i]; l++) { - printString += BoxDrawingCharacters::Horizontal; + printString += tableDrawingCharacters->Horizontal; } if (i != colsWidth.size() - 1) { - printString += BoxDrawingCharacters::UpAndHorizontal; + printString += tableDrawingCharacters->UpAndHorizontal; } } if (j >= k) { - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::Horizontal; - printString += BoxDrawingCharacters::UpAndHorizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->Horizontal; + printString += tableDrawingCharacters->UpAndHorizontal; } for (auto i = j + 1; i < colsWidth.size(); i++) { for (auto l = 0u; l < colsWidth[i]; l++) { - printString += BoxDrawingCharacters::Horizontal; + printString += tableDrawingCharacters->Horizontal; } if (i != colsWidth.size() - 1) { - printString += BoxDrawingCharacters::UpAndHorizontal; + printString += tableDrawingCharacters->UpAndHorizontal; } } - printString += BoxDrawingCharacters::UpAndLeft; + printString += tableDrawingCharacters->UpAndLeft; printf("%s\n", printString.c_str()); } // print query result (numFlatTuples & tuples) - if (numTuples == 1) { - printf("(1 tuple)\n"); - } else { - printf("(%" PRIu64 " tuples", numTuples); - if (rowTruncated) { - printf(", %" PRIu64 " shown", maxRowSize); + if (stats) { + if (numTuples == 1) { + printf("(1 tuple)\n"); + } else { + printf("(%" PRIu64 " tuples", numTuples); + if (rowTruncated) { + printf(", %" PRIu64 " shown", maxRowSize); + } + printf(")\n"); } - printf(")\n"); - } - if (colsWidth.size() == 1) { - printf("(1 column)\n"); - } else { - printf("(%" PRIu64 " columns", (uint64_t)colsWidth.size()); - if (colTruncated) { - printf(", %" PRIu64 " shown", colsPrinted); + if (colsWidth.size() == 1) { + printf("(1 column)\n"); + } else { + printf("(%" PRIu64 " columns", (uint64_t)colsWidth.size()); + if (colTruncated) { + printf(", %" PRIu64 " shown", colsPrinted); + } + printf(")\n"); } - printf(")\n"); + printf("Time: %.2fms (compiling), %.2fms (executing)\n", + querySummary->getCompilingTime(), querySummary->getExecutionTime()); } - printf("Time: %.2fms (compiling), %.2fms (executing)\n", querySummary->getCompilingTime(), - querySummary->getExecutionTime()); } } diff --git a/tools/shell/include/embedded_shell.h b/tools/shell/include/embedded_shell.h index caac2db4ab..7e5b7f34eb 100644 --- a/tools/shell/include/embedded_shell.h +++ b/tools/shell/include/embedded_shell.h @@ -2,10 +2,21 @@ #include "linenoise.h" #include "main/kuzu.h" +#include "output.h" namespace kuzu { namespace main { +const int defaultMaxRows = 20; + +struct ShellConfig { + const char* path_to_history = ""; + uint64_t maxRowSize = defaultMaxRows; + uint32_t maxPrintWidth = 0; + std::unique_ptr drawingCharacters = std::make_unique(); + bool stats = true; +}; + /** * Embedded shell simulate a session that directly connects to the system. */ @@ -13,7 +24,7 @@ class EmbeddedShell { public: EmbeddedShell(std::shared_ptr database, std::shared_ptr conn, - const char* pathToHistory); + ShellConfig& shellConfig); void run(); @@ -26,18 +37,34 @@ class EmbeddedShell { void printExecutionResult(QueryResult& queryResult) const; + void printTruncatedExecutionResult(QueryResult& queryResult) const; + + std::string printJsonExecutionResult(QueryResult& queryResult) const; + + std::string printHtmlExecutionResult(QueryResult& queryResult) const; + + std::string printLatexExecutionResult(QueryResult& queryResult) const; + + std::string printLineExecutionResult(QueryResult& queryResult) const; + void updateTableNames(); void setMaxRows(const std::string& maxRowsString); void setMaxWidth(const std::string& maxWidthString); + void setMode(const std::string& modeString); + + void setStats(const std::string& statsString); + private: std::shared_ptr database; std::shared_ptr conn; const char* path_to_history; uint64_t maxRowSize; uint32_t maxPrintWidth; + std::unique_ptr drawingCharacters; + bool stats; }; } // namespace main diff --git a/tools/shell/include/output.h b/tools/shell/include/output.h new file mode 100644 index 0000000000..cdc9478c05 --- /dev/null +++ b/tools/shell/include/output.h @@ -0,0 +1,231 @@ +#include +#include +#include + +namespace kuzu { +namespace main { + +enum class PrintType : uint8_t { + BOX, + TABLE, + CSV, + TSV, + MARKDOWN, + COLUMN, + LIST, + TRASH, + JSON, + JSONLINES, + HTML, + LATEX, + LINE, + UNKNOWN, +}; + +struct PrintTypeUtils { + static PrintType fromString(const std::string& str) { + std::string strLower = str; + std::transform(strLower.begin(), strLower.end(), strLower.begin(), ::tolower); + if (str == "box") { + return PrintType::BOX; + } else if (str == "table") { + return PrintType::TABLE; + } else if (str == "csv") { + return PrintType::CSV; + } else if (str == "tsv") { + return PrintType::TSV; + } else if (str == "markdown") { + return PrintType::MARKDOWN; + } else if (str == "column") { + return PrintType::COLUMN; + } else if (str == "list") { + return PrintType::LIST; + } else if (str == "trash") { + return PrintType::TRASH; + } else if (str == "json") { + return PrintType::JSON; + } else if (str == "jsonlines") { + return PrintType::JSONLINES; + } else if (str == "html") { + return PrintType::HTML; + } else if (str == "latex") { + return PrintType::LATEX; + } else if (str == "line") { + return PrintType::LINE; + } + return PrintType::UNKNOWN; + } + + static bool isTableType(PrintType type) { + return type == PrintType::TABLE || type == PrintType::BOX || type == PrintType::MARKDOWN || + type == PrintType::COLUMN; + } +}; + +struct DrawingCharacters { + const PrintType printType; + const char* TupleDelimiter = " "; + + virtual ~DrawingCharacters() = default; + +protected: + explicit DrawingCharacters(PrintType pt) : printType(pt) {} +}; + +struct BaseTableDrawingCharacters : public DrawingCharacters { + const char* DownAndRight = ""; + const char* Horizontal = ""; + const char* DownAndHorizontal = ""; + const char* DownAndLeft = ""; + const char* Vertical = ""; + const char* VerticalAndRight = ""; + const char* VerticalAndHorizontal = ""; + const char* VerticalAndLeft = ""; + const char* UpAndRight = ""; + const char* UpAndHorizontal = ""; + const char* UpAndLeft = ""; + const char* MiddleDot = "."; + const char* Truncation = "..."; + bool TopLine = true; + bool BottomLine = true; + bool Types = true; + +protected: + explicit BaseTableDrawingCharacters(PrintType pt) : DrawingCharacters(pt){}; +}; + +struct BoxDrawingCharacters : public BaseTableDrawingCharacters { + BoxDrawingCharacters() : BaseTableDrawingCharacters(PrintType::BOX) { + DownAndRight = "\u250C"; + Horizontal = "\u2500"; + DownAndHorizontal = "\u252C"; + DownAndLeft = "\u2510"; + Vertical = "\u2502"; + VerticalAndRight = "\u251C"; + VerticalAndHorizontal = "\u253C"; + VerticalAndLeft = "\u2524"; + UpAndRight = "\u2514"; + UpAndHorizontal = "\u2534"; + UpAndLeft = "\u2518"; + MiddleDot = "\u00B7"; + } +}; + +struct TableDrawingCharacters : public BaseTableDrawingCharacters { + TableDrawingCharacters() : BaseTableDrawingCharacters(PrintType::TABLE) { + DownAndRight = "+"; + Horizontal = "-"; + DownAndHorizontal = "+"; + DownAndLeft = "+"; + Vertical = "|"; + VerticalAndRight = "+"; + VerticalAndHorizontal = "+"; + VerticalAndLeft = "+"; + UpAndRight = "+"; + UpAndHorizontal = "+"; + UpAndLeft = "+"; + } +}; + +struct CSVDrawingCharacters : public DrawingCharacters { + CSVDrawingCharacters() : DrawingCharacters(PrintType::CSV) { TupleDelimiter = ","; } +}; + +struct TSVDrawingCharacters : public DrawingCharacters { + TSVDrawingCharacters() : DrawingCharacters(PrintType::TSV) { TupleDelimiter = "\t"; } +}; + +struct MarkdownDrawingCharacters : public BaseTableDrawingCharacters { + MarkdownDrawingCharacters() : BaseTableDrawingCharacters(PrintType::MARKDOWN) { + DownAndRight = "|"; + Horizontal = "-"; + DownAndHorizontal = "|"; + DownAndLeft = "|"; + Vertical = "|"; + VerticalAndRight = "|"; + VerticalAndHorizontal = "|"; + VerticalAndLeft = "|"; + UpAndRight = "|"; + UpAndHorizontal = "|"; + UpAndLeft = "|"; + TopLine = false; + BottomLine = false; + Types = false; + } +}; + +struct ColumnDrawingCharacters : public BaseTableDrawingCharacters { + ColumnDrawingCharacters() : BaseTableDrawingCharacters(PrintType::COLUMN) { + DownAndRight = ""; + Horizontal = "-"; + DownAndHorizontal = ""; + DownAndLeft = ""; + Vertical = " "; + VerticalAndRight = " "; + VerticalAndHorizontal = " "; + VerticalAndLeft = ""; + UpAndRight = ""; + UpAndHorizontal = ""; + UpAndLeft = ""; + TopLine = false; + BottomLine = false; + } +}; + +struct ListDrawingCharacters : public DrawingCharacters { + ListDrawingCharacters() : DrawingCharacters(PrintType::LIST) { TupleDelimiter = "|"; } +}; + +struct TrashDrawingCharacters : public DrawingCharacters { + TrashDrawingCharacters() : DrawingCharacters(PrintType::TRASH) {} +}; + +struct JSONDrawingCharacters : public DrawingCharacters { + const char* ArrayOpen = "["; + const char* ArrayClose = "]"; + const char* ObjectOpen = "{"; + const char* ObjectClose = "}"; + const char* KeyValue = "\""; + const char* KeyDelimiter = ":"; + JSONDrawingCharacters() : DrawingCharacters(PrintType::JSON) { TupleDelimiter = ","; } + +protected: + explicit JSONDrawingCharacters(PrintType pt) : DrawingCharacters(pt) {} +}; + +struct JSONLinesDrawingCharacters : public JSONDrawingCharacters { + JSONLinesDrawingCharacters() : JSONDrawingCharacters(PrintType::JSONLINES) { + TupleDelimiter = ","; + } +}; + +struct HTMLDrawingCharacters : public DrawingCharacters { + const char* TableOpen = ""; + const char* TableClose = "
"; + const char* RowOpen = ""; + const char* RowClose = ""; + const char* CellOpen = ""; + const char* CellClose = ""; + const char* HeaderOpen = ""; + const char* HeaderClose = ""; + const char* LineBreak = "
"; + HTMLDrawingCharacters() : DrawingCharacters(PrintType::HTML) {} +}; + +struct LatexDrawingCharacters : public DrawingCharacters { + const char* TableOpen = "\\begin{tabular}"; + const char* TableClose = "\\end{tabular}"; + const char* AlignOpen = "{"; + const char* AlignClose = "}"; + const char* Line = "\\hline"; + const char* EndLine = "\\\\"; + const char* ColumnAlign = "l"; + LatexDrawingCharacters() : DrawingCharacters(PrintType::LATEX) { TupleDelimiter = "&"; } +}; + +struct LineDrawingCharacters : public DrawingCharacters { + LineDrawingCharacters() : DrawingCharacters(PrintType::LINE) { TupleDelimiter = " = "; } +}; + +} // namespace main +} // namespace kuzu diff --git a/tools/shell/shell_runner.cpp b/tools/shell/shell_runner.cpp index 6b87c9ea58..f8537b39bf 100644 --- a/tools/shell/shell_runner.cpp +++ b/tools/shell/shell_runner.cpp @@ -10,6 +10,40 @@ using namespace kuzu::main; using namespace kuzu::common; +int setConfigOutputMode(const std::string mode, ShellConfig& shell) { + if (mode == "box") { + shell.drawingCharacters = std::make_unique(); + } else if (mode == "table") { + shell.drawingCharacters = std::make_unique(); + } else if (mode == "csv") { + shell.drawingCharacters = std::make_unique(); + } else if (mode == "tsv") { + shell.drawingCharacters = std::make_unique(); + } else if (mode == "markdown") { + shell.drawingCharacters = std::make_unique(); + } else if (mode == "column") { + shell.drawingCharacters = std::make_unique(); + } else if (mode == "list") { + shell.drawingCharacters = std::make_unique(); + } else if (mode == "trash") { + shell.drawingCharacters = std::make_unique(); + } else if (mode == "json") { + shell.drawingCharacters = std::make_unique(); + } else if (mode == "jsonlines") { + shell.drawingCharacters = std::make_unique(); + } else if (mode == "html") { + shell.drawingCharacters = std::make_unique(); + } else if (mode == "latex") { + shell.drawingCharacters = std::make_unique(); + } else if (mode == "line") { + shell.drawingCharacters = std::make_unique(); + } else { + std::cerr << "Cannot parse '" << mode << "' as output mode." << '\n'; + return 1; + } + return 0; +} + int main(int argc, char* argv[]) { args::ArgumentParser parser("KuzuDB Shell"); args::Positional inputDirFlag(parser, "databasePath", @@ -26,6 +60,9 @@ int main(int argc, char* argv[]) { args::ValueFlag historyPathFlag(parser, "", "Path to directory for shell history", {'p', "path_history"}); args::Flag version(parser, "version", "Display current database version", {'v', "version"}); + args::ValueFlag mode(parser, "mode", "Set the output mode of the shell", + {'m', "mode"}); + args::Flag stats(parser, "no_stats", "Disable query stats", {'s', "no_stats", "nostats"}); std::vector lCaseArgsStrings; for (auto i = 0; i < argc; ++i) { @@ -85,6 +122,17 @@ int main(int argc, char* argv[]) { return 1; } + ShellConfig shellConfig; + shellConfig.path_to_history = pathToHistory.c_str(); + if (mode) { + if (setConfigOutputMode(args::get(mode), shellConfig) != 0) { + return 1; + } + } + if (stats) { + shellConfig.stats = false; + } + auto databasePath = args::get(inputDirFlag); std::shared_ptr database; std::shared_ptr conn; @@ -96,14 +144,14 @@ int main(int argc, char* argv[]) { return 1; } if (DBConfig::isDBPathInMemory(databasePath)) { - std::cout << "Opened the database under in in-memory mode." << '\n'; + std::cout << "Opened the database under in-memory mode." << '\n'; } else { std::cout << "Opened the database at path: " << databasePath << " in " << (readOnlyMode ? "read-only mode" : "read-write mode") << "." << '\n'; } std::cout << "Enter \":help\" for usage hints." << '\n' << std::flush; try { - auto shell = EmbeddedShell(database, conn, pathToHistory.c_str()); + auto shell = EmbeddedShell(database, conn, shellConfig); shell.run(); } catch (std::exception& e) { std::cerr << e.what() << '\n'; diff --git a/tools/shell/test/test_shell_commands.py b/tools/shell/test/test_shell_commands.py index 2054848444..73c175a47f 100644 --- a/tools/shell/test/test_shell_commands.py +++ b/tools/shell/test/test_shell_commands.py @@ -11,6 +11,8 @@ def test_help(temp_db) -> None: " :quit exit from shell", " :max_rows [max_rows] set maximum number of rows for display (default: 20)", " :max_width [max_width] set maximum width in characters for display", + " :mode [mode] set output mode (default: box)", + " :stats [on|off] toggle query stats on or off", "", " Note: you can change and see several system configurations, such as num-threads, ", " timeout, and progress_bar using Cypher CALL statements.", @@ -158,6 +160,260 @@ def test_max_width(temp_db, csv_path) -> None: result.check_stdout("(1 column)") +def output_mode_verification(result) -> None: + result.check_stdout( + [ + "Available output modes:", + " box (default): Tables using unicode box-drawing characters", + " column: Output in columns", + " csv: Comma-separated values", + " html: HTML table", + " json: Results in a JSON array", + " jsonlines: Results in a NDJSON format", + " latex: LaTeX tabular environment code", + " line: One value per line", + " list: Values delimited by \"|\"", + " markdown: Markdown table", + " table: Tables using ASCII characters", + " tsv: Tab-separated values", + " trash: No output", + ], + ) + + +def test_set_mode(temp_db) -> None: + # test default mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("\u2502 a \u2502 b \u2502") + result.check_stdout("\u2502 Databases Rule \u2502 kuzu is cool \u2502") + + # test column mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode column") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("a b") + result.check_stdout("Databases Rule kuzu is cool") + + # test csv mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode csv") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("a,b") + result.check_stdout("Databases Rule,kuzu is cool") + + # test box mode + test = ( + ShellTest() + .add_argument(temp_db) + # box is default so need to switch to another mode first + .statement(":mode csv") + .statement(":mode box") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("\u2502 a \u2502 b \u2502") + result.check_stdout("\u2502 Databases Rule \u2502 kuzu is cool \u2502") + + # test html mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode html") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("") + result.check_stdout("") + result.check_stdout("") + result.check_stdout("") + result.check_stdout("") + result.check_stdout("") + result.check_stdout("") + result.check_stdout("
ab
Databases Rulekuzu is cool
") + + # test json mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode json") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout('[{"a":"Databases Rule","b":"kuzu is cool"}]') + + # test jsonlines mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode jsonlines") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout('{"a":"Databases Rule","b":"kuzu is cool"}') + + # test latex mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode latex") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("\\begin{tabular}{ll}") + result.check_stdout("\\hline") + result.check_stdout("a&b\\\\") + result.check_stdout("\\hline") + result.check_stdout("Databases Rule&kuzu is cool\\\\") + result.check_stdout("\\hline") + result.check_stdout("\\end{tabular}") + + # test line mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode line") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("a = Databases Rule") + result.check_stdout("b = kuzu is cool") + + # test list mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode list") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("a|b") + result.check_stdout("Databases Rule|kuzu is cool") + + # test markdown mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode markdown") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("| a | b |") + result.check_stdout("| Databases Rule | kuzu is cool |") + + # test table mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode table") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("| a | b |") + result.check_stdout("| Databases Rule | kuzu is cool |") + + # test tsv mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode tsv") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("a\tb") + result.check_stdout("Databases Rule\tkuzu is cool") + + # test trash mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode trash") + .statement('RETURN RANGE(0, 10) AS a;') + ) + result = test.run() + result.check_not_stdout("[0,1,2,3,4,5,6,7,8,9,10]") + + # test mode info + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode invalid") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + output_mode_verification(result) + + # test invalid mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":mode invalid") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("Cannot parse 'invalid' as output mode.") + output_mode_verification(result) + + +def test_stats(temp_db) -> None: + # test stats default + test = ( + ShellTest() + .add_argument(temp_db) + .statement('RETURN "Databases Rule" AS a;') + ) + result = test.run() + result.check_stdout("(1 tuple)") + result.check_stdout("(1 column)") + result.check_stdout("Time: ") + + # test stats off + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":stats off") + .statement('RETURN "Databases Rule" AS a;') + ) + result = test.run() + result.check_not_stdout("(1 tuple)") + result.check_not_stdout("(1 column)") + result.check_not_stdout("Time: ") + + # test stats on + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":stats on") + .statement('RETURN "Databases Rule" AS a;') + ) + result = test.run() + result.check_stdout("(1 tuple)") + result.check_stdout("(1 column)") + result.check_stdout("Time: ") + + # test stats invalid + test = ( + ShellTest() + .add_argument(temp_db) + .statement(":stats invalid") + .statement('RETURN "Databases Rule" AS a;') + ) + result = test.run() + result.check_stdout("Cannot parse 'invalid' to toggle stats. Expect 'on' or 'off'.") + + def test_bad_command(temp_db) -> None: test = ShellTest().add_argument(temp_db).statement(":maxrows").statement(":quiy").statement("clearr;") result = test.run() diff --git a/tools/shell/test/test_shell_flags.py b/tools/shell/test/test_shell_flags.py index 00324402de..3225e2e2f9 100644 --- a/tools/shell/test/test_shell_flags.py +++ b/tools/shell/test/test_shell_flags.py @@ -10,7 +10,7 @@ def test_database_path(temp_db) -> None: # no database path test = ShellTest() result = test.run() - result.check_stdout("Opened the database under in in-memory mode.") + result.check_stdout("Opened the database under in-memory mode.") # valid database path test = ShellTest().add_argument(temp_db).statement('RETURN "databases rule" AS a;') @@ -163,6 +163,243 @@ def test_version(temp_db, flag) -> None: result.check_stdout(KUZU_VERSION) +@pytest.mark.parametrize( + "flag", + [ + "-m", + "--mode", + ], +) +def test_mode(temp_db, flag) -> None: + # test default mode + test = ( + ShellTest() + .add_argument(temp_db) + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("\u2502 a \u2502 b \u2502") + result.check_stdout("\u2502 Databases Rule \u2502 kuzu is cool \u2502") + + # test column mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("column") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("a b") + result.check_stdout("Databases Rule kuzu is cool") + + # test csv mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("csv") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("a,b") + result.check_stdout("Databases Rule,kuzu is cool") + + # test box mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("box") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("\u2502 a \u2502 b \u2502") + result.check_stdout("\u2502 Databases Rule \u2502 kuzu is cool \u2502") + + # test html mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("html") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("") + result.check_stdout("") + result.check_stdout("") + result.check_stdout("") + result.check_stdout("") + result.check_stdout("") + result.check_stdout("") + result.check_stdout("
ab
Databases Rulekuzu is cool
") + + # test json mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("json") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout('[{"a":"Databases Rule","b":"kuzu is cool"}]') + + # test jsonlines mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("jsonlines") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout('{"a":"Databases Rule","b":"kuzu is cool"}') + + # test latex mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("latex") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("\\begin{tabular}{ll}") + result.check_stdout("\\hline") + result.check_stdout("a&b\\\\") + result.check_stdout("\\hline") + result.check_stdout("Databases Rule&kuzu is cool\\\\") + result.check_stdout("\\hline") + result.check_stdout("\\end{tabular}") + + # test line mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("line") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("a = Databases Rule") + result.check_stdout("b = kuzu is cool") + + # test list mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("list") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("a|b") + result.check_stdout("Databases Rule|kuzu is cool") + + # test markdown mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("markdown") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("| a | b |") + result.check_stdout("| Databases Rule | kuzu is cool |") + + # test table mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("table") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("| a | b |") + result.check_stdout("| Databases Rule | kuzu is cool |") + + # test tsv mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("tsv") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stdout("a\tb") + result.check_stdout("Databases Rule\tkuzu is cool") + + # test trash mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("trash") + .statement('RETURN RANGE(0, 10) AS a;') + ) + result = test.run() + result.check_not_stdout("[0,1,2,3,4,5,6,7,8,9,10]") + + # test mode info + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stderr(f"Flag '{flag.replace('-', '')}' requires an argument but received none") + + # test invalid mode + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .add_argument("invalid") + .statement('RETURN "Databases Rule" AS a, "kuzu is cool" AS b;') + ) + result = test.run() + result.check_stderr("Cannot parse 'invalid' as output mode.") + + +@pytest.mark.parametrize( + "flag", + [ + "-s", + "--nostats", + "--no_stats", + ], +) +def test_no_stats(temp_db, flag) -> None: + # test stats off + test = ( + ShellTest() + .add_argument(temp_db) + .add_argument(flag) + .statement('RETURN "Databases Rule" AS a;') + ) + result = test.run() + result.check_not_stdout("(1 tuple)") + result.check_not_stdout("(1 column)") + result.check_not_stdout("Time: ") + + # test stats on + test = ( + ShellTest() + .add_argument(temp_db) + .statement('RETURN "Databases Rule" AS a;') + ) + result = test.run() + result.check_stdout("(1 tuple)") + result.check_stdout("(1 column)") + result.check_stdout("Time: ") + + def test_bad_flag(temp_db) -> None: # without database path test = ShellTest().add_argument("-b")