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