/* * Copyright (C) 2019-present ScyllaDB */ /* * SPDX-License-Identifier: AGPL-3.0-or-later */ #include #include // use boost::regex instead of std::regex due // to stack overflow in debug mode #include #include "test/lib/cql_test_env.hh" #include "test/lib/cql_assertions.hh" #include #include #include #include #include "transport/messages/result_message.hh" #include "types/user.hh" #include "types/map.hh" #include "types/list.hh" #include "types/set.hh" #include "db/config.hh" #include "db/paxos_grace_seconds_extension.hh" #include "cql3/cql_config.hh" #include "cql3/type_json.hh" #include "cql3/column_identifier.hh" #include "test/lib/exception_utils.hh" #include "alternator/tags_extension.hh" #include "cdc/cdc_extension.hh" #include // // A helper class to serialize result set output to a formatted JSON // class json_visitor final : public cql_transport::messages::result_message::visitor { Json::Value& _root; public: json_visitor(Json::Value& root) : _root(root) { } virtual void visit(const cql_transport::messages::result_message::void_message&) override { _root["status"] = "ok"; } virtual void visit(const cql_transport::messages::result_message::set_keyspace& m) override { _root["status"] = "ok"; } virtual void visit(const cql_transport::messages::result_message::prepared::cql& m) override { _root["status"] = "ok"; } virtual void visit(const cql_transport::messages::result_message::prepared::thrift& m) override { assert(false); } virtual void visit(const cql_transport::messages::result_message::schema_change& m) override { _root["status"] = "ok"; } virtual void visit(const cql_transport::messages::result_message::bounce_to_shard& m) override { assert(false); } virtual void visit(const cql_transport::messages::result_message::exception& m) override { m.throw_me(); } virtual void visit(const cql_transport::messages::result_message::rows& m) override { Json::Value& output_rows = _root["rows"]; const auto input_rows = m.rs().result_set().rows(); const auto& meta = m.rs().result_set().get_metadata().get_names(); for (auto&& in_row: input_rows) { Json::Value out_row; for (unsigned i = 0; i < meta.size(); ++i) { const cql3::column_specification& col = *meta[i]; const bytes_opt& cell = in_row[i]; if (cell.has_value()) { out_row[col.name->text()] = fmt::format("{}", to_json_string(*col.type, cell)); } } output_rows.append(out_row); } } }; // Prepare query_options with serial consistency std::unique_ptr repl_options() { const auto& so = cql3::query_options::specific_options::DEFAULT; auto qo = std::make_unique( db::consistency_level::ONE, std::vector{}, // Ensure (optional) serial consistency is always specified. cql3::query_options::specific_options{ so.page_size, so.state, db::consistency_level::SERIAL, so.timestamp, } ); return qo; } // Read-evaluate-print-loop for CQL void repl(seastar::app_template& app) { auto ext = std::make_shared(); ext->add_schema_extension(alternator::tags_extension::NAME); ext->add_schema_extension(cdc::cdc_extension::NAME); ext->add_schema_extension(db::paxos_grace_seconds_extension::NAME); auto db_cfg = ::make_shared(std::move(ext)); db_cfg->enable_user_defined_functions({true}, db::config::config_source::CommandLine); db_cfg->experimental_features(db::experimental_features_t::all(), db::config::config_source::CommandLine); do_with_cql_env_thread([] (cql_test_env& e) { // Comments allowed by CQL - -- and // const boost::regex comment_re("^[[:space:]]*((--|//).*)?$"); // A comment is not a delimiter even if ends with one const boost::regex delimiter_re("^(?![[:space:]]*(--|//)).*;[[:space:]]*$"); while (std::cin) { std::string line; std::ostringstream stmt; if (!std::getline(std::cin, line)) { break; } // Handle multiline input and comments if (boost::regex_match(line.begin(), line.end(), comment_re)) { std::cout << line << std::endl; continue; } stmt << line << std::endl; while (!boost::regex_match(line.begin(), line.end(), delimiter_re)) { // Read the rest of input until delimiter or EOF if (!std::getline(std::cin, line)) { break; } stmt << line << std::endl; } // Print the statement std::cout << stmt.str(); Json::Value json; auto execute = [&json, &stmt, &e] () mutable { return seastar::async([&] () mutable -> std::optional { auto qo = repl_options(); auto msg = e.execute_cql(stmt.str(), std::move(qo)).get0(); if (msg->move_to_shard()) { return *msg->move_to_shard(); } json_visitor visitor(json); msg->accept(visitor); return std::nullopt; }); }; try { auto shard = execute().get0(); if (shard) { smp::submit_to(*shard, std::move(execute)).get(); } } catch (std::exception& e) { json["status"] = "error"; json["message"] = fmt::format("{}", e); } std::cout << json << std::endl; } }, db_cfg).get0(); } // Reset stdin/stdout/log streams to locations pointed // on the command line. void apply_configuration(const boost::program_options::variables_map& cfg) { if (cfg.contains("input")) { static std::ifstream input(cfg["input"].as()); std::cin.rdbuf(input.rdbuf()); } FILE *redirect_cerr = fopen(cfg["log"].as().c_str(), "w"); if (redirect_cerr == NULL) { throw std::system_error(errno, std::iostream_category(), "Failed to open the log file"); } dup2(fileno(redirect_cerr), STDERR_FILENO); fclose(redirect_cerr); if (cfg.contains("output")) { FILE *redirect_cout = fopen(cfg["output"].as().c_str(), "w"); if (redirect_cout == NULL) { throw std::system_error(errno, std::iostream_category(), "Failed to open the output file"); } dup2(fileno(redirect_cout), STDOUT_FILENO); fclose(redirect_cout); } } int main(int argc, char* argv[]) { namespace bpo = boost::program_options; namespace fs = std::filesystem; seastar::app_template::config cfg; cfg.name = fmt::format(R"({} - An embedded single-node version of Scylla. Runs read-evaluate-print loop, reading commands from stdin, evaluating them and printing output, formatted as JSON, to stdout. Creates a temporary database in /tmp and deletes it at exit. Pre-configures a default keyspace, naturally, with replication factor 1. Used in unit tests as a test driver for .test.cql files. Available )", argv[0]); seastar::app_template app(cfg); /* Define options for input, output and log file. */ app.add_options() ("input", bpo::value(), "Input file with CQL, defaults to stdin") ("output", bpo::value(), "Output file for data, defaults to stdout") ("log", bpo::value()->default_value( fmt::format("{}.log", fs::path(argv[0]).stem().string())), "Output file for Scylla log"); return app.run(argc, argv, [&app] { apply_configuration(app.configuration()); return seastar::async([&app] { return repl(app); }); }); }