Lines 100.00% 19 / 19
Functions and Methods 100.00% 4 / 4
Classes and Traits 100.00% 1 / 1
Name Lines Functions and Methods CRAP Classes and Traits
MariaDbSchemaInspector 100.00% 19 / 19 100.00% 4 / 4 14 100.00% 1 / 1
 normalizeColumnType 100.00% 3 / 3 100.00% 1 / 1 2
 columnTypeMatches 100.00% 6 / 6 100.00% 1 / 1 4
 castSafety 100.00% 4 / 4 100.00% 1 / 1 3
 normalizeColumnDefault 100.00% 6 / 6 100.00% 1 / 1 5
1<?php
2
3declare(strict_types=1);
4
5namespace BlueprintAU\Radiant\Database\Schema\Inspectors;
6
7use BlueprintAU\Radiant\Database\Schema\Enums\CastSafety;
8use BlueprintAU\Radiant\Database\Schema\Enums\ColumnType;
9use Override;
10
11/**
12 * Reads the live schema on MariaDB — `information_schema`, as MySQL
13 * reports it with four divergences.
14 *
15 * MariaDB implements `JSON` as an alias for `LONGTEXT`, so a declared
16 * Json column reads back as `longtext` — matched here to stop the differ
17 * re-planning every Json column forever. MariaDB also retains integer
18 * display widths that MySQL 8 dropped (`bigint(20)`), stripped here,
19 * renders `current_timestamp()` (lowercase, with parens) where MySQL
20 * reports `CURRENT_TIMESTAMP`, and reports the literal string `'NULL'`
21 * for a nullable column with no default where MySQL reports a real
22 * null — the default and type normalizations keep an
23 * `Expression('CURRENT_TIMESTAMP')` column and a nullable-no-default
24 * column converging on the first plan.
25 *
26 * @see \BlueprintAU\Radiant\Database\Schema\Inspectors\MySqlSchemaInspector
27 */
28final class MariaDbSchemaInspector extends MySqlSchemaInspector
29{
30    /**
31     * The MariaDB spellings of a current-timestamp default.
32     *
33     * @var list<string>
34     */
35    private const CURRENT_TIMESTAMP_SPELLINGS = ['current_timestamp()', 'now()'];
36
37    /**
38     * Normalize a live column type's MariaDB spelling.
39     *
40     * MariaDB retains integer display widths that MySQL 8 dropped
41     * (`bigint(20)` where MySQL reports `bigint`) — the widths have no
42     * semantic effect, so they are stripped. The exception is
43     * `tinyint(1)`: it IS the Boolean rendering, not a display width,
44     * and the type comparison needs it verbatim.
45     *
46     * @param  string  $type
47     * @return string
48     */
49    #[Override]
50    protected function normalizeColumnType(string $type): string
51    {
52        if (strtolower($type) === 'tinyint(1)') {
53            return 'tinyint(1)';
54        }
55
56        return preg_replace('/^(tinyint|smallint|mediumint|bigint|int)\(\d+\)/', '$1', $type) ?? $type;
57    }
58
59    /**
60     * Whether a live column's native type text matches the declared
61     * logical type — the MariaDB mapping over the MySQL one.
62     *
63     * @param  string  $liveType
64     * @param  ColumnType  $declaredType
65     * @param  int|null  $declaredLength
66     * @param  int|null  $declaredPrecision
67     * @param  int|null  $declaredScale
68     * @return bool
69     */
70    #[Override]
71    public function columnTypeMatches(string $liveType, ColumnType $declaredType, int|null $declaredLength, int|null $declaredPrecision = null, int|null $declaredScale = null): bool
72    {
73        // MariaDB stores JSON as LONGTEXT: a declared Json column reads
74        // back as `longtext` (no length suffix).
75        if ($declaredType === ColumnType::Json
76            && strtolower($liveType) === 'longtext'
77            && $declaredLength === null) {
78            return true;
79        }
80
81        // MariaDB retains integer display widths MySQL 8 dropped — strip
82        // them so the comparison sees the bare type text.
83        $liveType = $this->normalizeColumnType(strtolower($liveType));
84
85        return parent::columnTypeMatches($liveType, $declaredType, $declaredLength, $declaredPrecision, $declaredScale);
86    }
87
88    /**
89     * The MariaDB modify-cast classification — a `longtext` column
90     * converts to a Json column exactly as MariaDB does when it applies
91     * its json_valid CHECK.
92     *
93     * @param  string  $liveType
94     * @param  ColumnType  $desiredType
95     * @return CastSafety
96     */
97    #[Override]
98    public function castSafety(string $liveType, ColumnType $desiredType): CastSafety
99    {
100        // The family comparisons see the same stripped text the differ
101        // read from the columns.
102        $liveType = $this->normalizeColumnType(strtolower($liveType));
103
104        if ($desiredType === ColumnType::Json && $liveType === 'longtext') {
105            return CastSafety::Safe;
106        }
107
108        return parent::castSafety($liveType, $desiredType);
109    }
110
111    /**
112     * Normalize a live column default's MariaDB spelling.
113     *
114     * MariaDB renders `current_timestamp()` (and `now()`) where MySQL
115     * reports `CURRENT_TIMESTAMP`, and reports the literal string
116     * `'NULL'` for a nullable column with no default where MySQL reports
117     * a real null.
118     *
119     * @param  mixed  $default
120     * @return mixed
121     */
122    #[\Override]
123    protected function normalizeColumnDefault(mixed $default): mixed
124    {
125        // A nullable column with no default reports the literal string
126        // 'NULL' — normalize to the real null MySQL reports.
127        if (is_string($default) && strtoupper($default) === 'NULL') {
128            return null;
129        }
130
131        if (!is_string($default)
132            || !in_array(strtolower(trim($default)), self::CURRENT_TIMESTAMP_SPELLINGS, true)) {
133            return $default;
134        }
135
136        return 'CURRENT_TIMESTAMP';
137    }
138}