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