packages/sql-semantics/tests/Unit/Core/Policy/SchemaRulesTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Core\Policy;
6
7use PHPUnit\Framework\TestCase;
8use SqlParser\Parser\Node;
9use SqlSemantics\Core\Binder;
10use SqlSemantics\Core\Dialect;
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\PostgreSql\Dialect as PostgreSqlDialect;
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\PostgreSql\QueryRules::class)]
62#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\PostgreSql\Platform::class)]
63#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\PostgreSql\TypeRules::class)]
64#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\PostgreSql\NameRules::class)]
65#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\PostgreSql\SchemaRules::class)]
66#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\Sqlite\QueryRules::class)]
67#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\Sqlite\Platform::class)]
68#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\Sqlite\TypeRules::class)]
69#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\Sqlite\NameRules::class)]
70#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\Sqlite\SchemaRules::class)]
71#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\MySql\QueryRules::class)]
72#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\MySql\Platform::class)]
73#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\MySql\TypeRules::class)]
74#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\MySql\NameRules::class)]
75#[\PHPUnit\Framework\Attributes\CoversClass(\SqlSemantics\Platform\MySql\SchemaRules::class)]
76#[\PHPUnit\Framework\Attributes\Medium]
77final class SchemaRulesTest extends TestCase
78{
79 public function testColumnNodesHonorsAnInjectedPolicy(): void
80 {
81 $node = new Node('application-column', 0, []);
82 $rules = self::createStub(\SqlSemantics\Core\Policy\SchemaRules::class);
83 $rules->method('columnNodes')->willReturn([[$node, []]]);
84 $platform = self::createStub(\SqlSemantics\Core\Platform::class);
85 $platform->method('schema')->willReturn($rules);
86 $dialect = self::createStub(Dialect::class);
87 $dialect->method('platform')->willReturn($platform);
88 $reader = new \SqlSemantics\Core\Ast\SchemaReader(new \SqlSemantics\Core\Ast\Identifiers($dialect), 'application', new \SqlSemantics\Core\Analysis\ValueReader([]));
89 self::assertSame([[$node, []]], $reader->columnNodes(new Node('application-table', 0, [])));
90 }
91 public function testValidateAcceptsOrdinaryDeclarations(): void
92 {
93 $accept = static fn (\SqlSemantics\Core\Policy\SchemaRules $rules): \SqlSemantics\Core\Policy\SchemaRules => $rules;
94 self::assertSame(PostgreSqlDialect::PostgreSql->platform()->schema()::class, $accept(PostgreSqlDialect::PostgreSql->platform()->schema())::class);
95 $schema = (new SchemaBuilder(PostgreSqlDialect::PostgreSql))->build('CREATE TABLE items (id INTEGER PRIMARY KEY, label TEXT)');
96 self::assertCount(1, $schema->tables);
97 }
98 public function testPrimaryNotNullPromotesAnIntegerKey(): void
99 {
100 $accept = static fn (\SqlSemantics\Core\Policy\SchemaRules $rules): \SqlSemantics\Core\Policy\SchemaRules => $rules;
101 self::assertSame(PostgreSqlDialect::PostgreSql->platform()->schema()::class, $accept(PostgreSqlDialect::PostgreSql->platform()->schema())::class);
102 $schema = (new SchemaBuilder(PostgreSqlDialect::PostgreSql))->build('CREATE TABLE items (id INTEGER PRIMARY KEY, label TEXT)');
103 self::assertSame(Nullability::NotNull, $schema->tables[0]->columns[0]->nullability);
104 }
105 public function testSchemaNodeRetainsOriginalDeclaration(): void
106 {
107 $accept = static fn (\SqlSemantics\Core\Policy\SchemaRules $rules): \SqlSemantics\Core\Policy\SchemaRules => $rules;
108 self::assertSame(PostgreSqlDialect::PostgreSql->platform()->schema()::class, $accept(PostgreSqlDialect::PostgreSql->platform()->schema())::class);
109 $schema = (new SchemaBuilder(PostgreSqlDialect::PostgreSql))->build('CREATE TABLE items (id INTEGER PRIMARY KEY, label TEXT)');
110 self::assertStringContainsString('CREATE TABLE', \SqlSemantics\Statement\Writer::render($schema->tables[0]->source));
111 }
112 public function testTableKeyDistinguishesNames(): void
113 {
114 $accept = static fn (\SqlSemantics\Core\Policy\SchemaRules $rules): \SqlSemantics\Core\Policy\SchemaRules => $rules;
115 self::assertSame(PostgreSqlDialect::PostgreSql->platform()->schema()::class, $accept(PostgreSqlDialect::PostgreSql->platform()->schema())::class);
116 $schema = (new SchemaBuilder(PostgreSqlDialect::PostgreSql))->build('CREATE TABLE first (id INT)', 'CREATE TABLE second (id INT)');
117 self::assertNotSame(PostgreSqlDialect::PostgreSql->platform()->schema()->tableKey($schema->tables[0]), PostgreSqlDialect::PostgreSql->platform()->schema()->tableKey($schema->tables[1]));
118 }
119 public function testQualifyPreservesExplicitNamespaces(): void
120 {
121 $accept = static fn (\SqlSemantics\Core\Policy\SchemaRules $rules): \SqlSemantics\Core\Policy\SchemaRules => $rules;
122 self::assertSame(PostgreSqlDialect::PostgreSql->platform()->schema()::class, $accept(PostgreSqlDialect::PostgreSql->platform()->schema())::class);
123 $schema = (new SchemaBuilder(PostgreSqlDialect::PostgreSql))->build('CREATE TABLE app.items (id INT)');
124 self::assertSame('app', $schema->tables[0]->schema);
125 }
126
127 public function testOptionsRetainsStructuredTableChoices(): void
128 {
129 $state = (new \SqlSemantics\Facade\Schema(\SqlSemantics\Platform\Sqlite\Dialect::Sqlite))->analyze('CREATE TABLE t (id TEXT PRIMARY KEY) STRICT');
130 self::assertNotEmpty($state->tables[0]->options);
131 }
132
133 public function testPrimaryOptionsNotNullAccountsForTablePolicy(): void
134 {
135 $dialect = \SqlSemantics\Platform\Sqlite\Dialect::Sqlite;
136 $source = $dialect->platform()->parser()->parse('CREATE TABLE t (id TEXT PRIMARY KEY) STRICT');
137 self::assertSame(true, $dialect->platform()->schema()->primaryOptionsNotNull($source));
138 }
139
140}
141