Skip to content

Commit ce08488

Browse files
committed
Add functional test cases for spatial type support in PostgreSQL
This commit adds functional testing for PostGIS spatial types (GEOMETRY and GEOGRAPHY) with schema introspection and CI integration. The implementation leverages PostgreSQL's native type system for introspection, making it compatible with any PostgreSQL instance without requiring PostGIS system tables to be accessible during schema operations.
1 parent 307483e commit ce08488

File tree

11 files changed

+1051
-8
lines changed

11 files changed

+1051
-8
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,31 @@ jobs:
133133
extension: "pdo_pgsql"
134134
config-file-suffix: "-stringify_fetches"
135135

136+
phpunit-postgis:
137+
name: "PHPUnit with PostGIS"
138+
needs: "phpunit-smoke-check"
139+
uses: ./.github/workflows/phpunit-postgis.yml
140+
with:
141+
php-version: ${{ matrix.php-version }}
142+
postgis-version: ${{ matrix.postgis-version }}
143+
extension: ${{ matrix.extension }}
144+
postgres-locale-provider: ${{ matrix.postgres-locale-provider }}
145+
146+
strategy:
147+
matrix:
148+
php-version:
149+
- "8.3"
150+
- "8.4"
151+
postgis-version:
152+
- "17-3.5"
153+
extension:
154+
- "pgsql"
155+
- "pdo_pgsql"
156+
include:
157+
- php-version: "8.2"
158+
postgis-version: "17-3.5"
159+
extension: "pgsql"
160+
136161
phpunit-mariadb:
137162
name: "PHPUnit with MariaDB"
138163
needs: "phpunit-smoke-check"
@@ -290,6 +315,7 @@ jobs:
290315
- "phpunit-smoke-check"
291316
- "phpunit-oracle"
292317
- "phpunit-postgres"
318+
- "phpunit-postgis"
293319
- "phpunit-mariadb"
294320
- "phpunit-mysql"
295321
- "phpunit-mssql"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: PHPUnit with PostGIS
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
php-version:
7+
required: true
8+
type: string
9+
postgis-version:
10+
required: true
11+
type: string
12+
extension:
13+
required: true
14+
type: string
15+
postgres-locale-provider:
16+
required: true
17+
type: string
18+
config-file-suffix:
19+
required: false
20+
type: string
21+
default: ''
22+
23+
jobs:
24+
phpunit-postgis:
25+
runs-on: ubuntu-24.04
26+
27+
services:
28+
postgres:
29+
image: postgis/postgis:${{ inputs.postgis-version }}
30+
ports:
31+
- '5432:5432'
32+
env:
33+
POSTGRES_PASSWORD: postgres
34+
POSTGRES_INITDB_ARGS: ${{ inputs.postgres-locale-provider == 'icu' && '--locale-provider=icu --icu-locale=en-US' || '' }}
35+
options: >-
36+
--health-cmd pg_isready
37+
38+
steps:
39+
- name: Checkout
40+
uses: actions/checkout@v4
41+
42+
- name: Install PHP
43+
uses: shivammathur/setup-php@v2
44+
with:
45+
php-version: ${{ inputs.php-version }}
46+
extensions: ${{ inputs.extension }}
47+
coverage: pcov
48+
ini-values: zend.assertions=1
49+
env:
50+
fail-fast: true
51+
52+
- name: Install dependencies with Composer
53+
uses: ramsey/composer-install@v3
54+
with:
55+
composer-options: '--ignore-platform-req=php+'
56+
57+
- name: Run PHPUnit
58+
run: vendor/bin/phpunit -c ci/github/phpunit/${{ inputs.extension }}${{ inputs.config-file-suffix }}.xml --coverage-clover=coverage.xml
59+
60+
- name: Upload coverage file
61+
uses: actions/upload-artifact@v4
62+
with:
63+
name: ${{ github.job }}-${{ inputs.postgis-version }}-php-${{ inputs.php-version }}-${{ inputs.extension }}${{ inputs.config-file-suffix }}.coverage
64+
path: coverage.xml

src/Connection.php

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,10 +458,13 @@ public function update(string $table, array $data, array $criteria = [], array $
458458
{
459459
$columns = $values = $conditions = $set = [];
460460

461+
$platform = $this->getDatabasePlatform();
462+
$typesMap = $this->normalizeTypes($types, array_keys($data));
463+
461464
foreach ($data as $columnName => $value) {
462465
$columns[] = $columnName;
463466
$values[] = $value;
464-
$set[] = $columnName . ' = ?';
467+
$set[] = $columnName . ' = ' . $this->getPlaceholderForColumn($columnName, $typesMap, $platform);
465468
}
466469

467470
[$criteriaColumns, $criteriaValues, $criteriaConditions] = $this->getCriteriaCondition($criteria);
@@ -505,10 +508,13 @@ public function insert(string $table, array $data, array $types = []): int|strin
505508
$values = [];
506509
$set = [];
507510

511+
$platform = $this->getDatabasePlatform();
512+
$typesMap = $this->normalizeTypes($types, array_keys($data));
513+
508514
foreach ($data as $columnName => $value) {
509515
$columns[] = $columnName;
510516
$values[] = $value;
511-
$set[] = '?';
517+
$set[] = $this->getPlaceholderForColumn($columnName, $typesMap, $platform);
512518
}
513519

514520
return $this->executeStatement(
@@ -538,6 +544,67 @@ private function extractTypeValues(array $columns, array $types): array
538544
return $typeValues;
539545
}
540546

547+
/**
548+
* Normalizes types array from positional or associative to associative format.
549+
*
550+
* @param array<int<0,max>, string|ParameterType|Type>|array<string, string|ParameterType|Type> $types
551+
* @param list<string> $columnNames
552+
*
553+
* @return array<string, string|ParameterType|Type>
554+
*/
555+
private function normalizeTypes(array $types, array $columnNames): array
556+
{
557+
if (count($types) === 0) {
558+
return [];
559+
}
560+
561+
// Already associative
562+
if (is_string(key($types))) {
563+
return $types;
564+
}
565+
566+
// Convert positional to associative
567+
$normalizedTypes = [];
568+
foreach ($columnNames as $i => $columnName) {
569+
if (isset($types[$i])) {
570+
$normalizedTypes[$columnName] = $types[$i];
571+
}
572+
}
573+
574+
return $normalizedTypes;
575+
}
576+
577+
/**
578+
* Gets the SQL placeholder for a column based on its type.
579+
*
580+
* @param array<string, string|ParameterType|Type> $typesMap
581+
*
582+
* @throws Exception
583+
*/
584+
private function getPlaceholderForColumn(
585+
string $columnName,
586+
array $typesMap,
587+
AbstractPlatform $platform,
588+
): string {
589+
if (! isset($typesMap[$columnName])) {
590+
return '?';
591+
}
592+
593+
$type = $typesMap[$columnName];
594+
595+
// Convert string type name to Type instance
596+
if (is_string($type)) {
597+
$type = Type::getType($type);
598+
}
599+
600+
// Use Type's SQL conversion if it's a Type instance
601+
if ($type instanceof Type) {
602+
return $type->convertToDatabaseValueSQL('?', $platform);
603+
}
604+
605+
return '?';
606+
}
607+
541608
/**
542609
* Quotes a string so it can be safely used as a table or column name, even if
543610
* it is a reserved name.

src/Platforms/PostgreSQL/PostgreSQLMetadataProvider.php

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,17 @@ private function createTableColumn(array $row): TableColumnMetadataRow
270270
$editor->setScale($parameters[1]);
271271
}
272272

273+
break;
274+
case 'geometry':
275+
case 'geography':
276+
$parameters = $this->parseColumnTypeParameters($completeType);
277+
if (count($parameters) > 0) {
278+
$editor->setGeometryType($parameters[0]);
279+
}
280+
281+
if (count($parameters) > 1) {
282+
$editor->setSrid($parameters[1]);
283+
}
273284
break;
274285
}
275286

@@ -294,20 +305,25 @@ private function createTableColumn(array $row): TableColumnMetadataRow
294305
/**
295306
* Parses the parameters between parenthesis in the data type.
296307
*
297-
* @return list<int>
308+
* @return list<int|string>
298309
*/
299310
private function parseColumnTypeParameters(string $type): array
300311
{
301-
if (preg_match('/\((\d+)(?:,(\d+))?\)/', $type, $matches) !== 1) {
312+
if (preg_match('/\(([\w]+)(?:,([\w]+))?\)/', $type, $matches) !== 1) {
302313
return [];
303314
}
304315

305-
$parameters = [(int) $matches[1]];
316+
$parameters = [$matches[1]];
306317

307318
if (isset($matches[2])) {
308-
$parameters[] = (int) $matches[2];
319+
$parameters[] = $matches[2];
309320
}
310321

322+
// Cast numeric strings to int automatically
323+
$parameters = array_map(static function ($param) {
324+
return ctype_digit($param) ? (int) $param : $param;
325+
}, $parameters);
326+
311327
return $parameters;
312328
}
313329

src/Schema/PostgreSQLSchemaManager.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
313313
/**
314314
* Parses the parameters between parenthesis in the data type.
315315
*
316-
* @return list<int>
316+
* @return list<int|string>
317317
*/
318318
private function parseColumnTypeParameters(string $type): array
319319
{
@@ -328,7 +328,7 @@ private function parseColumnTypeParameters(string $type): array
328328
}
329329

330330
// Cast numeric strings to int automatically
331-
$parameters = array_map(function ($param) {
331+
$parameters = array_map(static function ($param) {
332332
return ctype_digit($param) ? (int) $param : $param;
333333
}, $parameters);
334334

tests/Functional/Schema/MySQLSchemaManagerTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,11 @@ public function testColumnIntrospection(): void
798798
$doctrineTypes = array_keys(Type::getTypesMap());
799799

800800
foreach ($doctrineTypes as $type) {
801+
// Skip GEOGRAPHY type - MySQL doesn't support it (PostgreSQL/PostGIS only)
802+
if ($type === Types::GEOGRAPHY) {
803+
continue;
804+
}
805+
801806
$columnEditor = Column::editor()
802807
->setUnquotedName('col_' . $type)
803808
->setTypeName($type);

0 commit comments

Comments
 (0)