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