packages/sql-semantics-mysql/tests/Unit/TypeRulesTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit;
6
7use PHPUnit\Framework\TestCase;
8use SqlParser\Lexer\Token;
9use SqlParser\Parser\Node;
10use SqlSemantics\Core\Binder;
11use SqlSemantics\Core\Model\Expression;
12use SqlSemantics\Core\Model\ExpressionKind;
13use SqlSemantics\Core\SchemaBuilder;
14use SqlSemantics\Core\Type\Nullability;
15use SqlSemantics\Core\Type\TypeDescriptor;
16use SqlSemantics\Platform\MySql\Dialect;
17
18#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\SemanticException::class)]
19#[\PHPUnit\Framework\Attributes\CoversClass(SchemaBuilder::class)]
20#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Schema::class)]
21#[\PHPUnit\Framework\Attributes\CoversClass(Binder::class)]
22#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\NullFacts::class)]
23#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\TypeResolution::class)]
24#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\IdentitySequence::class)]
25#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\SyntaxGuard::class)]
26#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\Scope::class)]
27#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\BoundRelation::class)]
28#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\FromBinder::class)]
29#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\SelectModifiersBinder::class)]
30#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\TableResolver::class)]
31#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\SelectBinder::class)]
32#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\ProjectionBinder::class)]
33#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\ExpressionRules::class)]
34#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\ExpressionBinder::class)]
35#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Binding\LiteralBinder::class)]
36#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Schema\ColumnDefinition::class)]
37#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Schema\TableDefinition::class)]
38#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Schema\ConstraintKind::class)]
39#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Schema\TableConstraint::class)]
40#[\PHPUnit\Framework\Attributes\CoversClass(Nullability::class)]
41#[\PHPUnit\Framework\Attributes\CoversClass(TypeDescriptor::class)]
42#[\PHPUnit\Framework\Attributes\CoversClass(Expression::class)]
43#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Model\Join::class)]
44#[\PHPUnit\Framework\Attributes\CoversClass(ExpressionKind::class)]
45#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Model\BoundSelect::class)]
46#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Model\TableUse::class)]
47#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Model\ColumnBinding::class)]
48#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Model\Ordering::class)]
49#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Model\OutputColumn::class)]
50#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Model\JoinKind::class)]
51#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Ast\TypeReader::class)]
52#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Ast\DialectParser::class)]
53#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Ast\TokenGroups::class)]
54#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Ast\Tree::class)]
55#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Ast\ColumnReader::class)]
56#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Ast\SchemaReader::class)]
57#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Ast\ConstraintReader::class)]
58#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Ast\Identifiers::class)]
59#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Ast\StatementList::class)]
60#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Core\Policy\SyntaxRules::class)]
61#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\MySql\QueryRules::class)]
62#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\MySql\Platform::class)]
63#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\MySql\TypeRules::class)]
64#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\MySql\NameRules::class)]
65#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\MySql\SchemaRules::class)]
66#[\PHPUnit\Framework\Attributes\CoversClass(Dialect::class)]
67#[\PHPUnit\Framework\Attributes\Medium]
68final class TypeRulesTest extends TestCase
69{
70    public function testReadPreservesModifiers(): void
71    {
72        $schema = (new SchemaBuilder(Dialect::MySql))->build('CREATE TABLE items (value DECIMAL(10, 2))');
73        self::assertSame(['10', '2'], $schema->tables[0]->columns[0]->type->modifiers);
74    }
75
76    public function testCanonicalResolvesInteger(): void
77    {
78        self::assertSame('integer', (new \SqlSemantics\Platform\MySql\TypeRules(Dialect::MySql))->canonical('INT'));
79    }
80
81    public function testAffinityUsesDeclaredTypePrecedence(): void
82    {
83        self::assertSame('integer', (new \SqlSemantics\Platform\MySql\TypeRules(Dialect::MySql))->affinity('FLOATING POINT'));
84    }
85
86    public function testTypeNameClassifiesIntegerToken(): void
87    {
88        self::assertSame('integer', (new \SqlSemantics\Platform\MySql\TypeRules(Dialect::MySql))->typeName(new Token(1, 'NUM', '42', 0)));
89    }
90
91    public function testIntegerModelsLargeMagnitude(): void
92    {
93        self::assertSame('integer', (new \SqlSemantics\Platform\MySql\TypeRules(Dialect::MySql))->integer('2147483648'));
94    }
95
96    public function testCommonPreservesHomogeneousType(): void
97    {
98        $source = new Node('value', 0, []);
99        $expression = new Expression(ExpressionKind::Literal, new TypeDescriptor(Dialect::MySql, 'integer'), Nullability::NotNull, $source, symbol: '1');
100        self::assertSame('integer', (new \SqlSemantics\Platform\MySql\TypeRules(Dialect::MySql))->common([$expression], $source)->name);
101    }
102
103    public function testBooleanNamesPredicateResult(): void
104    {
105        self::assertSame('integer', (new \SqlSemantics\Platform\MySql\TypeRules(Dialect::MySql))->boolean()->name);
106    }
107
108    public function testArithmeticRetainsLanguageSemantics(): void
109    {
110        $source = new Node('value', 0, []);
111        $expression = new Expression(ExpressionKind::Literal, new TypeDescriptor(Dialect::MySql, 'integer'), Nullability::NotNull, $source, symbol: '1');
112        self::assertSame('bigint', (new \SqlSemantics\Platform\MySql\TypeRules(Dialect::MySql))->arithmetic('+', [$expression], $source)->name);
113    }
114
115    public function testPredicateAcceptsBooleanResults(): void
116    {
117        $source = new Node('value', 0, []);
118        $expression = new Expression(ExpressionKind::Literal, (new \SqlSemantics\Platform\MySql\TypeRules(Dialect::MySql))->boolean(), Nullability::NotNull, $source);
119        (new \SqlSemantics\Platform\MySql\TypeRules(Dialect::MySql))->predicate($expression);
120        self::assertSame($source, $expression->source);
121    }
122
123    public function testCoalescePreservesOperands(): void
124    {
125        $source = new Node('value', 0, []);
126        $expression = new Expression(ExpressionKind::Literal, new TypeDescriptor(Dialect::MySql, 'integer'), Nullability::NotNull, $source, symbol: '1');
127        self::assertSame([$expression], (new \SqlSemantics\Platform\MySql\TypeRules(Dialect::MySql))->coalesce([$expression], $expression->type));
128    }
129
130    public function testProjectPreservesTypedValues(): void
131    {
132        $source = new Node('value', 0, []);
133        $expression = new Expression(ExpressionKind::Literal, new TypeDescriptor(Dialect::MySql, 'integer'), Nullability::NotNull, $source, symbol: '1');
134        self::assertSame($expression, (new \SqlSemantics\Platform\MySql\TypeRules(Dialect::MySql))->project($expression));
135    }
136}
137