Skip to content
Open
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
26 changes: 26 additions & 0 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,31 @@ jobs:
extension: "pdo_pgsql"
config-file-suffix: "-stringify_fetches"

phpunit-postgis:
name: "PHPUnit with PostGIS"
needs: "phpunit-smoke-check"
uses: ./.github/workflows/phpunit-postgis.yml
with:
php-version: ${{ matrix.php-version }}
postgis-version: ${{ matrix.postgis-version }}
extension: ${{ matrix.extension }}
postgres-locale-provider: ${{ matrix.postgres-locale-provider }}

strategy:
matrix:
php-version:
- "8.3"
- "8.4"
Comment on lines +151 to +152
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- "8.3"
- "8.4"
- "8.5"

I don't expect any new insights from testing on multiple PHP versions, do you? Let's not bloat the matrix too much. Let's test with the lastest PHP version on both extensions and on the lowest supported PHP version with one extension only.

postgis-version:
- "17-3.5"
extension:
- "pgsql"
- "pdo_pgsql"
include:
- php-version: "8.2"
postgis-version: "17-3.5"
extension: "pgsql"

phpunit-mariadb:
name: "PHPUnit with MariaDB"
needs: "phpunit-smoke-check"
Expand Down Expand Up @@ -295,6 +320,7 @@ jobs:
- "phpunit-smoke-check"
- "phpunit-oracle"
- "phpunit-postgres"
- "phpunit-postgis"
- "phpunit-mariadb"
- "phpunit-mysql"
- "phpunit-mssql"
Expand Down
64 changes: 64 additions & 0 deletions .github/workflows/phpunit-postgis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: PHPUnit with PostGIS

on:
workflow_call:
inputs:
php-version:
required: true
type: string
postgis-version:
required: true
type: string
extension:
required: true
type: string
postgres-locale-provider:
required: true
type: string
config-file-suffix:
required: false
type: string
default: ''

jobs:
phpunit-postgis:
runs-on: ubuntu-24.04

services:
postgres:
image: postgis/postgis:${{ inputs.postgis-version }}
ports:
- '5432:5432'
env:
POSTGRES_PASSWORD: postgres
POSTGRES_INITDB_ARGS: ${{ inputs.postgres-locale-provider == 'icu' && '--locale-provider=icu --icu-locale=en-US' || '' }}
options: >-
--health-cmd pg_isready
steps:
- name: Checkout
uses: actions/checkout@v4
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
uses: actions/checkout@v4
uses: actions/checkout@v6


- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ inputs.php-version }}
extensions: ${{ inputs.extension }}
coverage: pcov
ini-values: zend.assertions=1
env:
fail-fast: true

- name: Install dependencies with Composer
uses: ramsey/composer-install@v3
with:
composer-options: '--ignore-platform-req=php+'

- name: Run PHPUnit
run: vendor/bin/phpunit -c ci/github/phpunit/${{ inputs.extension }}${{ inputs.config-file-suffix }}.xml --coverage-clover=coverage.xml

- name: Upload coverage file
uses: actions/upload-artifact@v4
with:
name: ${{ github.job }}-${{ inputs.postgis-version }}-php-${{ inputs.php-version }}-${{ inputs.extension }}${{ inputs.config-file-suffix }}.coverage
path: coverage.xml
71 changes: 69 additions & 2 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -458,10 +458,13 @@
{
$columns = $values = $conditions = $set = [];

$platform = $this->getDatabasePlatform();
$typesMap = $this->normalizeTypes($types, array_keys($data));

foreach ($data as $columnName => $value) {
$columns[] = $columnName;
$values[] = $value;
$set[] = $columnName . ' = ?';
$set[] = $columnName . ' = ' . $this->getPlaceholderForColumn($columnName, $typesMap, $platform);
}

[$criteriaColumns, $criteriaValues, $criteriaConditions] = $this->getCriteriaCondition($criteria);
Expand Down Expand Up @@ -505,10 +508,13 @@
$values = [];
$set = [];

$platform = $this->getDatabasePlatform();
$typesMap = $this->normalizeTypes($types, array_keys($data));

foreach ($data as $columnName => $value) {
$columns[] = $columnName;
$values[] = $value;
$set[] = '?';
$set[] = $this->getPlaceholderForColumn($columnName, $typesMap, $platform);
}

return $this->executeStatement(
Expand Down Expand Up @@ -538,6 +544,67 @@
return $typeValues;
}

/**
* Normalizes types array from positional or associative to associative format.
*
* @param array<int<0,max>, string|ParameterType|Type>|array<string, string|ParameterType|Type> $types
* @param list<string> $columnNames
*
* @return array<string, string|ParameterType|Type>
*/
private function normalizeTypes(array $types, array $columnNames): array
{
Comment on lines +547 to +556
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have to do this?

if (count($types) === 0) {
return [];
}

// Already associative
if (is_string(key($types))) {
return $types;

Check failure on line 563 in src/Connection.php

View workflow job for this annotation

GitHub Actions / Static Analysis / PHPStan (PHP: 8.5)

Method Doctrine\DBAL\Connection::normalizeTypes() should return array<string, Doctrine\DBAL\ParameterType|Doctrine\DBAL\Types\Type|string> but returns array<int|string, Doctrine\DBAL\ParameterType|Doctrine\DBAL\Types\Type|string>.
}

// Convert positional to associative
$normalizedTypes = [];
foreach ($columnNames as $i => $columnName) {
if (isset($types[$i])) {
$normalizedTypes[$columnName] = $types[$i];
}
}

return $normalizedTypes;
}

/**
* Gets the SQL placeholder for a column based on its type.
*
* @param array<string, string|ParameterType|Type> $typesMap
*
* @throws Exception
*/
private function getPlaceholderForColumn(
string $columnName,
array $typesMap,
AbstractPlatform $platform,
): string {
if (! isset($typesMap[$columnName])) {
return '?';
}

$type = $typesMap[$columnName];

// Convert string type name to Type instance
if (is_string($type)) {
$type = Type::getType($type);
}

// Use Type's SQL conversion if it's a Type instance
if ($type instanceof Type) {
return $type->convertToDatabaseValueSQL('?', $platform);
}

return '?';
}

/**
* Quotes a string so it can be safely used as a table or column name, even if
* it is a reserved name.
Expand Down
134 changes: 102 additions & 32 deletions src/Platforms/AbstractMySQLPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\InvalidColumnType\ColumnValuesRequired;
use Doctrine\DBAL\Platforms\Exception\NotSupported;
use Doctrine\DBAL\Platforms\Keywords\KeywordList;
use Doctrine\DBAL\Platforms\Keywords\MySQLKeywords;
use Doctrine\DBAL\Platforms\MySQL\MySQLMetadataProvider;
Expand Down Expand Up @@ -34,6 +35,7 @@
use function sprintf;
use function str_replace;
use function strtolower;
use function strtoupper;

/**
* Provides the base implementation for the lowest versions of supported MySQL-like database platforms.
Expand Down Expand Up @@ -219,6 +221,65 @@ public function getBooleanTypeDeclarationSQL(array $column): string
return 'TINYINT';
}

/**
* {@inheritDoc}
*/
public function getGeometryTypeDeclarationSQL(array $column): string
{
$geometryType = $column['geometryType'] ?? 'GEOMETRY';
$srid = $column['srid'] ?? null;

$sql = strtoupper($geometryType);

if ($srid !== null) {
// MySQL 8.0.3+ supports SRID attribute using conditional comment syntax
$sql .= sprintf(' /*!80003 SRID %d */', $srid);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to emit those version-conditional comments? We should know which version of MySQL we're connecting to.

}

return $sql;
}

/**
* {@inheritDoc}
*/
public function getGeometryFromGeoJSONSQL(string $sqlExpr): string
{
return sprintf('ST_GeomFromGeoJSON(%s)', $sqlExpr);
}

/**
* {@inheritDoc}
*/
public function getGeometryAsGeoJSONSQL(string $sqlExpr): string
{
// MySQL 5.7.5+ / MariaDB 10.2.4+ - maxdecimaldigits=15 (full precision), options=2 (include SRID in CRS)
return sprintf('ST_AsGeoJSON(%s, 15, 2)', $sqlExpr);
}

/**
* {@inheritDoc}
*/
public function getGeographyTypeDeclarationSQL(array $column): string
{
throw NotSupported::new(__METHOD__);
}

/**
* {@inheritDoc}
*/
public function getGeographyFromGeoJSONSQL(string $sqlExpr): string
{
throw NotSupported::new(__METHOD__);
}

/**
* {@inheritDoc}
*/
public function getGeographyAsGeoJSONSQL(string $sqlExpr): string
{
throw NotSupported::new(__METHOD__);
}

/**
* {@inheritDoc}
*
Expand Down Expand Up @@ -760,38 +821,47 @@ public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level):
protected function initializeDoctrineTypeMappings(): void
{
$this->doctrineTypeMapping = [
'bigint' => Types::BIGINT,
'binary' => Types::BINARY,
'blob' => Types::BLOB,
'char' => Types::STRING,
'date' => Types::DATE_MUTABLE,
'datetime' => Types::DATETIME_MUTABLE,
'decimal' => Types::DECIMAL,
'double' => Types::FLOAT,
'enum' => Types::ENUM,
'float' => Types::SMALLFLOAT,
'int' => Types::INTEGER,
'integer' => Types::INTEGER,
'json' => Types::JSON,
'longblob' => Types::BLOB,
'longtext' => Types::TEXT,
'mediumblob' => Types::BLOB,
'mediumint' => Types::INTEGER,
'mediumtext' => Types::TEXT,
'numeric' => Types::DECIMAL,
'real' => Types::FLOAT,
'set' => Types::SIMPLE_ARRAY,
'smallint' => Types::SMALLINT,
'string' => Types::STRING,
'text' => Types::TEXT,
'time' => Types::TIME_MUTABLE,
'timestamp' => Types::DATETIME_MUTABLE,
'tinyblob' => Types::BLOB,
'tinyint' => Types::BOOLEAN,
'tinytext' => Types::TEXT,
'varbinary' => Types::BINARY,
'varchar' => Types::STRING,
'year' => Types::DATE_MUTABLE,
'bigint' => Types::BIGINT,
'binary' => Types::BINARY,
'blob' => Types::BLOB,
'char' => Types::STRING,
'date' => Types::DATE_MUTABLE,
'datetime' => Types::DATETIME_MUTABLE,
'decimal' => Types::DECIMAL,
'double' => Types::FLOAT,
'enum' => Types::ENUM,
'float' => Types::SMALLFLOAT,
'geomcollection' => Types::GEOMETRY,
'geometry' => Types::GEOMETRY,
'geometrycollection' => Types::GEOMETRY,
'int' => Types::INTEGER,
'integer' => Types::INTEGER,
'json' => Types::JSON,
'linestring' => Types::GEOMETRY,
'longblob' => Types::BLOB,
'longtext' => Types::TEXT,
'mediumblob' => Types::BLOB,
'mediumint' => Types::INTEGER,
'mediumtext' => Types::TEXT,
'multilinestring' => Types::GEOMETRY,
'multipoint' => Types::GEOMETRY,
'multipolygon' => Types::GEOMETRY,
'numeric' => Types::DECIMAL,
'point' => Types::GEOMETRY,
'polygon' => Types::GEOMETRY,
'real' => Types::FLOAT,
'set' => Types::SIMPLE_ARRAY,
'smallint' => Types::SMALLINT,
'string' => Types::STRING,
'text' => Types::TEXT,
'time' => Types::TIME_MUTABLE,
'timestamp' => Types::DATETIME_MUTABLE,
'tinyblob' => Types::BLOB,
'tinyint' => Types::BOOLEAN,
'tinytext' => Types::TEXT,
'varbinary' => Types::BINARY,
'varchar' => Types::STRING,
'year' => Types::DATE_MUTABLE,
];
}

Expand Down
Loading
Loading