packages/sql-catalog/tests/Unit/Extension/Laravel/QueryStateTest.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\Evaluation\ArrayEntry;
11use SqlCatalog\Core\Evaluation\ArrayTerm;
12use SqlCatalog\Core\Evaluation\Domain;
13use SqlCatalog\Core\Evaluation\LiteralTerm;
14use SqlCatalog\Core\Evaluation\ObjectTerm;
15use SqlCatalog\Core\Evaluation\OpaqueTerm;
16use SqlCatalog\Core\Evaluation\PatternTerm;
17use SqlCatalog\Core\Text\LiteralText;
18use SqlCatalog\Core\Text\TextGeneralization;
19use SqlCatalog\Core\Text\TextHole;
20use SqlCatalog\Core\Text\TextPattern;
21use SqlCatalog\Core\Type\TypeShape;
22use SqlCatalog\Extension\Laravel\QueryState;
23
24#[CoversClass(QueryState::class)]
25#[UsesClass(Domain::class)]
26#[UsesClass(ArrayTerm::class)]
27#[UsesClass(ArrayEntry::class)]
28#[UsesClass(ObjectTerm::class)]
29#[UsesClass(LiteralTerm::class)]
30#[UsesClass(OpaqueTerm::class)]
31#[UsesClass(PatternTerm::class)]
32#[UsesClass(TextPattern::class)]
33#[UsesClass(TextHole::class)]
34#[UsesClass(LiteralText::class)]
35#[UsesClass(TextGeneralization::class)]
36#[UsesClass(TypeShape::class)]
37final class QueryStateTest extends TestCase
38{
39 public function testGetLeavesUnsetFieldsNull(): void
40 {
41 self::assertNull((new QueryState())->get('limit')->soleLiteral()?->value);
42 }
43
44 public function testWithPreservesTheOriginalState(): void
45 {
46 $before = new QueryState(['table' => Domain::literal('users')]);
47 $after = $before->with('table', Domain::literal('posts'));
48 self::assertSame('users', $before->string('table'));
49 self::assertSame('posts', $after->string('table'));
50 }
51
52 public function testStringRejectsNonStringDomains(): void
53 {
54 self::assertNull((new QueryState(['limit' => Domain::literal(1)]))->string('limit'));
55 self::assertNull((new QueryState(['table' => Domain::unknown()]))->string('table'));
56 }
57
58 public function testItemsRetainsOrderedBindings(): void
59 {
60 $values = [Domain::literal('first'), Domain::literal(2)];
61 self::assertSame($values, (new QueryState(['whereBindings' => QueryState::list($values)]))->items('whereBindings'));
62 self::assertSame([], (new QueryState())->items('whereBindings'));
63 }
64
65 public function testAppendPreservesEarlierBindings(): void
66 {
67 $state = (new QueryState())->append('whereBindings', [Domain::literal(1)])->append('whereBindings', [Domain::literal(2)]);
68 self::assertSame([1, 2], array_map(static fn (Domain $value): mixed => $value->soleLiteral()?->value, $state->items('whereBindings')));
69 }
70
71 public function testListPreservesUnknownValuesAsBoundParameters(): void
72 {
73 $value = Domain::unknown('request value');
74 self::assertSame([$value], QueryState::list([$value])->soleArray()?->positional());
75 self::assertTrue(QueryState::list([])->soleArray()?->complete);
76 }
77
78 public function testRejectCannotBeErasedByAValidMutation(): void
79 {
80 $state = (new QueryState())->reject('unknown scope')->with('table', Domain::literal('users'));
81 self::assertFalse($state->get('problem')->isExact());
82 self::assertSame('users', $state->string('table'));
83 }
84
85 public function testFromKeepsAnUninitializedBuilderOpen(): void
86 {
87 self::assertFalse(QueryState::from(new ObjectTerm('Builder'))->get('problem')->isExact());
88 }
89
90 public function testArrayRoundTripsMetadataAndBindingDomains(): void
91 {
92 $state = new QueryState(['table' => Domain::literal('users'), 'limit' => Domain::literal(1)]);
93 self::assertSame($state->fields, $state->array()->named());
94 }
95
96 public function testObjectPreservesAllocationIdentity(): void
97 {
98 $object = new ObjectTerm('Builder', identity: 'allocation');
99 $updated = (new QueryState(['table' => Domain::literal('users')]))->object($object);
100 self::assertSame('allocation', $updated->identity);
101 self::assertSame('Builder', $updated->className);
102 self::assertSame('users', QueryState::from($updated)->string('table'));
103 self::assertNull($object->state);
104 }
105}
106