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