Skip to content

Commit 3038eb6

Browse files
committed
Merge branch '3.x'
# Conflicts: # src/Database/Database.php
2 parents ce3986e + 8062cfc commit 3038eb6

File tree

20 files changed

+1584
-21
lines changed

20 files changed

+1584
-21
lines changed

src/Database/Adapter.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,13 @@ abstract public function getSupportForBatchCreateAttributes(): bool;
10941094
*/
10951095
abstract public function getSupportForSpatialAttributes(): bool;
10961096

1097+
/**
1098+
* Are object (JSON) attributes supported?
1099+
*
1100+
* @return bool
1101+
*/
1102+
abstract public function getSupportForObject(): bool;
1103+
10971104
/**
10981105
* Does the adapter support null values in spatial indexes?
10991106
*

src/Database/Adapter/MariaDB.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2135,6 +2135,11 @@ public function getSupportForSpatialAttributes(): bool
21352135
return true;
21362136
}
21372137

2138+
public function getSupportForObject(): bool
2139+
{
2140+
return false;
2141+
}
2142+
21382143
/**
21392144
* Get Support for Null Values in Spatial Indexes
21402145
*

src/Database/Adapter/Mongo.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2790,6 +2790,11 @@ public function getSupportForBatchCreateAttributes(): bool
27902790
return true;
27912791
}
27922792

2793+
public function getSupportForObject(): bool
2794+
{
2795+
return false;
2796+
}
2797+
27932798
/**
27942799
* Get current attribute count from collection document
27952800
*

src/Database/Adapter/Pool.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,11 @@ public function decodePolygon(string $wkb): array
592592
return $this->delegate(__FUNCTION__, \func_get_args());
593593
}
594594

595+
public function getSupportForObject(): bool
596+
{
597+
return $this->delegate(__FUNCTION__, \func_get_args());
598+
}
599+
595600
public function castingBefore(Document $collection, Document $document): Document
596601
{
597602
return $this->delegate(__FUNCTION__, \func_get_args());

src/Database/Adapter/Postgres.php

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -889,7 +889,8 @@ public function createIndex(string $collection, string $id, string $type, array
889889
Database::INDEX_HNSW_COSINE,
890890
Database::INDEX_HNSW_DOT => 'INDEX',
891891
Database::INDEX_UNIQUE => 'UNIQUE INDEX',
892-
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_HNSW_EUCLIDEAN . ', ' . Database::INDEX_HNSW_COSINE . ', ' . Database::INDEX_HNSW_DOT),
892+
Database::INDEX_OBJECT => 'INDEX',
893+
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_OBJECT . ', ' . Database::INDEX_HNSW_EUCLIDEAN . ', ' . Database::INDEX_HNSW_COSINE . ', ' . Database::INDEX_HNSW_DOT),
893894
};
894895

895896
$key = "\"{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}\"";
@@ -908,6 +909,7 @@ public function createIndex(string $collection, string $id, string $type, array
908909
Database::INDEX_HNSW_EUCLIDEAN => " USING HNSW ({$attributes} vector_l2_ops)",
909910
Database::INDEX_HNSW_COSINE => " USING HNSW ({$attributes} vector_cosine_ops)",
910911
Database::INDEX_HNSW_DOT => " USING HNSW ({$attributes} vector_ip_ops)",
912+
Database::INDEX_OBJECT => " USING GIN ({$attributes})",
911913
default => " ({$attributes})",
912914
};
913915

@@ -1656,6 +1658,62 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att
16561658
}
16571659
}
16581660

1661+
/**
1662+
* Handle JSONB queries
1663+
*
1664+
* @param Query $query
1665+
* @param array<string, mixed> $binds
1666+
* @param string $attribute
1667+
* @param string $alias
1668+
* @param string $placeholder
1669+
* @return string
1670+
*/
1671+
protected function handleObjectQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string
1672+
{
1673+
switch ($query->getMethod()) {
1674+
case Query::TYPE_EQUAL:
1675+
case Query::TYPE_NOT_EQUAL: {
1676+
$isNot = $query->getMethod() === Query::TYPE_NOT_EQUAL;
1677+
$conditions = [];
1678+
foreach ($query->getValues() as $key => $value) {
1679+
$binds[":{$placeholder}_{$key}"] = json_encode($value);
1680+
$fragment = "{$alias}.{$attribute} @> :{$placeholder}_{$key}::jsonb";
1681+
$conditions[] = $isNot ? "NOT (" . $fragment . ")" : $fragment;
1682+
}
1683+
$separator = $isNot ? ' AND ' : ' OR ';
1684+
return empty($conditions) ? '' : '(' . implode($separator, $conditions) . ')';
1685+
}
1686+
1687+
case Query::TYPE_CONTAINS:
1688+
case Query::TYPE_NOT_CONTAINS: {
1689+
$isNot = $query->getMethod() === Query::TYPE_NOT_CONTAINS;
1690+
$conditions = [];
1691+
foreach ($query->getValues() as $key => $value) {
1692+
if (count($value) === 1) {
1693+
$jsonKey = array_key_first($value);
1694+
$jsonValue = $value[$jsonKey];
1695+
1696+
// If scalar (e.g. "skills" => "typescript"),
1697+
// wrap it to express array containment: {"skills": ["typescript"]}
1698+
// If it's already an object/associative array (e.g. "config" => ["lang" => "en"]),
1699+
// keep as-is to express object containment.
1700+
if (!\is_array($jsonValue)) {
1701+
$value[$jsonKey] = [$jsonValue];
1702+
}
1703+
}
1704+
$binds[":{$placeholder}_{$key}"] = json_encode($value);
1705+
$fragment = "{$alias}.{$attribute} @> :{$placeholder}_{$key}::jsonb";
1706+
$conditions[] = $isNot ? "NOT (" . $fragment . ")" : $fragment;
1707+
}
1708+
$separator = $isNot ? ' AND ' : ' OR ';
1709+
return empty($conditions) ? '' : '(' . implode($separator, $conditions) . ')';
1710+
}
1711+
1712+
default:
1713+
throw new DatabaseException('Query method ' . $query->getMethod() . ' not supported for object attributes');
1714+
}
1715+
}
1716+
16591717
/**
16601718
* Get SQL Condition
16611719
*
@@ -1679,6 +1737,10 @@ protected function getSQLCondition(Query $query, array &$binds): string
16791737
return $this->handleSpatialQueries($query, $binds, $attribute, $alias, $placeholder);
16801738
}
16811739

1740+
if ($query->isObjectAttribute()) {
1741+
return $this->handleObjectQueries($query, $binds, $attribute, $alias, $placeholder);
1742+
}
1743+
16821744
switch ($query->getMethod()) {
16831745
case Query::TYPE_OR:
16841746
case Query::TYPE_AND:
@@ -1860,6 +1922,9 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
18601922
case Database::VAR_DATETIME:
18611923
return 'TIMESTAMP(3)';
18621924

1925+
case Database::VAR_OBJECT:
1926+
return 'JSONB';
1927+
18631928
case Database::VAR_POINT:
18641929
return 'GEOMETRY(POINT,' . Database::DEFAULT_SRID . ')';
18651930

@@ -1873,7 +1938,7 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
18731938
return "VECTOR({$size})";
18741939

18751940
default:
1876-
throw new DatabaseException('Unknown Type: ' . $type . '. Must be one of ' . Database::VAR_STRING . ', ' . Database::VAR_INTEGER . ', ' . Database::VAR_FLOAT . ', ' . Database::VAR_BOOLEAN . ', ' . Database::VAR_DATETIME . ', ' . Database::VAR_RELATIONSHIP . ', ' . Database::VAR_POINT . ', ' . Database::VAR_LINESTRING . ', ' . Database::VAR_POLYGON);
1941+
throw new DatabaseException('Unknown Type: ' . $type . '. Must be one of ' . Database::VAR_STRING . ', ' . Database::VAR_INTEGER . ', ' . Database::VAR_FLOAT . ', ' . Database::VAR_BOOLEAN . ', ' . Database::VAR_DATETIME . ', ' . Database::VAR_RELATIONSHIP . ', ' . Database::VAR_OBJECT . ', ' . Database::VAR_POINT . ', ' . Database::VAR_LINESTRING . ', ' . Database::VAR_POLYGON);
18771942
}
18781943
}
18791944

@@ -2106,6 +2171,16 @@ public function getSupportForSpatialAttributes(): bool
21062171
return true;
21072172
}
21082173

2174+
/**
2175+
* Are object (JSONB) attributes supported?
2176+
*
2177+
* @return bool
2178+
*/
2179+
public function getSupportForObject(): bool
2180+
{
2181+
return true;
2182+
}
2183+
21092184
/**
21102185
* Does the adapter support null values in spatial indexes?
21112186
*

src/Database/Adapter/SQL.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,15 @@ public function getAttributeWidth(Document $collection): int
11741174
$total += 7;
11751175
break;
11761176

1177+
case Database::VAR_OBJECT:
1178+
/**
1179+
* JSONB/JSON type
1180+
* Only the pointer contributes 20 bytes to the row size
1181+
* Data is stored externally
1182+
*/
1183+
$total += 20;
1184+
break;
1185+
11771186
case Database::VAR_POINT:
11781187
$total += $this->getMaxPointSize();
11791188
break;

src/Database/Adapter/SQLite.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,11 @@ public function getSupportForSpatialAttributes(): bool
10081008
return false; // SQLite doesn't have native spatial support
10091009
}
10101010

1011+
public function getSupportForObject(): bool
1012+
{
1013+
return false;
1014+
}
1015+
10111016
public function getSupportForSpatialIndexNull(): bool
10121017
{
10131018
return false; // SQLite doesn't have native spatial support

src/Database/Database.php

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ class Database
4949
public const VAR_ID = 'id';
5050
public const VAR_UUID7 = 'uuid7';
5151

52+
// object type
53+
public const VAR_OBJECT = 'object';
54+
5255
// Vector types
5356
public const VAR_VECTOR = 'vector';
5457

@@ -67,11 +70,20 @@ class Database
6770
self::VAR_POLYGON
6871
];
6972

73+
// All types which requires filters
74+
public const ATTRIBUTE_FILTER_TYPES = [
75+
...self::SPATIAL_TYPES,
76+
self::VAR_VECTOR,
77+
self::VAR_OBJECT,
78+
self::VAR_DATETIME
79+
];
80+
7081
// Index Types
7182
public const INDEX_KEY = 'key';
7283
public const INDEX_FULLTEXT = 'fulltext';
7384
public const INDEX_UNIQUE = 'unique';
7485
public const INDEX_SPATIAL = 'spatial';
86+
public const INDEX_OBJECT = 'object';
7587
public const INDEX_HNSW_EUCLIDEAN = 'hnsw_euclidean';
7688
public const INDEX_HNSW_COSINE = 'hnsw_cosine';
7789
public const INDEX_HNSW_DOT = 'hnsw_dot';
@@ -633,6 +645,35 @@ function (?string $value) {
633645
return is_array($decoded) ? $decoded : $value;
634646
}
635647
);
648+
649+
self::addFilter(
650+
Database::VAR_OBJECT,
651+
/**
652+
* @param mixed $value
653+
* @return mixed
654+
*/
655+
function (mixed $value) {
656+
if (!\is_array($value)) {
657+
return $value;
658+
}
659+
660+
return \json_encode($value);
661+
},
662+
/**
663+
* @param string|null $value
664+
* @return array|null
665+
*/
666+
function (?string $value) {
667+
if (is_null($value)) {
668+
return null;
669+
}
670+
if (!is_string($value)) {
671+
return $value;
672+
}
673+
$decoded = json_decode($value, true);
674+
return is_array($decoded) ? $decoded : $value;
675+
}
676+
);
636677
}
637678

638679
/**
@@ -1557,7 +1598,7 @@ public function delete(?string $database = null): bool
15571598
public function createCollection(string $id, array $attributes = [], array $indexes = [], ?array $permissions = null, bool $documentSecurity = true): Document
15581599
{
15591600
foreach ($attributes as &$attribute) {
1560-
if (in_array($attribute['type'], Database::SPATIAL_TYPES) || $attribute['type'] === Database::VAR_VECTOR) {
1601+
if (in_array($attribute['type'], self::ATTRIBUTE_FILTER_TYPES)) {
15611602
$existingFilters = $attribute['filters'] ?? [];
15621603
if (!is_array($existingFilters)) {
15631604
$existingFilters = [$existingFilters];
@@ -1644,6 +1685,7 @@ public function createCollection(string $id, array $attributes = [], array $inde
16441685
$this->adapter->getSupportForAttributes(),
16451686
$this->adapter->getSupportForMultipleFulltextIndexes(),
16461687
$this->adapter->getSupportForIdenticalIndexes(),
1688+
$this->adapter->getSupportForObject(),
16471689
);
16481690
foreach ($indexes as $index) {
16491691
if (!$validator->isValid($index)) {
@@ -1963,11 +2005,8 @@ public function createAttribute(string $collection, string $id, string $type, in
19632005
if ($collection->isEmpty()) {
19642006
throw new NotFoundException('Collection not found');
19652007
}
1966-
if (in_array($type, Database::SPATIAL_TYPES)) {
1967-
$filters[] = $type;
1968-
$filters = array_unique($filters);
1969-
}
1970-
if ($type === Database::VAR_VECTOR) {
2008+
2009+
if (in_array($type, self::ATTRIBUTE_FILTER_TYPES)) {
19712010
$filters[] = $type;
19722011
$filters = array_unique($filters);
19732012
}
@@ -2252,6 +2291,17 @@ private function validateAttribute(
22522291
case self::VAR_DATETIME:
22532292
case self::VAR_RELATIONSHIP:
22542293
break;
2294+
case self::VAR_OBJECT:
2295+
if (!$this->adapter->getSupportForObject()) {
2296+
throw new DatabaseException('Object attributes are not supported');
2297+
}
2298+
if (!empty($size)) {
2299+
throw new DatabaseException('Size must be empty for object attributes');
2300+
}
2301+
if (!empty($array)) {
2302+
throw new DatabaseException('Object attributes cannot be arrays');
2303+
}
2304+
break;
22552305
case self::VAR_POINT:
22562306
case self::VAR_LINESTRING:
22572307
case self::VAR_POLYGON:
@@ -2310,6 +2360,9 @@ private function validateAttribute(
23102360
if ($this->adapter->getSupportForSpatialAttributes()) {
23112361
\array_push($supportedTypes, ...self::SPATIAL_TYPES);
23122362
}
2363+
if ($this->adapter->getSupportForObject()) {
2364+
$supportedTypes[] = self::VAR_OBJECT;
2365+
}
23132366
throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . implode(', ', $supportedTypes));
23142367
}
23152368

@@ -2360,7 +2413,7 @@ protected function validateDefaultTypes(string $type, mixed $default): void
23602413

23612414
if ($defaultType === 'array') {
23622415
// Spatial types require the array itself
2363-
if (!in_array($type, Database::SPATIAL_TYPES)) {
2416+
if (!in_array($type, Database::SPATIAL_TYPES) && $type != Database::VAR_OBJECT) {
23642417
foreach ($default as $value) {
23652418
$this->validateDefaultTypes($type, $value);
23662419
}
@@ -2685,7 +2738,17 @@ public function updateAttribute(string $collection, string $id, ?string $type =
26852738
throw new DatabaseException('Size must be empty');
26862739
}
26872740
break;
2688-
2741+
case self::VAR_OBJECT:
2742+
if (!$this->adapter->getSupportForObject()) {
2743+
throw new DatabaseException('Object attributes are not supported');
2744+
}
2745+
if (!empty($size)) {
2746+
throw new DatabaseException('Size must be empty for object attributes');
2747+
}
2748+
if (!empty($array)) {
2749+
throw new DatabaseException('Object attributes cannot be arrays');
2750+
}
2751+
break;
26892752
case self::VAR_POINT:
26902753
case self::VAR_LINESTRING:
26912754
case self::VAR_POLYGON:
@@ -2860,6 +2923,7 @@ public function updateAttribute(string $collection, string $id, ?string $type =
28602923
$this->adapter->getSupportForAttributes(),
28612924
$this->adapter->getSupportForMultipleFulltextIndexes(),
28622925
$this->adapter->getSupportForIdenticalIndexes(),
2926+
$this->adapter->getSupportForObject(),
28632927
);
28642928

28652929
foreach ($indexes as $index) {
@@ -4044,8 +4108,14 @@ public function createIndex(string $collection, string $id, string $type, array
40444108
}
40454109
break;
40464110

4111+
case self::INDEX_OBJECT:
4112+
if (!$this->adapter->getSupportForObject()) {
4113+
throw new DatabaseException('Object indexes are not supported');
4114+
}
4115+
break;
4116+
40474117
default:
4048-
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_HNSW_EUCLIDEAN . ', ' . Database::INDEX_HNSW_COSINE . ', ' . Database::INDEX_HNSW_DOT);
4118+
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_OBJECT . ', ' . Database::INDEX_HNSW_EUCLIDEAN . ', ' . Database::INDEX_HNSW_COSINE . ', ' . Database::INDEX_HNSW_DOT);
40494119
}
40504120

40514121
/** @var array<Document> $collectionAttributes */
@@ -4100,6 +4170,7 @@ public function createIndex(string $collection, string $id, string $type, array
41004170
$this->adapter->getSupportForAttributes(),
41014171
$this->adapter->getSupportForMultipleFulltextIndexes(),
41024172
$this->adapter->getSupportForIdenticalIndexes(),
4173+
$this->adapter->getSupportForObject(),
41034174
);
41044175
if (!$validator->isValid($index)) {
41054176
throw new IndexException($validator->getDescription());

0 commit comments

Comments
 (0)