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 @@ -133,6 +133,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"
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 @@ -290,6 +315,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

- 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 @@ public function update(string $table, array $data, array $criteria = [], array $
{
$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 @@ public function insert(string $table, array $data, array $types = []): int|strin
$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 @@ private function extractTypeValues(array $columns, array $types): array
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
{
if (count($types) === 0) {
return [];
}

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

// 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 @@ -33,6 +34,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 @@ -218,6 +220,65 @@ public function getBooleanTypeDeclarationSQL(array $column): string
return 'TINYINT(1)';
}

/**
* {@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);
}

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 @@ -759,38 +820,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