packages/sql-catalog/tests/Unit/Core/Text/TextGeneralizationTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Core\Text;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlCatalog\Core\Text\LiteralText;
11use SqlCatalog\Core\Text\Origin;
12use SqlCatalog\Core\Text\TextGeneralization;
13use SqlCatalog\Core\Text\TextHole;
14use SqlCatalog\Core\Text\TextPattern;
15use SqlCatalog\Core\Type\TypeShape;
16
17#[CoversClass(TextGeneralization::class)]
18#[UsesClass(LiteralText::class)]
19#[UsesClass(TextHole::class)]
20#[UsesClass(TextPattern::class)]
21#[UsesClass(TypeShape::class)]
22final class TextGeneralizationTest extends TestCase
23{
24    public function testMergeKeepsWhatTheTwoSidesAgreeOn(): void
25    {
26        $merged = (new TextGeneralization(Origin::Loop))->merge(
27            TextPattern::fromText('SELECT a FROM t'),
28            TextPattern::fromText('SELECT b FROM t'),
29        );
30        self::assertSame('SELECT {$} FROM t', $merged->display());
31    }
32
33    public function testMergeReturnsTheSameShapeWhenBothSidesAgree(): void
34    {
35        $merged = (new TextGeneralization(Origin::Loop))->merge(
36            TextPattern::fromText('SELECT 1'),
37            TextPattern::fromText('SELECT 1'),
38        );
39        self::assertSame('SELECT 1', $merged->display());
40    }
41
42    public function testMergeCoversAGrowingSuffix(): void
43    {
44        $merged = (new TextGeneralization(Origin::Loop))->merge(
45            TextPattern::fromText('WHERE 1 AND x'),
46            TextPattern::fromText('WHERE 1 AND x AND x'),
47        );
48        self::assertSame('WHERE 1 AND x{$}', $merged->display());
49    }
50
51    public function testMergeResolvedAnswersOnlyForTwoResolvedStrings(): void
52    {
53        $generalization = new TextGeneralization(Origin::Branch);
54        $exact = TextPattern::fromText('SELECT a FROM t');
55        $gapped = TextPattern::fromHole(new TextHole(Origin::Loop, TypeShape::unknown()));
56
57        self::assertSame(
58            'SELECT {$} FROM t',
59            $generalization->mergeResolved($exact, TextPattern::fromText('SELECT b FROM t'))?->display(),
60        );
61        self::assertNull($generalization->mergeResolved($exact, $gapped));
62    }
63
64    public function testSharedPrefixLengthCountsTheLeadingBytesTwoStringsShare(): void
65    {
66        $generalization = new TextGeneralization(Origin::Branch);
67
68        self::assertSame(3, $generalization->sharedPrefixLength('abcd', 'abcx'));
69        self::assertSame(0, $generalization->sharedPrefixLength('a', 'b'));
70        self::assertSame(2, $generalization->sharedPrefixLength('ab', 'abcd'));
71    }
72
73    public function testMergeAllFoldsEveryAlternative(): void
74    {
75        $merged = (new TextGeneralization(Origin::Branch))->mergeAll([
76            TextPattern::fromText('ORDER BY a'),
77            TextPattern::fromText('ORDER BY b'),
78            TextPattern::fromText('ORDER BY c'),
79        ]);
80        self::assertSame('ORDER BY {$}', $merged->display());
81    }
82
83    public function testMergeAllOfNothingIsEmpty(): void
84    {
85        self::assertTrue((new TextGeneralization(Origin::Branch))->mergeAll([])->isEmpty());
86    }
87
88    public function testAtomsSplitTextIntoCharactersAndKeepGapsWhole(): void
89    {
90        $hole = new TextHole(Origin::Loop, TypeShape::unknown());
91        $atoms = (new TextGeneralization(Origin::Loop))->atoms(
92            TextPattern::fromSegments([new LiteralText('ab'), $hole]),
93        );
94        self::assertSame(['a', 'b', $hole], $atoms);
95    }
96
97    public function testCommonPrefixLengthCountsSharedLeadingAtoms(): void
98    {
99        $generalization = new TextGeneralization(Origin::Loop);
100        self::assertSame(2, $generalization->commonPrefixLength(['a', 'b', 'c'], ['a', 'b', 'x']));
101        self::assertSame(0, $generalization->commonPrefixLength(['a'], ['b']));
102    }
103
104    public function testCommonSuffixLengthStopsAtTheGivenLimit(): void
105    {
106        $generalization = new TextGeneralization(Origin::Loop);
107        self::assertSame(2, $generalization->commonSuffixLength(['x', 'b', 'c'], ['y', 'b', 'c'], 3));
108        self::assertSame(1, $generalization->commonSuffixLength(['x', 'b', 'c'], ['y', 'b', 'c'], 1));
109    }
110
111    public function testSameAtomComparesGapsByOrigin(): void
112    {
113        $generalization = new TextGeneralization(Origin::Loop);
114        $left = new TextHole(Origin::Loop, TypeShape::unknown());
115        $right = new TextHole(Origin::Loop, TypeShape::of(['string']));
116        $other = new TextHole(Origin::External, TypeShape::unknown());
117        self::assertTrue($generalization->sameAtom($left, $right));
118        self::assertFalse($generalization->sameAtom($left, $other));
119        self::assertFalse($generalization->sameAtom($left, 'a'));
120        self::assertTrue($generalization->sameAtom('a', 'a'));
121    }
122
123    public function testRebuildTurnsAtomsBackIntoSegments(): void
124    {
125        $segments = (new TextGeneralization(Origin::Loop))->rebuild(['a', 'b']);
126        self::assertSame('ab', TextPattern::fromSegments($segments)->display());
127    }
128
129    public function testGapTypeIsAStringOnlyWhenBothSidesResolved(): void
130    {
131        $generalization = new TextGeneralization(Origin::Loop);
132        $exact = TextPattern::fromText('a');
133        $inexact = TextPattern::fromHole(new TextHole(Origin::Loop, TypeShape::unknown()));
134        self::assertSame('string', $generalization->gapType($exact, $exact)->display());
135        self::assertSame('mixed', $generalization->gapType($exact, $inexact)->display());
136    }
137}
138