Skip to content

Commit a7cddba

Browse files
committed
Add spatial index support for PostgreSQL/PostGIS
Implement spatial index creation and introspection for PostgreSQL using the GIST (Generalized Search Tree) index method — the standard access method for spatial data in PostGIS. This commit introduces a new getIndexMethodSQL() hook in AbstractPlatform for platform-specific index clauses, and overrides it in PostgreSQLPlatform to emit "USING GIST" for spatial indexes. SQL generation examples: MySQL → CREATE SPATIAL INDEX idx ON table (col) PostgreSQL → CREATE INDEX idx ON table USING GIST (col)
1 parent 70f21c9 commit a7cddba

File tree

6 files changed

+118
-3
lines changed

6 files changed

+118
-3
lines changed

src/Platforms/AbstractPlatform.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,11 +1255,22 @@ public function getCreateIndexSQL(Index $index, string $table): string
12551255
}
12561256

12571257
$query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table;
1258+
$query .= $this->getIndexMethodSQL($index);
12581259
$query .= ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index);
12591260

12601261
return $query;
12611262
}
12621263

1264+
/**
1265+
* Returns the index method clause for platforms that support it.
1266+
*
1267+
* @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1268+
*/
1269+
protected function getIndexMethodSQL(Index $index): string
1270+
{
1271+
return '';
1272+
}
1273+
12631274
/**
12641275
* Adds condition for partial index.
12651276
*/

src/Platforms/PostgreSQL/PostgreSQLMetadataProvider.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,11 +391,13 @@ private function getIndexColumns(?string $schemaName, ?string $tableName): itera
391391
ic.relname,
392392
i.indisunique,
393393
pg_get_expr(indpred, indrelid),
394-
attname
394+
attname,
395+
am.amname
395396
FROM pg_index i
396397
JOIN pg_class AS c ON c.oid = i.indrelid
397398
JOIN pg_namespace n ON n.oid = c.relnamespace
398399
JOIN pg_class AS ic ON ic.oid = i.indexrelid
400+
JOIN pg_am am ON am.oid = ic.relam
399401
JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS keys(attnum, ord)
400402
ON TRUE
401403
JOIN pg_attribute a
@@ -412,11 +414,21 @@ private function getIndexColumns(?string $schemaName, ?string $tableName): itera
412414
);
413415

414416
foreach ($this->connection->iterateNumeric($sql, $params) as $row) {
417+
// Determine index type based on access method and uniqueness
418+
// GIST indexes are used for spatial data in PostGIS
419+
if ($row[6] === 'gist') {
420+
$type = IndexType::SPATIAL;
421+
} elseif ($row[3]) {
422+
$type = IndexType::UNIQUE;
423+
} else {
424+
$type = IndexType::REGULAR;
425+
}
426+
415427
yield new IndexColumnMetadataRow(
416428
schemaName: $row[0],
417429
tableName: $row[1],
418430
indexName: $row[2],
419-
type: $row[3] ? IndexType::UNIQUE : IndexType::REGULAR,
431+
type: $type,
420432
isClustered: false,
421433
predicate: $row[4],
422434
columnName: $row[5],

src/Platforms/PostgreSQLPlatform.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,20 @@ public function createMetadataProvider(Connection $connection): PostgreSQLMetada
877877
return new PostgreSQLMetadataProvider($connection, $this);
878878
}
879879

880+
/**
881+
* {@inheritDoc}
882+
*
883+
* For spatial indexes on PostgreSQL/PostGIS, we need to use GIST index method.
884+
*/
885+
protected function getIndexMethodSQL(Index $index): string
886+
{
887+
if ($index->getType() === Index\IndexType::SPATIAL) {
888+
return ' USING GIST';
889+
}
890+
891+
return '';
892+
}
893+
880894
public function createSchemaManager(Connection $connection): PostgreSQLSchemaManager
881895
{
882896
return new PostgreSQLSchemaManager($connection, $this);

src/Schema/PostgreSQLSchemaManager.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,20 @@ protected function _getPortableTableIndexesList(array $rows, string $tableName):
168168
return parent::_getPortableTableIndexesList(array_map(
169169
/** @param array<string, mixed> $row */
170170
static function (array $row): array {
171+
$flags = [];
172+
173+
// GIST indexes are used for spatial data in PostGIS
174+
if (isset($row['index_method']) && $row['index_method'] === 'gist') {
175+
$flags = ['SPATIAL'];
176+
}
177+
171178
return [
172179
'key_name' => $row['relname'],
173180
'non_unique' => ! $row['indisunique'],
174181
'primary' => (bool) $row['indisprimary'],
175182
'where' => $row['where'],
176183
'column_name' => $row['attname'],
184+
'flags' => $flags,
177185
];
178186
},
179187
$rows,
@@ -458,11 +466,13 @@ protected function selectIndexColumns(string $databaseName, ?string $tableName =
458466
i.indkey,
459467
i.indrelid,
460468
pg_get_expr(indpred, indrelid) AS "where",
461-
quote_ident(attname) AS attname
469+
quote_ident(attname) AS attname,
470+
am.amname AS index_method
462471
FROM pg_index i
463472
JOIN pg_class AS c ON c.oid = i.indrelid
464473
JOIN pg_namespace n ON n.oid = c.relnamespace
465474
JOIN pg_class AS ic ON ic.oid = i.indexrelid
475+
JOIN pg_am am ON am.oid = ic.relam
466476
JOIN LATERAL UNNEST(i.indkey) WITH ORDINALITY AS keys(attnum, ord)
467477
ON TRUE
468478
JOIN pg_attribute a

tests/Functional/Schema/PostgreSQL/PostGISTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
88
use Doctrine\DBAL\Schema\Column;
9+
use Doctrine\DBAL\Schema\Index;
10+
use Doctrine\DBAL\Schema\Index\IndexType;
911
use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
1012
use Doctrine\DBAL\Schema\Table;
1113
use Doctrine\DBAL\Tests\Functional\SpatialTestCase;
@@ -339,4 +341,55 @@ public function testAlterTableAlterSpatialColumn(): void
339341
self::assertSame('Polygon', $geog->getGeometryType());
340342
self::assertSame(SpatialReferenceSystems::SRID_NAD27, $geog->getSrid());
341343
}
344+
345+
public function testSpatialIndex(): void
346+
{
347+
$index = Index::editor()
348+
->setUnquotedName('spatial_idx')
349+
->setType(IndexType::SPATIAL)
350+
->setUnquotedColumnNames('location')
351+
->create();
352+
353+
$table = Table::editor()
354+
->setUnquotedName(self::TABLE_NAME)
355+
->setColumns(
356+
Column::editor()
357+
->setUnquotedName('id')
358+
->setTypeName(Types::INTEGER)
359+
->setAutoincrement(true)
360+
->create(),
361+
Column::editor()
362+
->setUnquotedName('location')
363+
->setTypeName(Types::GEOMETRY)
364+
->setGeometryType('POINT')
365+
->setSrid(4326)
366+
->create(),
367+
)
368+
->setPrimaryKeyConstraint(
369+
PrimaryKeyConstraint::editor()
370+
->setUnquotedColumnNames('id')
371+
->create(),
372+
)
373+
->setIndexes($index)
374+
->create();
375+
376+
$schemaManager = $this->connection->createSchemaManager();
377+
$schemaManager->createTable($table);
378+
379+
$onlineTable = $schemaManager->introspectTableByUnquotedName(self::TABLE_NAME);
380+
381+
// Verify the table structure is maintained
382+
self::assertTrue(
383+
$schemaManager->createComparator()
384+
->compareTables($table, $onlineTable)
385+
->isEmpty(),
386+
);
387+
388+
// Verify the spatial index exists
389+
self::assertTrue($onlineTable->hasIndex('spatial_idx'));
390+
391+
// Verify the index type is SPATIAL
392+
$spatialIndex = $onlineTable->getIndex('spatial_idx');
393+
self::assertSame(IndexType::SPATIAL, $spatialIndex->getType());
394+
}
342395
}

tests/Platforms/PostgreSQLPlatformTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Doctrine\DBAL\Schema\Column;
1010
use Doctrine\DBAL\Schema\ColumnDiff;
1111
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
12+
use Doctrine\DBAL\Schema\Index;
1213
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
1314
use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
1415
use Doctrine\DBAL\Schema\Sequence;
@@ -1280,4 +1281,18 @@ public function testReturnsGeographyAsGeoJSONSQL(): void
12801281
$this->platform->getGeographyAsGeoJSONSQL('geog_col'),
12811282
);
12821283
}
1284+
1285+
public function testCreateSpatialIndexSQL(): void
1286+
{
1287+
$index = Index::editor()
1288+
->setUnquotedName('spatial_idx')
1289+
->setType(Index\IndexType::SPATIAL)
1290+
->setUnquotedColumnNames('location')
1291+
->create();
1292+
1293+
self::assertSame(
1294+
'CREATE INDEX spatial_idx ON spatial_table USING GIST (location)',
1295+
$this->platform->getCreateIndexSQL($index, 'spatial_table'),
1296+
);
1297+
}
12831298
}

0 commit comments

Comments
 (0)