Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 52 additions & 13 deletions src/Driver/MySQL/Schema/MySQLTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ class MySQLTable extends AbstractTable
*/
private ?string $version = null;

/**
* Memoized `SHOW INDEXES` result, shared between {@see fetchIndexes()} and
* {@see fetchPrimaryKeys()}.
*/
private ?array $indexRows = null;

/**
* Change table engine. Such operation will be applied only at moment of table creation.
*
Expand Down Expand Up @@ -67,6 +73,7 @@ public function getEngine(): string
/**
* Populate table schema with values from database.
*/
#[\Override]
protected function initSchema(State $state): void
{
parent::initSchema($state);
Expand All @@ -80,6 +87,7 @@ protected function initSchema(State $state): void
)->fetch()['Engine'];
}

#[\Override]
protected function isIndexColumnSortingSupported(): bool
{
if (!$this->version) {
Expand All @@ -93,6 +101,7 @@ protected function isIndexColumnSortingSupported(): bool
return \version_compare($this->version, '8.0', '>=');
}

#[\Override]
protected function fetchColumns(): array
{
$query = "SHOW FULL COLUMNS FROM {$this->driver->identifier($this->getFullName())}";
Expand All @@ -109,13 +118,18 @@ protected function fetchColumns(): array
return $result;
}

protected function fetchIndexes(): array
#[\Override]
protected function resetIntrospectionCache(): void
{
$query = "SHOW INDEXES FROM {$this->driver->identifier($this->getFullName())}";
$this->indexRows = null;
}

#[\Override]
protected function fetchIndexes(): array
{
//Gluing all index definitions together
$schemas = [];
foreach ($this->driver->query($query) as $index) {
foreach ($this->indexRows() as $index) {
if ($index['Key_name'] === 'PRIMARY') {
//Skipping PRIMARY index
continue;
Expand All @@ -132,26 +146,38 @@ protected function fetchIndexes(): array
return $result;
}

#[\Override]
protected function fetchReferences(): array
{
$references = $this->driver->query(
'SELECT * FROM `information_schema`.`referential_constraints`
WHERE `constraint_schema` = ? AND `table_name` = ?',
[$this->driver->getSource(), $this->getFullName()],
)->fetchAll();

if ($references === []) {
return [];
}

// `key_column_usage` is an expensive view, it must not be queried per constraint.
$usage = [];
$rows = $this->driver->query(
'SELECT * FROM `information_schema`.`key_column_usage`
WHERE `table_schema` = ? AND `table_name` = ? AND `referenced_table_name` IS NOT NULL
ORDER BY `constraint_name`, `ordinal_position`',
[$this->driver->getSource(), $this->getFullName()],
);

foreach ($rows as $row) {
$usage[$row['CONSTRAINT_NAME']][] = $row;
}

$result = [];
foreach ($references as $schema) {
$columns = $this->driver->query(
'SELECT * FROM `information_schema`.`key_column_usage`
WHERE `constraint_name` = ? AND `table_schema` = ? AND `table_name` = ?',
[$schema['CONSTRAINT_NAME'], $this->driver->getSource(), $this->getFullName()],
)->fetchAll();

$schema['COLUMN_NAME'] = [];
$schema['REFERENCED_COLUMN_NAME'] = [];

foreach ($columns as $column) {
foreach ($usage[$schema['CONSTRAINT_NAME']] ?? [] as $column) {
$schema['COLUMN_NAME'][] = $column['COLUMN_NAME'];
$schema['REFERENCED_COLUMN_NAME'][] = $column['REFERENCED_COLUMN_NAME'];
}
Expand All @@ -169,12 +195,11 @@ protected function fetchReferences(): array
/**
* Fetching primary keys from table.
*/
#[\Override]
protected function fetchPrimaryKeys(): array
{
$query = "SHOW INDEXES FROM {$this->driver->identifier($this->getFullName())}";

$primaryKeys = [];
foreach ($this->driver->query($query) as $index) {
foreach ($this->indexRows() as $index) {
if ($index['Key_name'] === 'PRIMARY') {
$primaryKeys[] = $index['Column_name'];
}
Expand All @@ -186,6 +211,7 @@ protected function fetchPrimaryKeys(): array
/**
* @psalm-param non-empty-string $name
*/
#[\Override]
protected function createColumn(string $name): AbstractColumn
{
return new MySQLColumn($this->getFullName(), $name, $this->driver->getTimezone());
Expand All @@ -194,6 +220,7 @@ protected function createColumn(string $name): AbstractColumn
/**
* @psalm-param non-empty-string $name
*/
#[\Override]
protected function createIndex(string $name): AbstractIndex
{
return new MySQLIndex($this->getFullName(), $name);
Expand All @@ -202,8 +229,20 @@ protected function createIndex(string $name): AbstractIndex
/**
* @psalm-param non-empty-string $name
*/
#[\Override]
protected function createForeign(string $name): AbstractForeignKey
{
return new MySQLForeignKey($this->getFullName(), $this->getPrefix(), $name);
}

/**
* `SHOW INDEXES` carries both the secondary indexes and the primary key, so it is executed once
* per introspection and split in PHP.
*/
private function indexRows(): array
{
return $this->indexRows ??= $this->driver
->query("SHOW INDEXES FROM {$this->driver->identifier($this->getFullName())}")
->fetchAll();
}
}
63 changes: 47 additions & 16 deletions src/Driver/Postgres/Schema/PostgresColumn.php
Original file line number Diff line number Diff line change
Expand Up @@ -305,12 +305,20 @@ class PostgresColumn extends AbstractColumn

/**
* @param DriverInterface $driver Postgres columns are bit more complex.
* @param array|null $checkConstraints Pre-fetched CHECK constraints of the whole table keyed by
* the textual `conkey` value. When `null` the constraints are resolved with a dedicated
* query (requires `tableOID` in the `$schema`).
* @param array|null $enumValues Pre-fetched native enum ranges keyed as `<schema>.<type>`.
* When `null` the range is resolved with a dedicated query.
*
* @psalm-param non-empty-string $table Table name.
*/
public static function createInstance(
string $table,
array $schema,
DriverInterface $driver,
?array $checkConstraints = null,
?array $enumValues = null,
): self {
$column = new self($table, $schema['column_name'], $driver->getTimezone());

Expand Down Expand Up @@ -365,7 +373,7 @@ public static function createInstance(
* Attention, this is not default enum type emulated via CHECK.
* This is real Postgres enum type.
*/
self::resolveEnum($driver, $column);
self::resolveEnum($driver, $schema, $column, $enumValues);
}

if ($column->type === 'timestamp' || $column->type === 'time' || $column->type === 'interval') {
Expand All @@ -382,7 +390,7 @@ public static function createInstance(

if (!empty($column->size) && \str_contains($column->type, 'char')) {
//Potential enum with manually created constraint (check in)
self::resolveConstrains($driver, $schema, $column);
self::resolveConstrains($driver, $schema, $column, $checkConstraints);
}

if ($column->type === 'interval' && \is_string($schema['interval_type'])) {
Expand All @@ -404,6 +412,7 @@ public static function createInstance(
return $column;
}

#[\Override]
public function getConstraints(): array
{
$constraints = parent::getConstraints();
Expand All @@ -418,6 +427,7 @@ public function getConstraints(): array
/**
* @psalm-return non-empty-string
*/
#[\Override]
public function getAbstractType(): string
{
return !empty($this->enumValues) ? 'enum' : parent::getAbstractType();
Expand Down Expand Up @@ -459,6 +469,7 @@ public function bigPrimary(): AbstractColumn
return $this->type('bigPrimary');
}

#[\Override]
public function enum(string|array $values): AbstractColumn
{
$this->enumValues = \array_map('strval', \is_array($values) ? $values : \func_get_args());
Expand Down Expand Up @@ -487,6 +498,7 @@ public function interval(int $size = 6, ?string $intervalType = null): AbstractC
/**
* @psalm-return non-empty-string
*/
#[\Override]
public function sqlStatement(DriverInterface $driver): string
{
$statement = [$driver->identifier($this->name), $this->type];
Expand Down Expand Up @@ -626,6 +638,7 @@ public function alterOperations(DriverInterface $driver, AbstractColumn $initial
return $operations;
}

#[\Override]
public function compare(AbstractColumn $initial): bool
{
if (parent::compare($initial)) {
Expand All @@ -638,6 +651,7 @@ public function compare(AbstractColumn $initial): bool
);
}

#[\Override]
public function getComment(): string
{
return $this->comment;
Expand All @@ -654,6 +668,7 @@ public function createComment(DriverInterface $driver): string
return "COMMENT ON COLUMN {$tableName}.{$identifier} IS " . $driver->quote($this->comment);
}

#[\Override]
protected static function isJson(AbstractColumn $column): bool
{
return $column->getAbstractType() === 'json' || $column->getAbstractType() === 'jsonb';
Expand All @@ -662,6 +677,7 @@ protected static function isJson(AbstractColumn $column): bool
/**
* @psalm-return non-empty-string
*/
#[\Override]
protected function quoteEnum(DriverInterface $driver): string
{
//Postgres enums are just constrained strings
Expand All @@ -675,17 +691,24 @@ private static function resolveConstrains(
DriverInterface $driver,
array $schema,
self $column,
?array $checkConstraints = null,
): void {
$query = "SELECT conname, pg_get_constraintdef(oid) as consrc FROM pg_constraint
WHERE conrelid = ? AND contype = 'c' AND conkey = ?";

$constraints = $driver->query(
$query,
[
$schema['tableOID'],
'{' . $schema['dtd_identifier'] . '}',
],
);
$conkey = '{' . $schema['dtd_identifier'] . '}';

if ($checkConstraints !== null) {
$constraints = $checkConstraints[$conkey] ?? [];
} else {
$query = "SELECT conname, pg_get_constraintdef(oid) as consrc FROM pg_constraint
WHERE conrelid = ? AND contype = 'c' AND conkey = ?";

$constraints = $driver->query(
$query,
[
$schema['tableOID'],
$conkey,
],
);
}

foreach ($constraints as $constraint) {
$values = static::parseEnumValues($constraint['consrc']);
Expand All @@ -701,11 +724,19 @@ private static function resolveConstrains(
/**
* Resolve native ENUM type if presented.
*/
private static function resolveEnum(DriverInterface $driver, self $column): void
{
$range = $driver->query('SELECT enum_range(NULL::' . $column->type . ')')->fetchColumn(0);
private static function resolveEnum(
DriverInterface $driver,
array $schema,
self $column,
?array $enumValues = null,
): void {
if ($enumValues !== null) {
$column->enumValues = $enumValues[$schema['udt_schema'] . '.' . $schema['udt_name']] ?? [];
} else {
$range = $driver->query('SELECT enum_range(NULL::' . $column->type . ')')->fetchColumn(0);

$column->enumValues = \explode(',', \substr($range, 1, -1));
$column->enumValues = \explode(',', \substr($range, 1, -1));
}

if (!empty($column->defaultValue)) {
//In database: 'value'::enumType
Expand Down
Loading
Loading