Skip to content
Draft
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
102 changes: 98 additions & 4 deletions docs/en/writing-migrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1284,7 +1284,15 @@ during index creation::
}
}

PostgreSQL adapters also supports Generalized Inverted Index ``gin`` indexes::
PostgreSQL Index Access Methods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

PostgreSQL supports several index access methods beyond the default B-tree.
Use the ``type`` option to specify the access method.

**GIN (Generalized Inverted Index)**

GIN indexes are useful for full-text search, arrays, and JSONB columns::

<?php

Expand All @@ -1294,9 +1302,95 @@ PostgreSQL adapters also supports Generalized Inverted Index ``gin`` indexes::
{
public function change(): void
{
$table = $this->table('users');
$table->addColumn('address', 'string')
->addIndex('address', ['type' => 'gin'])
$table = $this->table('articles');
$table->addColumn('tags', 'jsonb')
->addIndex('tags', ['type' => 'gin'])
->create();
}
}

**GiST (Generalized Search Tree)**

GiST indexes support geometric data, range types, and full-text search.
For trigram similarity searches (requires the ``pg_trgm`` extension), use
the ``opclass`` option::

<?php

use Migrations\BaseMigration;

class MyNewMigration extends BaseMigration
{
public function change(): void
{
$table = $this->table('products');
$table->addColumn('name', 'string')
->addIndex('name', [
'type' => 'gist',
'opclass' => ['name' => 'gist_trgm_ops'],
])
->create();
}
}

**BRIN (Block Range Index)**

BRIN indexes are highly efficient for large, naturally-ordered tables like
time-series data. They are much smaller than B-tree indexes but only work well
when data is physically ordered by the indexed column::

<?php

use Migrations\BaseMigration;

class MyNewMigration extends BaseMigration
{
public function change(): void
{
$table = $this->table('sensor_readings');
$table->addColumn('recorded_at', 'timestamp')
->addColumn('value', 'decimal')
->addIndex('recorded_at', ['type' => 'brin'])
->create();
}
}

**SP-GiST (Space-Partitioned GiST)**

SP-GiST indexes work well for data with natural clustering, like IP addresses
or phone numbers::

<?php

use Migrations\BaseMigration;

class MyNewMigration extends BaseMigration
{
public function change(): void
{
$table = $this->table('access_logs');
$table->addColumn('client_ip', 'inet')
->addIndex('client_ip', ['type' => 'spgist'])
->create();
}
}

**Hash**

Hash indexes handle simple equality comparisons. They are rarely needed since
B-tree handles equality efficiently too::

<?php

use Migrations\BaseMigration;

class MyNewMigration extends BaseMigration
{
public function change(): void
{
$table = $this->table('sessions');
$table->addColumn('session_id', 'string', ['limit' => 64])
->addIndex('session_id', ['type' => 'hash'])
->create();
}
}
Expand Down
38 changes: 29 additions & 9 deletions src/Db/Adapter/PostgresAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,18 @@ class PostgresAdapter extends AbstractAdapter
self::TYPE_NATIVE_UUID,
];

private const GIN_INDEX_TYPE = 'gin';
/**
* PostgreSQL index access methods that require USING clause.
*
* @var array<string>
*/
private const ACCESS_METHOD_TYPES = [
Index::GIN,
Index::GIST,
Index::SPGIST,
Index::BRIN,
Index::HASH,
];

/**
* Columns with comments
Expand Down Expand Up @@ -915,8 +926,16 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
}

$order = $index->getOrder() ?? [];
$columnNames = array_map(function ($columnName) use ($order) {
$opclass = $index->getOpclass() ?? [];
$columnNames = array_map(function ($columnName) use ($order, $opclass) {
$ret = '"' . $columnName . '"';

// Add operator class if specified (e.g., gist_trgm_ops)
if (isset($opclass[$columnName])) {
$ret .= ' ' . $opclass[$columnName];
}

// Add ordering if specified (e.g., ASC NULLS FIRST)
if (isset($order[$columnName])) {
$ret .= ' ' . $order[$columnName];
}
Expand All @@ -927,11 +946,11 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
$include = $index->getInclude();
$includedColumns = $include ? sprintf(' INCLUDE ("%s")', implode('","', $include)) : '';

$createIndexSentence = 'CREATE %sINDEX%s %s ON %s ';
if ($index->getType() === self::GIN_INDEX_TYPE) {
$createIndexSentence .= ' USING ' . $index->getType() . '(%s) %s;';
} else {
$createIndexSentence .= '(%s)%s%s;';
// Build USING clause for access method types (gin, gist, spgist, brin, hash)
$indexType = $index->getType();
$usingClause = '';
if (in_array($indexType, self::ACCESS_METHOD_TYPES, true)) {
$usingClause = ' USING ' . $indexType;
}
Comment on lines +949 to 954
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Being able to create these index types is a nice improvement. Should the creation of these index types be done in cakephp/database though so that we can also reflect these index types? Without the reflection aspect snapshot generation will be lossy.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

With cakephp/cakephp#19369 going into 5.4, do we still already merge this for support now? Until full loss-less support will come in 5.4?

$where = '';
$whereClause = $index->getWhere();
Expand All @@ -940,11 +959,12 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
}

return sprintf(
$createIndexSentence,
$index->getType() === Index::UNIQUE ? 'UNIQUE ' : '',
'CREATE %sINDEX%s %s ON %s%s (%s)%s%s;',
$indexType === Index::UNIQUE ? 'UNIQUE ' : '',
$index->getConcurrently() ? ' CONCURRENTLY' : '',
$this->quoteColumnName((string)$indexName),
$this->quoteTableName($tableName),
$usingClause,
implode(',', $columnNames),
$includedColumns,
$where,
Expand Down
72 changes: 71 additions & 1 deletion src/Db/Table/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,46 @@ class Index extends DatabaseIndex
*/
public const FULLTEXT = 'fulltext';

/**
* PostgreSQL index access method: Generalized Inverted Index.
* Useful for full-text search, arrays, and JSONB columns.
*
* @var string
*/
public const GIN = 'gin';

/**
* PostgreSQL index access method: Generalized Search Tree.
* Useful for geometric data, range types, and full-text search.
*
* @var string
*/
public const GIST = 'gist';

/**
* PostgreSQL index access method: Space-Partitioned GiST.
* Useful for data with natural clustering like IP addresses or phone numbers.
*
* @var string
*/
public const SPGIST = 'spgist';

/**
* PostgreSQL index access method: Block Range Index.
* Highly efficient for large, naturally-ordered tables like time-series data.
*
* @var string
*/
public const BRIN = 'brin';

/**
* PostgreSQL index access method: Hash index.
* Handles simple equality comparisons. Rarely needed since B-tree handles equality efficiently.
*
* @var string
*/
public const HASH = 'hash';

/**
* Constructor
*
Expand All @@ -47,6 +87,7 @@ class Index extends DatabaseIndex
* @param array<string>|null $include The included columns for covering indexes.
* @param ?string $where The where clause for partial indexes.
* @param bool $concurrent Whether to create the index concurrently.
* @param array<string, string>|null $opclass The operator class for each column (PostgreSQL).
*/
public function __construct(
protected string $name = '',
Expand All @@ -57,6 +98,7 @@ public function __construct(
protected ?array $include = null,
protected ?string $where = null,
protected bool $concurrent = false,
protected ?array $opclass = null,
) {
}

Expand Down Expand Up @@ -149,6 +191,34 @@ public function getConcurrently(): bool
return $this->concurrent;
}

/**
* Set the operator class for index columns.
*
* Operator classes specify which operators the index can use. This is primarily
* useful in PostgreSQL for specialized index types like GiST with trigram support.
*
* Example: ['column_name' => 'gist_trgm_ops']
*
* @param array<string, string> $opclass Map of column names to operator classes.
* @return $this
*/
public function setOpclass(array $opclass)
{
$this->opclass = $opclass;

return $this;
}

/**
* Get the operator class configuration for index columns.
*
* @return array<string, string>|null
*/
public function getOpclass(): ?array
{
return $this->opclass;
}

/**
* Utility method that maps an array of index options to this object's methods.
*
Expand All @@ -159,7 +229,7 @@ public function getConcurrently(): bool
public function setOptions(array $options)
{
// Valid Options
$validOptions = ['concurrently', 'type', 'unique', 'name', 'limit', 'order', 'include', 'where'];
$validOptions = ['concurrently', 'type', 'unique', 'name', 'limit', 'order', 'include', 'where', 'opclass'];
foreach ($options as $option => $value) {
if (!in_array($option, $validOptions, true)) {
throw new RuntimeException(sprintf('"%s" is not a valid index option.', $option));
Expand Down
Loading
Loading