Lines 100.00% 25 / 25
Functions and Methods 100.00% 5 / 5
Classes and Traits 100.00% 1 / 1
Name Lines Functions and Methods CRAP Classes and Traits
SchemaInspector 100.00% 25 / 25 100.00% 5 / 5 19 100.00% 1 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 getDefaultSchemaGrammar n/a 0 / 0 n/a 0 / 0 0
 tables n/a 0 / 0 n/a 0 / 0 0
 table n/a 0 / 0 n/a 0 / 0 0
 hasTable 100.00% 1 / 1 100.00% 1 / 1 1
 referencingTables n/a 0 / 0 n/a 0 / 0 0
 columnTypeMatches n/a 0 / 0 n/a 0 / 0 0
 castSafety 100.00% 3 / 3 100.00% 1 / 1 2
 liveTypeFamily 100.00% 8 / 8 100.00% 1 / 1 8
 desiredTypeFamily 100.00% 12 / 12 100.00% 1 / 1 7
1<?php
2
3declare(strict_types=1);
4
5namespace BlueprintAU\Radiant\Database\Schema\Inspectors;
6
7/**
8 * Reads the live schema — the read-side twin of the write-side
9 * {@see \BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar}.
10 *
11 * @template TSchemaGrammar of \BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar = \BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar
12 */
13abstract class SchemaInspector
14{
15    /**
16     * The dialect's schema grammar — cached (one instantiation per
17     * inspector, not per comparison).
18     *
19     * @var TSchemaGrammar
20     */
21    public readonly \BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar $schemaGrammar;
22
23    /**
24     * Create an inspector over the owning connection's PDO.
25     *
26     * @param  \PDO  $pdo
27     */
28    public function __construct(
29        protected readonly \PDO $pdo,
30    ) {
31        $this->schemaGrammar = $this->getDefaultSchemaGrammar();
32    }
33
34    /**
35     * The dialect's schema grammar — the factory hook.
36     *
37     * @return TSchemaGrammar
38     */
39    abstract protected function getDefaultSchemaGrammar(): \BlueprintAU\Radiant\Database\Schema\Grammars\SchemaGrammar;
40
41    /**
42     * Every table name in the live schema.
43     *
44     * @return list<string>
45     */
46    abstract public function tables(): array;
47
48    /**
49     * One table's live schema.
50     *
51     * @param  string  $name
52     * @return LiveTable
53     * @throws \RuntimeException
54     */
55    abstract public function table(string $name): LiveTable;
56
57    /**
58     * Whether a table exists in the live schema.
59     *
60     * @param  string  $name
61     * @return bool
62     */
63    final public function hasTable(string $name): bool
64    {
65        return in_array($name, $this->tables(), true);
66    }
67
68    /**
69     * The live tables that declare a foreign key into the given table —
70     * its referencing children.
71     *
72     * @param  string  $table
73     * @return list<string>
74     */
75    abstract public function referencingTables(string $table): array;
76
77    /**
78     * Whether a live column's native type text matches the declared
79     * logical type — the content-drift comparison.
80     *
81     * @param  string  $liveType
82     * @param  \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType  $declaredType
83     * @param  int|null  $declaredLength
84     * @param  int|null  $declaredPrecision
85     * @param  int|null  $declaredScale  Fractional digits for a decimal column.
86     * @return bool
87     */
88    abstract public function columnTypeMatches(string $liveType, \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType $declaredType, int|null $declaredLength, int|null $declaredPrecision = null, int|null $declaredScale = null): bool;
89
90    /**
91     * How safely a live column's values convert to the desired type —
92     * the modify-cast classification. The default is lenient (MySQL and
93     * SQLite coerce almost anything): a same-family change is Safe,
94     * anything else is Risky, and nothing is Uncastable. A stricter
95     * dialect (Postgres) overrides this.
96     *
97     * @param  string  $liveType
98     * @param  \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType  $desiredType
99     * @return \BlueprintAU\Radiant\Database\Schema\Enums\CastSafety
100     */
101    public function castSafety(string $liveType, \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType $desiredType): \BlueprintAU\Radiant\Database\Schema\Enums\CastSafety
102    {
103        return $this->liveTypeFamily($liveType) === $this->desiredTypeFamily($desiredType)
104            ? \BlueprintAU\Radiant\Database\Schema\Enums\CastSafety::Safe
105            : \BlueprintAU\Radiant\Database\Schema\Enums\CastSafety::Risky;
106    }
107
108    /**
109     * The coarse family a live native type string belongs to.
110     *
111     * @param  string  $liveType
112     * @return string
113     */
114    protected function liveTypeFamily(string $liveType): string
115    {
116        $base = strtolower(preg_replace('/\(.*$/', '', trim($liveType)) ?? $liveType);
117
118        return match (true) {
119            str_contains($base, 'int') => 'number',
120            in_array($base, ['numeric', 'decimal', 'float', 'double', 'double precision', 'real'], true) => 'number',
121            in_array($base, ['bool', 'boolean'], true) => 'bool',
122            in_array($base, ['date', 'time', 'timestamp', 'timestamptz', 'datetime'], true) => 'temporal',
123            in_array($base, ['json', 'jsonb'], true) => 'json',
124            in_array($base, ['blob', 'bytea', 'binary', 'varbinary'], true) => 'binary',
125            default => 'string',
126        };
127    }
128
129    /**
130     * The coarse family a desired logical type belongs to.
131     *
132     * @param  \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType  $desiredType
133     * @return string
134     */
135    protected function desiredTypeFamily(\BlueprintAU\Radiant\Database\Schema\Enums\ColumnType $desiredType): string
136    {
137        return match ($desiredType) {
138            \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType::Int,
139            \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType::BigInt,
140            \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType::Decimal,
141            \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType::Float => 'number',
142            \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType::Boolean => 'bool',
143            \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType::Date,
144            \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType::DateTime,
145            \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType::Timestamp => 'temporal',
146            \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType::Json => 'json',
147            \BlueprintAU\Radiant\Database\Schema\Enums\ColumnType::Binary => 'binary',
148            default => 'string',
149        };
150    }
151}