Lines 96.65% 231 / 239
Methods 78.94% 30 / 38
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 wrap 100.00% 1 / 1 100.00% 1 / 1 1
 type 100.00% 15 / 15 100.00% 1 / 1 16
 autoIncrement 100.00% 1 / 1 100.00% 1 / 1 1
 autoIncrementBeforePrimaryKey 100.00% 1 / 1 100.00% 1 / 1 1
 compilePartialIndexClause 100.00% 1 / 1 100.00% 1 / 1 1
 assertValidIdentifier 100.00% 1 / 1 100.00% 1 / 1 1
 compileDropIndex 100.00% 2 / 2 100.00% 1 / 1 1
 compileAddColumns 93.75% 15 / 16 0.00% 0 / 1 4.00
 addedNotNullWithoutDefault 75.00% 3 / 4 0.00% 0 / 1 4.25
 compileDropColumns 88.88% 8 / 9 0.00% 0 / 1 2.01
 compileDropColumn 100.00% 1 / 1 100.00% 1 / 1 1
 compileRebuildTable 97.43% 38 / 39 0.00% 0 / 1 8
 compileCopyTableWithBackfill 100.00% 13 / 13 100.00% 1 / 1 3
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileCreate 100.00% 1 / 1 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileTableBody 100.00% 22 / 22 100.00% 1 / 1 5
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileCheckConstraint 75.00% 3 / 4 0.00% 0 / 1 2.06
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileForeignKeyConstraint 91.66% 11 / 12 0.00% 0 / 1 6.02
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileDeferrableClause 100.00% 3 / 3 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] withTemporaryDefault 100.00% 3 / 3 100.00% 1 / 1 2
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileSetColumnDefault 0.00% 0 / 1 0.00% 0 / 1 2
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileDropColumnDefault 0.00% 0 / 1 0.00% 0 / 1 2
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileDrop 100.00% 1 / 1 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileRenameTable 100.00% 1 / 1 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileRenameColumn 100.00% 2 / 2 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileCopyTable 100.00% 4 / 4 100.00% 1 / 1 2
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileModifyColumn 100.00% 3 / 3 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileAddForeignKey 100.00% 3 / 3 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileDropForeignKey 100.00% 3 / 3 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileAddCheck 100.00% 3 / 3 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileDropCheck 100.00% 3 / 3 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileIndexes 100.00% 18 / 18 100.00% 1 / 1 4
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileNullsNotDistinctClause 100.00% 5 / 5 100.00% 1 / 1 2
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileAddColumn 100.00% 1 / 1 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileColumnDefinition 100.00% 16 / 16 100.00% 1 / 1 11
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileEnumCheckClause 100.00% 8 / 8 100.00% 1 / 1 4
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] compileDefault 100.00% 7 / 7 100.00% 1 / 1 4
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] requireLength 100.00% 3 / 3 100.00% 1 / 1 2
 [BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar] suffix 100.00% 7 / 7 100.00% 1 / 1 2
19final class SqliteSchemaGrammar extends SchemaGrammar
20{
21    /**
22     * Wrap an identifier in SQLite double quotes.
23     *
24     * @param  string  $value
25     * @return string
26     */
27    protected function wrap(string $value): string
28    {
29        return '"' . str_replace('"', '""', $value) . '"';
30    }
31
32    /**
33     * Map a logical column type to SQLite's native type.
34     *
35     * Datetime types render declared precision (`datetime(3)`) for
36     * cross-dialect DDL parity; SQLite's type affinity ignores it, and the
37     * driver stores whatever textual precision the bound value carries.
38     *
39     * @param  ColumnType  $type
40     * @param  int|null  $length
41     * @param  int|null  $precision
42     * @param  int|null  $scale
43     * @return string
44     */
45    public function type(ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null): string
46    {
47        return match ($type) {
48            ColumnType::String => 'varchar' . $this->suffix($this->requireLength($length)),
49            ColumnType::Char => 'char' . $this->suffix($this->requireLength($length)),
50            ColumnType::Text => 'text',
51            ColumnType::BigInt => 'integer',
52            ColumnType::Int => 'int',
53            ColumnType::Decimal => 'numeric' . $this->suffix($precision, $scale),
54            ColumnType::Float => 'double',
55            ColumnType::Boolean => 'tinyint(1)',
56            ColumnType::Date => 'text',
57            ColumnType::DateTime => 'datetime' . $this->suffix($precision),
58            ColumnType::Timestamp => 'timestamp' . $this->suffix($precision),
59            ColumnType::Json => 'text',
60            ColumnType::Enum => 'varchar' . $this->suffix($this->requireLength($length)),
61            ColumnType::Binary => 'blob',
62            ColumnType::Uuid => 'text',
63        };
64    }
65
66    /**
67     * The SQLite auto-increment clause.
68     *
69     * @return string
70     */
71    protected function autoIncrement(): string
72    {
73        return 'AUTOINCREMENT';
74    }
75
76    /**
77     * Whether the auto-increment clause renders before `PRIMARY KEY`.
78     *
79     * @return bool
80     */
81    protected function autoIncrementBeforePrimaryKey(): bool
82    {
83        return false;
84    }
85
86    /**
87     * SQLite renders partial (filtered) indexes â€” `CREATE INDEX ... WHERE
88     * predicate`.
89     *
90     * @param  string  $predicate
91     * @return string
92     */
93    protected function compilePartialIndexClause(string $predicate): string
94    {
95        return 'WHERE ' . $predicate;
96    }
97
98    /**
99     * SQLite has no practical identifier length cap.
100     *
101     * @param  string  $name
102     * @return void
103     */
104    public function assertValidIdentifier(string $name): void
105    {
106    }
107
108    /**
109     * SQLite drops an index by name alone: `DROP INDEX name`.
110     *
111     * @param  string  $name
112     * @param  string  $table
113     * @return string
114     */
115    public function compileDropIndex(string $name, string $table): string
116    {
117        unset($table);
118
119        return 'DROP INDEX ' . $this->wrap($name);
120    }
121
122    /**
123     * Compile the `ALTER TABLE ... ADD COLUMN` statements.
124     *
125     * SQLite accepts a single `ADD COLUMN` clause per statement, so a
126     * multi-column add is one statement per column.
127     *
128     * @param  Blueprint  $blueprint
129     * @return list<string>
130     */
131    #[\Override]
132    public function compileAddColumns(Blueprint $blueprint): array
133    {
134        $table = $blueprint->getTable();
135        $columns = $blueprint->getColumns();
136        if ($columns === []) {
137            throw new \InvalidArgumentException('Cannot add columns with no columns defined.');
138        }
139
140        $backfills = $blueprint->getBackfills();
141
142        // A NOT NULL column without a default cannot be added to a
143        // non-empty table â€” its backfill renders as a temporary inline
144        // DEFAULT so the ADD succeeds and fills the existing rows (the
145        // rebuild, not this path, produces the no-final-default shape).
146        $statements = array_map(
147            fn (array $column): string => 'ALTER TABLE ' . $this->wrap($table)
148                . ' ADD COLUMN ' . $this->compileAddColumn($this->withTemporaryDefault($column, $backfills)),
149            $columns,
150        );
151
152        // SQLite cannot SET/DROP a column default in place, so a backfill
153        // on an in-place (nullable) add is an UPDATE of the existing rows.
154        // A NOT NULL-no-default column already got its backfill from the
155        // inline DEFAULT above â€” no UPDATE for it.
156        foreach ($backfills as $column => $value) {
157            if ($this->addedNotNullWithoutDefault($blueprint, $column)) {
158                continue;
159            }
160
161            $statements[] = 'UPDATE ' . $this->wrap($table) . ' SET ' . $this->wrap($column)
162                . ' = ' . $this->compileDefault($value) . ' WHERE ' . $this->wrap($column) . ' IS NULL';
163        }
164
165        return $statements;
166    }
167
168    /**
169     * Whether the named column is an added NOT NULL column without a
170     * declared default.
171     *
172     * @param  Blueprint  $blueprint
173     * @param  string  $name
174     * @return bool
175     */
176    private function addedNotNullWithoutDefault(Blueprint $blueprint, string $name): bool
177    {
178        foreach ($blueprint->getColumns() as $column) {
179            if ($column['name'] === $name) {
180                return $column['nullable'] !== true && $column['default'] === null;
181            }
182        }
183
184        return false;
185    }
186
187    /**
188     * Compile the `ALTER TABLE ... DROP COLUMN` statements.
189     *
190     * SQLite 3.35+ drops columns natively, one `DROP COLUMN` clause per
191     * statement.
192     *
193     * @param  Blueprint  $blueprint
194     * @return list<string>
195     */
196    #[\Override]
197    public function compileDropColumns(Blueprint $blueprint): array
198    {
199        $table = $blueprint->getTable();
200        $columns = $blueprint->getDropColumns();
201        if ($columns === []) {
202            throw new \InvalidArgumentException('Cannot drop columns with no columns defined.');
203        }
204
205        return array_map(
206            fn (string $column): string => 'ALTER TABLE ' . $this->wrap($table)
207                . ' DROP COLUMN ' . $this->compileDropColumn($column),
208            $columns,
209        );
210    }
211
212    /**
213     * Compile one column's `DROP COLUMN` clause.
214     *
215     * @param  string  $column
216     * @return string
217     */
218    #[\Override]
219    protected function compileDropColumn(string $column): string
220    {
221        return $this->wrap($column);
222    }
223
224    /**
225     * Compile the full table-rebuild sequence â€” SQLite's answer to every
226     * change it cannot make in place (content drift, FK/CHECK changes).
227     *
228     * The order is critical: create-new â†’ copy â†’ drop-old â†’ rename. The
229     * copy projects the intersection of the live and desired column names.
230     *
231     * @param  Blueprint  $desired
232     * @param  string  $tempName
233     * @param  list<string>  $liveColumns
234     * @param  bool  $foreignKeyConstraintsEnabled  Whether the connection currently has `PRAGMA foreign_keys` ON.
235     * @return list<string>
236     * @throws \InvalidArgumentException
237     */
238    public function compileRebuildTable(
239        Blueprint $desired,
240        string $tempName,
241        array $liveColumns,
242        bool $foreignKeyConstraintsEnabled,
243    ): array {
244        $table = $desired->getTable();
245
246        // The copy projection: live âˆ© desired â€” the data that survives.
247        $desiredNames = array_map(fn (array $column) => $column['name'], $desired->getColumns());
248        $copyColumns = array_values(array_intersect($liveColumns, $desiredNames));
249
250        if ($copyColumns === []) {
251            throw new \InvalidArgumentException(sprintf(
252                'A table rebuild of [%s] would copy no columns (live [%s] vs desired [%s]); '
253                . 'the rebuild is refused â€” drop and re-create the table explicitly instead.',
254                $table,
255                implode(', ', $liveColumns),
256                implode(', ', $desiredNames),
257            ));
258        }
259
260        // The temp blueprint: same desired shape, bound to the temp name,
261        // WITHOUT indexes (they are re-created from the original blueprint
262        // after the rename â€” derived names must carry the final table
263        // name, never the temp name).
264        $tempBlueprint = $desired->forTable($tempName);
265
266        // An added NOT NULL column has no source in the copy projection â€”
267        // the copy supplies its value for every surviving row: the
268        // blueprint's explicit backfill(), else the column's declared
269        // default â€” never a guessed zero value. A NOT NULL added column
270        // with neither fails fast at compile time. A column WITH a value
271        // keeps its NOT NULL in the temp definition (its copy projection
272        // can never produce NULL), so the rebuild lands the final shape in
273        // one apply; only a nullable added column copies as its natural
274        // NULL and needs no relaxation at all.
275        $declaredBackfills = $desired->getBackfills();
276        $backfills = [];
277
278        foreach ($desired->getColumns() as $column) {
279            if (in_array($column['name'], $liveColumns, true)) {
280                continue; // Not an added column â€” it has a copy source.
281            }
282
283            if ($column['nullable'] === true) {
284                continue; // Nullable added columns copy as their natural NULL.
285            }
286
287            $value = $declaredBackfills[$column['name']] ?? $column['default'] ?? null;
288
289            if ($value === null) {
290                throw new \InvalidArgumentException(sprintf(
291                    'A table rebuild of [%s] cannot add the NOT NULL column [%s]: it declares no '
292                    . 'default and no backfill() value to fill the existing rows with. Declare a '
293                    . 'default on the column, provide one with Blueprint::backfill(), or make the '
294                    . 'column nullable.',
295                    $table,
296                    $column['name'],
297                ));
298            }
299
300            $backfills[$column['name']] = $value;
301        }
302
303        $statements = array_values(array_filter([
304            $foreignKeyConstraintsEnabled ? 'PRAGMA foreign_keys = OFF' : null,
305            'CREATE TABLE ' . $this->wrap($tempName) . ' (' . $this->compileTableBody($tempBlueprint) . ')',
306            $this->compileCopyTableWithBackfill($table, $tempName, $copyColumns, $backfills),
307            $this->compileDrop($table),
308            $this->compileRenameTable($tempName, $table),
309            $foreignKeyConstraintsEnabled ? 'PRAGMA foreign_keys = ON' : null,
310        ]));
311
312        return $statements;
313    }
314
315    /**
316     * Compile the data-copy statement, backfilling the added NOT NULL
317     * columns with their values.
318     *
319     * @param  string  $from
320     * @param  string  $to
321     * @param  list<string>  $columns  The live âˆ© desired projection.
322     * @param  array<string, mixed>  $backfills  Added column => backfill value.
323     * @return string
324     */
325    private function compileCopyTableWithBackfill(string $from, string $to, array $columns, array $backfills): string
326    {
327        if ($backfills === []) {
328            return $this->compileCopyTable($from, $to, $columns);
329        }
330
331        $targetColumns = [...$columns, ...array_keys($backfills)];
332
333        $selectExpressions = array_map(
334            fn (string $column): string => $this->wrap($column),
335            $columns,
336        );
337
338        foreach ($backfills as $default) {
339            $selectExpressions[] = $this->compileDefault($default);
340        }
341
342        return 'INSERT INTO ' . $this->wrap($to)
343            . ' (' . implode(', ', array_map(fn (string $column) => $this->wrap($column), $targetColumns)) . ')'
344            . ' SELECT ' . implode(', ', $selectExpressions)
345            . ' FROM ' . $this->wrap($from);
346    }
347}

Inherited from BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar

49    final public function compileCreate(Blueprint $blueprint): string
50    {
51        return 'CREATE TABLE ' . $this->wrap($blueprint->getTable()) . ' (' . $this->compileTableBody($blueprint) . ')';
52    }
60    final public function compileTableBody(Blueprint $blueprint): string
61    {
62        $columns = $blueprint->getColumns();
63        if ($columns === []) {
64            throw new \InvalidArgumentException('Cannot create a table with no columns.');
65        }
66
67        // A composite primary key is declared table-level, not per-column.
68        $primaryKeys = array_values(array_filter(
69            $columns,
70            fn (array $column) => $column['primaryKey'] === true,
71        ));
72        $composite = count($primaryKeys) > 1;
73
74        $definitions = array_map(
75            fn (array $column) => $this->compileColumnDefinition($column, $composite),
76            $columns,
77        );
78
79        if ($composite) {
80            $definitions[] = 'PRIMARY KEY (' . implode(', ', array_map(
81                fn (array $column) => $this->wrap($column['name']),
82                $primaryKeys,
83            )) . ')';
84        }
85
86        foreach ($blueprint->getForeignKeys() as $foreignKey) {
87            $definitions[] = $this->compileForeignKeyConstraint($foreignKey);
88        }
89
90        foreach ($blueprint->getChecks() as $check) {
91            $definitions[] = $this->compileCheckConstraint($check);
92        }
93
94        return implode(', ', $definitions);
95    }
103    protected function compileCheckConstraint(array $check): string
104    {
105        if ($check['name'] !== null) {
106            $this->assertValidIdentifier($check['name']);
107            return 'CONSTRAINT ' . $this->wrap($check['name']) . ' CHECK (' . $check['expression'] . ')';
108        }
109
110        return 'CHECK (' . $check['expression'] . ')';
111    }
119    protected function compileForeignKeyConstraint(array $foreignKey): string
120    {
121        $table = array_shift($foreignKey['references']);
122        if ($table === null) {
123            throw new \InvalidArgumentException('A foreign key constraint requires a referenced table.');
124        }
125
126        // Statement assembly: the constraint body plus optional clauses â€”
127        // the concatenate() join (absent clause = '' segment, dropped),
128        // NOT a list join. Column lists inside the parens stay list-joins.
129        return $this->concatenate([
130            'FOREIGN KEY (' . implode(', ', array_map(fn (string $column) => $this->wrap($column), $foreignKey['columns'])) . ')',
131            'REFERENCES ' . $this->wrap($table) . ' (' . implode(', ', array_map(fn (string $column) => $this->wrap($column), $foreignKey['references'])) . ')',
132            $foreignKey['onDelete'] === null ? '' : 'ON DELETE ' . $foreignKey['onDelete']->value,
133            $foreignKey['onUpdate'] === null ? '' : 'ON UPDATE ' . $foreignKey['onUpdate']->value,
134            $foreignKey['initiallyDeferred'] || $foreignKey['deferrable']
135                ? $this->compileDeferrableClause($foreignKey['initiallyDeferred'])
136                : '',
137        ]);
138    }
147    protected function compileDeferrableClause(bool $initiallyDeferred): string
148    {
149        throw new UnsupportedFeatureException(
150            'This dialect does not support DEFERRABLE foreign keys.'
151        );
152    }
203    protected function withTemporaryDefault(array $column, array $backfills): array
204    {
205        if (!array_key_exists($column['name'], $backfills)) {
206            return $column;
207        }
208
209        return [...$column, 'default' => $backfills[$column['name']]];
210    }
222    protected function compileSetColumnDefault(string $table, string $column, mixed $default): string
223    {
224        throw new UnsupportedFeatureException('This dialect does not support altering a column default.');
225    }
236    protected function compileDropColumnDefault(string $table, string $column): string
237    {
238        throw new UnsupportedFeatureException('This dialect does not support altering a column default.');
239    }
271    final public function compileDrop(string $table): string
272    {
273        return 'DROP TABLE ' . $this->wrap($table);
274    }
283    final public function compileRenameTable(string $from, string $to): string
284    {
285        return 'ALTER TABLE ' . $this->wrap($from) . ' RENAME TO ' . $this->wrap($to);
286    }
296    final public function compileRenameColumn(string $table, string $from, string $to): string
297    {
298        return 'ALTER TABLE ' . $this->wrap($table) . ' RENAME COLUMN '
299            . $this->wrap($from) . ' TO ' . $this->wrap($to);
300    }
311    final public function compileCopyTable(string $from, string $to, array $columns): string
312    {
313        if ($columns === []) {
314            throw new \InvalidArgumentException('A table copy requires at least one column.');
315        }
316
317        $wrapped = implode(', ', array_map(fn (string $column) => $this->wrap($column), $columns));
318
319        return 'INSERT INTO ' . $this->wrap($to) . ' (' . $wrapped . ') SELECT ' . $wrapped . ' FROM ' . $this->wrap($from);
320    }
330    public function compileModifyColumn(Blueprint $blueprint): array
331    {
332        throw new UnsupportedFeatureException(
333            'This dialect does not support modifying columns in place; the change requires a table rebuild.'
334        );
335    }
347    public function compileAddForeignKey(string $table, array $foreignKey, string $name): string
348    {
349        throw new UnsupportedFeatureException(
350            'This dialect does not support adding a foreign key constraint in place; the change requires a table rebuild.'
351        );
352    }
363    public function compileDropForeignKey(string $table, string $name): string
364    {
365        throw new UnsupportedFeatureException(
366            'This dialect does not support dropping a foreign key constraint in place; the change requires a table rebuild.'
367        );
368    }
380    public function compileAddCheck(string $table, string $name, string $expression): string
381    {
382        throw new UnsupportedFeatureException(
383            'This dialect does not support adding a CHECK constraint in place; the change requires a table rebuild.'
384        );
385    }
396    public function compileDropCheck(string $table, string $name): string
397    {
398        throw new UnsupportedFeatureException(
399            'This dialect does not support dropping a CHECK constraint in place; the change requires a table rebuild.'
400        );
401    }
418    final public function compileIndexes(Blueprint $blueprint): array
419    {
420        $table = $blueprint->getTable();
421
422        return array_map(
423            function (array $index) use ($table): string {
424                // Names on the blueprint are FINAL (built at declaration
425                // time â€” user-set names verbatim, derived names with the
426                // table prefix and kind suffix). The grammar renders them
427                // as-is; its only job is quoting + dialect validation.
428                $this->assertValidIdentifier($index['name']);
429
430                $sql = ($index['unique'] ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
431                    . $this->wrap($index['name'])
432                    . ' ON ' . $this->wrap($table)
433                    . ' (' . implode(', ', array_map(fn (string $column) => $this->wrap($column), $index['columns'])) . ')';
434
435                // The option clauses are optional segments of the one
436                // statement â€” statement assembly, not a list join. The
437                // NULLS clause rides EVERY unique index (the base carries
438                // the default semantics implicitly as ''; Postgres pins
439                // them explicitly); a plain index renders neither.
440                return $this->concatenate([
441                    $sql,
442                    $index['unique']
443                        ? $this->compileNullsNotDistinctClause($index['nullsNotDistinct'])
444                        : '',
445                    $index['where'] === null ? '' : $this->compilePartialIndexClause($index['where']),
446                ]);
447            },
448            $blueprint->getIndexes(),
449        );
450    }
459    protected function compileNullsNotDistinctClause(bool $nullsNotDistinct): string
460    {
461        if ($nullsNotDistinct) {
462            throw new UnsupportedFeatureException(
463                'This dialect does not support NULLS NOT DISTINCT on a unique index.'
464            );
465        }
466
467        return '';
468    }
500    protected function compileAddColumn(array $column): string
501    {
502        return $this->compileColumnDefinition($column);
503    }
524    protected function compileColumnDefinition(array $column, bool $composite = false): string
525    {
526        $name = $this->wrap($column['name']);
527        $type = $this->type($column['type'], $column['length'], $column['precision'], $column['scale']);
528
529        // A composite PK has no generated id in the ORM's contract (the
530        // caller assigns every key part â€” insertGetId is a single-column
531        // concept), so the auto-increment clause is suppressed on its
532        // member columns. On SQLite it would be outright invalid:
533        // AUTOINCREMENT is only legal on a single-column INTEGER PRIMARY
534        // KEY. Auto-increment placement is otherwise dialect-dependent:
535        // MySQL/Postgres render it before PRIMARY KEY (`AUTO_INCREMENT
536        // PRIMARY KEY`, `GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY`),
537        // while SQLite requires it after (`INTEGER PRIMARY KEY
538        // AUTOINCREMENT`).
539        $autoIncrement = $column['autoIncrement'] === true && !$composite;
540        $beforeKey = $autoIncrement && $this->autoIncrementBeforePrimaryKey();
541        $afterKey = $autoIncrement && !$this->autoIncrementBeforePrimaryKey();
542
543        // Statement assembly: the optional clauses are concatenate()
544        // segments â€” NOT NULL / DEFAULT / UNIQUE / the auto-increment and
545        // PRIMARY KEY clauses each may be absent. No list items here.
546        return $this->concatenate([
547            $name,
548            $type,
549            $column['nullable'] !== true ? 'NOT NULL' : '',
550            $column['default'] !== null ? 'DEFAULT ' . $this->compileDefault($column['default']) : '',
551            $column['unique'] === true ? 'UNIQUE' : '',
552            $beforeKey ? $this->autoIncrement() : '',
553            // A single-column PK is declared inline; a composite PK is
554            // declared table-level (see compileCreate), so a column that is
555            // part of a composite PK must not also get an inline PRIMARY KEY.
556            $column['primaryKey'] === true && !$composite ? 'PRIMARY KEY' : '',
557            $afterKey ? $this->autoIncrement() : '',
558            $this->compileEnumCheckClause($column),
559        ]);
560    }
569    protected function compileEnumCheckClause(array $column): string
570    {
571        $values = $column['values'] ?? null;
572
573        if ($column['type'] !== ColumnType::Enum || $values === null || $values === []) {
574            return '';
575        }
576
577        $literals = implode(', ', array_map(
578            fn (string $value) => $this->quoteLiteral($value),
579            $values,
580        ));
581
582        return 'CHECK (' . $this->wrap($column['name']) . ' IN (' . $literals . '))';
583    }
592    protected function compileDefault(mixed $default): string
593    {
594        if ($default instanceof \BlueprintAU\Radiant\Database\Query\Expression) {
595            return $default->value;
596        }
597        if (is_scalar($default) || $default === null) {
598            return $this->quoteLiteral($default);
599        }
600        throw new \InvalidArgumentException(
601            'A column default must be a scalar or an Expression; got ' . get_debug_type($default) . '.'
602        );
603    }
629    protected function requireLength(?int $length): int
630    {
631        if ($length === null) {
632            throw new \InvalidArgumentException('A string column requires a length.');
633        }
634        return $length;
635    }
644    protected function suffix(\Stringable|int|null ...$parts): string
645    {
646        // Drop the nulls, then stringify what's left. The filter is
647        // explicit (`!== null`), not the default falsy filter: a numeric
648        // part of 0 stringifies to '0', which is falsy and would be
649        // silently dropped.
650        $rendered = array_map(
651            fn (\Stringable|int $part): string => (string) $part,
652            array_filter($parts, fn (\Stringable|int|null $part): bool => $part !== null),
653        );
654
655        if ($rendered === []) {
656            return '';
657        }
658
659        return '(' . implode(',', $rendered) . ')';
660    }