Skip to content

Commit 8062cfc

Browse files
authored
Merge pull request #741 from utopia-php/var_object
Object(json) attribute support for postgres
2 parents 6c953e5 + 9a01de3 commit 8062cfc

20 files changed

Lines changed: 1587 additions & 21 deletions

src/Database/Adapter.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,13 @@ abstract public function getSupportForBatchCreateAttributes(): bool;
10721072
*/
10731073
abstract public function getSupportForSpatialAttributes(): bool;
10741074

1075+
/**
1076+
* Are object (JSON) attributes supported?
1077+
*
1078+
* @return bool
1079+
*/
1080+
abstract public function getSupportForObject(): bool;
1081+
10751082
/**
10761083
* Does the adapter support null values in spatial indexes?
10771084
*

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
@@ -585,6 +585,11 @@ public function decodePolygon(string $wkb): array
585585
return $this->delegate(__FUNCTION__, \func_get_args());
586586
}
587587

588+
public function getSupportForObject(): bool
589+
{
590+
return $this->delegate(__FUNCTION__, \func_get_args());
591+
}
592+
588593
public function castingBefore(Document $collection, Document $document): Document
589594
{
590595
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
@@ -1175,6 +1175,15 @@ public function getAttributeWidth(Document $collection): int
11751175
$total += 7;
11761176
break;
11771177

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

0 commit comments

Comments
 (0)