fisharebest /
webtrees
| 1 | <?php |
||
| 2 | |||
| 3 | /** |
||
| 4 | * webtrees: online genealogy |
||
| 5 | * Copyright (C) 2025 webtrees development team |
||
| 6 | * This program is free software: you can redistribute it and/or modify |
||
| 7 | * it under the terms of the GNU General Public License as published by |
||
| 8 | * the Free Software Foundation, either version 3 of the License, or |
||
| 9 | * (at your option) any later version. |
||
| 10 | * This program is distributed in the hope that it will be useful, |
||
| 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 13 | * GNU General Public License for more details. |
||
| 14 | * You should have received a copy of the GNU General Public License |
||
| 15 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
||
| 16 | */ |
||
| 17 | |||
| 18 | declare(strict_types=1); |
||
| 19 | |||
| 20 | namespace Fisharebest\Webtrees; |
||
| 21 | |||
| 22 | use Closure; |
||
| 23 | use Doctrine\DBAL\Configuration; |
||
| 24 | use Doctrine\DBAL\Connection; |
||
| 25 | use Doctrine\DBAL\DriverManager; |
||
| 26 | use Doctrine\DBAL\Query\Expression\ExpressionBuilder; |
||
| 27 | use Doctrine\DBAL\Query\QueryBuilder; |
||
| 28 | use Doctrine\DBAL\Schema\Column; |
||
| 29 | use Doctrine\DBAL\Schema\DefaultExpression; |
||
| 30 | use Doctrine\DBAL\Schema\ForeignKeyConstraint; |
||
| 31 | use Doctrine\DBAL\Schema\ForeignKeyConstraint\ReferentialAction; |
||
| 32 | use Doctrine\DBAL\Schema\Index; |
||
| 33 | use Doctrine\DBAL\Schema\Index\IndexType; |
||
| 34 | use Doctrine\DBAL\Schema\PrimaryKeyConstraint; |
||
| 35 | use Doctrine\DBAL\Types\AsciiStringType; |
||
| 36 | use Doctrine\DBAL\Types\DateTimeImmutableType; |
||
| 37 | use Doctrine\DBAL\Types\FloatType; |
||
| 38 | use Doctrine\DBAL\Types\IntegerType; |
||
| 39 | use Doctrine\DBAL\Types\StringType; |
||
| 40 | use Doctrine\DBAL\Types\TextType; |
||
| 41 | use Illuminate\Database\Capsule\Manager; |
||
| 42 | use Illuminate\Database\Query\Builder; |
||
| 43 | use Illuminate\Database\Query\Expression; |
||
| 44 | use PDO; |
||
| 45 | use PDOException; |
||
| 46 | use RuntimeException; |
||
| 47 | use SensitiveParameter; |
||
| 48 | |||
| 49 | use function str_starts_with; |
||
| 50 | |||
| 51 | final class DB extends Manager |
||
| 52 | { |
||
| 53 | // Supported drivers |
||
| 54 | public const string MYSQL = 'mysql'; |
||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 55 | public const string POSTGRES = 'pgsql'; |
||
| 56 | public const string SQLITE = 'sqlite'; |
||
| 57 | public const string SQL_SERVER = 'sqlsrv'; |
||
| 58 | |||
| 59 | private const array COLLATION_ASCII = [ |
||
| 60 | self::MYSQL => 'ascii_bin', |
||
| 61 | self::POSTGRES => 'C', |
||
| 62 | self::SQLITE => 'BINARY', |
||
| 63 | self::SQL_SERVER => 'Latin1_General_Bin', |
||
| 64 | ]; |
||
| 65 | |||
| 66 | // MySQL 5.x uses utf8mb4_unicode_ci (Unicode 4.0) for utf8mb4 |
||
| 67 | // MySQL 5.7 uses utf8mb4_unicode_520_ci (Unicode 5.2) for utf8mb4 |
||
| 68 | // MySQL 8.x uses utf8mb4_0900_ai_ci (Unicode 9.0) for utf8mb4 |
||
| 69 | // MySQL 9.x uses utf8mb4_uca1400_ai_ci (Unicode 14.0) for utf8mb4 |
||
| 70 | // Just specify the character set and let MySQL choose the latest collation |
||
| 71 | private const array CHARSET_UTF8 = [ |
||
| 72 | self::MYSQL => 'utf8mb4', |
||
| 73 | self::POSTGRES => null, |
||
| 74 | self::SQLITE => null, |
||
| 75 | self::SQL_SERVER => null, |
||
| 76 | ]; |
||
| 77 | |||
| 78 | private const array COLLATION_UTF8 = [ |
||
| 79 | self::MYSQL => null, |
||
| 80 | self::POSTGRES => 'und-x-icu', |
||
| 81 | self::SQLITE => 'NOCASE', |
||
| 82 | self::SQL_SERVER => 'utf8_CI_AI', |
||
| 83 | ]; |
||
| 84 | |||
| 85 | private const array TABLE_OPTIONS = [ |
||
| 86 | self::MYSQL => ['charset' => 'utf8mb4'], |
||
| 87 | self::POSTGRES => [], |
||
| 88 | self::SQLITE => [], |
||
| 89 | self::SQL_SERVER => [], |
||
| 90 | ]; |
||
| 91 | |||
| 92 | private const array REGEX_OPERATOR = [ |
||
| 93 | self::MYSQL => 'REGEXP', |
||
| 94 | self::POSTGRES => '~', |
||
| 95 | self::SQLITE => 'REGEXP', |
||
| 96 | self::SQL_SERVER => 'REGEXP', |
||
| 97 | ]; |
||
| 98 | |||
| 99 | private const array GROUP_CONCAT_FUNCTION = [ |
||
| 100 | self::MYSQL => 'GROUP_CONCAT(%s)', |
||
| 101 | self::POSTGRES => "STRING_AGG(%s, ',')", |
||
| 102 | self::SQLITE => 'GROUP_CONCAT(%s)', |
||
| 103 | self::SQL_SERVER => "STRING_AGG(%s, ',')", |
||
| 104 | ]; |
||
| 105 | |||
| 106 | private const array DRIVER_INITIALIZATION = [ |
||
| 107 | self::MYSQL => "SET NAMES utf8mb4, sql_mode := 'ANSI,STRICT_ALL_TABLES', TIME_ZONE := '+00:00', SQL_BIG_SELECTS := 1, GROUP_CONCAT_MAX_LEN := 1048576", |
||
| 108 | self::POSTGRES => "CREATE COLLATION IF NOT EXISTS webtrees_ci_ai (provider=icu, locale='und', deterministic=false)", |
||
| 109 | self::SQLITE => 'PRAGMA foreign_keys = ON', |
||
| 110 | self::SQL_SERVER => 'SET language us_english', // For timestamp columns |
||
| 111 | ]; |
||
| 112 | |||
| 113 | private static Connection $dbal_connection; |
||
| 114 | |||
| 115 | public static function connect( |
||
| 116 | #[SensitiveParameter] |
||
| 117 | string $driver, |
||
| 118 | #[SensitiveParameter] |
||
| 119 | string $host, |
||
| 120 | #[SensitiveParameter] |
||
| 121 | string $port, |
||
| 122 | #[SensitiveParameter] |
||
| 123 | string $database, |
||
| 124 | #[SensitiveParameter] |
||
| 125 | string $username, |
||
| 126 | #[SensitiveParameter] |
||
| 127 | string $password, |
||
| 128 | #[SensitiveParameter] |
||
| 129 | string $prefix, |
||
| 130 | #[SensitiveParameter] |
||
| 131 | string $key, |
||
| 132 | #[SensitiveParameter] |
||
| 133 | string $certificate, |
||
| 134 | #[SensitiveParameter] |
||
| 135 | string $ca, |
||
| 136 | #[SensitiveParameter] |
||
| 137 | bool $verify_certificate, |
||
| 138 | ): void { |
||
| 139 | $options = [ |
||
| 140 | // Some drivers do this and some don't. Make them consistent. |
||
| 141 | PDO::ATTR_STRINGIFY_FETCHES => true, |
||
| 142 | ]; |
||
| 143 | |||
| 144 | // MySQL/MariaDB support encrypted connections |
||
| 145 | if ($driver === self::MYSQL && $key !== '' && $certificate !== '' && $ca !== '') { |
||
| 146 | $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $verify_certificate; |
||
| 147 | $options[PDO::MYSQL_ATTR_SSL_KEY] = Webtrees::ROOT_DIR . 'data/' . $key; |
||
| 148 | $options[PDO::MYSQL_ATTR_SSL_CERT] = Webtrees::ROOT_DIR . 'data/' . $certificate; |
||
| 149 | $options[PDO::MYSQL_ATTR_SSL_CA] = Webtrees::ROOT_DIR . 'data/' . $ca; |
||
| 150 | } |
||
| 151 | |||
| 152 | if ($driver === self::SQLITE && $database !== ':memory:') { |
||
| 153 | $database = Webtrees::ROOT_DIR . 'data/' . $database . '.sqlite'; |
||
| 154 | } |
||
| 155 | |||
| 156 | // doctrine/dbal |
||
| 157 | |||
| 158 | $parameters = match ($driver) { |
||
| 159 | self::MYSQL => [ |
||
| 160 | 'driver' => 'pdo_mysql', |
||
| 161 | 'dbname' => $database, |
||
| 162 | 'user' => $username, |
||
| 163 | 'password' => $password, |
||
| 164 | 'port' => $port, |
||
| 165 | 'charset' => 'utf8mb4', |
||
| 166 | ], |
||
| 167 | self::POSTGRES => [ |
||
| 168 | 'driver' => 'pdo_pgsql', |
||
| 169 | 'dbname' => $database, |
||
| 170 | 'user' => $username, |
||
| 171 | 'password' => $password, |
||
| 172 | 'port' => $port, |
||
| 173 | 'charset' => 'utf8', |
||
| 174 | ], |
||
| 175 | self::SQLITE => [ |
||
| 176 | 'driver' => 'pdo_sqlite', |
||
| 177 | 'path' => $database, |
||
| 178 | ], |
||
| 179 | self::SQL_SERVER => [ |
||
| 180 | 'driver' => 'pdo_sqlsrv', |
||
| 181 | 'dbname' => $database, |
||
| 182 | 'user' => $username, |
||
| 183 | 'password' => $password, |
||
| 184 | 'port' => $port, |
||
| 185 | ], |
||
| 186 | }; |
||
| 187 | |||
| 188 | $configuration = new Configuration(); |
||
| 189 | $configuration->setSchemaAssetsFilter(schemaAssetsFilter: self::schemaAssetsFilter(...)); |
||
| 190 | |||
| 191 | self::$dbal_connection = DriverManager::getConnection(params: $parameters, config: $configuration); |
||
| 192 | |||
| 193 | // illuminate/database |
||
| 194 | |||
| 195 | $capsule = new self(); |
||
| 196 | $capsule->addConnection([ |
||
| 197 | 'driver' => $driver, |
||
| 198 | 'host' => $host, |
||
| 199 | 'port' => $port, |
||
| 200 | 'database' => $database, |
||
| 201 | 'username' => $username, |
||
| 202 | 'password' => $password, |
||
| 203 | 'prefix' => $prefix, |
||
| 204 | 'prefix_indexes' => true, |
||
| 205 | 'options' => $options, |
||
| 206 | 'trust_server_certificate' => true, // For SQL-Server - #5246 |
||
| 207 | ]); |
||
| 208 | $capsule->setAsGlobal(); |
||
| 209 | |||
| 210 | // Eager-load the connection to prevent database credentials appearing in error logs. |
||
| 211 | try { |
||
| 212 | self::pdo(); |
||
| 213 | } catch (PDOException $exception) { |
||
| 214 | throw new RuntimeException($exception->getMessage()); |
||
| 215 | } |
||
| 216 | |||
| 217 | $sql = self::DRIVER_INITIALIZATION[$driver]; |
||
| 218 | |||
| 219 | if ($sql !== '') { |
||
| 220 | self::exec($sql); |
||
| 221 | } |
||
| 222 | } |
||
| 223 | |||
| 224 | private static function schemaAssetsFilter(string $asset): bool |
||
| 225 | { |
||
| 226 | return str_starts_with(haystack: $asset, needle: parent::connection()->getTablePrefix()); |
||
| 227 | } |
||
| 228 | |||
| 229 | public static function driverName(): string |
||
| 230 | { |
||
| 231 | return self::pdo()->getAttribute(PDO::ATTR_DRIVER_NAME); |
||
| 232 | } |
||
| 233 | |||
| 234 | public static function exec(string $sql): int|false |
||
| 235 | { |
||
| 236 | return self::pdo()->exec($sql); |
||
| 237 | } |
||
| 238 | |||
| 239 | public static function lastInsertId(): int |
||
| 240 | { |
||
| 241 | $return = self::pdo()->lastInsertId(); |
||
| 242 | |||
| 243 | if ($return === false) { |
||
| 244 | throw new RuntimeException('Unable to retrieve last insert ID'); |
||
| 245 | } |
||
| 246 | |||
| 247 | // All IDs are integers in our schema. |
||
| 248 | return (int) $return; |
||
| 249 | } |
||
| 250 | |||
| 251 | private static function pdo(): PDO |
||
| 252 | { |
||
| 253 | return parent::connection()->getPdo(); |
||
| 254 | } |
||
| 255 | |||
| 256 | /** |
||
| 257 | * @param non-empty-string $identifier |
||
| 258 | * |
||
| 259 | * @return non-empty-string |
||
| 260 | */ |
||
| 261 | public static function prefix(string $identifier): string |
||
| 262 | { |
||
| 263 | return parent::connection()->getTablePrefix() . $identifier; |
||
| 264 | } |
||
| 265 | |||
| 266 | /** |
||
| 267 | * SQL-Server needs to be told that we are going to insert into an identity column. |
||
| 268 | * |
||
| 269 | * @param non-empty-string $table |
||
| 270 | * @param Closure(): void $callback |
||
| 271 | */ |
||
| 272 | public static function identityInsert(string $table, Closure $callback): void |
||
| 273 | { |
||
| 274 | if (self::driverName() === self::SQL_SERVER) { |
||
| 275 | self::exec(sql: 'SET IDENTITY_INSERT [' . self::prefix(identifier: $table) . '] ON'); |
||
| 276 | } |
||
| 277 | |||
| 278 | $callback(); |
||
| 279 | |||
| 280 | if (self::driverName() === self::SQL_SERVER) { |
||
| 281 | self::exec(sql: 'SET IDENTITY_INSERT [' . self::prefix(identifier: $table) . '] OFF'); |
||
| 282 | } |
||
| 283 | } |
||
| 284 | |||
| 285 | public static function rollBack(): void |
||
| 286 | { |
||
| 287 | parent::connection()->rollBack(); |
||
| 288 | } |
||
| 289 | |||
| 290 | /** |
||
| 291 | * @internal |
||
| 292 | * |
||
| 293 | * @param list<string> $expressions |
||
| 294 | */ |
||
| 295 | public static function concat(array $expressions): string |
||
| 296 | { |
||
| 297 | if (self::driverName() === self::SQL_SERVER) { |
||
| 298 | return 'CONCAT(' . implode(', ', $expressions) . ')'; |
||
| 299 | } |
||
| 300 | |||
| 301 | // ANSI standard. MySQL uses this with ANSI mode |
||
| 302 | return '(' . implode(' || ', $expressions) . ')'; |
||
| 303 | } |
||
| 304 | |||
| 305 | /** |
||
| 306 | * @internal |
||
| 307 | */ |
||
| 308 | public static function iLike(): string |
||
| 309 | { |
||
| 310 | if (self::driverName() === self::POSTGRES) { |
||
| 311 | return 'ILIKE'; |
||
| 312 | } |
||
| 313 | |||
| 314 | return 'LIKE'; |
||
| 315 | } |
||
| 316 | |||
| 317 | /** |
||
| 318 | * @internal |
||
| 319 | */ |
||
| 320 | public static function groupConcat(string $column): string |
||
| 321 | { |
||
| 322 | return sprintf(self::GROUP_CONCAT_FUNCTION[self::driverName()], $column); |
||
| 323 | } |
||
| 324 | |||
| 325 | /** |
||
| 326 | * @return Expression<string> |
||
| 327 | */ |
||
| 328 | public static function binaryColumn(string $column, string|null $alias = null): Expression |
||
| 329 | { |
||
| 330 | if (self::driverName() === self::MYSQL) { |
||
| 331 | $sql = 'CAST(' . $column . ' AS binary)'; |
||
| 332 | } else { |
||
| 333 | $sql = $column; |
||
| 334 | } |
||
| 335 | |||
| 336 | if ($alias !== null) { |
||
| 337 | $sql .= ' AS ' . $alias; |
||
| 338 | } |
||
| 339 | |||
| 340 | return new Expression($sql); |
||
| 341 | } |
||
| 342 | |||
| 343 | public static function regexOperator(): string |
||
| 344 | { |
||
| 345 | return self::REGEX_OPERATOR[self::driverName()]; |
||
| 346 | } |
||
| 347 | |||
| 348 | /** |
||
| 349 | * @return array<string,string> |
||
| 350 | */ |
||
| 351 | public static function tableOptions(): array |
||
| 352 | { |
||
| 353 | return self::TABLE_OPTIONS[self::driverName()]; |
||
| 354 | } |
||
| 355 | |||
| 356 | /** |
||
| 357 | * PHPSTAN can't detect the magic methods in the parent class. |
||
| 358 | */ |
||
| 359 | public static function query(): Builder |
||
| 360 | { |
||
| 361 | return parent::connection()->query(); |
||
| 362 | } |
||
| 363 | |||
| 364 | public static function getDBALConnection(): Connection |
||
| 365 | { |
||
| 366 | return self::$dbal_connection; |
||
| 367 | } |
||
| 368 | |||
| 369 | public static function select(string ...$expressions): QueryBuilder |
||
| 370 | { |
||
| 371 | return self::$dbal_connection |
||
| 372 | ->createQueryBuilder() |
||
| 373 | ->select(...$expressions); |
||
| 374 | } |
||
| 375 | |||
| 376 | /** |
||
| 377 | * @param non-empty-string $table |
||
| 378 | * @param array<array-key,array<string,int|float|string|null>> $rows |
||
| 379 | */ |
||
| 380 | public static function insert(string $table, array $rows): void |
||
| 381 | { |
||
| 382 | foreach ($rows as $row) { |
||
| 383 | self::getDBALConnection()->insert(table: self::prefix($table), data: $row); |
||
| 384 | } |
||
| 385 | } |
||
| 386 | |||
| 387 | public static function delete(string ...$expressions): QueryBuilder |
||
| 388 | { |
||
| 389 | return self::$dbal_connection |
||
| 390 | ->createQueryBuilder() |
||
| 391 | ->delete(...$expressions); |
||
| 392 | } |
||
| 393 | |||
| 394 | public static function expression(): ExpressionBuilder |
||
| 395 | { |
||
| 396 | return self::$dbal_connection->createExpressionBuilder(); |
||
| 397 | } |
||
| 398 | |||
| 399 | /** |
||
| 400 | * @param non-empty-string $name |
||
| 401 | */ |
||
| 402 | public static function char(string $name, int $length, bool $nullable = false, string|null $default = null): Column |
||
| 403 | { |
||
| 404 | return Column::editor() |
||
| 405 | ->setUnquotedName(name: $name) |
||
| 406 | ->setType(type: new AsciiStringType()) |
||
| 407 | ->setLength(length: $length) |
||
| 408 | ->setFixed(fixed: true) |
||
| 409 | ->setNotNull(notNull: !$nullable) |
||
| 410 | ->setDefaultValue(defaultValue: $default) |
||
| 411 | ->setCollation(collation: self::COLLATION_ASCII[self::driverName()]) |
||
| 412 | ->create(); |
||
| 413 | } |
||
| 414 | |||
| 415 | /** |
||
| 416 | * @param non-empty-string $name |
||
| 417 | */ |
||
| 418 | public static function varchar(string $name, int $length, bool $nullable = false, string|null $default = null): Column |
||
| 419 | { |
||
| 420 | return Column::editor() |
||
| 421 | ->setUnquotedName(name: $name) |
||
| 422 | ->setType(type: new AsciiStringType()) |
||
| 423 | ->setLength(length: $length) |
||
| 424 | ->setFixed(fixed: false) |
||
| 425 | ->setNotNull(notNull: !$nullable) |
||
| 426 | ->setDefaultValue(defaultValue: $default) |
||
| 427 | ->setCollation(collation: self::COLLATION_ASCII[self::driverName()]) |
||
| 428 | ->create(); |
||
| 429 | } |
||
| 430 | |||
| 431 | /** |
||
| 432 | * @param non-empty-string $name |
||
| 433 | */ |
||
| 434 | public static function nchar(string $name, int $length, bool $nullable = false, string|null $default = null): Column |
||
| 435 | { |
||
| 436 | return Column::editor() |
||
| 437 | ->setUnquotedName(name: $name) |
||
| 438 | ->setType(type: new StringType()) |
||
| 439 | ->setLength(length: $length) |
||
| 440 | ->setFixed(fixed: true) |
||
| 441 | ->setNotNull(notNull: !$nullable) |
||
| 442 | ->setDefaultValue(defaultValue: $default) |
||
| 443 | ->setCharset(charset: self::CHARSET_UTF8[self::driverName()]) |
||
| 444 | ->setCollation(collation: self::COLLATION_UTF8[self::driverName()]) |
||
| 445 | ->create(); |
||
| 446 | } |
||
| 447 | |||
| 448 | /** |
||
| 449 | * @param non-empty-string $name |
||
| 450 | */ |
||
| 451 | public static function nvarchar(string $name, int $length, bool $nullable = false, string|null $default = null): Column |
||
| 452 | { |
||
| 453 | return Column::editor() |
||
| 454 | ->setUnquotedName(name: $name) |
||
| 455 | ->setType(type: new StringType()) |
||
| 456 | ->setLength(length: $length) |
||
| 457 | ->setFixed(fixed: false) |
||
| 458 | ->setNotNull(notNull: !$nullable) |
||
| 459 | ->setDefaultValue(defaultValue: $default) |
||
| 460 | ->setCharset(charset: self::CHARSET_UTF8[self::driverName()]) |
||
| 461 | ->setCollation(collation: self::COLLATION_UTF8[self::driverName()]) |
||
| 462 | ->create(); |
||
| 463 | } |
||
| 464 | |||
| 465 | /** |
||
| 466 | * @param non-empty-string $name |
||
| 467 | */ |
||
| 468 | public static function integer(string $name, bool $autoincrement = false, bool $nullable = false, int|null $default = null): Column |
||
| 469 | { |
||
| 470 | return Column::editor() |
||
| 471 | ->setUnquotedName(name: $name) |
||
| 472 | ->setType(type: new IntegerType()) |
||
| 473 | ->setAutoincrement($autoincrement) |
||
| 474 | ->setNotNull(notNull: !$nullable) |
||
| 475 | ->setDefaultValue(defaultValue: $default) |
||
| 476 | ->create(); |
||
| 477 | } |
||
| 478 | |||
| 479 | /** |
||
| 480 | * @param non-empty-string $name |
||
| 481 | */ |
||
| 482 | public static function float(string $name, bool $nullable = false): Column |
||
| 483 | { |
||
| 484 | return Column::editor() |
||
| 485 | ->setUnquotedName(name: $name) |
||
| 486 | ->setType(type: new FloatType()) |
||
| 487 | ->setNotNull(notNull: !$nullable) |
||
| 488 | ->create(); |
||
| 489 | } |
||
| 490 | |||
| 491 | /** |
||
| 492 | * @param non-empty-string $name |
||
| 493 | */ |
||
| 494 | public static function text(string $name): Column |
||
| 495 | { |
||
| 496 | return Column::editor() |
||
| 497 | ->setUnquotedName(name: $name) |
||
| 498 | ->setType(type: new TextType()) |
||
| 499 | ->setCharset(charset: self::CHARSET_UTF8[self::driverName()]) |
||
| 500 | ->setCollation(collation: self::COLLATION_UTF8[self::driverName()]) |
||
| 501 | ->create(); |
||
| 502 | } |
||
| 503 | |||
| 504 | /** |
||
| 505 | * @param non-empty-string $name |
||
| 506 | */ |
||
| 507 | public static function timestamp(string $name, int $precision = 0, DefaultExpression|null $default = null): Column |
||
| 508 | { |
||
| 509 | return Column::editor() |
||
| 510 | ->setUnquotedName(name: $name) |
||
| 511 | ->setType(type: new DateTimeImmutableType()) |
||
| 512 | ->setPrecision($precision) |
||
| 513 | ->setDefaultValue(defaultValue: $default) |
||
| 514 | ->create(); |
||
| 515 | } |
||
| 516 | |||
| 517 | /** |
||
| 518 | * @param non-empty-list<non-empty-string> $columns |
||
| 519 | */ |
||
| 520 | public static function primaryKey(array $columns): PrimaryKeyConstraint |
||
| 521 | { |
||
| 522 | return PrimaryKeyConstraint::editor() |
||
| 523 | ->setUnquotedColumnNames(...$columns) |
||
| 524 | ->create(); |
||
| 525 | } |
||
| 526 | |||
| 527 | /** |
||
| 528 | * @param non-empty-string $name |
||
| 529 | * @param non-empty-list<non-empty-string> $columns |
||
| 530 | */ |
||
| 531 | public static function index(string $name, array $columns): Index |
||
| 532 | { |
||
| 533 | return Index::editor() |
||
| 534 | ->setType(IndexType::REGULAR) |
||
| 535 | ->setUnquotedName(self::prefix($name)) |
||
| 536 | ->setUnquotedColumnNames(...$columns) |
||
| 537 | ->create(); |
||
| 538 | } |
||
| 539 | |||
| 540 | /** |
||
| 541 | * @param non-empty-string $name |
||
| 542 | * @param non-empty-list<non-empty-string> $columns |
||
| 543 | */ |
||
| 544 | public static function uniqueIndex(string $name, array $columns): Index |
||
| 545 | { |
||
| 546 | return Index::editor() |
||
| 547 | ->setType(IndexType::UNIQUE) |
||
| 548 | ->setUnquotedName(self::prefix($name)) |
||
| 549 | ->setUnquotedColumnNames(...$columns) |
||
| 550 | ->create(); |
||
| 551 | } |
||
| 552 | |||
| 553 | /** |
||
| 554 | * @param non-empty-string $name |
||
| 555 | * @param non-empty-array<non-empty-string> $local_columns |
||
| 556 | * @param non-empty-string $foreign_table |
||
| 557 | * @param non-empty-array<non-empty-string> $foreign_columns |
||
| 558 | */ |
||
| 559 | public static function foreignKey( |
||
| 560 | string $name, |
||
| 561 | array $local_columns, |
||
| 562 | string $foreign_table, |
||
| 563 | array|null $foreign_columns = null, |
||
| 564 | ReferentialAction $on_delete = ReferentialAction::NO_ACTION, |
||
| 565 | ReferentialAction $on_update = ReferentialAction::NO_ACTION, |
||
| 566 | ): ForeignKeyConstraint { |
||
| 567 | $foreign_columns ??= $local_columns; |
||
| 568 | |||
| 569 | return ForeignKeyConstraint::editor() |
||
| 570 | ->setUnquotedName(self::prefix($name)) |
||
| 571 | ->setUnquotedReferencingColumnNames(...$local_columns) |
||
| 572 | ->setUnquotedReferencedTableName(self::prefix($foreign_table)) |
||
| 573 | ->setUnquotedReferencedColumnNames(...$foreign_columns) |
||
| 574 | ->setOnDeleteAction($on_delete) |
||
| 575 | ->setOnUpdateAction($on_update) |
||
| 576 | ->create(); |
||
| 577 | } |
||
| 578 | } |
||
| 579 |