packages/sql-semantics/tests/Unit/Core/Binding/LiteralBinderTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Core\Binding;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\Medium;
10use PHPUnit\Framework\TestCase;
11use SqlSemantics\Core\Binder;
12use SqlSemantics\Core\SchemaBuilder;
13use SqlSemantics\Core\SemanticException;
14use SqlSemantics\Core\Type\Nullability;
15use SqlSemantics\Platform\MySql\Dialect as MySqlDialect;
16use SqlSemantics\Platform\PostgreSql\Dialect as PostgreSqlDialect;
17use SqlSemantics\Platform\Sqlite\Dialect as SqliteDialect;
18
19#[CoversClass(\SqlSemantics\Core\Binding\LiteralBinder::class)]
20#[CoversClass(\SqlSemantics\Core\Binding\ExpressionBinder::class)]
21#[CoversClass(\SqlSemantics\Core\Binding\ExpressionRules::class)]
22#[CoversClass(\SqlSemantics\Core\Binding\FromBinder::class)]
23#[CoversClass(\SqlSemantics\Core\Binding\NullFacts::class)]
24#[CoversClass(\SqlSemantics\Core\Binding\ProjectionBinder::class)]
25#[CoversClass(\SqlSemantics\Core\Binding\SelectBinder::class)]
26#[CoversClass(\SqlSemantics\Core\Binding\SyntaxGuard::class)]
27#[CoversClass(\SqlSemantics\Core\Binding\SelectModifiersBinder::class)]
28#[CoversClass(\SqlSemantics\Core\Binding\TypeResolution::class)]
29#[CoversClass(Binder::class)]
30#[CoversClass(SchemaBuilder::class)]
31#[CoversClass(\SqlSemantics\Core\Ast\DialectParser::class)]
32#[CoversClass(\SqlSemantics\Core\Ast\ColumnReader::class)]
33#[CoversClass(\SqlSemantics\Core\Ast\ConstraintReader::class)]
34#[CoversClass(\SqlSemantics\Core\Ast\Identifiers::class)]
35#[CoversClass(\SqlSemantics\Core\Ast\SchemaReader::class)]
36#[CoversClass(\SqlSemantics\Core\Ast\StatementList::class)]
37#[CoversClass(\SqlSemantics\Core\Ast\TokenGroups::class)]
38#[CoversClass(\SqlSemantics\Core\Ast\Tree::class)]
39#[CoversClass(\SqlSemantics\Core\Ast\TypeReader::class)]
40#[CoversClass(\SqlSemantics\Core\Binding\BoundRelation::class)]
41#[CoversClass(\SqlSemantics\Core\Binding\IdentitySequence::class)]
42#[CoversClass(\SqlSemantics\Core\Binding\Scope::class)]
43#[CoversClass(\SqlSemantics\Core\Binding\TableResolver::class)]
44#[CoversClass(\SqlSemantics\Core\Model\ColumnBinding::class)]
45#[CoversClass(\SqlSemantics\Core\Model\Expression::class)]
46#[CoversClass(\SqlSemantics\Core\Model\Join::class)]
47#[CoversClass(\SqlSemantics\Core\Model\Ordering::class)]
48#[CoversClass(\SqlSemantics\Core\Model\OutputColumn::class)]
49#[CoversClass(\SqlSemantics\Core\Model\BoundSelect::class)]
50#[CoversClass(\SqlSemantics\Core\Model\TableUse::class)]
51#[CoversClass(\SqlSemantics\Core\Schema::class)]
52#[CoversClass(\SqlSemantics\Core\Schema\ColumnDefinition::class)]
53#[CoversClass(\SqlSemantics\Core\Schema\TableConstraint::class)]
54#[CoversClass(\SqlSemantics\Core\Schema\TableDefinition::class)]
55#[CoversClass(SemanticException::class)]
56#[CoversClass(\SqlSemantics\Core\Type\TypeDescriptor::class)]
57#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Core\Policy\SyntaxRules::class)]
58#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\PostgreSql\QueryRules::class)]
59#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\PostgreSql\Platform::class)]
60#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\PostgreSql\TypeRules::class)]
61#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\PostgreSql\NameRules::class)]
62#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\PostgreSql\SchemaRules::class)]
63#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\Sqlite\QueryRules::class)]
64#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\Sqlite\Platform::class)]
65#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\Sqlite\TypeRules::class)]
66#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\Sqlite\NameRules::class)]
67#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\Sqlite\SchemaRules::class)]
68#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\MySql\QueryRules::class)]
69#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\MySql\Platform::class)]
70#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\MySql\TypeRules::class)]
71#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\MySql\NameRules::class)]
72#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\MySql\SchemaRules::class)]
73#[Medium]
74final class LiteralBinderTest extends TestCase
75{
76    public function testIntegerClassifiesPostgresWidthsWithoutPhpOverflow(): void
77    {
78        $schema = (new SchemaBuilder(PostgreSqlDialect::PostgreSql))->build();
79        $statement = (new Binder($schema))->bind('SELECT 2147483647, 2147483648, 9223372036854775808');
80        self::assertSame('integer', $statement->outputs[0]->expression->type->name);
81        self::assertSame('bigint', $statement->outputs[1]->expression->type->name);
82        self::assertSame('numeric', $statement->outputs[2]->expression->type->name);
83    }
84
85    public function testBindParametersRemainExplicitlyUnknownWithoutBindings(): void
86    {
87        $schema = (new SchemaBuilder(PostgreSqlDialect::PostgreSql))->build();
88        $statement = (new Binder($schema))->bind('SELECT $1');
89        self::assertSame(\SqlSemantics\Core\Model\ExpressionKind::Parameter, $statement->outputs[0]->expression->kind);
90        self::assertSame(Nullability::Unknown, $statement->outputs[0]->expression->nullability);
91        self::assertSame('unknown', $statement->outputs[0]->expression->type->name);
92    }
93
94    #[DataProvider('providerDecimalIntegers')]
95    public function testIntegerResolvesDecimalBoundaries(string $literal, string $expected): void
96    {
97        self::assertSame($expected, (new \SqlSemantics\Core\Binding\LiteralBinder(PostgreSqlDialect::PostgreSql))->integer($literal));
98    }
99
100    /**
101     * @return iterable<string, array{string, string}>
102     */
103    public static function providerDecimalIntegers(): iterable
104    {
105        yield '0' => ['0', 'integer'];
106        yield '00001' => ['00001', 'integer'];
107        yield '2147483646' => ['2147483646', 'integer'];
108        yield '2147483647' => ['2147483647', 'integer'];
109        yield '2147483648' => ['2147483648', 'bigint'];
110        yield '999999999' => ['999999999', 'integer'];
111        yield '9999999999' => ['9999999999', 'bigint'];
112        yield '10000000000' => ['10000000000', 'bigint'];
113        yield '0002147483648' => ['0002147483648', 'bigint'];
114        yield '9223372036854775806' => ['9223372036854775806', 'bigint'];
115        yield '9223372036854775807' => ['9223372036854775807', 'bigint'];
116        yield '9223372036854775808' => ['9223372036854775808', 'numeric'];
117        yield '10000000000000000000' => ['10000000000000000000', 'numeric'];
118    }
119
120    public function testBindDistinguishesMysqlUnsignedAndDecimalBoundaries(): void
121    {
122        $schema = (new SchemaBuilder(MySqlDialect::MySql))->build();
123        $statement = (new Binder($schema))->bind('SELECT 2147483648, 9223372036854775808, 18446744073709551616');
124        self::assertSame('bigint', $statement->outputs[0]->expression->type->name);
125        self::assertSame('bigint unsigned', $statement->outputs[1]->expression->type->name);
126        self::assertSame('numeric', $statement->outputs[2]->expression->type->name);
127    }
128
129    public function testBindDistinguishesSqliteOverflowAsReal(): void
130    {
131        $schema = (new SchemaBuilder(SqliteDialect::Sqlite))->build();
132        $statement = (new Binder($schema))->bind('SELECT 9223372036854775807, 9223372036854775808');
133        self::assertSame('integer', $statement->outputs[0]->expression->type->name);
134        self::assertSame('real', $statement->outputs[1]->expression->type->name);
135    }
136
137    public function testTypeNameRetainsLiteralCategoriesAndRejectsIdentifiers(): void
138    {
139        $reader = new \SqlSemantics\Core\Binding\LiteralBinder(PostgreSqlDialect::PostgreSql);
140        self::assertSame('numeric', $reader->typeName(new \SqlParser\Lexer\Token(1, 'FCONST', '1.25', 0)));
141        self::assertSame('unknown', $reader->typeName(new \SqlParser\Lexer\Token(1, 'SCONST', "'value'", 0)));
142        self::assertNull($reader->typeName(new \SqlParser\Lexer\Token(1, 'IDENT', 'value', 0)));
143    }
144}
145