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