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
2 changes: 0 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"ext-dom": "*",
"ext-hash": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
Expand All @@ -42,7 +41,6 @@
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0",
"symfony/console": "^6.4 || ^7.3 || ^8.0",
"symfony/http-foundation": "^6.4 || ^7.3 || ^8.0",
"symfony/polyfill-php83": "^1.33",
"symfony/string": "^6.4 || ^7.3 || ^8.0",
"symfony/uid": "^6.4 || ^7.3 || ^8.0",
"webmozart/glob": "^3.0 || ^4.0"
Expand Down
3 changes: 1 addition & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions documentation/components/libs/postgresql.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,32 @@ $result = $client->transaction(function ($client) {
$client->close();
```

### Array Types

PostgreSQL arrays are supported through type-specific converters. Use `typed()` with the appropriate array type:

```php
<?php

use function Flow\PostgreSql\DSL\{pgsql_client, pgsql_connection, typed, pgsql_type_int4_array, pgsql_type_text_array};

$client = pgsql_client(pgsql_connection('host=localhost dbname=mydb'));

// Integer array
$client->execute(
'INSERT INTO scores (values) VALUES ($1)',
[typed([100, 200, 300], pgsql_type_int4_array())]
);

// Text array
$client->execute(
'INSERT INTO tags (names) VALUES ($1)',
[typed(['php', 'postgresql'], pgsql_type_text_array())]
);
```

See [Type System](/documentation/components/libs/postgresql/client-types.md) for all supported array types.

### Detailed Documentation

- [Connection](/documentation/components/libs/postgresql/client-connection.md) - Connection parameters, DSN parsing,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ $name = $client->fetchScalarString('SELECT name FROM users WHERE id = $1', [1]);
// $name is guaranteed to be string
```

These methods throw `InvalidTypeException` if the value cannot be converted to the expected type.
These methods throw `QueryException` if the value cannot be converted to the expected type.

## execute() - Data Modification

Expand Down
335 changes: 222 additions & 113 deletions documentation/components/libs/postgresql/client-types.md

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/core/etl/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"require": {
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
"ext-json": "*",
"ext-mbstring": "*",
"psr/clock": "^1.0",
"brick/math": "^0.11 || ^0.12 || ^0.13 || ^0.14",
"flow-php/types": "self.version",
Expand Down
1 change: 0 additions & 1 deletion src/lib/postgresql/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
],
"require": {
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
"flow-php/types": "self.version",
"google/protobuf": "^4.0"
},
"require-dev": {
Expand Down
8 changes: 2 additions & 6 deletions src/lib/postgresql/src/Flow/PostgreSql/AST/Nodes/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ public function alias() : ?string
return null;
}

$aliasname = $alias->getAliasname();

return $aliasname !== '' ? $aliasname : null;
return $alias->getAliasname() ?: null;
}

public function name() : string
Expand All @@ -38,8 +36,6 @@ public function raw() : RangeVar

public function schema() : ?string
{
$schemaname = $this->rangeVar->getSchemaname();

return $schemaname !== '' ? $schemaname : null;
return $this->rangeVar->getSchemaname() ?: null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,6 @@ private function createIntegerNode(int $value) : Node

private function hasOrderBy(SelectStmt $stmt) : bool
{
$sortClause = $stmt->getSortClause();

return $sortClause !== null && \count($sortClause) > 0;
return \count($stmt->getSortClause() ?? []) > 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,7 @@ private function createIntegerNode(int $value) : Node

private function hasOrderBy(SelectStmt $stmt) : bool
{
$sortClause = $stmt->getSortClause();

return $sortClause !== null && \count($sortClause) > 0;
return \count($stmt->getSortClause() ?? []) > 0;
}

private function isSetOperation(SelectStmt $stmt) : bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public static function fromParams(
/**
* Create from a PostgreSQL connection string.
*/
public static function fromString(string $connectionString) : self
public static function fromString(#[\SensitiveParameter] string $connectionString) : self
{
return new self($connectionString);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public static function tooManyRows(int $count) : self
return new self(\sprintf('Expected exactly one row, but %d were returned', $count));
}

public static function unexpectedScalarType(string $expected, string $actual) : self
{
return new self(\sprintf('Expected scalar of type %s, got %s', $expected, $actual));
}

/**
* Get the full SQL query that caused the exception.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

final class ValueConversionException extends ClientException
{
public static function ambiguousArrayType() : self
{
return new self('Array parameters require explicit type specification. Use typed($array, PostgreSqlType::INT4_ARRAY) or similar to specify the target array type.');
}

public static function cannotConvert(mixed $value, string $targetType) : self
{
$valueType = \get_debug_type($value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,27 @@
namespace Flow\PostgreSql\Client\Infrastructure\PgSql;

use function Flow\PostgreSql\DSL\{begin, commit, release_savepoint, rollback, savepoint};
use function Flow\Types\DSL\{type_boolean, type_datetime, type_float, type_integer, type_json, type_list, type_string, type_uuid};
use Flow\PostgreSql\AST\Transformers\{ExplainConfig, ExplainModifier};
use Flow\PostgreSql\Client\{Client, ConnectionParameters, Cursor, RowMapper, TransactionContext, TypedValue};
use Flow\PostgreSql\Client\Exception\{ConnectionException, MappingException, QueryException, TransactionException};
use Flow\PostgreSql\Client\Types\{PostgreSqlType, ValueConverter, ValueConverters};
use Flow\PostgreSql\Client\Exception\{ConnectionException, MappingException, QueryException, TransactionException, ValueConversionException};
use Flow\PostgreSql\Client\Types\{PostgreSqlType, ResultCaster, ValueConverters};
use Flow\PostgreSql\Explain\ExplainParser;
use Flow\PostgreSql\Explain\Plan\Plan;
use Flow\PostgreSql\Parser;
use Flow\PostgreSql\QueryBuilder\SqlQuery;
use Flow\Types\Value\{Json, Uuid};
use PgSql\{Connection, Result};

final class PgSqlClient implements Client
{
private bool $autoCommit = true;

private readonly ResultCaster $resultCaster;

private readonly TransactionContext $transactionContext;

private function __construct(private ?Connection $connection, private readonly ValueConverters $valueConverters, private readonly ?RowMapper $defaultMapper = null)
{
$this->resultCaster = new ResultCaster();
$this->transactionContext = new TransactionContext();
}

Expand All @@ -36,7 +37,9 @@ public static function connect(
?ValueConverters $valueConverters = null,
?RowMapper $mapper = null,
) : self {
self::assertExtensionLoaded();
if (!\extension_loaded('pgsql')) {
throw ConnectionException::extensionNotLoaded('pgsql');
}

\error_clear_last();
$connection = @\pg_connect($params->connectionString);
Expand Down Expand Up @@ -98,7 +101,7 @@ public function cursor(SqlQuery|string $sql, array $parameters = []) : Cursor
{
$result = $this->query($sql, $parameters);

return new PgSqlCursor($result, $this->valueConverters, $this->defaultMapper);
return new PgSqlCursor($result, $this->defaultMapper);
}

public function execute(SqlQuery|string $sql, array $parameters = []) : int
Expand All @@ -119,13 +122,8 @@ public function explain(SqlQuery|string $sql, array $parameters = [], ?ExplainCo
$parsed->traverse(new ExplainModifier($config));
$explainQuery = $parsed->deparse();

$jsonOutput = $this->fetchScalar($explainQuery, $parameters);

if ($jsonOutput instanceof Json) {
$jsonOutput = $jsonOutput->toString();
}
$jsonOutput = $this->fetchScalarString($explainQuery, $parameters);

/** @var string $jsonOutput */
return (new ExplainParser())->parse($jsonOutput);
}

Expand Down Expand Up @@ -235,9 +233,7 @@ public function fetchOneInto(
array $parameters = [],
?RowMapper $mapper = null,
) : object {
$row = $this->fetchOne($sql, $parameters);

return $this->resolveMapper($mapper)->map($class, $row);
return $this->resolveMapper($mapper)->map($class, $this->fetchOne($sql, $parameters));
}

public function fetchScalar(SqlQuery|string $sql, array $parameters = []) : mixed
Expand All @@ -259,12 +255,7 @@ public function fetchScalar(SqlQuery|string $sql, array $parameters = []) : mixe
}

if ($value !== null) {
$oid = \pg_field_type_oid($result, 0);
$type = PostgreSqlType::tryFrom($oid);

if ($type !== null) {
$value = $this->valueConverters->forPostgreSqlType($type)->toPhp($value, $type);
}
$value = $this->resultCaster->cast($value, \pg_field_type($result, 0));
}

\pg_free_result($result);
Expand All @@ -274,22 +265,46 @@ public function fetchScalar(SqlQuery|string $sql, array $parameters = []) : mixe

public function fetchScalarBool(SqlQuery|string $sql, array $parameters = []) : bool
{
return type_boolean()->assert($this->fetchScalar($sql, $parameters));
$value = $this->fetchScalar($sql, $parameters);

if (!\is_bool($value)) {
throw QueryException::unexpectedScalarType('bool', \get_debug_type($value));
}

return $value;
}

public function fetchScalarFloat(SqlQuery|string $sql, array $parameters = []) : float
{
return type_float()->assert($this->fetchScalar($sql, $parameters));
$value = $this->fetchScalar($sql, $parameters);

if (!\is_float($value)) {
throw QueryException::unexpectedScalarType('float', \get_debug_type($value));
}

return $value;
}

public function fetchScalarInt(SqlQuery|string $sql, array $parameters = []) : int
{
return type_integer()->assert($this->fetchScalar($sql, $parameters));
$value = $this->fetchScalar($sql, $parameters);

if (!\is_int($value)) {
throw QueryException::unexpectedScalarType('int', \get_debug_type($value));
}

return $value;
}

public function fetchScalarString(SqlQuery|string $sql, array $parameters = []) : string
{
return type_string()->assert($this->fetchScalar($sql, $parameters));
$value = $this->fetchScalar($sql, $parameters);

if (!\is_string($value)) {
throw QueryException::unexpectedScalarType('string', \get_debug_type($value));
}

return $value;
}

public function getTransactionNestingLevel() : int
Expand Down Expand Up @@ -397,11 +412,14 @@ private function convertParameters(array $parameters) : array
if ($value === null) {
$converted[] = null;
} elseif ($value instanceof TypedValue) {
$converter = $this->valueConverters->forFlowType($value->type);
$converter = $this->valueConverters->forPostgreSqlType($value->targetType);
$converted[] = $converter->toDatabase($value->value);
} else {
$converter = $this->detectConverterForValue($value);
$converted[] = $converter->toDatabase($value);
if (\is_array($value)) {
throw ValueConversionException::ambiguousArrayType();
}

$converted[] = $this->valueConverters->forPostgreSqlType(PostgreSqlType::TEXT)->toDatabase($value);
}
}

Expand All @@ -419,14 +437,12 @@ private function convertRow(Result $result, array $row) : array
$i = 0;

foreach ($row as $column => $value) {
$key = (string) $column;

if ($value === null) {
$converted[$column] = null;
$converted[$key] = null;
} else {
$oid = \pg_field_type_oid($result, $i);
$type = PostgreSqlType::tryFrom($oid);
$converted[$column] = $type !== null
? $this->valueConverters->forPostgreSqlType($type)->toPhp($value, $type)
: $value;
$converted[$key] = $this->resultCaster->cast($value, \pg_field_type($result, $i));
}

$i++;
Expand All @@ -435,23 +451,6 @@ private function convertRow(Result $result, array $row) : array
return $converted;
}

/**
* @return ValueConverter<mixed>
*/
private function detectConverterForValue(mixed $value) : ValueConverter
{
return match (true) {
\is_bool($value) => $this->valueConverters->forFlowType(type_boolean()),
\is_int($value) => $this->valueConverters->forFlowType(type_integer()),
\is_float($value) => $this->valueConverters->forFlowType(type_float()),
$value instanceof \DateTimeInterface => $this->valueConverters->forFlowType(type_datetime()),
$value instanceof Uuid => $this->valueConverters->forFlowType(type_uuid()),
$value instanceof Json => $this->valueConverters->forFlowType(type_json()),
\is_array($value) => $this->valueConverters->forFlowType(type_list(type_string())),
default => $this->valueConverters->forFlowType(type_string()),
};
}

private function executeTransactionCommand(SqlQuery $query, callable $exceptionFactory) : void
{
/** @var Connection $connection */
Expand Down Expand Up @@ -479,8 +478,7 @@ private function query(SqlQuery|string $sql, array $parameters) : Result
$connection = $this->connection;

$query = $sql instanceof SqlQuery ? $sql->toSql() : $sql;
$params = $this->convertParameters($parameters);
$result = @\pg_query_params($connection, $query, $params);
$result = @\pg_query_params($connection, $query, $this->convertParameters($parameters));

if ($result === false) {
throw QueryException::executionFailed($query, \pg_last_error($connection) ?: 'Unknown error');
Expand All @@ -499,11 +497,4 @@ private function resolveMapper(?RowMapper $mapper) : RowMapper

return $resolved;
}

private static function assertExtensionLoaded() : void
{
if (!\extension_loaded('pgsql')) {
throw ConnectionException::extensionNotLoaded('pgsql');
}
}
}
Loading
Loading