Skip to content

Commit 0b81371

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 9916c25 commit 0b81371

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
@@ -1251,11 +1251,22 @@ public function getCreateIndexSQL(Index $index, string $table): string
12511251
}
12521252

12531253
$query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table;
1254+
$query .= $this->getIndexMethodSQL($index);
12541255
$query .= ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index);
12551256

12561257
return $query;
12571258
}
12581259

1260+
/**
1261+
* Returns the index method clause for platforms that support it.
1262+
*
1263+
* @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1264+
*/
1265+
protected function getIndexMethodSQL(Index $index): string
1266+
{
1267+
return '';
1268+
}
1269+
12591270
/**
12601271
* Adds condition for partial index.
12611272
*/

src/Platforms/PostgreSQL/PostgreSQLMetadataProvider.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,11 +388,13 @@ private function getIndexColumns(?string $schemaName, ?string $tableName): itera
388388
ic.relname,
389389
i.indisunique,
390390
pg_get_expr(indpred, indrelid),
391-
attname
391+
attname,
392+
am.amname
392393
FROM pg_index i
393394
JOIN pg_class AS c ON c.oid = i.indrelid
394395
JOIN pg_namespace n ON n.oid = c.relnamespace
395396
JOIN pg_class AS ic ON ic.oid = i.indexrelid
397+
JOIN pg_am am ON am.oid = ic.relam
396398
JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS keys(attnum, ord)
397399
ON TRUE
398400
JOIN pg_attribute a
@@ -409,11 +411,21 @@ private function getIndexColumns(?string $schemaName, ?string $tableName): itera
409411
);
410412

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

src/Platforms/PostgreSQLPlatform.php

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

892+
/**
893+
* {@inheritDoc}
894+
*
895+
* For spatial indexes on PostgreSQL/PostGIS, we need to use GIST index method.
896+
*/
897+
protected function getIndexMethodSQL(Index $index): string
898+
{
899+
if ($index->getType() === Index\IndexType::SPATIAL) {
900+
return ' USING GIST';
901+
}
902+
903+
return '';
904+
}
905+
892906
public function createSchemaManager(Connection $connection): PostgreSQLSchemaManager
893907
{
894908
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;
@@ -344,4 +346,55 @@ public function testAlterTableAlterSpatialColumn(): void
344346
self::assertSame('Polygon', $geog->getGeometryType());
345347
self::assertSame(4267, $geog->getSrid());
346348
}
349+
350+
public function testSpatialIndex(): void
351+
{
352+
$index = Index::editor()
353+
->setUnquotedName('spatial_idx')
354+
->setType(IndexType::SPATIAL)
355+
->setUnquotedColumnNames('location')
356+
->create();
357+
358+
$table = Table::editor()
359+
->setUnquotedName(self::TABLE_NAME)
360+
->setColumns(
361+
Column::editor()
362+
->setUnquotedName('id')
363+
->setTypeName(Types::INTEGER)
364+
->setAutoincrement(true)
365+
->create(),
366+
Column::editor()
367+
->setUnquotedName('location')
368+
->setTypeName(Types::GEOMETRY)
369+
->setGeometryType('POINT')
370+
->setSrid(4326)
371+
->create(),
372+
)
373+
->setPrimaryKeyConstraint(
374+
PrimaryKeyConstraint::editor()
375+
->setUnquotedColumnNames('id')
376+
->create(),
377+
)
378+
->setIndexes($index)
379+
->create();
380+
381+
$schemaManager = $this->connection->createSchemaManager();
382+
$schemaManager->createTable($table);
383+
384+
$onlineTable = $schemaManager->introspectTableByUnquotedName(self::TABLE_NAME);
385+
386+
// Verify the table structure is maintained
387+
self::assertTrue(
388+
$schemaManager->createComparator()
389+
->compareTables($table, $onlineTable)
390+
->isEmpty(),
391+
);
392+
393+
// Verify the spatial index exists
394+
self::assertTrue($onlineTable->hasIndex('spatial_idx'));
395+
396+
// Verify the index type is SPATIAL
397+
$spatialIndex = $onlineTable->getIndex('spatial_idx');
398+
self::assertSame(IndexType::SPATIAL, $spatialIndex->getType());
399+
}
347400
}

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;
@@ -1273,4 +1274,18 @@ public function testReturnsGeographyAsGeoJSONSQL(): void
12731274
$this->platform->getGeographyAsGeoJSONSQL('geog_col'),
12741275
);
12751276
}
1277+
1278+
public function testCreateSpatialIndexSQL(): void
1279+
{
1280+
$index = Index::editor()
1281+
->setUnquotedName('spatial_idx')
1282+
->setType(Index\IndexType::SPATIAL)
1283+
->setUnquotedColumnNames('location')
1284+
->create();
1285+
1286+
self::assertSame(
1287+
'CREATE INDEX spatial_idx ON spatial_table USING GIST (location)',
1288+
$this->platform->getCreateIndexSQL($index, 'spatial_table'),
1289+
);
1290+
}
12761291
}

0 commit comments

Comments
 (0)