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
16 changes: 15 additions & 1 deletion documentation/upgrading.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,20 @@ Please follow the instructions for your specific version to ensure a smooth upgr

## Upgrading from 0.39.x to 0.40.x

### 1) `flow-php/telemetry` - Log severity filtering moved to a pipeline middleware
### 1) `flow-php/postgresql` - column and domain defaults are modeled as `ColumnDefault`

| Before | After |
|---------------------------------------------------------------|-----------------------------------------------------------------------------|
| `Column::$default` / `Domain::$default` type `?string` | `?Flow\PostgreSql\Schema\ColumnDefault` |
| `new Column('c', $type, true, "'0'")` | `new Column('c', $type, true, ColumnDefault::fromExpression("'0'", $type))` |
| `$column->default` (string) | `$column->default?->literal` / `$column->default?->applicableSql()` |
| `ColumnShape['default']` / `DomainShape['default']` `?string` | `?array{literal: string, type: ?ColumnTypeShape, kind: string}` |

`Column::create()` / `Domain::create()` still accept `bool|float|int|string|Expression|null`.
Schema arrays serialized by `Column::normalize()` / `Domain::normalize()` before 0.40 must be
regenerated — `fromArray()` reads the nested `default` shape only.

### 2) `flow-php/telemetry` - Log severity filtering moved to a pipeline middleware

| Before | After |
|-----------------------------------------------------------------|-----------------------------------------------------------------------------------------|
Expand Down Expand Up @@ -122,6 +135,7 @@ to_pgsql_table($client, 'users')->withTypesMap(new EntryTypesMap(
```

### 6) `flow-php/symfony-postgresql-messenger` - `messenger_messages` time columns use `timestamp` instead of

`timestamptz`

| Column type for `created_at`, `available_at`, `delivered_at` | Before | After |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public function test_default_value(): void
'flags',
);

static::assertSame('true', $table->column('active')->default);
static::assertSame('true', $table->column('active')->default?->literal);
}

public function test_empty_schema_throws(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function test_queue_name_has_default_value(): void
$provider = new MessengerCatalogProvider();
$queueNameColumn = $provider->get()->get('public')->tables[0]->column('queue_name');

static::assertSame("'default'", $queueNameColumn->default);
static::assertSame("'default'", $queueNameColumn->default?->literal);
}

public function test_table_has_custom_name_and_schema(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
use Flow\PostgreSql\Parser\ColumnTypeParser;
use Flow\PostgreSql\Parser\ExpressionParser;
use Flow\PostgreSql\Parser\TriggerDefinitionParser;
use Flow\PostgreSql\Protobuf\AST\Integer;
use Flow\PostgreSql\QueryBuilder\Condition\ComparisonOperator;
use Flow\PostgreSql\QueryBuilder\Expression\Literal;
use Flow\PostgreSql\QueryBuilder\Schema\ColumnType;
use Flow\PostgreSql\QueryBuilder\Schema\ReferentialAction;
use Flow\PostgreSql\Schema\Catalog;
use Flow\PostgreSql\Schema\CatalogProvider;
use Flow\PostgreSql\Schema\Column;
use Flow\PostgreSql\Schema\ColumnDefault;
use Flow\PostgreSql\Schema\Constraint\CheckConstraint;
use Flow\PostgreSql\Schema\Constraint\ExcludeConstraint;
use Flow\PostgreSql\Schema\Constraint\ForeignKey;
Expand Down Expand Up @@ -74,15 +74,13 @@
use function Flow\PostgreSql\DSL\when;
use function Flow\Types\DSL\type_array;
use function Flow\Types\DSL\type_boolean;
use function Flow\Types\DSL\type_instance_of;
use function Flow\Types\DSL\type_integer;
use function Flow\Types\DSL\type_null;
use function Flow\Types\DSL\type_string;
use function Flow\Types\DSL\type_structure;
use function Flow\Types\DSL\type_union;
use function preg_match;
use function sprintf;
use function str_replace;
use function strtolower;
use function trim;

Expand Down Expand Up @@ -131,57 +129,6 @@ private function mapReferentialAction(string $code): ReferentialAction
return ReferentialAction::tryFrom($code) ?? ReferentialAction::NO_ACTION;
}

/**
* Strip implicit type casts from default values that PostgreSQL adds for storage.
*
* PostgreSQL stores `'pending'` as `'pending'::character varying` — the cast is implicit
* and redundant since the column type is already known. This method parses the expression
* and removes the outer TypeCast when it wraps a simple constant.
*/
private function normalizeDefault(?string $default): ?string
{
if ($default === null) {
return null;
}

$node = $this->expressionParser->parse($default);
$typeCast = $node->getTypeCast();

if ($typeCast === null) {
return $default;
}

$inner = $typeCast->getArg();

if ($inner === null) {
return $default;
}

$aConst = $inner->getAConst();

if ($aConst === null) {
return $default;
}

$sval = $aConst->getSval();

if ($sval !== null) {
return "'" . str_replace("'", "''", $sval->getSval()) . "'";
}

if ($aConst->getIval() !== null) {
return (string) type_instance_of(Integer::class)->assert($aConst->getIval())->getIval();
}

$fval = $aConst->getFval();

if ($fval !== null) {
return $fval->getFval();
}

return $default;
}

/**
* @return list<string>
*/
Expand Down Expand Up @@ -318,12 +265,15 @@ private function readColumns(string $tableName, string $schemaName): array
$isGenerated = $generated !== '';

$defaultValue = type_union(type_string(), type_null())->assert($row['default_value'] ?? null);
$columnType = $this->columnTypeParser->parse(type_string()->assert($row['type_name']));

$columns[] = new Column(
type_string()->assert($row['name']),
$this->columnTypeParser->parse(type_string()->assert($row['type_name'])),
$columnType,
type_boolean()->assert($row['nullable']),
$isGenerated || $isIdentity ? null : $this->normalizeDefault($defaultValue),
$isGenerated || $isIdentity || $defaultValue === null
? null
: ColumnDefault::fromExpression($defaultValue, $columnType),
$isIdentity,
$isIdentity ? IdentityGeneration::from($identity) : null,
$isGenerated,
Expand Down Expand Up @@ -411,12 +361,13 @@ private function readDomains(string $schemaName): array
$row = type_array()->assert($row);
$name = type_string()->assert($row['name']);
$defaultValue = type_union(type_string(), type_null())->assert($row['default_value'] ?? null);
$baseType = $this->columnTypeParser->parse(type_string()->assert($row['base_type']));

$domains[] = new Domain(
$name,
$this->columnTypeParser->parse(type_string()->assert($row['base_type'])),
$baseType,
type_boolean()->assert($row['nullable']),
$this->normalizeDefault($defaultValue),
$defaultValue === null ? null : ColumnDefault::fromExpression($defaultValue, $baseType),
$this->readDomainCheckConstraints($name, $schemaName),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,22 @@ public function isEqual(self $other): bool
);
}

/**
* Same base type ignoring type modifiers (precision/scale).
*
* A column default's stored cast (e.g. '0'::numeric) carries the base type but no typmod, while
* the column itself is typed numeric(10,3). Comparing those for stale-cast drift must ignore the
* typmod and look only at the base type identity.
*/
public function isSameBaseType(self $other): bool
{
return (
self::normalizedName($this->name) === self::normalizedName($other->name)
&& $this->normalizedSchema() === $other->normalizedSchema()
&& $this->isArray === $other->isArray
);
}

/**
* @return ColumnTypeShape
*/
Expand Down
17 changes: 11 additions & 6 deletions src/lib/postgresql/src/Flow/PostgreSql/Schema/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@

/**
* @phpstan-import-type ColumnTypeShape from ColumnType
* @phpstan-import-type ColumnDefaultShape from ColumnDefault
*
* @phpstan-type ColumnShape = array{name: string, type: ColumnTypeShape, nullable: bool, default?: ?string, is_identity?: bool, identity_generation?: ?string, is_generated?: bool, generation_expression?: ?string, ordinal_position?: ?int}
* @phpstan-type ColumnShape = array{name: string, type: ColumnTypeShape, nullable: bool, default?: ?ColumnDefaultShape, is_identity?: bool, identity_generation?: ?string, is_generated?: bool, generation_expression?: ?string, ordinal_position?: ?int}
*/
final readonly class Column
{
Expand All @@ -21,7 +22,7 @@ public function __construct(
public string $name,
public ColumnType $type,
public bool $nullable,
public ?string $default = null,
public ?ColumnDefault $default = null,
public bool $isIdentity = false,
public ?IdentityGeneration $identityGeneration = null,
public bool $isGenerated = false,
Expand All @@ -44,11 +45,13 @@ public static function create(
?string $generationExpression = null,
?int $ordinalPosition = null,
): self {
$formattedDefault = (new ColumnDefaultFormatter())->format($default);

return new self(
$name,
$type,
$nullable,
(new ColumnDefaultFormatter())->format($default),
$formattedDefault === null ? null : ColumnDefault::fromExpression($formattedDefault, $type),
$isIdentity,
$identityGeneration,
$isGenerated,
Expand All @@ -66,7 +69,9 @@ public static function fromArray(array $data): self
name: $data['name'],
type: ColumnType::fromArray($data['type']),
nullable: $data['nullable'],
default: $data['default'] ?? null,
default: array_key_exists('default', $data) && $data['default'] !== null
? ColumnDefault::fromArray($data['default'])
: null,
isIdentity: $data['is_identity'] ?? false,
identityGeneration: array_key_exists('identity_generation', $data) && $data['identity_generation'] !== null
? IdentityGeneration::from($data['identity_generation'])
Expand All @@ -87,7 +92,7 @@ public function isEqualStructure(self $other): bool
return (
$this->type->isEqual($other->type)
&& $this->nullable === $other->nullable
&& $this->default === $other->default
&& ColumnDefault::nullableEquals($this->default, $other->default)
&& $this->isIdentity === $other->isIdentity
&& $this->identityGeneration === $other->identityGeneration
&& $this->isGenerated === $other->isGenerated
Expand All @@ -104,7 +109,7 @@ public function normalize(): array
'name' => $this->name,
'type' => $this->type->normalize(),
'nullable' => $this->nullable,
'default' => $this->default,
'default' => $this->default?->normalize(),
'is_identity' => $this->isIdentity,
'identity_generation' => $this->identityGeneration?->value,
'is_generated' => $this->isGenerated,
Expand Down
Loading
Loading