packages/ztd-query-sqlite/tests/Unit/Rewrite/SqliteQueryGuardTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Rewrite;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use RuntimeException;
10use ZtdQuery\Platform\Sqlite\Rewrite\SqliteQueryGuard;
11use ZtdQuery\Platform\Sqlite\Sql\Attach\SqliteInMemoryAttachStatement;
12use ZtdQuery\Platform\Sqlite\Sql\SqliteLexicalMasker;
13use ZtdQuery\Platform\Sqlite\Sql\SqliteParser;
14use ZtdQuery\Rewrite\QueryKind;
15
16#[CoversClass(SqliteQueryGuard::class)]
17#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Attach\AttachTokens::class)]
18#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentColumnParser::class)]
19#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentParser::class)]
20#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\ValueListParser::class)]
21#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Insert\InsertClauseParser::class)]
22#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\ExpressionSpan::class)]
23#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\IdentifierDecoder::class)]
24#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\LiteralMasker::class)]
25#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\OpaqueSqlSpan::class)]
26#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\QuotedSpan::class)]
27#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\TopLevelKeywordScanner::class)]
28#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\RelationSourceParser::class)]
29#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementClassifier::class)]
30#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementStructure::class)]
31#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\TargetTableParser::class)]
32#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Update\UpdateClauseParser::class)]
33#[UsesClass(SqliteInMemoryAttachStatement::class)]
34#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\SqliteLexerProfile::class)]
35#[UsesClass(SqliteLexicalMasker::class)]
36#[UsesClass(SqliteParser::class)]
37#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Diagnostic\SqliteReadOnlyDiagnosticStatement::class)]
38#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\SqliteSelectRelationParser::class)]
39final class SqliteQueryGuardTest extends \PHPUnit\Framework\TestCase
40{
41 public function testSelectClassifiesAsRead(): void
42 {
43 $guard = new SqliteQueryGuard(new SqliteParser());
44 self::assertSame(QueryKind::READ, $guard->classify('SELECT * FROM users'));
45 }
46
47 public function testInsertClassifiesAsWriteSimulated(): void
48 {
49 $guard = new SqliteQueryGuard(new SqliteParser());
50 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify("INSERT INTO users (name) VALUES ('Alice')"));
51 }
52
53 public function testUpdateClassifiesAsWriteSimulated(): void
54 {
55 $guard = new SqliteQueryGuard(new SqliteParser());
56 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify("UPDATE users SET name = 'Bob' WHERE id = 1"));
57 }
58
59 public function testDeleteClassifiesAsWriteSimulated(): void
60 {
61 $guard = new SqliteQueryGuard(new SqliteParser());
62 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('DELETE FROM users WHERE id = 1'));
63 }
64
65 public function testCreateTableClassifiesAsDdlSimulated(): void
66 {
67 $guard = new SqliteQueryGuard(new SqliteParser());
68 self::assertSame(QueryKind::DDL_SIMULATED, $guard->classify('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)'));
69 }
70
71 public function testDropTableClassifiesAsDdlSimulated(): void
72 {
73 $guard = new SqliteQueryGuard(new SqliteParser());
74 self::assertSame(QueryKind::DDL_SIMULATED, $guard->classify('DROP TABLE users'));
75 }
76
77 public function testGarbageInputReturnsNull(): void
78 {
79 $guard = new SqliteQueryGuard(new SqliteParser());
80
81 self::assertNull($guard->classify('NOT VALID SQL %%%'));
82 }
83
84 public function testReplaceClassifiesAsWriteSimulated(): void
85 {
86 $guard = new SqliteQueryGuard(new SqliteParser());
87 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify("REPLACE INTO users (id, name) VALUES (1, 'Alice')"));
88 }
89
90 public function testInsertOrReplaceClassifiesAsWriteSimulated(): void
91 {
92 $guard = new SqliteQueryGuard(new SqliteParser());
93 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify("INSERT OR REPLACE INTO users (id, name) VALUES (1, 'Alice')"));
94 }
95
96 public function testAlterTableClassifiesAsDdlSimulated(): void
97 {
98 $guard = new SqliteQueryGuard(new SqliteParser());
99 self::assertSame(QueryKind::DDL_SIMULATED, $guard->classify('ALTER TABLE users ADD COLUMN email TEXT'));
100 }
101
102 public function testUnsupportedReturnsNull(): void
103 {
104 $guard = new SqliteQueryGuard(new SqliteParser());
105 self::assertNull($guard->classify('CREATE INDEX idx ON users (name)'));
106 }
107
108 public function testClassifiesReadOnlyExplainAsRead(): void
109 {
110 $guard = new SqliteQueryGuard(new SqliteParser());
111 self::assertSame(QueryKind::READ, $guard->classify('EXPLAIN SELECT * FROM users'));
112 self::assertSame(QueryKind::READ, $guard->classify('EXPLAIN QUERY PLAN SELECT * FROM users'));
113 self::assertSame(QueryKind::READ, $guard->classify('EXPLAIN QUERY PLAN DELETE FROM users'));
114 self::assertNull($guard->classify('EXPLAIN ANALYZE DELETE FROM users'));
115 }
116
117 public function testClassifiesOnlyInMemoryAttachAsRead(): void
118 {
119 $guard = new SqliteQueryGuard(new SqliteParser());
120 self::assertSame(QueryKind::READ, $guard->classify("ATTACH DATABASE ':memory:' AS db2"));
121 self::assertNull($guard->classify("ATTACH 'test.sqlite' AS db2"));
122 }
123
124 public function testEmptyReturnsNull(): void
125 {
126 $guard = new SqliteQueryGuard(new SqliteParser());
127 self::assertNull($guard->classify(''));
128 }
129
130 public function testCteSelectClassifiesAsRead(): void
131 {
132 $guard = new SqliteQueryGuard(new SqliteParser());
133 self::assertSame(QueryKind::READ, $guard->classify('WITH cte AS (SELECT 1) SELECT * FROM cte'));
134 }
135
136 public function testAssertAllowedDoesNotThrowForSelect(): void
137 {
138 $guard = new SqliteQueryGuard(new SqliteParser());
139 $guard->assertAllowed('SELECT * FROM users');
140 self::addToAssertionCount(1);
141 }
142
143 public function testAssertAllowedThrowsForUnsupported(): void
144 {
145 $guard = new SqliteQueryGuard(new SqliteParser());
146 $this->expectException(RuntimeException::class);
147 $guard->assertAllowed('GRANT ALL ON users TO admin');
148 }
149
150 public function testClassifyReturnsNullForEmptyString(): void
151 {
152 $guard = new SqliteQueryGuard(new SqliteParser());
153 $result = $guard->classify('');
154 self::assertNull($result);
155 }
156
157 public function testClassifyReturnsNullForCreateIndex(): void
158 {
159 $guard = new SqliteQueryGuard(new SqliteParser());
160 $result = $guard->classify('CREATE INDEX idx_name ON users (name)');
161 self::assertNull($result);
162 }
163
164 public function testSelectLowercaseClassifiesAsRead(): void
165 {
166 $guard = new SqliteQueryGuard(new SqliteParser());
167 self::assertSame(QueryKind::READ, $guard->classify('select * from users'));
168 }
169
170 public function testInsertLowercaseClassifiesAsWriteSimulated(): void
171 {
172 $guard = new SqliteQueryGuard(new SqliteParser());
173 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify("insert into users (name) values ('Alice')"));
174 }
175
176 public function testDeleteLowercaseClassifiesAsWriteSimulated(): void
177 {
178 $guard = new SqliteQueryGuard(new SqliteParser());
179 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('delete from users where id = 1'));
180 }
181
182 public function testUpdateLowercaseClassifiesAsWriteSimulated(): void
183 {
184 $guard = new SqliteQueryGuard(new SqliteParser());
185 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify("update users set name = 'x'"));
186 }
187
188 public function testAssertAllowedForInsert(): void
189 {
190 $guard = new SqliteQueryGuard(new SqliteParser());
191 $guard->assertAllowed('INSERT INTO t (id) VALUES (1)');
192 self::addToAssertionCount(1);
193 }
194
195 public function testAssertAllowedForCreateTable(): void
196 {
197 $guard = new SqliteQueryGuard(new SqliteParser());
198 $guard->assertAllowed('CREATE TABLE t (id INTEGER)');
199 self::addToAssertionCount(1);
200 }
201}
202