packages/sql-catalog/tests/Unit/Extension/Laravel/LaravelExtensionTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Extension\Laravel;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlCatalog\Core\Extension\SinkCallKind;
11use SqlCatalog\Core\Extension\SinkSpec;
12use SqlCatalog\Core\Sql\StatementKind;
13use SqlCatalog\Extension\Laravel\LaravelExtension;
14
15#[CoversClass(LaravelExtension::class)]
16#[UsesClass(SinkSpec::class)]
17#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\FreeNames::class)]
18#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\ModifiedNames::class)]
19#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Objects\CallbackEffects::class)]
20#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\SliceExecutor::class)]
21#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Slice\AssignmentSteps::class)]
22#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Slice\BackwardSlicer::class)]
23#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Slice\LoopPasses::class)]
24#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\SourceTree::class)]
25#[UsesClass(\SqlCatalog\Core\Analysis\EvaluationBudget::class)]
26#[UsesClass(\SqlCatalog\Extension\Laravel\BuilderCalls::class)]
27#[UsesClass(\SqlCatalog\Extension\Laravel\BuilderQueries::class)]
28#[UsesClass(\SqlCatalog\Extension\Laravel\CallModel::class)]
29#[UsesClass(\SqlCatalog\Extension\Laravel\CallbackModel::class)]
30#[UsesClass(\SqlCatalog\Core\Extension\Model\ModelContext::class)]
31#[UsesClass(\SqlCatalog\Core\Extension\Model\ModelSet::class)]
32#[UsesClass(\SqlCatalog\Core\Php\DeclaredGlobals::class)]
33#[UsesClass(\SqlCatalog\Core\Php\NodeText::class)]
34#[UsesClass(\SqlCatalog\Core\Php\ProgramIndex::class)]
35final class LaravelExtensionTest extends TestCase
36{
37    public function testNameIsHowTheCommandLineSelectsIt(): void
38    {
39        self::assertSame('laravel', (new LaravelExtension(\SqlCatalog\Facade\Builtins::dialects()))->name());
40    }
41
42    public function testDescriptionMentionsWhatItCovers(): void
43    {
44        self::assertStringContainsString('Laravel', (new LaravelExtension(\SqlCatalog\Facade\Builtins::dialects()))->description());
45    }
46
47    public function testSinksCoverBothTheFacadeAndTheConnection(): void
48    {
49        $kinds = array_map(static fn (SinkSpec $sink): SinkCallKind => $sink->callKind, (new LaravelExtension(\SqlCatalog\Facade\Builtins::dialects()))->sinks());
50        self::assertContains(SinkCallKind::StaticCall, $kinds);
51        self::assertContains(SinkCallKind::Method, $kinds);
52    }
53
54    public function testSinksTakeTheStatementFirstAndTheBindingsSecond(): void
55    {
56        $sinks = (new LaravelExtension(\SqlCatalog\Facade\Builtins::dialects()))->sinks();
57        self::assertSame(0, $sinks[0]->sqlParameter);
58        self::assertSame(1, $sinks[0]->valuesParameter);
59    }
60
61    public function testMethodsNameTheKindEachOneImplies(): void
62    {
63        $methods = (new LaravelExtension(\SqlCatalog\Facade\Builtins::dialects()))->methods();
64        self::assertSame(StatementKind::Select, $methods['select']);
65        self::assertSame(StatementKind::Insert, $methods['insert']);
66        self::assertNull($methods['statement']);
67    }
68    public function testEverySinkIsDescribedExactly(): void
69    {
70        $described = array_map(
71            static fn (SinkSpec $sink): string => implode('|', [
72                $sink->id,
73                $sink->callKind->value,
74                $sink->receiverType ?? '-',
75                $sink->name,
76                $sink->role->value,
77                $sink->sqlParameter ?? '-',
78                $sink->valuesParameter ?? '-',
79                $sink->valuesFrom ?? '-',
80                $sink->nameParameter ?? '-',
81                $sink->valueParameter ?? '-',
82                ($sink->kind === null ? '-' : $sink->kind->value) . '/' . ($sink->handleType ?? '-'),
83            ]),
84            array_values(array_filter((new LaravelExtension(\SqlCatalog\Facade\Builtins::dialects()))->sinks(), static fn (SinkSpec $sink): bool => $sink->role === \SqlCatalog\Core\Extension\SinkRole::Query)),
85        );
86
87        self::assertSame(
88            [
89            'laravel.db.select|static|Illuminate\Support\Facades\DB|select|query|0|1|-|-|-|select/-',
90            'laravel.connection.select|method|Illuminate\Database\Connection|select|query|0|1|-|-|-|select/-',
91            'laravel.db.selectOne|static|Illuminate\Support\Facades\DB|selectOne|query|0|1|-|-|-|select/-',
92            'laravel.connection.selectOne|method|Illuminate\Database\Connection|selectOne|query|0|1|-|-|-|select/-',
93            'laravel.db.scalar|static|Illuminate\Support\Facades\DB|scalar|query|0|1|-|-|-|select/-',
94            'laravel.connection.scalar|method|Illuminate\Database\Connection|scalar|query|0|1|-|-|-|select/-',
95            'laravel.db.insert|static|Illuminate\Support\Facades\DB|insert|query|0|1|-|-|-|insert/-',
96            'laravel.connection.insert|method|Illuminate\Database\Connection|insert|query|0|1|-|-|-|insert/-',
97            'laravel.db.update|static|Illuminate\Support\Facades\DB|update|query|0|1|-|-|-|update/-',
98            'laravel.connection.update|method|Illuminate\Database\Connection|update|query|0|1|-|-|-|update/-',
99            'laravel.db.delete|static|Illuminate\Support\Facades\DB|delete|query|0|1|-|-|-|delete/-',
100            'laravel.connection.delete|method|Illuminate\Database\Connection|delete|query|0|1|-|-|-|delete/-',
101            'laravel.db.statement|static|Illuminate\Support\Facades\DB|statement|query|0|1|-|-|-|-/-',
102            'laravel.connection.statement|method|Illuminate\Database\Connection|statement|query|0|1|-|-|-|-/-',
103            'laravel.db.unprepared|static|Illuminate\Support\Facades\DB|unprepared|query|0|1|-|-|-|-/-',
104            'laravel.connection.unprepared|method|Illuminate\Database\Connection|unprepared|query|0|1|-|-|-|-/-',
105            ],
106            $described,
107        );
108    }
109
110    public function testBuilderMethodsDeclareReadsWritesAndCompoundExecutions(): void
111    {
112        $methods = (new LaravelExtension(\SqlCatalog\Facade\Builtins::dialects()))->builderMethods();
113        self::assertCount(37, $methods);
114        self::assertSame(StatementKind::Select, $methods['sole']);
115        self::assertSame(StatementKind::Select, $methods['get']);
116        self::assertSame(StatementKind::Insert, $methods['insert']);
117        self::assertSame(StatementKind::Update, $methods['update']);
118        self::assertSame(StatementKind::Delete, $methods['delete']);
119        self::assertNull($methods['save']);
120        $builders = array_filter((new LaravelExtension(\SqlCatalog\Facade\Builtins::dialects()))->sinks(), static fn (SinkSpec $sink): bool => $sink->role === \SqlCatalog\Core\Extension\SinkRole::Modelled);
121        self::assertCount(count($methods) * 4, $builders);
122    }
123
124    public function testGlobalsAreEmptyBecauseTheHandleIsNotReachedThroughOne(): void
125    {
126        self::assertSame([], (new LaravelExtension(\SqlCatalog\Facade\Builtins::dialects()))->globals());
127    }
128    public function testModelsRegistersLaravelSemanticsThroughTheOptionalContract(): void
129    {
130        $index = new \SqlCatalog\Core\Php\ProgramIndex();
131        $budget = new \SqlCatalog\Core\Analysis\EvaluationBudget();
132        $names = new \SqlCatalog\Core\Analysis\Derivation\FreeNames();
133        $modified = new \SqlCatalog\Core\Analysis\Derivation\ModifiedNames($names);
134        $callbacks = new \SqlCatalog\Core\Analysis\Derivation\Objects\CallbackEffects(new \SqlCatalog\Core\Analysis\Derivation\Slice\BackwardSlicer(new \SqlCatalog\Core\Analysis\Derivation\SourceTree([]), $budget, $names, $modified), new \SqlCatalog\Core\Analysis\Derivation\SliceExecutor(new \SqlCatalog\Core\Php\DeclaredGlobals(), new \SqlCatalog\Core\Php\TypeReader(), $modified, new \SqlCatalog\Core\Php\NodeText(), $budget));
135        $context = new \SqlCatalog\Core\Extension\Model\ModelContext($index, $callbacks, 'sqlite');
136        $models = (new LaravelExtension(\SqlCatalog\Facade\Builtins::dialects()))->models($context);
137        self::assertCount(1, $models->calls);
138        self::assertArrayHasKey('laravel.builder', $models->queries);
139        self::assertTrue($models->matchesClass('Illuminate\\Database\\MySqlConnection', 'Illuminate\\Database\\Connection'));
140    }
141
142}
143