Prepared Statements (#43)

Changed PREPARE syntax to be closer to the standard.
This commit is contained in:
Pedro Flemming 2017-05-29 16:22:13 +02:00 committed by GitHub
parent 5e6cd2d84f
commit f85a5e7b52
14 changed files with 984 additions and 978 deletions

View File

@ -19,14 +19,19 @@ LIBFLAGS = -shared
TARGET = libsqlparser.so TARGET = libsqlparser.so
INSTALL = /usr/local INSTALL = /usr/local
CTESTFLAGS = -Wall -Isrc/ -Itest/ -L./ -std=c++11 -lstdc++ -O3 CTESTFLAGS = -Wall -Isrc/ -Itest/ -L./ -std=c++11 -lstdc++
# Set compile mode to -g or -O3. # Set compile mode to -g or -O3.
MODE_LOG = ""
mode ?= release mode ?= release
ifeq ($(mode), debug) ifeq ($(mode), debug)
CFLAGS += -g CFLAGS += -g
CTESTFLAGS += -g
MODE_LOG = "Building in \033[1;31mdebug\033[0m mode"
else else
CFLAGS += -O3 CFLAGS += -O3
CTESTFLAGS += -O3
MODE_LOG = "Building in \033[0;32mrelease\033[0m mode ('make mode=debug' for debug mode)"
endif endif
GMAKE = make mode=$(mode) GMAKE = make mode=$(mode)
@ -36,7 +41,6 @@ all: library
library: $(TARGET) library: $(TARGET)
$(TARGET): $(LIBOBJ) $(TARGET): $(LIBOBJ)
echo $(mode)
$(CXX) $(LIBFLAGS) -o $(TARGET) $(LIBOBJ) $(CXX) $(LIBFLAGS) -o $(TARGET) $(LIBOBJ)
$(SRCPARSER)/flex_lexer.o: $(SRCPARSER)/flex_lexer.cpp $(SRCPARSER)/bison_parser.cpp $(SRCPARSER)/flex_lexer.o: $(SRCPARSER)/flex_lexer.cpp $(SRCPARSER)/bison_parser.cpp
@ -108,3 +112,5 @@ format:
astyle --options=astyle.options $(ALLTEST) astyle --options=astyle.options $(ALLTEST)
astyle --options=astyle.options $(EXAMPLESRC) astyle --options=astyle.options $(EXAMPLESRC)
log_mode:
@echo $(MODE_LOG)

View File

@ -1,5 +1,6 @@
#include "SQLParserResult.h" #include "SQLParserResult.h"
#include <algorithm>
namespace hsql { namespace hsql {
@ -95,4 +96,15 @@ namespace hsql {
errorColumn_ = -1; errorColumn_ = -1;
} }
// Does NOT take ownership.
void SQLParserResult::addParameter(Expr* parameter) {
parameters_.push_back(parameter);
std::sort(parameters_.begin(), parameters_.end(),
[](const Expr* a, const Expr* b) { return a->ival < b->ival; });
}
const std::vector<Expr*>& SQLParserResult::parameters() {
return parameters_;
}
} // namespace hsql } // namespace hsql

View File

@ -63,6 +63,11 @@ namespace hsql {
// Deletes all statements and other data within the result. // Deletes all statements and other data within the result.
void reset(); void reset();
// Does NOT take ownership.
void addParameter(Expr* parameter);
const std::vector<Expr*>& parameters();
private: private:
// List of statements within the result. // List of statements within the result.
std::vector<SQLStatement*> statements_; std::vector<SQLStatement*> statements_;
@ -78,6 +83,9 @@ namespace hsql {
// Column number of the occurrance of the error in the query. // Column number of the occurrance of the error in the query.
int errorColumn_; int errorColumn_;
// Does NOT have ownership.
std::vector<Expr*> parameters_;
}; };
} // namespace hsql } // namespace hsql

File diff suppressed because it is too large Load Diff

View File

@ -48,7 +48,7 @@
extern int hsql_debug; extern int hsql_debug;
#endif #endif
/* "%code requires" blocks. */ /* "%code requires" blocks. */
#line 36 "bison_parser.y" /* yacc.c:1909 */ #line 34 "bison_parser.y" /* yacc.c:1909 */
// %code requires block // %code requires block
@ -58,18 +58,18 @@ extern int hsql_debug;
// Auto update column and line number // Auto update column and line number
#define YY_USER_ACTION \ #define YY_USER_ACTION \
yylloc->first_line = yylloc->last_line; \ yylloc->first_line = yylloc->last_line; \
yylloc->first_column = yylloc->last_column; \ yylloc->first_column = yylloc->last_column; \
for(int i = 0; yytext[i] != '\0'; i++) { \ for(int i = 0; yytext[i] != '\0'; i++) { \
yylloc->total_column++; \ yylloc->total_column++; \
if(yytext[i] == '\n') { \ if(yytext[i] == '\n') { \
yylloc->last_line++; \ yylloc->last_line++; \
yylloc->last_column = 0; \ yylloc->last_column = 0; \
} \ } \
else { \ else { \
yylloc->last_column++; \ yylloc->last_column++; \
} \ } \
} }
#line 75 "bison_parser.h" /* yacc.c:1909 */ #line 75 "bison_parser.h" /* yacc.c:1909 */
@ -214,7 +214,7 @@ extern int hsql_debug;
union HSQL_STYPE union HSQL_STYPE
{ {
#line 95 "bison_parser.y" /* yacc.c:1909 */ #line 92 "bison_parser.y" /* yacc.c:1909 */
double fval; double fval;
int64_t ival; int64_t ival;

View File

@ -24,8 +24,6 @@ int yyerror(YYLTYPE* llocp, SQLParserResult* result, yyscan_t scanner, const cha
return 0; return 0;
} }
%} %}
/********************************* /*********************************
** Section 2: Bison Parser Declarations ** Section 2: Bison Parser Declarations
@ -42,18 +40,18 @@ int yyerror(YYLTYPE* llocp, SQLParserResult* result, yyscan_t scanner, const cha
// Auto update column and line number // Auto update column and line number
#define YY_USER_ACTION \ #define YY_USER_ACTION \
yylloc->first_line = yylloc->last_line; \ yylloc->first_line = yylloc->last_line; \
yylloc->first_column = yylloc->last_column; \ yylloc->first_column = yylloc->last_column; \
for(int i = 0; yytext[i] != '\0'; i++) { \ for(int i = 0; yytext[i] != '\0'; i++) { \
yylloc->total_column++; \ yylloc->total_column++; \
if(yytext[i] == '\n') { \ if(yytext[i] == '\n') { \
yylloc->last_line++; \ yylloc->last_line++; \
yylloc->last_column = 0; \ yylloc->last_column = 0; \
} \ } \
else { \ else { \
yylloc->last_column++; \ yylloc->last_column++; \
} \ } \
} }
} }
// Define the names of the created files (defined in Makefile) // Define the names of the created files (defined in Makefile)
@ -77,7 +75,6 @@ int yyerror(YYLTYPE* llocp, SQLParserResult* result, yyscan_t scanner, const cha
@$.first_line = 0; @$.first_line = 0;
@$.last_line = 0; @$.last_line = 0;
@$.total_column = 0; @$.total_column = 0;
@$.placeholder_id = 0;
}; };
@ -184,13 +181,13 @@ int yyerror(YYLTYPE* llocp, SQLParserResult* result, yyscan_t scanner, const cha
%type <delete_stmt> delete_statement truncate_statement %type <delete_stmt> delete_statement truncate_statement
%type <update_stmt> update_statement %type <update_stmt> update_statement
%type <drop_stmt> drop_statement %type <drop_stmt> drop_statement
%type <sval> table_name opt_alias alias file_path %type <sval> table_name opt_alias alias file_path prepare_target_query
%type <bval> opt_not_exists opt_distinct %type <bval> opt_not_exists opt_distinct
%type <uval> import_file_type opt_join_type column_type %type <uval> import_file_type opt_join_type column_type
%type <table> from_clause table_ref table_ref_atomic table_ref_name %type <table> from_clause table_ref table_ref_atomic table_ref_name
%type <table> join_clause table_ref_name_no_alias %type <table> join_clause table_ref_name_no_alias
%type <expr> expr operand scalar_expr unary_expr binary_expr logic_expr exists_expr %type <expr> expr operand scalar_expr unary_expr binary_expr logic_expr exists_expr
%type <expr> function_expr between_expr star_expr expr_alias placeholder_expr %type <expr> function_expr between_expr star_expr expr_alias param_expr
%type <expr> column_name literal int_literal num_literal string_literal %type <expr> column_name literal int_literal num_literal string_literal
%type <expr> comp_expr opt_where join_condition opt_having case_expr in_expr %type <expr> comp_expr opt_where join_condition opt_having case_expr in_expr
%type <limit> opt_limit opt_top %type <limit> opt_limit opt_top
@ -238,11 +235,21 @@ int yyerror(YYLTYPE* llocp, SQLParserResult* result, yyscan_t scanner, const cha
// Defines our general input. // Defines our general input.
input: input:
statement_list opt_semicolon { statement_list opt_semicolon {
for (SQLStatement* stmt : *$1) { for (SQLStatement* stmt : *$1) {
// Transfers ownership of the statement. // Transfers ownership of the statement.
result->addStatement(stmt); result->addStatement(stmt);
} }
delete $1;
unsigned param_id = 0;
for (void* param : yyloc.param_list) {
if (param != nullptr) {
Expr* expr = (Expr*) param;
expr->ival = param_id;
result->addParameter(expr);
++param_id;
}
}
delete $1;
} }
; ;
@ -253,11 +260,7 @@ statement_list:
; ;
statement: statement:
prepare_statement { prepare_statement
$1->setPlaceholders(yyloc.placeholder_list);
yyloc.placeholder_list.clear();
$$ = $1;
}
| preparable_statement | preparable_statement
; ;
@ -279,22 +282,15 @@ preparable_statement:
* Prepared Statement * Prepared Statement
******************************/ ******************************/
prepare_statement: prepare_statement:
PREPARE IDENTIFIER ':' preparable_statement { PREPARE IDENTIFIER FROM prepare_target_query {
$$ = new PrepareStatement(); $$ = new PrepareStatement();
$$->name = $2; $$->name = $2;
$$->query = new SQLParserResult($4); $$->query = $4;
}
| PREPARE IDENTIFIER '{' statement_list opt_semicolon '}' {
$$ = new PrepareStatement();
$$->name = $2;
$$->query = new SQLParserResult();
for (SQLStatement* stmt : *$4) {
$$->query->addStatement(stmt);
}
delete $4;
} }
; ;
prepare_target_query: STRING
execute_statement: execute_statement:
EXECUTE IDENTIFIER { EXECUTE IDENTIFIER {
$$ = new ExecuteStatement(); $$ = new ExecuteStatement();
@ -717,7 +713,7 @@ column_name:
literal: literal:
string_literal string_literal
| num_literal | num_literal
| placeholder_expr | param_expr
; ;
string_literal: string_literal:
@ -738,10 +734,11 @@ star_expr:
'*' { $$ = new Expr(kExprStar); } '*' { $$ = new Expr(kExprStar); }
; ;
placeholder_expr: param_expr:
'?' { '?' {
$$ = Expr::makePlaceholder(yylloc.total_column); $$ = Expr::makeParameter(yylloc.total_column);
yyloc.placeholder_list.push_back($$); $$->ival2 = yyloc.param_list.size();
yyloc.param_list.push_back($$);
} }
; ;

View File

@ -22,15 +22,12 @@ struct HSQL_CUST_LTYPE {
int total_column; int total_column;
// Placeholder // Parameters.
int placeholder_id; // int param_id;
std::vector<void*> placeholder_list; std::vector<void*> param_list;
}; };
#define HSQL_LTYPE HSQL_CUST_LTYPE #define HSQL_LTYPE HSQL_CUST_LTYPE
#define HSQL_LTYPE_IS_DECLARED 1 #define HSQL_LTYPE_IS_DECLARED 1
#endif #endif

View File

@ -118,8 +118,8 @@ namespace hsql {
return e; return e;
} }
Expr* Expr::makePlaceholder(int id) { Expr* Expr::makeParameter(int id) {
Expr* e = new Expr(kExprPlaceholder); Expr* e = new Expr(kExprParameter);
e->ival = id; e->ival = id;
return e; return e;
} }
@ -160,7 +160,7 @@ namespace hsql {
} }
bool Expr::isLiteral() const { bool Expr::isLiteral() const {
return isType(kExprLiteralInt) || isType(kExprLiteralFloat) || isType(kExprLiteralString) || isType(kExprPlaceholder); return isType(kExprLiteralInt) || isType(kExprLiteralFloat) || isType(kExprLiteralString) || isType(kExprParameter);
} }
bool Expr::hasAlias() const { bool Expr::hasAlias() const {

View File

@ -17,7 +17,7 @@ namespace hsql {
kExprLiteralString, kExprLiteralString,
kExprLiteralInt, kExprLiteralInt,
kExprStar, kExprStar,
kExprPlaceholder, kExprParameter,
kExprColumnRef, kExprColumnRef,
kExprFunctionRef, kExprFunctionRef,
kExprOperator, kExprOperator,
@ -124,7 +124,7 @@ namespace hsql {
static Expr* makeFunctionRef(char* func_name, std::vector<Expr*>* exprList, bool distinct); static Expr* makeFunctionRef(char* func_name, std::vector<Expr*>* exprList, bool distinct);
static Expr* makePlaceholder(int id); static Expr* makeParameter(int id);
static Expr* makeSelect(SelectStatement* select); static Expr* makeSelect(SelectStatement* select);

View File

@ -0,0 +1,15 @@
#include "PrepareStatement.h"
namespace hsql {
// PrepareStatement
PrepareStatement::PrepareStatement() :
SQLStatement(kStmtPrepare),
name(NULL),
query(NULL) {}
PrepareStatement::~PrepareStatement() {
free(name);
free(query);
}
} // namespace hsql

View File

@ -1,32 +1,20 @@
#ifndef __SQLPARSER__PREPARE_STATEMENT_H__ #ifndef __SQLPARSER__PREPARE_STATEMENT_H__
#define __SQLPARSER__PREPARE_STATEMENT_H__ #define __SQLPARSER__PREPARE_STATEMENT_H__
#include "../SQLParserResult.h"
#include "SQLStatement.h" #include "SQLStatement.h"
#include "SelectStatement.h"
#include <algorithm>
namespace hsql { namespace hsql {
// Represents SQL Prepare statements. // Represents SQL Prepare statements.
// Example: "PREPARE ins_prep: SELECT * FROM t1 WHERE c1 = ? AND c2 = ?" // Example: PREPARE test FROM 'SELECT * FROM test WHERE a = ?;'
struct PrepareStatement : SQLStatement { struct PrepareStatement : SQLStatement {
PrepareStatement(); PrepareStatement();
virtual ~PrepareStatement(); virtual ~PrepareStatement();
// When setting the placeholders we need to make sure that they are in the correct order.
// To ensure that, during parsing we store the character position use that to sort the list here.
void setPlaceholders(std::vector<void*> ph);
char* name; char* name;
// The result that is stored within this prepared statement. // The query that is supposed to be prepared.
SQLParserResult* query; char* query;
// The expressions are not owned by this statement.
// Rather they are owned by the query and destroyed, when
// the query is destroyed.
std::vector<Expr*> placeholders;
}; };
} // namsepace hsql } // namsepace hsql

View File

@ -139,29 +139,6 @@ namespace hsql {
} }
} }
// PrepareStatement
PrepareStatement::PrepareStatement() :
SQLStatement(kStmtPrepare),
name(NULL),
query(NULL) {}
PrepareStatement::~PrepareStatement() {
delete query;
free(name);
}
void PrepareStatement::setPlaceholders(std::vector<void*> ph) {
for (void* e : ph) {
if (e != NULL)
placeholders.push_back((Expr*) e);
}
// Sort by col-id
std::sort(placeholders.begin(), placeholders.end(), [](Expr * i, Expr * j) -> bool { return (i->ival < j->ival); });
// Set the placeholder id on the Expr. This replaces the previously stored column id
for (uintmax_t i = 0; i < placeholders.size(); ++i) placeholders[i]->ival = i;
}
// SelectStatement.h // SelectStatement.h
// OrderDescription // OrderDescription

View File

@ -3,7 +3,8 @@
#include "sql_asserts.h" #include "sql_asserts.h"
#include "SQLParser.h" #include "SQLParser.h"
using hsql::kExprPlaceholder; using hsql::kExprParameter;
using hsql::kExprLiteralInt;
using hsql::kStmtDrop; using hsql::kStmtDrop;
using hsql::kStmtExecute; using hsql::kStmtExecute;
@ -21,63 +22,76 @@ using hsql::SelectStatement;
TEST(PrepareSingleStatementTest) { TEST(PrepareSingleStatementTest) {
const std::string query = "PREPARE test: SELECT * FROM students WHERE grade = ?;"; TEST_PARSE_SINGLE_SQL(
TEST_PARSE_SINGLE_SQL(query, kStmtPrepare, PrepareStatement, result, prepare); "PREPARE test FROM 'SELECT * FROM students WHERE grade = ?';",
kStmtPrepare,
PrepareStatement,
result,
prepare);
const SelectStatement* select = (const SelectStatement*) prepare->query->getStatement(0); ASSERT_STREQ(prepare->name, "test");
ASSERT_STREQ(prepare->query, "SELECT * FROM students WHERE grade = ?");
TEST_PARSE_SINGLE_SQL(
prepare->query,
kStmtSelect,
SelectStatement,
result2,
select);
ASSERT_EQ(result2.parameters().size(), 1);
ASSERT(select->whereClause->expr2->isType(kExprParameter))
ASSERT_EQ(select->whereClause->expr2->ival, 0)
ASSERT(select->whereClause->isSimpleOp('='));
ASSERT_EQ(select->whereClause->expr2, prepare->placeholders[0])
} }
TEST(PrepareMultiStatementTest) { TEST(DeallocatePrepareStatementTest) {
const std::string query = "PREPARE test {" TEST_PARSE_SINGLE_SQL(
"INSERT INTO test VALUES(?);" "DEALLOCATE PREPARE test;",
"SELECT ?, test FROM test WHERE c1 = ?;" kStmtDrop,
"};" DropStatement,
"PREPARE stmt: SELECT * FROM data WHERE c1 = ?;" result,
"DEALLOCATE PREPARE stmt;"; drop);
TEST_PARSE_SQL_QUERY(query, result, 3);
TEST_CAST_STMT(result, 0, kStmtPrepare, PrepareStatement, prep1);
TEST_CAST_STMT(result, 1, kStmtPrepare, PrepareStatement, prep2);
TEST_CAST_STMT(result, 2, kStmtDrop, DropStatement, drop);
// Prepare Statement #1
ASSERT_STREQ(prep1->name, "test");
ASSERT_EQ(prep1->placeholders.size(), 3);
ASSERT_EQ(prep1->query->size(), 2);
TEST_CAST_STMT((*prep1->query), 0, kStmtInsert, InsertStatement, insert);
TEST_CAST_STMT((*prep1->query), 1, kStmtSelect, SelectStatement, select);
ASSERT(insert->values->at(0)->isType(kExprPlaceholder));
ASSERT(select->selectList->at(0)->isType(kExprPlaceholder));
ASSERT(select->whereClause->expr2->isType(kExprPlaceholder));
// Check IDs of placeholders
ASSERT_EQ(insert->values->at(0)->ival, 0);
ASSERT_EQ(insert->values->at(0), prep1->placeholders[0]);
ASSERT_EQ(select->selectList->at(0)->ival, 1);
ASSERT_EQ(select->selectList->at(0), prep1->placeholders[1]);
ASSERT_EQ(select->whereClause->expr2->ival, 2);
ASSERT_EQ(select->whereClause->expr2, prep1->placeholders[2]);
// Prepare Statement #2
ASSERT_STREQ(prep2->name, "stmt");
ASSERT_EQ(prep2->placeholders.size(), 1);
// Deallocate Statement
ASSERT_EQ(drop->type, kDropPreparedStatement); ASSERT_EQ(drop->type, kDropPreparedStatement);
ASSERT_STREQ(drop->name, "stmt"); ASSERT_STREQ(drop->name, "test");
} }
TEST(StatementWithParameters) {
TEST_PARSE_SINGLE_SQL(
"SELECT * FROM test WHERE a = ? AND b = ?",
kStmtSelect,
SelectStatement,
result,
stmt);
const hsql::Expr* eq1 = stmt->whereClause->expr;
const hsql::Expr* eq2 = stmt->whereClause->expr2;
ASSERT_EQ(result.parameters().size(), 2);
ASSERT(eq1->isSimpleOp('='))
ASSERT(eq1->expr->isType(hsql::kExprColumnRef))
ASSERT(eq1->expr2->isType(kExprParameter))
ASSERT_EQ(eq1->expr2->ival, 0)
ASSERT_EQ(result.parameters()[0], eq1->expr2);
ASSERT(eq2->isSimpleOp('='))
ASSERT(eq2->expr->isType(hsql::kExprColumnRef))
ASSERT(eq2->expr2->isType(kExprParameter))
ASSERT_EQ(eq2->expr2->ival, 1)
ASSERT_EQ(result.parameters()[1], eq2->expr2);
}
TEST(ExecuteStatementTest) { TEST(ExecuteStatementTest) {
TEST_PARSE_SINGLE_SQL("EXECUTE test(1, 2);", kStmtExecute, ExecuteStatement, result, stmt); TEST_PARSE_SINGLE_SQL(
"EXECUTE test(1, 2);",
kStmtExecute,
ExecuteStatement,
result,
stmt);
ASSERT_STREQ(stmt->name, "test"); ASSERT_STREQ(stmt->name, "test");
ASSERT_EQ(stmt->parameters->size(), 2); ASSERT_EQ(stmt->parameters->size(), 2);

View File

@ -11,6 +11,7 @@ SELECT * FROM t1 UNION (SELECT * FROM t2 UNION SELECT * FROM t3) ORDER BY col1;
SELECT TOP 10 * FROM t1 ORDER BY col1, col2; SELECT TOP 10 * FROM t1 ORDER BY col1, col2;
SELECT a, MAX(b), MAX(c, d), CUSTOM(q, UP(r)) AS f FROM t1; SELECT a, MAX(b), MAX(c, d), CUSTOM(q, UP(r)) AS f FROM t1;
SELECT * FROM t WHERE a BETWEEN 1 and c; SELECT * FROM t WHERE a BETWEEN 1 and c;
SELECT * FROM t WHERE a = ? AND b = ?;
SELECT City.name, Product.category, SUM(price) FROM fact INNER JOIN City ON fact.city_id = City.id INNER JOIN Product ON fact.product_id = Product.id GROUP BY City.name, Product.category; SELECT City.name, Product.category, SUM(price) FROM fact INNER JOIN City ON fact.city_id = City.id INNER JOIN Product ON fact.product_id = Product.id GROUP BY City.name, Product.category;
# JOIN # JOIN
SELECT t1.a, t1.b, t2.c FROM "table" AS t1 JOIN (SELECT * FROM foo JOIN bar ON foo.id = bar.id) t2 ON t1.a = t2.b WHERE (t1.b OR NOT t1.a) AND t2.c = 12.5 SELECT t1.a, t1.b, t2.c FROM "table" AS t1 JOIN (SELECT * FROM foo JOIN bar ON foo.id = bar.id) t2 ON t1.a = t2.b WHERE (t1.b OR NOT t1.a) AND t2.c = 12.5
@ -37,8 +38,8 @@ UPDATE students SET grade = 1.0;
# DROP # DROP
DROP TABLE students; DROP TABLE students;
# PREPARE # PREPARE
PREPARE prep_inst: INSERT INTO test VALUES (?, ?, ?); PREPARE prep_inst FROM 'INSERT INTO test VALUES (?, ?, ?)';
PREPARE prep2 { INSERT INTO test VALUES (?, 0, 0); INSERT INTO test VALUES (0, ?, 0); INSERT INTO test VALUES (0, 0, ?); }; PREPARE prep2 FROM 'INSERT INTO test VALUES (?, 0, 0); INSERT INTO test VALUES (0, ?, 0); INSERT INTO test VALUES (0, 0, ?);';
EXECUTE prep_inst(1, 2, 3); EXECUTE prep_inst(1, 2, 3);
EXECUTE prep; EXECUTE prep;
DEALLOCATE PREPARE prep; DEALLOCATE PREPARE prep;
@ -50,3 +51,4 @@ DEALLOCATE PREPARE prep;
!CREATE TABLE "table" FROM TBL FILE 'students.tbl';SELECT 1 !CREATE TABLE "table" FROM TBL FILE 'students.tbl';SELECT 1
!CREATE TABLE "table" FROM TBL FILE 'students.tbl';1 !CREATE TABLE "table" FROM TBL FILE 'students.tbl';1
!INSERT INTO test_table VALUESd (1, 2, 'test'); !INSERT INTO test_table VALUESd (1, 2, 'test');
!SELECT * FROM t WHERE a = ? AND b = ?;SELECT 1;