Lines 94.64% 53 / 56
Methods 85.71% 6 / 7
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 defaultOptions 100.00% 3 / 3 100.00% 1 / 1 1
 connect 70.00% 7 / 10 0.00% 0 / 1 2.11
 validConfig 100.00% 7 / 7 100.00% 1 / 1 5
 [BlueprintAU\Radiant\Database\Connectors\SqlConnector] forcedOptions 100.00% 1 / 1 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Connectors\SqlConnector] validDsnField 100.00% 11 / 11 100.00% 1 / 1 5
 [BlueprintAU\Radiant\Database\Connectors\SqlConnector] validateOptions 100.00% 13 / 13 100.00% 1 / 1 6
 [BlueprintAU\Radiant\Database\Connectors\SqlConnector] createPdo 100.00% 11 / 11 100.00% 1 / 1 2
18final class SqliteConnector extends SqlConnector
19{
20    /**
21     * SQLite-specific default PDO attributes — merged over the base
22     * defaults.
23     *
24     * The busy timeout is set via \PDO::ATTR_TIMEOUT, which pdo_sqlite
25     * maps to SQLite's busy timeout in seconds.
26     *
27     * @return array<int, int|bool>
28     */
29    #[\Override]
30    protected function defaultOptions(): array
31    {
32        return [
33            \PDO::ATTR_TIMEOUT => 5, // seconds → SQLite busy timeout (5000ms)
34        ];
35    }
36
37    /**
38     * Create a SQLite connection from the given config.
39     *
40     * @param  array{database: mixed, options?: PdoOptions, ...<mixed>}  $config
41     * @return SqliteConnection
42     * @throws \InvalidArgumentException
43     */
44    #[Override]
45    public function connect(array $config): SqlConnection
46    {
47        $this->validConfig($config);
48        $path = $config['database'];
49
50        if (!is_string($path)) {
51            throw new \InvalidArgumentException(
52                'SQLite database must be a path string; got ' . get_debug_type($path) . '.'
53            );
54        }
55
56        $options = $config['options'] ?? [];
57        $pdo = $this->createPdo("sqlite:{$path}", null, null, $options);
58
59        // SQLite ships with foreign key enforcement OFF by default — orphaned
60        // rows and no cascades unless it is enabled explicitly. This is a
61        // post-connect PRAGMA (not a PDO attribute), so it runs here rather
62        // than in the options layers. It is forced unconditionally: a
63        // database relying on broken FK data is the user's own setup problem,
64        // but a library silently dropping FK guarantees would be the library's.
65        $pdo->exec('PRAGMA foreign_keys = ON');
66
67        return new SqliteConnection($pdo);
68    }
69
70    /**
71     * Validate the shape of a SQLite connection config.
72     *
73     * @param  array<string,mixed>  $config
74     * @throws \InvalidArgumentException
75     */
76    #[Override]
77    public function validConfig(array $config): void
78    {
79        parent::validConfig($config);
80
81        if (!isset($config['database']) || !is_string($config['database']) || $config['database'] === '') {
82            throw new \InvalidArgumentException(
83                'SQLite database must be a non-empty path string; got '
84                . (isset($config['database']) ? get_debug_type($config['database']) : 'nothing')
85                . '.'
86            );
87        }
88    }
89}

Inherited from BlueprintAU\Radiant\Database\Connectors\SqlConnector

52    protected function forcedOptions(): array
53    {
54        return [];
55    }
91    protected function validDsnField(mixed $value, string $field): string
92    {
93        if (!is_string($value) || $value === '') {
94            throw new \InvalidArgumentException(
95                'The "' . $field . '" config field must be a non-empty string; got '
96                . ($value === null ? 'nothing' : get_debug_type($value)) . '.'
97            );
98        }
99        if (preg_match('/[;\s\x00-\x1f\x7f]/', $value) === 1) {
100            throw new \InvalidArgumentException(
101                'The "' . $field . '" config field must not contain semicolons, whitespace '
102                . 'or control characters (they are DSN metacharacters); got a value that does.'
103            );
104        }
105        return $value;
106    }
114    final protected function validateOptions(mixed $options): void
115    {
116        if (!is_array($options)) {
117            throw new \InvalidArgumentException(
118                'PDO options must be an array of attributes; got ' . get_debug_type($options) . '.'
119            );
120        }
121
122        foreach ($options as $key => $value) {
123            if (!is_int($key)) {
124                throw new \InvalidArgumentException(
125                    'PDO option keys must be integer attribute constants (PDO::ATTR_*); got ' . get_debug_type($key) . '.'
126                );
127            }
128
129            if (!is_scalar($value) && !is_array($value)) {
130                throw new \InvalidArgumentException(
131                    'PDO option values must be a scalar or array; got ' . get_debug_type($value) . ' for option ' . $key . '.'
132                );
133            }
134        }
135    }
152    final protected function createPdo(string $dsn, ?string $username, #[\SensitiveParameter] ?string $password, array $options): \Pdo
153    {
154        $this->validateOptions($options);
155        $options = array_replace(
156            self::DEFAULT_OPTIONS,
157            static::defaultOptions(),
158            $options,
159            self::FORCED_OPTIONS,
160            static::forcedOptions(),
161        );
162        try {
163            return \PDO::connect($dsn, $username, $password, $options);
164        } catch (\PDOException $e) {
165            throw new ConnectionException("Could not connect to database.", $e->getCode(), $e);
166        }
167    }