packages/sql-semantics-sqlite/src/SchemaRules.php
1<?php
2
3declare(strict_types=1);
4
5namespace SqlSemantics\Platform\Sqlite;
6
7use SqlParser\Parser\Node;
8use SqlSemantics\Core\Ast\Identifiers;
9use SqlSemantics\Core\Ast\Tree;
10use SqlSemantics\Core\Policy\SchemaRules as Contract;
11use SqlSemantics\Core\Schema\ColumnDefinition;
12use SqlSemantics\Core\Schema\ConstraintKind;
13use SqlSemantics\Core\Schema\TableConstraint;
14use SqlSemantics\Core\Schema\TableDefinition;
15
16/**
17 * Sqlite SchemaRules implementation.
18 *
19 * @visibility SqlSemantics
20 */
21final class SchemaRules implements Contract
22{
23 /**
24 * Rejects declarations whose column state requires evaluating another relation.
25 */
26 public function validate(Node $source, Node $header): void
27 {
28 Tree::assertChildren($source, ['create_table', 'create_table_args'], []);
29 Tree::assertChildren($header, ['createkw', 'nm', 'dbnm', 'ifnotexists'], ['TABLE']);
30 $arguments = Tree::child($source, ['create_table_args']);
31 if ($arguments === null) {
32 Tree::unsupported($source, 'table arguments');
33 }
34 Tree::assertChildren($arguments, ['columnlist', 'conslist_opt', 'table_option_set'], ['(', ')']);
35 return;
36 }
37
38 /**
39 * @return list<Node>
40 */
41 public function options(Node $source): array
42 {
43 return array_values(array_filter(Tree::outer($source, ['table_option']), static fn (Node $node): bool => $node->tokens() !== []));
44 }
45
46 /**
47 * Reports table-level primary key nullability.
48 */
49 public function primaryOptionsNotNull(Node $source): bool
50 {
51 foreach ($this->options($source) as $option) {
52 if (in_array(strtoupper(Tree::text($option)), ['STRICT', 'WITHOUT ROWID'], true)) {
53 return true;
54 }
55 }
56 return false;
57 }
58
59 /**
60 * @return list<array{Node, list<Node>}>
61 */
62 public function columnNodes(Node $create): array
63 {
64 $columns = [];
65 foreach ($create->find('columnlist') as $list) {
66 $column = Tree::child($list, ['columnname']);
67 if ($column !== null) {
68 $attributes = Tree::child($list, ['carglist']);
69 $columns[] = [$column, $attributes === null ? [] : Tree::outer($attributes, ['ccons'])];
70 }
71 }
72 return array_reverse($columns);
73 }
74
75 /**
76 * @param list<string> $primary
77 * @param list<TableConstraint> $constraints
78 */
79 public function primaryNotNull(ColumnDefinition $column, array $primary, array $constraints): bool
80 {
81 if ($column->type->name !== 'integer' || count($primary) !== 1) {
82 return false;
83 }
84 foreach ($constraints as $constraint) {
85 if ($constraint->kind === ConstraintKind::PrimaryKey && $constraint->inline && $constraint->descending) {
86 return false;
87 }
88 }
89 return true;
90 }
91
92 /**
93 * Selects the syntax node that owns the complete declaration.
94 */
95 public function schemaNode(Node $statement, Node $create): Node
96 {
97 return $statement;
98 }
99
100 /**
101 * Returns the declaration identity used for duplicate detection.
102 */
103 public function tableKey(TableDefinition $table): string
104 {
105 return strtolower($table->schema . "\x00" . $table->name);
106 }
107
108 /**
109 * @param list<string> $parts
110 * @return list<string>
111 */
112 public function qualify(Node $header, array $parts, Identifiers $identifiers): array
113 {
114 $database = Tree::child($header, ['dbnm']);
115 return $database === null ? $parts : [$parts[0], ...$identifiers->parts($database)];
116 }
117}
118