From bf6655a534a6775d30cafa67bd801276bda1d98d Mon Sep 17 00:00:00 2001 From: polo Date: Tue, 13 Aug 2024 23:45:21 +0200 Subject: =?UTF-8?q?VERSION=200.2=20doctrine=20ORM=20et=20entit=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dbal/src/Schema/PostgreSQLSchemaManager.php | 572 +++++++++++++++++++++ 1 file changed, 572 insertions(+) create mode 100644 vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php (limited to 'vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php') diff --git a/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php b/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php new file mode 100644 index 0000000..9af16c9 --- /dev/null +++ b/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php @@ -0,0 +1,572 @@ + + */ +class PostgreSQLSchemaManager extends AbstractSchemaManager +{ + private ?string $currentSchema = null; + + /** + * {@inheritDoc} + */ + public function listSchemaNames(): array + { + return $this->connection->fetchFirstColumn( + <<<'SQL' +SELECT schema_name +FROM information_schema.schemata +WHERE schema_name NOT LIKE 'pg\_%' +AND schema_name != 'information_schema' +SQL, + ); + } + + public function createSchemaConfig(): SchemaConfig + { + $config = parent::createSchemaConfig(); + + $config->setName($this->getCurrentSchema()); + + return $config; + } + + /** + * Returns the name of the current schema. + * + * @throws Exception + */ + protected function getCurrentSchema(): ?string + { + return $this->currentSchema ??= $this->determineCurrentSchema(); + } + + /** + * Determines the name of the current schema. + * + * @throws Exception + */ + protected function determineCurrentSchema(): string + { + $currentSchema = $this->connection->fetchOne('SELECT current_schema()'); + assert(is_string($currentSchema)); + + return $currentSchema; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint + { + $onUpdate = null; + $onDelete = null; + + if ( + preg_match( + '(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', + $tableForeignKey['condef'], + $match, + ) === 1 + ) { + $onUpdate = $match[1]; + } + + if ( + preg_match( + '(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', + $tableForeignKey['condef'], + $match, + ) === 1 + ) { + $onDelete = $match[1]; + } + + $result = preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values); + assert($result === 1); + + // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get + // the idea to trim them here. + $localColumns = array_map('trim', explode(',', $values[1])); + $foreignColumns = array_map('trim', explode(',', $values[3])); + $foreignTable = $values[2]; + + return new ForeignKeyConstraint( + $localColumns, + $foreignTable, + $foreignColumns, + $tableForeignKey['conname'], + ['onUpdate' => $onUpdate, 'onDelete' => $onDelete], + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition(array $view): View + { + return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition(array $table): string + { + $currentSchema = $this->getCurrentSchema(); + + if ($table['schema_name'] === $currentSchema) { + return $table['table_name']; + } + + return $table['schema_name'] . '.' . $table['table_name']; + } + + /** + * {@inheritDoc} + * + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + */ + protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array + { + $buffer = []; + foreach ($tableIndexes as $row) { + $colNumbers = array_map('intval', explode(' ', $row['indkey'])); + $columnNameSql = sprintf( + 'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC', + $row['indrelid'], + implode(' ,', $colNumbers), + ); + + $indexColumns = $this->connection->fetchAllAssociative($columnNameSql); + + // required for getting the order of the columns right. + foreach ($colNumbers as $colNum) { + foreach ($indexColumns as $colRow) { + if ($colNum !== $colRow['attnum']) { + continue; + } + + $buffer[] = [ + 'key_name' => $row['relname'], + 'column_name' => trim($colRow['attname']), + 'non_unique' => ! $row['indisunique'], + 'primary' => $row['indisprimary'], + 'where' => $row['where'], + ]; + } + } + } + + return parent::_getPortableTableIndexesList($buffer, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableDatabaseDefinition(array $database): string + { + return $database['datname']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableSequenceDefinition(array $sequence): Sequence + { + if ($sequence['schemaname'] !== 'public') { + $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname']; + } else { + $sequenceName = $sequence['relname']; + } + + return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnDefinition(array $tableColumn): Column + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + $length = null; + + if ( + in_array(strtolower($tableColumn['type']), ['varchar', 'bpchar'], true) + && preg_match('/\((\d*)\)/', $tableColumn['complete_type'], $matches) === 1 + ) { + $length = (int) $matches[1]; + } + + $autoincrement = $tableColumn['attidentity'] === 'd'; + + $matches = []; + + assert(array_key_exists('default', $tableColumn)); + assert(array_key_exists('complete_type', $tableColumn)); + + if ($tableColumn['default'] !== null) { + if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches) === 1) { + $tableColumn['default'] = $matches[1]; + } elseif (preg_match('/^NULL::/', $tableColumn['default']) === 1) { + $tableColumn['default'] = null; + } + } + + if ($length === -1 && isset($tableColumn['atttypmod'])) { + $length = $tableColumn['atttypmod'] - 4; + } + + if ((int) $length <= 0) { + $length = null; + } + + $fixed = false; + + if (! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $precision = null; + $scale = 0; + $jsonb = null; + + $dbType = strtolower($tableColumn['type']); + if ( + $tableColumn['domain_type'] !== null + && $tableColumn['domain_type'] !== '' + && ! $this->platform->hasDoctrineTypeMappingFor($tableColumn['type']) + ) { + $dbType = strtolower($tableColumn['domain_type']); + $tableColumn['complete_type'] = $tableColumn['domain_complete_type']; + } + + $type = $this->platform->getDoctrineTypeMapping($dbType); + + switch ($dbType) { + case 'smallint': + case 'int2': + case 'int': + case 'int4': + case 'integer': + case 'bigint': + case 'int8': + $length = null; + break; + + case 'bool': + case 'boolean': + if ($tableColumn['default'] === 'true') { + $tableColumn['default'] = true; + } + + if ($tableColumn['default'] === 'false') { + $tableColumn['default'] = false; + } + + $length = null; + break; + + case 'json': + case 'text': + case '_varchar': + case 'varchar': + $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']); + break; + + case 'char': + case 'bpchar': + $fixed = true; + break; + + case 'float': + case 'float4': + case 'float8': + case 'double': + case 'double precision': + case 'real': + case 'decimal': + case 'money': + case 'numeric': + if ( + preg_match( + '([A-Za-z]+\(([0-9]+),([0-9]+)\))', + $tableColumn['complete_type'], + $match, + ) === 1 + ) { + $precision = (int) $match[1]; + $scale = (int) $match[2]; + $length = null; + } + + break; + + case 'year': + $length = null; + break; + + // PostgreSQL 9.4+ only + case 'jsonb': + $jsonb = true; + break; + } + + if ( + is_string($tableColumn['default']) && preg_match( + "('([^']+)'::)", + $tableColumn['default'], + $match, + ) === 1 + ) { + $tableColumn['default'] = $match[1]; + } + + $options = [ + 'length' => $length, + 'notnull' => (bool) $tableColumn['isnotnull'], + 'default' => $tableColumn['default'], + 'precision' => $precision, + 'scale' => $scale, + 'fixed' => $fixed, + 'autoincrement' => $autoincrement, + ]; + + if (isset($tableColumn['comment'])) { + $options['comment'] = $tableColumn['comment']; + } + + $column = new Column($tableColumn['field'], Type::getType($type), $options); + + if (! empty($tableColumn['collation'])) { + $column->setPlatformOption('collation', $tableColumn['collation']); + } + + if ($column->getType() instanceof JsonType) { + $column->setPlatformOption('jsonb', $jsonb); + } + + return $column; + } + + /** + * Parses a default value expression as given by PostgreSQL + */ + private function parseDefaultExpression(?string $default): ?string + { + if ($default === null) { + return $default; + } + + return str_replace("''", "'", $default); + } + + protected function selectTableNames(string $databaseName): Result + { + $sql = <<<'SQL' +SELECT quote_ident(table_name) AS table_name, + table_schema AS schema_name +FROM information_schema.tables +WHERE table_catalog = ? + AND table_schema NOT LIKE 'pg\_%' + AND table_schema != 'information_schema' + AND table_name != 'geometry_columns' + AND table_name != 'spatial_ref_sys' + AND table_type = 'BASE TABLE' +SQL; + + return $this->connection->executeQuery($sql, [$databaseName]); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' c.relname AS table_name, n.nspname AS schema_name,'; + } + + $sql .= <<<'SQL' + a.attnum, + quote_ident(a.attname) AS field, + t.typname AS type, + format_type(a.atttypid, a.atttypmod) AS complete_type, + (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation, + (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, + (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM + pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type, + a.attnotnull AS isnotnull, + a.attidentity, + (SELECT 't' + FROM pg_index + WHERE c.oid = pg_index.indrelid + AND pg_index.indkey[0] = a.attnum + AND pg_index.indisprimary = 't' + ) AS pri, + (SELECT pg_get_expr(adbin, adrelid) + FROM pg_attrdef + WHERE c.oid = pg_attrdef.adrelid + AND pg_attrdef.adnum=a.attnum + ) AS default, + (SELECT pg_description.description + FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid + ) AS comment + FROM pg_attribute a + INNER JOIN pg_class c + ON c.oid = a.attrelid + INNER JOIN pg_type t + ON t.oid = a.atttypid + INNER JOIN pg_namespace n + ON n.oid = c.relnamespace + LEFT JOIN pg_depend d + ON d.objid = c.oid + AND d.deptype = 'e' + AND d.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_class') +SQL; + + $conditions = array_merge([ + 'a.attnum > 0', + "c.relkind = 'r'", + 'd.refobjid IS NULL', + ], $this->buildQueryConditions($tableName)); + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY a.attnum'; + + return $this->connection->executeQuery($sql); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,'; + } + + $sql .= <<<'SQL' + quote_ident(ic.relname) AS relname, + i.indisunique, + i.indisprimary, + i.indkey, + i.indrelid, + pg_get_expr(indpred, indrelid) AS "where" + FROM pg_index i + JOIN pg_class AS tc ON tc.oid = i.indrelid + JOIN pg_namespace tn ON tn.oid = tc.relnamespace + JOIN pg_class AS ic ON ic.oid = i.indexrelid + WHERE ic.oid IN ( + SELECT indexrelid + FROM pg_index i, pg_class c, pg_namespace n +SQL; + + $conditions = array_merge([ + 'c.oid = i.indrelid', + 'c.relnamespace = n.oid', + ], $this->buildQueryConditions($tableName)); + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ')'; + + return $this->connection->executeQuery($sql); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,'; + } + + $sql .= <<<'SQL' + quote_ident(r.conname) as conname, + pg_get_constraintdef(r.oid, true) as condef + FROM pg_constraint r + JOIN pg_class AS tc ON tc.oid = r.conrelid + JOIN pg_namespace tn ON tn.oid = tc.relnamespace + WHERE r.conrelid IN + ( + SELECT c.oid + FROM pg_class c, pg_namespace n +SQL; + + $conditions = array_merge(['n.oid = c.relnamespace'], $this->buildQueryConditions($tableName)); + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ") AND r.contype = 'f'"; + + return $this->connection->executeQuery($sql); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + $sql = <<<'SQL' +SELECT c.relname, + CASE c.relpersistence WHEN 'u' THEN true ELSE false END as unlogged, + obj_description(c.oid, 'pg_class') AS comment +FROM pg_class c + INNER JOIN pg_namespace n + ON n.oid = c.relnamespace +SQL; + + $conditions = array_merge(["c.relkind = 'r'"], $this->buildQueryConditions($tableName)); + + $sql .= ' WHERE ' . implode(' AND ', $conditions); + + return $this->connection->fetchAllAssociativeIndexed($sql); + } + + /** @return list */ + private function buildQueryConditions(?string $tableName): array + { + $conditions = []; + + if ($tableName !== null) { + if (str_contains($tableName, '.')) { + [$schemaName, $tableName] = explode('.', $tableName); + $conditions[] = 'n.nspname = ' . $this->platform->quoteStringLiteral($schemaName); + } else { + $conditions[] = 'n.nspname = ANY(current_schemas(false))'; + } + + $identifier = new Identifier($tableName); + $conditions[] = 'c.relname = ' . $this->platform->quoteStringLiteral($identifier->getName()); + } + + $conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')"; + + return $conditions; + } +} -- cgit v1.2.3