Skip to content

Commit 10cf2bd

Browse files
* added gin index
* updated not operator
1 parent 4483496 commit 10cf2bd

File tree

8 files changed

+1125
-711
lines changed

8 files changed

+1125
-711
lines changed

src/Database/Adapter/Pool.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,4 +568,9 @@ public function decodePolygon(string $wkb): array
568568
{
569569
return $this->delegate(__FUNCTION__, \func_get_args());
570570
}
571+
572+
public function getSupportForObject(): bool
573+
{
574+
return $this->delegate(__FUNCTION__, \func_get_args());
575+
}
571576
}

src/Database/Adapter/Postgres.php

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -860,14 +860,15 @@ public function createIndex(string $collection, string $id, string $type, array
860860
Database::INDEX_FULLTEXT => 'INDEX',
861861
Database::INDEX_UNIQUE => 'UNIQUE INDEX',
862862
Database::INDEX_SPATIAL => 'INDEX',
863-
default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT . ', ' . Database::INDEX_SPATIAL),
863+
Database::INDEX_GIN => 'INDEX',
864+
default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT . ', ' . Database::INDEX_SPATIAL . ', ' . Database::INDEX_GIN),
864865
};
865866

866867
$key = "\"{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}\"";
867868
$attributes = \implode(', ', $attributes);
868869

869-
// Spatial indexes can't include _tenant because GIST indexes require all columns to have compatible operator classes
870-
if ($this->sharedTables && $type !== Database::INDEX_FULLTEXT && $type !== Database::INDEX_SPATIAL) {
870+
// Spatial and GIN indexes can't include _tenant because GIST/GIN indexes require all columns to have compatible operator classes
871+
if ($this->sharedTables && $type !== Database::INDEX_FULLTEXT && $type !== Database::INDEX_SPATIAL && $type !== Database::INDEX_GIN) {
871872
// Add tenant as first index column for best performance
872873
$attributes = "_tenant, {$attributes}";
873874
}
@@ -879,6 +880,11 @@ public function createIndex(string $collection, string $id, string $type, array
879880
$sql .= " USING GIST";
880881
}
881882

883+
// Add USING GIN for JSONB indexes
884+
if ($type === Database::INDEX_GIN) {
885+
$sql .= " USING GIN";
886+
}
887+
882888
$sql .= " ({$attributes});";
883889

884890
$sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql);
@@ -1576,44 +1582,41 @@ protected function handleObjectQueries(Query $query, array &$binds, string $attr
15761582
{
15771583
switch ($query->getMethod()) {
15781584
case Query::TYPE_EQUAL:
1585+
case Query::TYPE_NOT_EQUAL: {
1586+
$isNot = $query->getMethod() === Query::TYPE_NOT_EQUAL;
15791587
$conditions = [];
15801588
foreach ($query->getValues() as $key => $value) {
1589+
$binds[":{$placeholder}_{$key}"] = json_encode($value);
15811590
if (is_array($value)) {
1582-
// JSONB containment operator @>
1583-
$binds[":{$placeholder}_{$key}"] = json_encode($value);
1584-
$conditions[] = "{$alias}.{$attribute} @> :{$placeholder}_{$key}::jsonb";
1591+
$fragment = "{$alias}.{$attribute} @> :{$placeholder}_{$key}::jsonb";
1592+
$conditions[] = $isNot ? "NOT (" . $fragment . ")" : $fragment;
15851593
} else {
1586-
// Direct equality
1587-
$binds[":{$placeholder}_{$key}"] = json_encode($value);
1588-
$conditions[] = "{$alias}.{$attribute} = :{$placeholder}_{$key}::jsonb";
1594+
$fragment = "{$alias}.{$attribute} = :{$placeholder}_{$key}::jsonb";
1595+
$conditions[] = $isNot ? "{$alias}.{$attribute} <> :{$placeholder}_{$key}::jsonb" : $fragment;
15891596
}
15901597
}
1591-
return empty($conditions) ? '' : '(' . implode(' OR ', $conditions) . ')';
1598+
$separator = $isNot ? ' AND ' : ' OR ';
1599+
return empty($conditions) ? '' : '(' . implode($separator, $conditions) . ')';
1600+
}
15921601

15931602
case Query::TYPE_CONTAINS:
1603+
case Query::TYPE_NOT_CONTAINS: {
1604+
$isNot = $query->getMethod() === Query::TYPE_NOT_CONTAINS;
15941605
$conditions = [];
15951606
foreach ($query->getValues() as $key => $value) {
1596-
if (is_array($value)) {
1597-
// For JSONB contains, we need to check if an array contains a specific element
1598-
// The JSONB containment operator @> checks if left contains right
1599-
// For array element containment: {"array": ["element"]} means array contains "element"
1600-
// For nested array containment: {"matrix": [[4,5,6]]} means matrix contains [4,5,6]
1601-
1602-
if (count($value) === 1) {
1603-
$jsonKey = array_key_first($value);
1604-
$jsonValue = $value[$jsonKey];
1605-
1606-
// Always wrap the value in an array to represent "array contains this element"
1607-
// - For scalar: 'react' becomes ["react"]
1608-
// - For array: [4,5,6] becomes [[4,5,6]]
1609-
$value[$jsonKey] = [$jsonValue];
1610-
}
1611-
1612-
$binds[":{$placeholder}_{$key}"] = json_encode($value);
1613-
$conditions[] = "{$alias}.{$attribute} @> :{$placeholder}_{$key}::jsonb";
1607+
if (count($value) === 1) {
1608+
$jsonKey = array_key_first($value);
1609+
$jsonValue = $value[$jsonKey];
1610+
// wrap to represent array; eg: key -> [value]
1611+
$value[$jsonKey] = [$jsonValue];
16141612
}
1613+
$binds[":{$placeholder}_{$key}"] = json_encode($value);
1614+
$fragment = "{$alias}.{$attribute} @> :{$placeholder}_{$key}::jsonb";
1615+
$conditions[] = $isNot ? "NOT (" . $fragment . ")" : $fragment;
16151616
}
1616-
return empty($conditions) ? '' : '(' . implode(' OR ', $conditions) . ')';
1617+
$separator = $isNot ? ' AND ' : ' OR ';
1618+
return empty($conditions) ? '' : '(' . implode($separator, $conditions) . ')';
1619+
}
16171620

16181621
default:
16191622
throw new DatabaseException('Query method ' . $query->getMethod() . ' not supported for object attributes');

src/Database/Database.php

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class Database
6969
public const INDEX_FULLTEXT = 'fulltext';
7070
public const INDEX_UNIQUE = 'unique';
7171
public const INDEX_SPATIAL = 'spatial';
72+
public const INDEX_GIN = 'gin';
7273
public const ARRAY_INDEX_LENGTH = 255;
7374

7475
// Relation Types
@@ -1412,6 +1413,7 @@ public function createCollection(string $id, array $attributes = [], array $inde
14121413
$this->adapter->getSupportForSpatialAttributes(),
14131414
$this->adapter->getSupportForSpatialIndexNull(),
14141415
$this->adapter->getSupportForSpatialIndexOrder(),
1416+
$this->adapter->getSupportForObject(),
14151417
);
14161418
foreach ($indexes as $index) {
14171419
if (!$validator->isValid($index)) {
@@ -2027,7 +2029,7 @@ protected function validateDefaultTypes(string $type, mixed $default): void
20272029

20282030
if ($defaultType === 'array') {
20292031
// spatial types require the array itself
2030-
if (!in_array($type, Database::SPATIAL_TYPES)) {
2032+
if (!in_array($type, Database::SPATIAL_TYPES) && $type != Database::TYPE_OBJECT) {
20312033
foreach ($default as $value) {
20322034
$this->validateDefaultTypes($type, $value);
20332035
}
@@ -2051,18 +2053,19 @@ protected function validateDefaultTypes(string $type, mixed $default): void
20512053
break;
20522054
case self::TYPE_OBJECT:
20532055
// Object types expect arrays as default values
2056+
var_dump($defaultType);
20542057
if ($defaultType !== 'array') {
20552058
throw new DatabaseException('Default value for object type must be an array');
20562059
}
2057-
break;
2060+
// no break
20582061
case self::VAR_POINT:
20592062
case self::VAR_LINESTRING:
20602063
case self::VAR_POLYGON:
20612064
// Spatial types expect arrays as default values
20622065
if ($defaultType !== 'array') {
20632066
throw new DatabaseException('Default value for spatial type ' . $type . ' must be an array');
20642067
}
2065-
break;
2068+
// no break
20662069
default:
20672070
throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . self::VAR_STRING . ', ' . self::VAR_INTEGER . ', ' . self::VAR_FLOAT . ', ' . self::VAR_BOOLEAN . ', ' . self::VAR_DATETIME . ', ' . self::VAR_RELATIONSHIP . ', ' . self::TYPE_OBJECT . ', ' . self::VAR_POINT . ', ' . self::VAR_LINESTRING . ', ' . self::VAR_POLYGON);
20682071
}
@@ -3315,8 +3318,14 @@ public function createIndex(string $collection, string $id, string $type, array
33153318
}
33163319
break;
33173320

3321+
case self::INDEX_GIN:
3322+
if (!$this->adapter->getSupportForObject()) {
3323+
throw new DatabaseException('GIN indexes are not supported');
3324+
}
3325+
break;
3326+
33183327
default:
3319-
throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT . ', ' . Database::INDEX_SPATIAL);
3328+
throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT . ', ' . Database::INDEX_SPATIAL . ', ' . Database::INDEX_GIN);
33203329
}
33213330

33223331
/** @var array<Document> $collectionAttributes */
@@ -3373,6 +3382,27 @@ public function createIndex(string $collection, string $id, string $type, array
33733382
}
33743383
}
33753384

3385+
if ($type === self::INDEX_GIN) {
3386+
if (count($attributes) !== 1) {
3387+
throw new IndexException('GIN index can be created on a single object attribute');
3388+
}
3389+
3390+
foreach ($attributes as $attr) {
3391+
if (!isset($indexAttributesWithTypes[$attr])) {
3392+
throw new IndexException('Attribute "' . $attr . '" not found in collection');
3393+
}
3394+
3395+
$attributeType = $indexAttributesWithTypes[$attr];
3396+
if ($attributeType !== self::TYPE_OBJECT) {
3397+
throw new IndexException('GIN index can only be created on object attributes. Attribute "' . $attr . '" is of type "' . $attributeType . '"');
3398+
}
3399+
}
3400+
3401+
if (!empty($orders)) {
3402+
throw new IndexException('GIN indexes do not support explicit orders. Remove the orders to create this index.');
3403+
}
3404+
}
3405+
33763406
$index = new Document([
33773407
'$id' => ID::custom($id),
33783408
'key' => $id,
@@ -3393,6 +3423,7 @@ public function createIndex(string $collection, string $id, string $type, array
33933423
$this->adapter->getSupportForSpatialAttributes(),
33943424
$this->adapter->getSupportForSpatialIndexNull(),
33953425
$this->adapter->getSupportForSpatialIndexOrder(),
3426+
$this->adapter->getSupportForObject(),
33963427
);
33973428
if (!$validator->isValid($index)) {
33983429
throw new IndexException($validator->getDescription());

src/Database/Validator/Index.php

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class Index extends Validator
3030

3131
protected bool $spatialIndexOrderSupport;
3232

33+
protected bool $objectIndexSupport;
34+
3335
/**
3436
* @param array<Document> $attributes
3537
* @param int $maxLength
@@ -38,16 +40,18 @@ class Index extends Validator
3840
* @param bool $spatialIndexSupport
3941
* @param bool $spatialIndexNullSupport
4042
* @param bool $spatialIndexOrderSupport
43+
* @param bool $objectIndexSupport
4144
* @throws DatabaseException
4245
*/
43-
public function __construct(array $attributes, int $maxLength, array $reservedKeys = [], bool $arrayIndexSupport = false, bool $spatialIndexSupport = false, bool $spatialIndexNullSupport = false, bool $spatialIndexOrderSupport = false)
46+
public function __construct(array $attributes, int $maxLength, array $reservedKeys = [], bool $arrayIndexSupport = false, bool $spatialIndexSupport = false, bool $spatialIndexNullSupport = false, bool $spatialIndexOrderSupport = false, bool $objectIndexSupport = false)
4447
{
4548
$this->maxLength = $maxLength;
4649
$this->reservedKeys = $reservedKeys;
4750
$this->arrayIndexSupport = $arrayIndexSupport;
4851
$this->spatialIndexSupport = $spatialIndexSupport;
4952
$this->spatialIndexNullSupport = $spatialIndexNullSupport;
5053
$this->spatialIndexOrderSupport = $spatialIndexOrderSupport;
54+
$this->objectIndexSupport = $objectIndexSupport;
5155

5256
foreach ($attributes as $attribute) {
5357
$key = \strtolower($attribute->getAttribute('key', $attribute->getAttribute('$id')));
@@ -305,6 +309,10 @@ public function isValid($value): bool
305309
return false;
306310
}
307311

312+
if (!$this->checkGinIndex($value)) {
313+
return false;
314+
}
315+
308316
return true;
309317
}
310318

@@ -378,6 +386,48 @@ public function checkSpatialIndex(Document $index): bool
378386
}
379387

380388

389+
return true;
390+
}
391+
392+
/**
393+
* @param Document $index
394+
* @return bool
395+
*/
396+
public function checkGinIndex(Document $index): bool
397+
{
398+
$type = $index->getAttribute('type');
399+
400+
$attributes = $index->getAttribute('attributes', []);
401+
$orders = $index->getAttribute('orders', []);
402+
403+
if ($type !== Database::INDEX_GIN) {
404+
return true;
405+
}
406+
407+
if (!$this->objectIndexSupport) {
408+
$this->message = 'GIN indexes are not supported';
409+
return false;
410+
}
411+
412+
if (count($attributes) !== 1) {
413+
$this->message = 'GIN index can be created on a single object attribute';
414+
return false;
415+
}
416+
417+
if (!empty($orders)) {
418+
$this->message = 'GIN indexes do not support explicit orders. Remove the orders to create this index.';
419+
return false;
420+
}
421+
422+
$attributeName = $attributes[0] ?? '';
423+
$attribute = $this->attributes[\strtolower($attributeName)] ?? new Document();
424+
$attributeType = $attribute->getAttribute('type', '');
425+
426+
if ($attributeType !== Database::TYPE_OBJECT) {
427+
$this->message = 'GIN index can only be created on object attributes. Attribute "' . $attributeName . '" is of type "' . $attributeType . '"';
428+
return false;
429+
}
430+
381431
return true;
382432
}
383433
}

tests/e2e/Adapter/Base.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Tests\E2E\Adapter\Scopes\DocumentTests;
99
use Tests\E2E\Adapter\Scopes\GeneralTests;
1010
use Tests\E2E\Adapter\Scopes\IndexTests;
11+
use Tests\E2E\Adapter\Scopes\ObjectAttributeTests;
1112
use Tests\E2E\Adapter\Scopes\PermissionTests;
1213
use Tests\E2E\Adapter\Scopes\RelationshipTests;
1314
use Tests\E2E\Adapter\Scopes\SpatialTests;
@@ -25,6 +26,7 @@ abstract class Base extends TestCase
2526
use PermissionTests;
2627
use RelationshipTests;
2728
use SpatialTests;
29+
use ObjectAttributeTests;
2830
use GeneralTests;
2931

3032
protected static string $namespace;

0 commit comments

Comments
 (0)