Skip to content

Commit 13ebc04

Browse files
committed
enh(Data): SQLParser test coverage and integration assertions #5370
Adds tests that lock down the parser-layer behaviour POCO's production code relies on, plus reproductions of recent fix motivations so any upstream sql-parser sync that regresses them fails loudly in the test layer rather than silently in MemoryDB / Utility. Data/testsuite/src/SQLParserTest: - testResetClearsParameters: reparse loop with reset() between iterations - the lifecycle the upstream sval-leak fix hardened. - testNamedParameter: :name placeholders parse to kExprParameterNamed with names {user, age}. - testAlterDropColumnIfExists: down-casts to AlterStatement + DropColumnAction and asserts columnName, ifExists, table name. - testDropDiscrimination: DROP TABLE vs DROP INDEX dispatch. - testDeleteShape: DeleteStatement::expr nullptr distinguishes truncate from filtered delete - what MemoryDB branches on for the truncate optimization. - testComments: -- line comments survive parsing across statements. - testTokenize: backstops the SQLParser::tokenize loop the upstream sval-leak fix rewrote. - Inline fix to the pre-existing testSQLParser failure formatter: L13 was rendering with no separator between line and column; replaced with L<line>:C<col> and switched bare fail() to failmsg() so the formatted message actually surfaces on the CppUnit report. Data/SQLite/testsuite/src/UtilityTest: - testBoundSQLNamed: pins the current pass-through behaviour of :name placeholders in Utility::boundSQL (Utility.cpp kExprParameterNamed branch sets paramIndex=npos so the span is copied verbatim and the supplied args are silently ignored). If this branch is ever taught to substitute values, the test must be rewritten. Data/SQLite/testsuite/src/MemoryDBTest: - testCommentPrefixedWithoutRowidRejected: covers both operator<< and session() bypass paths with a leading -- comment. Pins the motivation for the leading-comment-skip rewrite in onStatement; the previous single-char trigger-marker check let -- prefixed CREATE bypass the trace-hook backstop. - testDropTableClassified: DROP TABLE survives close-reopen via the schema log, proving the kStmtDrop + kDropTable dispatch. - testAlterTableClassified: ALTER TABLE ADD COLUMN survives close- reopen. ADD COLUMN is not modelled as an AlterAction by hsql so this exercises the first-keyword DDL fallback in onStatement.
1 parent bc5ced8 commit 13ebc04

6 files changed

Lines changed: 305 additions & 2 deletions

File tree

Data/SQLite/testsuite/src/MemoryDBTest.cpp

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,102 @@ void MemoryDBTest::testConcurrentAccess()
12491249
}
12501250

12511251

1252+
void MemoryDBTest::testCommentPrefixedWithoutRowidRejected()
1253+
{
1254+
// Two paths. (a) The operator<< prophylactic check mentionsWithoutRowid()
1255+
// requires CREATE as the first non-whitespace token and therefore does NOT
1256+
// see through leading line comments - the CREATE statement is allowed to
1257+
// reach the in-memory SQLite. (b) The session() bypass skips even that
1258+
// check. In both cases the reactive trace-hook backstop must catch the
1259+
// WITHOUT ROWID and poison the instance so subsequent operations throw.
1260+
// This is what the leading-comment-skip rewrite in onStatement was added
1261+
// to guarantee; previously the single-char "if (sql[0] == '-')" treated
1262+
// any --prefixed user SQL as a SQLite trigger marker and skipped it.
1263+
1264+
// (a) operator<< path with leading comment
1265+
{
1266+
MemoryDB db(_dir);
1267+
db << "-- a leading comment\nCREATE TABLE t(id INTEGER PRIMARY KEY, v TEXT) WITHOUT ROWID", now;
1268+
try
1269+
{
1270+
std::string h("hi");
1271+
db << "INSERT INTO t(v) VALUES(:v)", use(h), now;
1272+
failmsg("trace-hook backstop must reject comment-prefixed WITHOUT ROWID on operator<< path");
1273+
}
1274+
catch (Poco::NotImplementedException&)
1275+
{
1276+
}
1277+
}
1278+
1279+
// (b) session() bypass with leading comment
1280+
{
1281+
MemoryDB db(_dir);
1282+
db.session() << "-- a leading comment\nCREATE TABLE t(id INTEGER PRIMARY KEY, v TEXT) WITHOUT ROWID", now;
1283+
try
1284+
{
1285+
std::string h("hi");
1286+
db << "INSERT INTO t(v) VALUES(:v)", use(h), now;
1287+
failmsg("trace-hook backstop must reject comment-prefixed WITHOUT ROWID via session() bypass");
1288+
}
1289+
catch (Poco::NotImplementedException&)
1290+
{
1291+
}
1292+
}
1293+
}
1294+
1295+
1296+
void MemoryDBTest::testDropTableClassified()
1297+
{
1298+
// DROP TABLE must be classified by the trace hook as kStmtDrop + kDropTable
1299+
// and replayed via the schema log on reopen so the table is genuinely absent,
1300+
// not just absent from main while still in a sealed shard. Pins the dispatch
1301+
// at the kStmtDrop branch in MemoryDB::onStatement.
1302+
{
1303+
MemoryDB db(_dir);
1304+
db << "CREATE TABLE t(id INTEGER PRIMARY KEY, v TEXT)", now;
1305+
std::string x("x");
1306+
db << "INSERT INTO t(v) VALUES(:v)", use(x), now;
1307+
db.flush();
1308+
1309+
db << "DROP TABLE t", now;
1310+
db.flush();
1311+
}
1312+
1313+
MemoryDB db(_dir);
1314+
int tcnt = -1;
1315+
db << "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='t'",
1316+
into(tcnt), now;
1317+
assertTrue (tcnt == 0);
1318+
}
1319+
1320+
1321+
void MemoryDBTest::testAlterTableClassified()
1322+
{
1323+
// ALTER TABLE ADD COLUMN: hsql parses this but does not currently model
1324+
// ADD COLUMN as an AlterAction (only DROP COLUMN is in the enum), so the
1325+
// trace hook's parser branch reports !isValid() and the keyword-fallback
1326+
// path classifies it as DDL via the first-keyword check. The schema log
1327+
// records the ALTER either way; on reopen the column must be present.
1328+
{
1329+
MemoryDB db(_dir);
1330+
db << "CREATE TABLE t(id INTEGER PRIMARY KEY, a TEXT)", now;
1331+
std::string a1("a1");
1332+
db << "INSERT INTO t(a) VALUES(:a)", use(a1), now;
1333+
1334+
db << "ALTER TABLE t ADD COLUMN b TEXT", now;
1335+
1336+
std::string a2("a2"), b2("b2");
1337+
db << "INSERT INTO t(a, b) VALUES(:a, :b)", use(a2), use(b2), now;
1338+
db.flush();
1339+
}
1340+
1341+
MemoryDB db(_dir);
1342+
std::string b;
1343+
db << "SELECT b FROM t WHERE id=2", into(b), now;
1344+
assertTrue (b == "b2");
1345+
}
1346+
1347+
12521348
CppUnit::Test* MemoryDBTest::suite()
12531349
{
12541350
CppUnit::TestSuite* pSuite = new CppUnit::TestSuite("MemoryDBTest");
@@ -1262,10 +1358,13 @@ CppUnit::Test* MemoryDBTest::suite()
12621358
CppUnit_addTest(pSuite, MemoryDBTest, testTruncate);
12631359
CppUnit_addTest(pSuite, MemoryDBTest, testWithoutRowidRejected);
12641360
CppUnit_addTest(pSuite, MemoryDBTest, testWithoutRowidRejectedViaSession);
1361+
CppUnit_addTest(pSuite, MemoryDBTest, testCommentPrefixedWithoutRowidRejected);
12651362
CppUnit_addTest(pSuite, MemoryDBTest, testLoadArchivedFalse);
12661363
CppUnit_addTest(pSuite, MemoryDBTest, testIdleFlush);
12671364
CppUnit_addTest(pSuite, MemoryDBTest, testCustomShardNamer);
12681365
CppUnit_addTest(pSuite, MemoryDBTest, testIndexPreservedAcrossReload);
1366+
CppUnit_addTest(pSuite, MemoryDBTest, testDropTableClassified);
1367+
CppUnit_addTest(pSuite, MemoryDBTest, testAlterTableClassified);
12691368
CppUnit_addTest(pSuite, MemoryDBTest, testHistoryView);
12701369
CppUnit_addTest(pSuite, MemoryDBTest, testDeleteSealedShardLoaded);
12711370
CppUnit_addTest(pSuite, MemoryDBTest, testDeleteSealedShardUnloaded);

Data/SQLite/testsuite/src/MemoryDBTest.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,13 @@ class MemoryDBTest: public CppUnit::TestCase
3434
void testTruncate();
3535
void testWithoutRowidRejected();
3636
void testWithoutRowidRejectedViaSession();
37+
void testCommentPrefixedWithoutRowidRejected();
3738
void testLoadArchivedFalse();
3839
void testIdleFlush();
3940
void testCustomShardNamer();
4041
void testIndexPreservedAcrossReload();
42+
void testDropTableClassified();
43+
void testAlterTableClassified();
4144
void testHistoryView();
4245
void testDeleteSealedShardLoaded();
4346
void testDeleteSealedShardUnloaded();

Data/SQLite/testsuite/src/UtilityTest.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,25 @@ void UtilityTest::testBoundSQLMixedStyles()
252252
}
253253

254254

255+
void UtilityTest::testBoundSQLNamed()
256+
{
257+
// Pins the current pass-through behaviour of the kExprParameterNamed
258+
// branch in Utility.cpp:131-134: paramIndex is set to npos and srcLen
259+
// to 1 + strlen(name), so renderRow at Utility.cpp:162-163 copies the
260+
// original :name span verbatim and the Style stays None - the arity
261+
// checks at lines 144/146 never fire and the supplied args are
262+
// silently ignored.
263+
//
264+
// Net effect: :name placeholders are a no-op pass-through; the args
265+
// are dropped and the returned SQL equals the input SQL. If Utility
266+
// is ever taught to actually substitute named placeholders, this
267+
// test must be rewritten.
268+
const std::string sql = "SELECT * FROM t WHERE u = :user AND a = :age";
269+
const std::string s = Utility::boundSQL(sql, std::string("John"), 42);
270+
assertTrue (s == sql);
271+
}
272+
273+
255274
void UtilityTest::testExecuteSQLSuccess()
256275
{
257276
Session session(Connector::KEY, ":memory:");
@@ -619,6 +638,7 @@ CppUnit::Test* UtilityTest::suite()
619638
CppUnit_addTest(pSuite, UtilityTest, testBoundSQLDollarOutOfRange);
620639
CppUnit_addTest(pSuite, UtilityTest, testBoundSQLDollarZero);
621640
CppUnit_addTest(pSuite, UtilityTest, testBoundSQLMixedStyles);
641+
CppUnit_addTest(pSuite, UtilityTest, testBoundSQLNamed);
622642
CppUnit_addTest(pSuite, UtilityTest, testExecuteSQLSuccess);
623643
CppUnit_addTest(pSuite, UtilityTest, testExecuteSQLConstraintFailure);
624644
CppUnit_addTest(pSuite, UtilityTest, testExecuteSQLMalformed);

Data/SQLite/testsuite/src/UtilityTest.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class UtilityTest: public CppUnit::TestCase
3636
void testBoundSQLDollarOutOfRange();
3737
void testBoundSQLDollarZero();
3838
void testBoundSQLMixedStyles();
39+
void testBoundSQLNamed();
3940
void testExecuteSQLSuccess();
4041
void testExecuteSQLConstraintFailure();
4142
void testExecuteSQLMalformed();

Data/testsuite/src/SQLParserTest.cpp

Lines changed: 174 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
#include "CppUnit/TestCaller.h"
1616
#include "CppUnit/TestSuite.h"
1717
#include <sstream>
18+
#include <set>
19+
#include <cstdint>
1820
#include "SQLParser.h"
21+
#include "sql/AlterStatement.h"
22+
#include "sql/DropStatement.h"
23+
#include "sql/DeleteStatement.h"
1924

2025

2126
namespace Poco::Data {
@@ -57,8 +62,8 @@ void SQLParserTest::testSQLParser()
5762
{
5863
std::ostringstream os;
5964
os << "Given string is not a valid SQL query.\n";
60-
os << result.errorMsg() << "(L" << result.errorLine() << result.errorColumn() << ")\n";
61-
fail(os.str());
65+
os << result.errorMsg() << " (L" << result.errorLine() << ":C" << result.errorColumn() << ")\n";
66+
failmsg(os.str());
6267
}
6368
assertTrue(result.isValid());
6469
assertEqual(8, result.size());
@@ -93,6 +98,165 @@ void SQLParserTest::testSQLParser()
9398
}
9499

95100

101+
void SQLParserTest::testInvalidSQL()
102+
{
103+
std::string query = "SELECT FROM WHERE ;";
104+
SQLParserResult result;
105+
SQLParser::parse(query, &result);
106+
assertTrue (!result.isValid());
107+
assertTrue (result.errorMsg() != nullptr && std::string(result.errorMsg()).length() > 0);
108+
}
109+
110+
111+
void SQLParserTest::testResetClearsParameters()
112+
{
113+
// Reuses the testSQLParser query (8 statements, 2 '?' parameters) and
114+
// reparses N times with reset() between. Pins the lifecycle path the
115+
// upstream commit 38d1a66 hardened (sval leaks across iterations).
116+
std::string query = "INSERT INTO Test VALUES ('1', 2, 3.5);"
117+
"SELECT * FROM Test WHERE First = ?;"
118+
"UPDATE Test SET value=1 WHERE First = '1';"
119+
"DELETE FROM Test WHERE First = ?;"
120+
"DROP TABLE table_name;"
121+
"ALTER TABLE mytable DROP COLUMN IF EXISTS mycolumn;"
122+
"PREPARE prep_inst FROM 'INSERT INTO test VALUES (?, ?, ?)';"
123+
"EXECUTE prep_inst(1, 2, 3);";
124+
125+
SQLParserResult result;
126+
for (int i = 0; i < 5; ++i)
127+
{
128+
SQLParser::parse(query, &result);
129+
assertTrue (result.isValid());
130+
assertEqual(8, result.size());
131+
assertEqual(2, static_cast<int>(result.parameters().size()));
132+
result.reset();
133+
}
134+
SQLParser::parse(query, &result);
135+
assertTrue (result.isValid());
136+
assertEqual(8, result.size());
137+
assertEqual(2, static_cast<int>(result.parameters().size()));
138+
}
139+
140+
141+
void SQLParserTest::testNamedParameter()
142+
{
143+
std::string query = "SELECT * FROM t WHERE u = :user AND a = :age";
144+
SQLParserResult result;
145+
SQLParser::parse(query, &result);
146+
assertTrue (result.isValid());
147+
assertEqual(1, result.size());
148+
assertEqual(2, static_cast<int>(result.parameters().size()));
149+
150+
std::set<std::string> names;
151+
for (const Expr* e: result.parameters())
152+
{
153+
assertTrue (e->type == kExprParameterNamed);
154+
assertTrue (e->name != nullptr);
155+
names.insert(std::string(e->name));
156+
}
157+
std::set<std::string> expected;
158+
expected.insert("user");
159+
expected.insert("age");
160+
assertTrue (names == expected);
161+
}
162+
163+
164+
void SQLParserTest::testAlterDropColumnIfExists()
165+
{
166+
std::string query = "ALTER TABLE mytable DROP COLUMN IF EXISTS mycolumn";
167+
SQLParserResult result;
168+
SQLParser::parse(query, &result);
169+
assertTrue (result.isValid());
170+
assertEqual(1, result.size());
171+
172+
const SQLStatement* stmt = result.getStatement(0);
173+
assertTrue (stmt->type() == kStmtAlter);
174+
const AlterStatement* alter = static_cast<const AlterStatement*>(stmt);
175+
assertTrue (alter->name == std::string("mytable"));
176+
177+
const DropColumnAction* action = static_cast<const DropColumnAction*>(alter->action);
178+
assertTrue (action->columnName == std::string("mycolumn"));
179+
assertTrue (action->ifExists);
180+
}
181+
182+
183+
void SQLParserTest::testDropDiscrimination()
184+
{
185+
std::string query = "DROP TABLE t; DROP INDEX idx_t;";
186+
SQLParserResult result;
187+
SQLParser::parse(query, &result);
188+
assertTrue (result.isValid());
189+
assertEqual(2, result.size());
190+
191+
const SQLStatement* s0 = result.getStatement(0);
192+
assertTrue (s0->type() == kStmtDrop);
193+
const DropStatement* d0 = static_cast<const DropStatement*>(s0);
194+
assertTrue (d0->type == kDropTable);
195+
assertTrue (d0->name == std::string("t"));
196+
197+
const SQLStatement* s1 = result.getStatement(1);
198+
assertTrue (s1->type() == kStmtDrop);
199+
const DropStatement* d1 = static_cast<const DropStatement*>(s1);
200+
assertTrue (d1->type == kDropIndex);
201+
}
202+
203+
204+
void SQLParserTest::testDeleteShape()
205+
{
206+
// MemoryDB.cpp:1232 specifically branches on d->expr == nullptr for
207+
// the truncate-detect optimization. Pin both shapes here so a parser
208+
// regression that flipped the meaning would break this test, not
209+
// just MemoryDB's truncate behavior under load.
210+
std::string query = "DELETE FROM t WHERE id=1; DELETE FROM t;";
211+
SQLParserResult result;
212+
SQLParser::parse(query, &result);
213+
assertTrue (result.isValid());
214+
assertEqual(2, result.size());
215+
216+
const DeleteStatement* d0 = static_cast<const DeleteStatement*>(result.getStatement(0));
217+
assertTrue (d0->expr != nullptr);
218+
219+
const DeleteStatement* d1 = static_cast<const DeleteStatement*>(result.getStatement(1));
220+
assertTrue (d1->expr == nullptr);
221+
assertTrue (d1->tableName == std::string("t"));
222+
}
223+
224+
225+
void SQLParserTest::testComments()
226+
{
227+
// hsql lexer supports -- line comments; the C-style /* ... */ form is
228+
// NOT supported by this lexer build, so we exercise only the line-
229+
// comment variant. MemoryDB.cpp's trace-hook skipLeading*Comments is
230+
// the production path that must handle both - that path is tested
231+
// downstream in MemoryDBTest::testCommentPrefixedWithoutRowidRejected.
232+
std::string query = "-- header\nSELECT * FROM t;\n-- trailing\nINSERT INTO t VALUES(1);";
233+
SQLParserResult result;
234+
SQLParser::parse(query, &result);
235+
assertTrue (result.isValid());
236+
assertEqual(2, result.size());
237+
assertTrue (result.getStatement(0)->type() == kStmtSelect);
238+
assertTrue (result.getStatement(1)->type() == kStmtInsert);
239+
}
240+
241+
242+
void SQLParserTest::testTokenize()
243+
{
244+
// Backstops the SQLParser::tokenize loop rewrite landed in upstream
245+
// commit 38d1a66 (sval-per-iteration leaks, NAMED_PARAM inclusion).
246+
std::string query = "SELECT id FROM t WHERE name = :who AND age = $1";
247+
std::vector<int16_t> tokens;
248+
try
249+
{
250+
SQLParser::tokenize(query, &tokens);
251+
assertTrue (!tokens.empty());
252+
}
253+
catch (...)
254+
{
255+
failmsg("SQLParser::tokenize threw an exception");
256+
}
257+
}
258+
259+
96260
void SQLParserTest::setUp()
97261
{
98262
}
@@ -108,6 +272,14 @@ CppUnit::Test* SQLParserTest::suite()
108272
CppUnit::TestSuite* pSuite = new CppUnit::TestSuite("SQLParserTest");
109273

110274
CppUnit_addTest(pSuite, SQLParserTest, testSQLParser);
275+
CppUnit_addTest(pSuite, SQLParserTest, testInvalidSQL);
276+
CppUnit_addTest(pSuite, SQLParserTest, testResetClearsParameters);
277+
CppUnit_addTest(pSuite, SQLParserTest, testNamedParameter);
278+
CppUnit_addTest(pSuite, SQLParserTest, testAlterDropColumnIfExists);
279+
CppUnit_addTest(pSuite, SQLParserTest, testDropDiscrimination);
280+
CppUnit_addTest(pSuite, SQLParserTest, testDeleteShape);
281+
CppUnit_addTest(pSuite, SQLParserTest, testComments);
282+
CppUnit_addTest(pSuite, SQLParserTest, testTokenize);
111283

112284
return pSuite;
113285
}

Data/testsuite/src/SQLParserTest.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ class SQLParserTest: public CppUnit::TestCase
2828
~SQLParserTest();
2929

3030
void testSQLParser();
31+
void testInvalidSQL();
32+
void testResetClearsParameters();
33+
void testNamedParameter();
34+
void testAlterDropColumnIfExists();
35+
void testDropDiscrimination();
36+
void testDeleteShape();
37+
void testComments();
38+
void testTokenize();
3139

3240
void setUp();
3341
void tearDown();

0 commit comments

Comments
 (0)