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
6 changes: 2 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,9 @@ composer.phar
# phpunit itself is not needed
phpunit.phar

# PHPUnit cache
/.phpunit.result.cache

# local phpunit config
# local phpunit config and cache
/phpunit.xml
/.phpunit.result.cache

# NPM packages
/node_modules
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Enh #284: Add tests for `binary` type and fix casting of default value (@Tigrov)
- Bug #287: Fix `bit` type (@Tigrov)
- Enh #289: Array parser refactoring (@Tigrov)
- Chg #288: Typecast refactoring (@Tigrov)

## 1.0.0 April 12, 2023

Expand Down
59 changes: 25 additions & 34 deletions src/ColumnSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@
use function array_walk_recursive;
use function bindec;
use function decbin;
use function in_array;
use function is_array;
use function is_int;
use function is_string;
use function json_decode;
use function str_pad;
use function strtolower;

/**
* Represents the metadata of a column in a database table for PostgreSQL Server.
Expand Down Expand Up @@ -72,32 +70,27 @@ final class ColumnSchema extends AbstractColumnSchema
*/
public function dbTypecast(mixed $value): mixed
{
if ($value === null) {
return null;
}

if ($value instanceof ExpressionInterface) {
if ($value === null || $value instanceof ExpressionInterface) {
return $value;
}

if ($this->dimension > 0) {
return new ArrayExpression($value, $this->getDbType(), $this->dimension);
}

if (in_array($this->getDbType(), [SchemaInterface::TYPE_JSON, SchemaInterface::TYPE_JSONB], true)) {
return new JsonExpression($value, $this->getDbType());
}
return match ($this->getType()) {
SchemaInterface::TYPE_JSON => new JsonExpression($value, $this->getDbType()),
Comment thread
darkdef marked this conversation as resolved.

if (is_string($value) && $this->getType() === SchemaInterface::TYPE_BINARY) {
/** explicitly setup PDO param type for binary column */
return new Param($value, PDO::PARAM_LOB);
}
SchemaInterface::TYPE_BINARY => is_string($value)
? new Param($value, PDO::PARAM_LOB) // explicitly setup PDO param type for binary column
: $this->typecast($value),

if (is_int($value) && $this->getType() === Schema::TYPE_BIT) {
return str_pad(decbin($value), $this->getSize() ?? 0, '0', STR_PAD_LEFT);
}
Schema::TYPE_BIT => is_int($value)
? str_pad(decbin($value), (int) $this->getSize(), '0', STR_PAD_LEFT)
: $this->typecast($value),

return $this->typecast($value);
default => $this->typecast($value),
};
}

/**
Expand Down Expand Up @@ -144,23 +137,21 @@ protected function phpTypecastValue(mixed $value): mixed
return null;
}

switch ($this->getType()) {
case Schema::TYPE_BIT:
return is_string($value) ? bindec($value) : $value;
case SchemaInterface::TYPE_BOOLEAN:
/** @psalm-var mixed $value */
$value = is_string($value) ? strtolower($value) : $value;

return match ($value) {
't', 'true' => true,
'f', 'false' => false,
default => (bool)$value,
};
case SchemaInterface::TYPE_JSON:
return json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR);
}
return match ($this->getType()) {
Schema::TYPE_BIT => is_string($value) ? bindec($value) : $value,

SchemaInterface::TYPE_BOOLEAN
=> match ($value) {
't' => true,
'f' => false,
default => (bool) $value,
},

SchemaInterface::TYPE_JSON
=> json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR),

return parent::phpTypecast($value);
default => parent::phpTypecast($value),
};
}

/**
Expand Down
176 changes: 65 additions & 111 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
* column_comment: string|null,
* modifier: int,
* is_nullable: bool,
* column_default: mixed,
* column_default: string|null,
* is_autoinc: bool,
* sequence_name: string|null,
* enum_values: array<array-key, float|int|string>|string|null,
Expand Down Expand Up @@ -532,30 +532,14 @@ protected function findConstraints(TableSchemaInterface $table): void
/** @psalm-var array{array{tableName: string, columns: array}} $constraints */
$constraints = [];

/**
* @psalm-var array<
* array{
* constraint_name: string,
* column_name: string,
* foreign_table_name: string,
* foreign_table_schema: string,
* foreign_column_name: string,
* }
* > $rows
*/
/** @psalm-var array<FindConstraintArray> $rows */
$rows = $this->db->createCommand($sql, [
':schemaName' => $table->getSchemaName(),
':tableName' => $table->getName(),
])->queryAll();

foreach ($rows as $constraint) {
/** @psalm-var array{
* constraint_name: string,
* column_name: string,
* foreign_table_name: string,
* foreign_table_schema: string,
* foreign_column_name: string,
* } $constraint */
/** @psalm-var FindConstraintArray $constraint */
$constraint = $this->normalizeRowKeyCase($constraint, false);

if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
Expand Down Expand Up @@ -756,61 +740,21 @@ protected function findColumns(TableSchemaInterface $table): bool
return false;
}

/** @psalm-var array $column */
foreach ($columns as $column) {
/** @psalm-var ColumnArray $column */
$column = $this->normalizeRowKeyCase($column, false);
/** @psalm-var ColumnArray $info */
foreach ($columns as $info) {
/** @psalm-var ColumnArray $info */
$info = $this->normalizeRowKeyCase($info, false);

/** @psalm-var ColumnSchema $loadColumnSchema */
$loadColumnSchema = $this->loadColumnSchema($column);
/** @psalm-var ColumnSchema $column */
$column = $this->loadColumnSchema($info);

$table->column($loadColumnSchema->getName(), $loadColumnSchema);
$table->column($column->getName(), $column);

/** @psalm-var mixed $defaultValue */
$defaultValue = $loadColumnSchema->getDefaultValue();

if ($loadColumnSchema->isPrimaryKey()) {
$table->primaryKey($loadColumnSchema->getName());
if ($column->isPrimaryKey()) {
$table->primaryKey($column->getName());

if ($table->getSequenceName() === null) {
$table->sequenceName($loadColumnSchema->getSequenceName());
}

$loadColumnSchema->defaultValue(null);
} elseif ($defaultValue) {
if (
is_string($defaultValue) &&
in_array(
$loadColumnSchema->getType(),
[self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME],
true
) &&
in_array(
strtoupper($defaultValue),
['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'],
true
)
) {
$loadColumnSchema->defaultValue(new Expression($defaultValue));
} elseif ($loadColumnSchema->getType() === 'boolean') {
$loadColumnSchema->defaultValue($defaultValue === 'true');
} elseif (is_string($defaultValue) && preg_match("/^B?'(.*?)'::/", $defaultValue, $matches)) {
if ($loadColumnSchema->getType() === 'binary' && str_starts_with($matches[1], '\\x')) {
$loadColumnSchema->defaultValue(hex2bin(substr($matches[1], 2)));
} else {
$loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[1]));
}
} elseif (
is_string($defaultValue) &&
preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $defaultValue, $matches)
) {
if ($matches[2] === 'NULL') {
$loadColumnSchema->defaultValue(null);
} else {
$loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[2]));
}
} else {
$loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($defaultValue));
$table->sequenceName($column->getSequenceName());
}
}
}
Expand All @@ -821,27 +765,7 @@ protected function findColumns(TableSchemaInterface $table): bool
/**
* Loads the column information into a {@see ColumnSchemaInterface} object.
*
* @psalm-param array{
* table_schema: string,
* table_name: string,
* column_name: string,
* data_type: string,
* type_type: string|null,
* type_scheme: string|null,
* character_maximum_length: int,
* column_comment: string|null,
* modifier: int,
* is_nullable: bool,
* column_default: mixed,
* is_autoinc: bool,
* sequence_name: string|null,
* enum_values: array<array-key, float|int|string>|string|null,
* numeric_precision: int|null,
* numeric_scale: int|null,
* size: string|null,
* is_pkey: bool|null,
* dimension: int
* } $info Column information.
* @psalm-param ColumnArray $info Column information.
*
* @return ColumnSchemaInterface The column schema object.
*/
Expand All @@ -852,16 +776,15 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface
$column->autoIncrement($info['is_autoinc']);
$column->comment($info['column_comment']);

if (!in_array($info['type_scheme'], [$this->defaultSchema, 'pg_catalog'], true)
) {
if (!in_array($info['type_scheme'], [$this->defaultSchema, 'pg_catalog'], true)) {
$column->dbType($info['type_scheme'] . '.' . $info['data_type']);
} else {
$column->dbType($info['data_type']);
}

$column->defaultValue($info['column_default']);
$column->enumValues(($info['enum_values'] !== null)
? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) : null);
$column->enumValues($info['enum_values'] !== null
? explode(',', str_replace(["''"], ["'"], $info['enum_values']))
: null);
$column->unsigned(false); // has no meaning in PG
$column->primaryKey((bool) $info['is_pkey']);
$column->precision($info['numeric_precision']);
Expand All @@ -871,35 +794,26 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface

/**
* pg_get_serial_sequence() doesn't track DEFAULT value change.
*
* GENERATED BY IDENTITY columns always have a null default value.
*
* @psalm-var mixed $defaultValue
*/
$defaultValue = $column->getDefaultValue();
$sequenceName = $info['sequence_name'] ?? null;
$defaultValue = $info['column_default'];

if (
isset($defaultValue) &&
is_string($defaultValue) &&
preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1
$defaultValue !== null
&& preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1
) {
$column->sequenceName(preg_replace(
['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
'',
$defaultValue
));
} elseif ($sequenceName !== null) {
$column->sequenceName($this->resolveTableName($sequenceName)->getFullName());
}

if (isset($this->typeMap[$column->getDbType() ?? ''])) {
$column->type($this->typeMap[$column->getDbType() ?? '']);
} else {
$column->type(self::TYPE_STRING);
} elseif ($info['sequence_name'] !== null) {
$column->sequenceName($this->resolveTableName($info['sequence_name'])->getFullName());
}

$column->type($this->typeMap[(string) $column->getDbType()] ?? self::TYPE_STRING);
$column->phpType($this->getColumnPhpType($column));
$column->defaultValue($this->normalizeDefaultValue($defaultValue, $column));

return $column;
}
Expand All @@ -920,6 +834,46 @@ protected function getColumnPhpType(ColumnSchemaInterface $column): string
return parent::getColumnPhpType($column);
}

/**
* Converts column's default value according to {@see ColumnSchema::phpType} after retrieval from the database.
*
* @param string|null $defaultValue The default value retrieved from the database.
* @param ColumnSchemaInterface $column The column schema object.
*
* @return mixed The normalized default value.
*/
private function normalizeDefaultValue(?string $defaultValue, ColumnSchemaInterface $column): mixed
{
if ($defaultValue === null || $column->isPrimaryKey()) {
return null;
}

if ($column->getType() === self::TYPE_BOOLEAN && in_array($defaultValue, ['true', 'false'], true)) {
return $defaultValue === 'true';
}

if (
in_array($column->getType(), [self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME], true)
&& in_array(strtoupper($defaultValue), ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'], true)
) {
return new Expression($defaultValue);
}

if (preg_match("/^B?'(.*?)'::/", $defaultValue, $matches) === 1) {
return $column->getType() === self::TYPE_BINARY && str_starts_with($matches[1], '\\x')
? hex2bin(substr($matches[1], 2))
: $column->phpTypecast($matches[1]);
}

if (preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $defaultValue, $matches) === 1) {
return $matches[2] !== 'NULL'
? $column->phpTypecast($matches[2])
: null;
}

return $column->phpTypecast($defaultValue);
}

/**
* Loads multiple types of constraints and returns the specified ones.
*
Expand Down
Loading