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