From 5067e2edc9cbcf9abea275dd27497600a822f668 Mon Sep 17 00:00:00 2001 From: Andrey Saranchin Date: Mon, 4 Dec 2023 19:57:11 +0300 Subject: [PATCH 1/3] Client: allow to decode data several times Data contains a pair of iterators, which are considered to be light - let's copy iterator before decoding data to allow to decode the same data several times (or it can be decoded, and then encoded to send to another tarantool from router). --- src/Client/ResponseReader.hpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Client/ResponseReader.hpp b/src/Client/ResponseReader.hpp index fd0f73c32..6be644c3a 100644 --- a/src/Client/ResponseReader.hpp +++ b/src/Client/ResponseReader.hpp @@ -104,7 +104,10 @@ struct Data { template bool decode(T& tuples) { - return mpp::decode(iters.first, tuples); + it_t itr = iters.first; + bool ok = mpp::decode(itr, tuples); + assert(itr == iters.second); + return ok; } static constexpr auto mpp = &Data::iters; From 21776e28dd31bd44705e773065d10a1aec422e96 Mon Sep 17 00:00:00 2001 From: Andrey Saranchin Date: Wed, 29 Nov 2023 14:33:47 +0300 Subject: [PATCH 2/3] Client: support execute and prepare requests The patch provides client with execute and prepare requests. Execute allows to pass an SQL statement or id of a prepared statement and parameters for it. Response body is populated with sql fields. Co-authored-by: AnastasMIPT --- src/Client/Connection.hpp | 55 ++++++ src/Client/IprotoConstants.hpp | 6 + src/Client/RequestEncoder.hpp | 59 +++++++ src/Client/ResponseReader.hpp | 47 ++++- test/ClientTest.cpp | 309 ++++++++++++++++++++++++++++++++- test/cfg.lua | 12 ++ 6 files changed, 482 insertions(+), 6 deletions(-) diff --git a/src/Client/Connection.hpp b/src/Client/Connection.hpp index 14a6a3469..7715a715c 100644 --- a/src/Client/Connection.hpp +++ b/src/Client/Connection.hpp @@ -177,6 +177,33 @@ class Connection template rid_t call(const std::string &func, const T &args); rid_t ping(); + + /** + * Execute the SQL statement contained in the 'statement' parameter. + * @param statement statement, which should conform to the rules for SQL grammar + * @param parameters tuple for placeholders in the statement + * @retval request id + */ + template + rid_t execute(const std::string& statement, const T& parameters); + + /** + * Execute the SQL statement contained in the 'statement' parameter. + * @param stmt_id the statement id obtained with prepare() + * @param parameters tuple for placeholders in the statement + * @retval request id + */ + template + rid_t execute(unsigned int stmt_id, const T& parameters); + + /** + * Prepare the SQL statement contained in the 'statement' parameter. + * The syntax and requirements for Connection::prepare() are the same + * as for Connection::execute(). + * @param statement statement, which should conform to the rules for SQL grammar + * @retval request id + */ + rid_t prepare(const std::string& statement); void setError(const std::string &msg, int errno_ = 0); bool hasError() const; @@ -578,6 +605,34 @@ decodeGreeting(Connection &conn) } ////////////////////////////BOX-like interface functions//////////////////////// +template +template +rid_t +Connection::execute(const std::string& statement, const T& parameters) +{ + impl->enc.encodeExecute(statement, parameters); + impl->connector.readyToSend(*this); + return RequestEncoder::getSync(); +} + +template +template +rid_t +Connection::execute(unsigned int stmt_id, const T& parameters) +{ + impl->enc.encodeExecute(stmt_id, parameters); + impl->connector.readyToSend(*this); + return RequestEncoder::getSync(); +} + +template +rid_t +Connection::prepare(const std::string& statement) +{ + impl->enc.encodePrepare(statement); + impl->connector.readyToSend(*this); + return RequestEncoder::getSync(); +} template template diff --git a/src/Client/IprotoConstants.hpp b/src/Client/IprotoConstants.hpp index fdac67ba8..94e001333 100644 --- a/src/Client/IprotoConstants.hpp +++ b/src/Client/IprotoConstants.hpp @@ -135,6 +135,12 @@ namespace Iproto { TYPE_ERROR = 1 << 15 }; + /** Keys of IPROTO_SQL_INFO map. */ + enum SqlInfoKey { + SQL_INFO_ROW_COUNT = 0x00, + SQL_INFO_AUTOINCREMENT_IDS = 0x01 + }; + enum ErrorStack { ERROR_STACK = 0x00 }; diff --git a/src/Client/RequestEncoder.hpp b/src/Client/RequestEncoder.hpp index ba6359ae8..40fda228c 100644 --- a/src/Client/RequestEncoder.hpp +++ b/src/Client/RequestEncoder.hpp @@ -85,6 +85,11 @@ class RequestEncoder { uint32_t limit = UINT32_MAX, uint32_t offset = 0, IteratorType iterator = EQ); template + size_t encodeExecute(const std::string& statement, const T& parameters); + template + size_t encodeExecute(unsigned int stmt_id, const T& parameters); + size_t encodePrepare(const std::string& statement); + template size_t encodeCall(const std::string &func, const T &args); size_t encodeAuth(std::string_view user, std::string_view passwd, const Greeting &greet); @@ -248,6 +253,60 @@ RequestEncoder::encodeSelect(const T &key, return request_size + PREHEADER_SIZE; } +template +template +size_t +RequestEncoder::encodeExecute(const std::string& statement, const T& parameters) +{ + iterator_t request_start = m_Buf.end(); + m_Buf.write('\xce'); + m_Buf.write(uint32_t{0}); + encodeHeader(Iproto::EXECUTE); + mpp::encode(m_Buf, mpp::as_map(std::forward_as_tuple( + MPP_AS_CONST(Iproto::SQL_TEXT), statement, + MPP_AS_CONST(Iproto::SQL_BIND), parameters, + MPP_AS_CONST(Iproto::OPTIONS), std::make_tuple()))); + uint32_t request_size = (m_Buf.end() - request_start) - PREHEADER_SIZE; + ++request_start; + request_start.set(__builtin_bswap32(request_size)); + return request_size + PREHEADER_SIZE; +} + +template +template +size_t +RequestEncoder::encodeExecute(unsigned int stmt_id, const T& parameters) +{ + iterator_t request_start = m_Buf.end(); + m_Buf.write('\xce'); + m_Buf.write(uint32_t{0}); + encodeHeader(Iproto::EXECUTE); + mpp::encode(m_Buf, mpp::as_map(std::forward_as_tuple( + MPP_AS_CONST(Iproto::STMT_ID), stmt_id, + MPP_AS_CONST(Iproto::SQL_BIND), parameters, + MPP_AS_CONST(Iproto::OPTIONS), std::make_tuple()))); + uint32_t request_size = (m_Buf.end() - request_start) - PREHEADER_SIZE; + ++request_start; + request_start.set(__builtin_bswap32(request_size)); + return request_size + PREHEADER_SIZE; +} + +template +size_t +RequestEncoder::encodePrepare(const std::string& statement) +{ + iterator_t request_start = m_Buf.end(); + m_Buf.write('\xce'); + m_Buf.write(uint32_t{0}); + encodeHeader(Iproto::PREPARE); + mpp::encode(m_Buf, mpp::as_map(std::forward_as_tuple( + MPP_AS_CONST(Iproto::SQL_TEXT), statement))); + uint32_t request_size = (m_Buf.end() - request_start) - PREHEADER_SIZE; + ++request_start; + request_start.set(__builtin_bswap32(request_size)); + return request_size + PREHEADER_SIZE; +} + template template size_t diff --git a/src/Client/ResponseReader.hpp b/src/Client/ResponseReader.hpp index 6be644c3a..aa8ac5b82 100644 --- a/src/Client/ResponseReader.hpp +++ b/src/Client/ResponseReader.hpp @@ -113,15 +113,60 @@ struct Data { static constexpr auto mpp = &Data::iters; }; +struct SqlInfo +{ + uint32_t row_count = 0; + std::vector autoincrement_ids; + + static constexpr auto mpp = std::make_tuple( + std::make_pair(Iproto::SQL_INFO_ROW_COUNT, &SqlInfo::row_count), + std::make_pair(Iproto::SQL_INFO_AUTOINCREMENT_IDS, &SqlInfo::autoincrement_ids) + ); +}; + +struct ColumnMap +{ + std::string field_name; + std::string field_type; + std::string collation; + std::optional span; + bool is_nullable = false; + bool is_autoincrement = false; + + static constexpr auto mpp = std::make_tuple( + std::make_pair(Iproto::FIELD_NAME, &ColumnMap::field_name), + std::make_pair(Iproto::FIELD_TYPE, &ColumnMap::field_type), + std::make_pair(Iproto::FIELD_COLL, &ColumnMap::collation), + std::make_pair(Iproto::FIELD_SPAN, &ColumnMap::span), + std::make_pair(Iproto::FIELD_IS_NULLABLE, &ColumnMap::is_nullable), + std::make_pair(Iproto::FIELD_IS_AUTOINCREMENT, &ColumnMap::is_autoincrement) + ); +}; + +struct Metadata +{ + std::vector column_maps; + + static constexpr auto mpp = &Metadata::column_maps; +}; + template struct Body { std::optional> error_stack; std::optional> data; + std::optional sql_info; + std::optional metadata; + std::optional stmt_id; + std::optional bind_count; static constexpr auto mpp = std::make_tuple( std::make_pair(Iproto::DATA, &Body::data), std::make_pair(Iproto::ERROR, std::make_tuple(std::make_pair( - Iproto::ERROR_STACK, &Body::error_stack))) + Iproto::ERROR_STACK, &Body::error_stack))), + std::make_pair(Iproto::SQL_INFO, &Body::sql_info), + std::make_pair(Iproto::METADATA, &Body::metadata), + std::make_pair(Iproto::STMT_ID, &Body::stmt_id), + std::make_pair(Iproto::BIND_COUNT, &Body::bind_count) ); }; diff --git a/test/ClientTest.cpp b/test/ClientTest.cpp index 74af7abdf..38dd022cc 100644 --- a/test/ClientTest.cpp +++ b/test/ClientTest.cpp @@ -123,12 +123,15 @@ printResponse(Response &response, Data data = std::vector()) " code=" << err.errcode << std::endl; return; } - assert(response.body.data != std::nullopt); - if (!response.body.data->decode(data)) { - std::cerr << "FAILED TO DECODE DATA" << std::endl; - abort(); + if (response.body.data != std::nullopt) { + if (!response.body.data->decode(data)) { + std::cerr << "FAILED TO DECODE DATA" << std::endl; + abort(); + } + printDatum(data); + } else { + std::cout << "Request has no data" << std::endl; } - printDatum(data); } template @@ -656,6 +659,300 @@ single_conn_call(Connector &client) client.close(conn); } +/** Statement processor that returns the string statement as-is. */ +class StmtProcessorNoop { +public: + template + static std::string& + process(Connector &client, + Connection &conn, + std::string &stmt) + { + (void)client; + (void)conn; + return stmt; + } +}; + +/** Statement processor that prepares statement and returns its id. */ +class StmtProcessorPrepare { +public: + template + static unsigned int + process(Connector &client, + Connection &conn, + std::string &stmt) + { + rid_t future = conn.prepare(stmt); + + client.wait(conn, future, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(future)); + std::optional> response = conn.getResponse(future); + fail_unless(response != std::nullopt); + fail_if(response->body.error_stack != std::nullopt); + fail_unless(response->body.stmt_id != std::nullopt); + fail_unless(response->body.bind_count != std::nullopt); + return response->body.stmt_id.value(); + } +}; + +/** + * Compares sql data of two given Body objects. + */ +template +void +check_sql_data(const Body &got, const Body &expected) +{ + /* Metadata. */ + fail_unless(got.metadata.has_value() == expected.metadata.has_value()); + if (got.metadata.has_value()) { + fail_unless(got.metadata->column_maps.size() == expected.metadata->column_maps.size()); + for (size_t i = 0; i < got.metadata->column_maps.size(); i++) { + const ColumnMap &got_cm = got.metadata->column_maps[i]; + const ColumnMap &expected_cm = expected.metadata->column_maps[i]; + fail_unless(got_cm.field_name == expected_cm.field_name); + fail_unless(got_cm.field_type == expected_cm.field_type); + fail_unless(got_cm.collation == expected_cm.collation); + fail_unless(got_cm.span == expected_cm.span); + fail_unless(got_cm.is_nullable == expected_cm.is_nullable); + fail_unless(got_cm.is_autoincrement == expected_cm.is_autoincrement); + } + } + + /* Statement id. */ + fail_unless(got.stmt_id == expected.stmt_id); + + /* Bind count. */ + fail_unless(got.bind_count == expected.bind_count); + + /* Sql info. */ + fail_unless(got.sql_info.has_value() == expected.sql_info.has_value()); + if (got.sql_info.has_value()) { + fail_unless(got.sql_info->row_count == expected.sql_info->row_count); + fail_unless(got.sql_info->autoincrement_ids == + expected.sql_info->autoincrement_ids); + } +} + +/** Single connection, several executes. */ +template +void +single_conn_sql(Connector &client) +{ + TEST_INIT(0); + + using Data_t = std::vector>; + Data_t data; + using Body_t = Body; + + Connection conn(client); + int rc = test_connect(client, conn, localhost, port); + fail_unless(rc == 0); + + TEST_CASE("CREATE TABLE"); + std::string stmt_str = "CREATE TABLE IF NOT EXISTS tsql (column1 UNSIGNED PRIMARY KEY, " + "column2 VARCHAR(50), column3 DOUBLE);"; + auto stmt = StmtProcessor::process(client, conn, stmt_str); + rid_t create_table = conn.execute(stmt, std::make_tuple()); + + client.wait(conn, create_table, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(create_table)); + std::optional> response = conn.getResponse(create_table); + fail_unless(response != std::nullopt); + fail_unless(response->body.error_stack == std::nullopt); + + Body_t sql_data_create_table; + sql_data_create_table.sql_info = SqlInfo{1, {}}; + check_sql_data(response->body, sql_data_create_table); + + TEST_CASE("Simple INSERT"); + stmt_str = "INSERT INTO tsql VALUES (20, 'first', 3.2), (21, 'second', 5.4)"; + stmt = StmtProcessor::process(client, conn, stmt_str); + rid_t insert = conn.execute(stmt, std::make_tuple()); + + client.wait(conn, insert, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(insert)); + response = conn.getResponse(insert); + fail_unless(response != std::nullopt); + fail_unless(response->body.error_stack == std::nullopt); + + /* Check metadata. */ + Body_t sql_data_insert; + sql_data_insert.sql_info = SqlInfo{2, {}}; + check_sql_data(response->body, sql_data_insert); + + TEST_CASE("INSERT with binding arguments"); + std::tuple args = std::make_tuple(1, "Timur", 12.8, + 2, "Nikita", -8.0, + 3, "Anastas", 345.298); + stmt_str = "INSERT INTO tsql VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?);"; + stmt = StmtProcessor::process(client, conn, stmt_str); + rid_t insert_args = conn.execute(stmt, args); + + client.wait(conn, insert_args, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(insert_args)); + response = conn.getResponse(insert_args); + fail_unless(response != std::nullopt); + + printResponse(*response, Data_t()); + Body_t sql_data_insert_bind; + sql_data_insert_bind.sql_info = SqlInfo{3, {}}; + check_sql_data(response->body, sql_data_insert_bind); + + TEST_CASE("SELECT"); + stmt_str = "SELECT * FROM SEQSCAN tsql;"; + stmt = StmtProcessor::process(client, conn, stmt_str); + rid_t select = conn.execute(stmt, std::make_tuple()); + + client.wait(conn, select, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(select)); + response = conn.getResponse(select); + fail_unless(response != std::nullopt); + fail_if(response->body.error_stack != std::nullopt); + fail_unless(response->body.data != std::nullopt); + fail_unless(response->body.data->decode(data)); + fail_unless(data.size() == 5); + printResponse(*response, Data_t()); + Body_t sql_data_select; + std::vector sql_data_select_columns = { + {"COLUMN1", "unsigned", "", std::nullopt, false, false}, + {"COLUMN2", "string", "", std::nullopt, false, false}, + {"COLUMN3", "double", "", std::nullopt, false, false}, + }; + sql_data_select.metadata = Metadata{sql_data_select_columns}; + check_sql_data(response->body, sql_data_select); + + TEST_CASE("DROP TABLE"); + stmt_str = "DROP TABLE IF EXISTS tsql;"; + stmt = StmtProcessor::process(client, conn, stmt_str); + rid_t drop_table = conn.execute(stmt, std::make_tuple()); + + client.wait(conn, drop_table, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(drop_table)); + response = conn.getResponse(drop_table); + fail_unless(response != std::nullopt); + fail_unless(response->body.sql_info != std::nullopt); + fail_if(response->body.error_stack != std::nullopt); + + TEST_CASE("ENABLE METADATA"); + stmt_str = "UPDATE \"_session_settings\" SET \"value\" = true WHERE \"name\" = 'sql_full_metadata';"; + stmt = StmtProcessor::process(client, conn, stmt_str); + rid_t enable_metadata = conn.execute(stmt, std::make_tuple()); + + client.wait(conn, enable_metadata, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(enable_metadata)); + response = conn.getResponse(enable_metadata); + fail_unless(response != std::nullopt); + fail_unless(response->body.sql_info != std::nullopt); + fail_if(response->body.error_stack != std::nullopt); + + TEST_CASE("CREATE TABLE with autoincrement and collation"); + stmt_str = "CREATE TABLE IF NOT EXISTS tsql " + "(column1 UNSIGNED PRIMARY KEY AUTOINCREMENT, " + "column2 STRING COLLATE \"unicode\", column3 DOUBLE);"; + stmt = StmtProcessor::process(client, conn, stmt_str); + create_table = conn.execute(stmt, std::make_tuple()); + client.wait(conn, create_table, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(create_table)); + response = conn.getResponse(create_table); + fail_unless(response != std::nullopt); + printResponse(*response, Data_t()); + fail_unless(response->body.error_stack == std::nullopt); + + Body_t sql_data_create_table_autoinc; + sql_data_create_table_autoinc.sql_info = SqlInfo{1, {}}; + check_sql_data(response->body, sql_data_create_table_autoinc); + + TEST_CASE("INSERT with autoincrement"); + std::tuple args2 = std::make_tuple( + nullptr, "Timur", 12.8, + nullptr, "Nikita", -8.0, + /* Null for the 1st field is in statement. */ + "Anastas", 345.298); + stmt_str = "INSERT INTO tsql VALUES (?, ?, ?), (?, ?, ?), (NULL, ?, ?);"; + stmt = StmtProcessor::process(client, conn, stmt_str); + insert = conn.execute(stmt, args2); + client.wait(conn, insert, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(insert)); + response = conn.getResponse(insert); + fail_unless(response != std::nullopt); + printResponse(*response, Data_t()); + fail_unless(response->body.error_stack == std::nullopt); + + Body_t sql_data_insert_autoinc; + sql_data_insert_autoinc.sql_info = SqlInfo{3, {1, 2, 3}}; + check_sql_data(response->body, sql_data_insert_autoinc); + + TEST_CASE("SELECT from space with autoinc and collation"); + stmt_str = "SELECT * FROM SEQSCAN tsql;"; + stmt = StmtProcessor::process(client, conn, stmt_str); + select = conn.execute(stmt, std::make_tuple()); + + client.wait(conn, select, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(select)); + response = conn.getResponse(select); + fail_unless(response != std::nullopt); + fail_if(response->body.error_stack != std::nullopt); + fail_unless(response->body.data != std::nullopt); + fail_unless(response->body.data->decode(data)); + fail_unless(data.size() == 3); + printResponse(*response, Data_t()); + Body_t sql_data_select_autoinc; + sql_data_select_columns = { + {"COLUMN1", "unsigned", "", std::nullopt, false, true}, + {"COLUMN2", "string", "unicode", std::nullopt, true, false}, + {"COLUMN3", "double", "", std::nullopt, true, false}, + }; + sql_data_select_autoinc.metadata = Metadata{sql_data_select_columns}; + check_sql_data(response->body, sql_data_select_autoinc); + + TEST_CASE("SELECT with span"); + stmt_str = "SELECT 1 AS x;"; + stmt = StmtProcessor::process(client, conn, stmt_str); + select = conn.execute(stmt, std::make_tuple()); + + client.wait(conn, select, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(select)); + response = conn.getResponse(select); + fail_unless(response != std::nullopt); + fail_if(response->body.error_stack != std::nullopt); + fail_unless(response->body.data != std::nullopt); + fail_unless(response->body.data->decode(data)); + fail_unless(data.size() == 1); + printResponse(*response, Data_t()); + Body_t sql_data_select_span; + sql_data_select_columns = {{"X", "integer", "", "1"}}; + sql_data_select_span.metadata = Metadata{sql_data_select_columns}; + check_sql_data(response->body, sql_data_select_span); + + /* Finally, drop the table. */ + stmt_str = "DROP TABLE IF EXISTS tsql;"; + stmt = StmtProcessor::process(client, conn, stmt_str); + drop_table = conn.execute(stmt, std::make_tuple()); + client.wait(conn, drop_table, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(drop_table)); + response = conn.getResponse(drop_table); + fail_unless(response != std::nullopt); + fail_unless(response->body.sql_info != std::nullopt); + fail_unless(response->body.error_stack == std::nullopt); + + TEST_CASE("DISABLE METADATA"); + stmt_str = "UPDATE \"_session_settings\" SET \"value\" = false WHERE \"name\" = 'sql_full_metadata';"; + stmt = StmtProcessor::process(client, conn, stmt_str); + rid_t disable_metadata = conn.execute(stmt, std::make_tuple()); + + client.wait(conn, disable_metadata, WAIT_TIMEOUT); + fail_unless(conn.futureIsReady(disable_metadata)); + response = conn.getResponse(disable_metadata); + fail_unless(response != std::nullopt); + fail_unless(response->body.sql_info != std::nullopt); + fail_if(response->body.error_stack != std::nullopt); + + client.close(conn); +} + + + /** Single connection, call procedure with arguments */ template void @@ -807,6 +1104,8 @@ int main() single_conn_upsert(client); single_conn_select(client); single_conn_call(client); + single_conn_sql(client); + single_conn_sql(client); replace_unix_socket(client); test_auth(client); /* diff --git a/test/cfg.lua b/test/cfg.lua index 7466a0db4..c46b2fa6b 100644 --- a/test/cfg.lua +++ b/test/cfg.lua @@ -40,6 +40,18 @@ end box.schema.user.grant('guest', 'read,write', 'space', 'T', nil, {if_not_exists=true}) box.schema.user.grant('guest', 'execute', 'universe', nil, {if_not_exists=true}) +-- Allow to create spaces +box.schema.user.grant('guest', 'read,write', 'space', '_space', nil, {if_not_exists=true}) +box.schema.user.grant('guest', 'read,write', 'space', '_index', nil, {if_not_exists=true}) +box.schema.user.grant('guest', 'create', 'space', nil, {if_not_exists=true}) +-- Allow to create spaces with autoincrement +box.schema.user.grant('guest', 'read,write', 'space', '_sequence', {if_not_exists=true}) +box.schema.user.grant('guest', 'read,write', 'space', '_sequence_data', {if_not_exists=true}) +box.schema.user.grant('guest', 'read,write', 'space', '_space_sequence', {if_not_exists=true}) +box.schema.user.grant('guest', 'drop,create', 'sequence', nil, {if_not_exists=true}) + +sset = box.space._session_settings +sset:update('sql_full_metadata', {{'=', 'value', true}}) if box.space.S then box.space.S:drop() end secret = box.schema.space.create('S') From cd8b81ee84a3359b765ccb77f8a6dacacd58e79e Mon Sep 17 00:00:00 2001 From: Andrey Saranchin Date: Tue, 5 Dec 2023 19:57:38 +0300 Subject: [PATCH 3/3] Misc: add an example for SQL in tntcxx The commit introduces new example `Sql`, which demonstrates usage of prepare and execute requests - in other words, it shows how to use SQL with tntcxx. The example is designed to cover all possible sql metadata fields. --- CMakeLists.txt | 5 + examples/CMakeLists.txt | 1 + examples/Sql.cpp | 251 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 examples/Sql.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ea49574ae..cf4694906 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -228,3 +228,8 @@ TNTCXX_TEST(NAME SimpleExample TYPE other SOURCES examples/Simple.cpp LIBRARIES ${COMMON_LIB} ) + +TNTCXX_TEST(NAME SqlExample TYPE other + SOURCES examples/Sql.cpp + LIBRARIES ${COMMON_LIB} +) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 0dc08a8b4..1b1da969d 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -9,3 +9,4 @@ SET(CMAKE_C_STANDARD 17) ADD_COMPILE_OPTIONS(-Wall -Wextra -Werror) ADD_EXECUTABLE(Simple Simple.cpp) +ADD_EXECUTABLE(Sql Sql.cpp) diff --git a/examples/Sql.cpp b/examples/Sql.cpp new file mode 100644 index 000000000..0c37d653f --- /dev/null +++ b/examples/Sql.cpp @@ -0,0 +1,251 @@ +/* + * Copyright 2010-2023, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +/** + * To build this example see CMakeLists.txt or Makefile in current directory. + * Prerequisites to run this test: + * 1. Run Tarantool instance on localhost and set listening port 3301; + * 2. Grant privileges for guest (box.schema.user.grant('guest', 'super')) + * 3. Compile and run ./Sql + */ + +#include "../src/Client/Connector.hpp" +#include "../src/Buffer/Buffer.hpp" + +#include "Reader.hpp" + +const char *address = "127.0.0.1"; +int port = 3301; +int WAIT_TIMEOUT = 1000; //milliseconds + +using Buf_t = tnt::Buffer<16 * 1024>; +#include "../src/Client/LibevNetProvider.hpp" +using Net_t = LibevNetProvider; + +template +std::vector +decodeUserTuple(Data &data) +{ + std::vector results; + bool ok = data.decode(results); + (void)ok; + assert(ok); + return results; +} + +template +void +printResponse(Response &response) +{ + std::cout << ">>> RESPONSE {" << std::endl; + if (response.body.error_stack != std::nullopt) { + Error err = (*response.body.error_stack)[0]; + std::cout << "RESPONSE ERROR: msg=" << err.msg << + " line=" << err.file << " file=" << err.file << + " errno=" << err.saved_errno << + " type=" << err.type_name << + " code=" << err.errcode << std::endl; + } + if (response.body.stmt_id != std::nullopt) + std::cout << "stmt_id: " << *response.body.stmt_id << std::endl; + if (response.body.bind_count != std::nullopt) + std::cout << "bind_count: " << *response.body.bind_count << std::endl; + if (response.body.sql_info != std::nullopt) { + std::cout << "row_count: " << response.body.sql_info->row_count << std::endl; + if (!response.body.sql_info->autoincrement_ids.empty()) { + std::cout << "autoincrements ids: "; + for (const auto &id : response.body.sql_info->autoincrement_ids) + std::cout << id << " "; + std::cout << std::endl; + } + } + if (response.body.metadata != std::nullopt) { + for (const auto &cm : response.body.metadata->column_maps) { + std::cout << "SQL column " << cm.field_name << + " of type " << cm.field_type; + if (cm.is_nullable) + std::cout << " nullable"; + if (cm.is_autoincrement) + std::cout << " autoincrement"; + if (!cm.collation.empty()) + std::cout << " with collation " << cm.collation; + if (cm.span != std::nullopt) + std::cout << " with span " << *cm.span; + std::cout << std::endl; } + } + if (response.body.data != std::nullopt) { + Data& data = *response.body.data; + std::vector tuples = decodeUserTuple(data); + if (tuples.empty()) { + std::cout << "Empty result" << std::endl; + } else { + for (auto const& t : tuples) + std::cout << t << std::endl; + } + } + std::cout << "}" << std::endl; +} + +int +main() +{ + /* + * Create default connector. + */ + Connector client; + /* + * Create single connection. Constructor takes only client reference. + */ + Connection conn(client); + /* + * Try to connect to given address:port. Current implementation is + * exception free, so we rely only on return codes. + */ + int rc = client.connect(conn, {.address = address, + .service = std::to_string(port), + /*.user = ...,*/ + /*.passwd = ...,*/ + /* .transport = STREAM_SSL, */}); + if (rc != 0) { + std::cerr << conn.getError().msg << std::endl; + return -1; + } + /* + * Now let's execute several sql requests. + * Note that any of :request() methods can't fail; they always + * return request id - the future (number) which is used to get + * response once it is received. Also note that at this step, + * requests are encoded (into msgpack format) and saved into + * output connection's buffer - they are ready to be sent. + * But network communication itself will be done later. + */ + /* + * Let's create a space. An empty tuple at the end means that we pass + * no arguments to the SQL statement. + */ + rid_t create_space = conn.execute( + "CREATE TABLE IF NOT EXISTS tntcxx_sql_example " + "(id UNSIGNED PRIMARY KEY AUTOINCREMENT, " + "str_id STRING, value DOUBLE);", + std::make_tuple()); + /* + * Fill the newly created table. + * Let's try to use different approaches in one statement to learn more. + * Firstly, generate tuple with arguments for the insert statement. Then, + * execute the insert statement and pass generated arguments. + */ + std::tuple tntcxx_sql_data = std::make_tuple( + /* + * Pass NULL as the first field - it will be generated + * in tarantool since it has autoincrement attribute. + */ + nullptr, "One", 12.8, + nullptr, "Two", -8.0, + /* NULL for the 1st field is written right in statement. */ + "Three", 345.298, + /* Pass the first field expilictly. */ + 10, "Ten", -308.098 + ); + rid_t fill_space = conn.execute( + "INSERT INTO tntcxx_sql_example VALUES " + "(?, ?, ?), (?, ?, ?), (NULL, ?, ?), (?, ?, ?);", + tntcxx_sql_data + ); + /* + * Let's read from tarantool. + * Add a parameter to select statement to reuse it later. + */ + std::string select_stmt = "SELECT * FROM tntcxx_sql_example WHERE id = ?"; + rid_t select_from_space = conn.execute(select_stmt, std::make_tuple(1)); + /* + * Let's enable sql metadata and then read from tarantool again. + * Note that each session has its own _session_settings space, so + * this statement enables metadata only for the connection in + * which it was executed. + */ + rid_t enable_metadata = conn.execute( + "UPDATE \"_session_settings\" SET \"value\" = true " + "WHERE \"name\" = 'sql_full_metadata';", std::make_tuple()); + rid_t select_with_meta = conn.execute(select_stmt, std::make_tuple(2)); + + /* Let's wait for all futures at once. */ + std::vector futures = { + create_space, fill_space, select_from_space, + enable_metadata, select_with_meta + }; + + /* No specified timeout means that we poll futures until they are ready. */ + client.waitAll(conn, futures); + for (size_t i = 0; i < futures.size(); ++i) { + assert(conn.futureIsReady(futures[i])); + Response response = conn.getResponse(futures[i]); + printResponse(response); + } + + /* Let's prepare select_stmt to use it later. */ + rid_t prepare_stmt = conn.prepare(select_stmt); + client.wait(conn, prepare_stmt); + assert(conn.futureIsReady(prepare_stmt)); + Response response = conn.getResponse(prepare_stmt); + printResponse(response); + assert(response.body.stmt_id.has_value()); + uint32_t stmt_id = *response.body.stmt_id; + + /* Let's read some data using prepared statement. */ + std::vector prepared_select_futures; + prepared_select_futures.push_back(conn.execute(stmt_id, std::make_tuple(3))); + prepared_select_futures.push_back(conn.execute(stmt_id, std::make_tuple(10))); + /* Again, wait for all futures at once. */ + client.waitAll(conn, prepared_select_futures); + for (size_t i = 0; i < prepared_select_futures.size(); ++i) { + assert(conn.futureIsReady(prepared_select_futures[i])); + Response response = + conn.getResponse(prepared_select_futures[i]); + printResponse(response); + } + + /* + * Let's calculate a very important statistic. + * Add integer and string at the beginning to suit UserTuple format. + */ + rid_t calculate_stmt = conn.execute( + "SELECT 0 as id, 'zero' as zero_id, sum(value) as important_statistic " + "FROM SEQSCAN tntcxx_sql_example;", + std::make_tuple() + ); + client.wait(conn, calculate_stmt); + assert(conn.futureIsReady(calculate_stmt)); + response = conn.getResponse(calculate_stmt); + printResponse(response); + + /* Finally, user is responsible for closing connections. */ + client.close(conn); + return 0; +}