packages/ztd-query-sqlite/tests/Unit/Rewrite/Context/ReferencedTableLookupTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Rewrite\Context;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use ZtdQuery\Platform\Sqlite\Rewrite\Context\ReferencedTableLookup;
11use ZtdQuery\Platform\Sqlite\Rewrite\Cte\SqliteCteShadowComposer;
12use ZtdQuery\Platform\Sqlite\Sql\SqliteParser;
13use ZtdQuery\Schema\TableDefinition;
14use ZtdQuery\Schema\TableDefinitionRegistry;
15use ZtdQuery\Schema\ViewDefinition;
16use ZtdQuery\Schema\ViewDefinitionSet;
17use ZtdQuery\Shadow\ShadowStore;
18
19#[CoversClass(ReferencedTableLookup::class)]
20#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentColumnParser::class)]
21#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentParser::class)]
22#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\ValueListParser::class)]
23#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Insert\InsertClauseParser::class)]
24#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\ExpressionSpan::class)]
25#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\IdentifierDecoder::class)]
26#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\LiteralMasker::class)]
27#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\OpaqueSqlSpan::class)]
28#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\QuotedSpan::class)]
29#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\TopLevelKeywordScanner::class)]
30#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\RelationSourceParser::class)]
31#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementClassifier::class)]
32#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementStructure::class)]
33#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\TargetTableParser::class)]
34#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Update\UpdateClauseParser::class)]
35#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\CteDependencies::class)]
36#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\CteHeaderParser::class)]
37#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\CtePrefixMerger::class)]
38#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\CteReferences::class)]
39#[UsesClass(SqliteCteShadowComposer::class)]
40#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\SqliteLexerProfile::class)]
41#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\SqliteLexicalMasker::class)]
42#[UsesClass(SqliteParser::class)]
43#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\SqliteSelectRelationParser::class)]
44final class ReferencedTableLookupTest extends TestCase
45{
46 public function testFindUnknownTableIgnoresUserCtesAndFindsUnknownJoinSources(): void
47 {
48 $registry = new TableDefinitionRegistry();
49 $store = new ShadowStore();
50 $views = new ViewDefinitionSet();
51 $lookup = new ReferencedTableLookup(new SqliteCteShadowComposer(), new SqliteParser(), $registry, $store, $views);
52 $store->set('users', [['id' => 1]]);
53 self::assertNull($lookup->findUnknownTable('WITH missing AS (SELECT 1) SELECT * FROM missing'));
54 self::assertSame('orders', $lookup->findUnknownTable('SELECT * FROM users JOIN orders ON users.id = orders.user_id'));
55 self::assertNull($lookup->findUnknownTable('DELETE FROM unknown'));
56 self::assertNull($lookup->findUnknownTable('SELECT * FROM users'));
57 }
58
59 public function testTableExistsRecognizesEachSchemaState(): void
60 {
61 $registry = new TableDefinitionRegistry();
62 $store = new ShadowStore();
63 $views = new ViewDefinitionSet();
64 $lookup = new ReferencedTableLookup(new SqliteCteShadowComposer(), new SqliteParser(), $registry, $store, $views);
65 $store->set('fixture', []);
66 $registry->register('known', new TableDefinition(['id'], [], [], [], []));
67 $registry->register('removed', new TableDefinition(['id'], [], [], [], []));
68 $registry->markRemoved('removed');
69 $views->register('view_users', new ViewDefinition('SELECT 1', []));
70 self::assertTrue($lookup->tableExists('fixture'));
71 self::assertTrue($lookup->tableExists('known'));
72 self::assertTrue($lookup->tableExists('removed'));
73 self::assertTrue($lookup->tableExists('view_users'));
74 self::assertFalse($lookup->tableExists('unknown'));
75 }
76
77 public function testHasSchemaContextIncludesViews(): void
78 {
79 $registry = new TableDefinitionRegistry();
80 $store = new ShadowStore();
81 $views = new ViewDefinitionSet();
82 $lookup = new ReferencedTableLookup(new SqliteCteShadowComposer(), new SqliteParser(), $registry, $store, $views);
83 self::assertFalse($lookup->hasSchemaContext());
84 $views->register('v', new ViewDefinition('SELECT 1', []));
85 self::assertTrue($lookup->hasSchemaContext());
86 }
87
88 public function testAssertKnownTablesRejectsUnknownSourcesWhenContextExists(): void
89 {
90 $registry = new TableDefinitionRegistry();
91 $store = new ShadowStore();
92 $views = new ViewDefinitionSet();
93 $lookup = new ReferencedTableLookup(new SqliteCteShadowComposer(), new SqliteParser(), $registry, $store, $views);
94 $registry->register('users', new TableDefinition(['id'], [], [], [], []));
95 $this->expectException(\ZtdQuery\Exception\UnknownSchemaException::class);
96 $lookup->assertKnownTables('SELECT * FROM missing', 'SELECT * FROM missing');
97 }
98
99 public function testAssertKnownTablesPermitsQueriesBeforeFixtureRegistration(): void
100 {
101 $registry = new TableDefinitionRegistry();
102 $store = new ShadowStore();
103 $views = new ViewDefinitionSet();
104 $lookup = new ReferencedTableLookup(new SqliteCteShadowComposer(), new SqliteParser(), $registry, $store, $views);
105 $lookup->assertKnownTables('SELECT * FROM external_table', 'SELECT * FROM external_table');
106 self::assertFalse($lookup->hasSchemaContext());
107 }
108
109}
110