Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7e3656b
added support for object type attribute
ArnabChatterjee20k Oct 17, 2025
6edfe1a
updated validators
ArnabChatterjee20k Oct 17, 2025
4483496
updated tests
ArnabChatterjee20k Oct 17, 2025
10cf2bd
* added gin index
ArnabChatterjee20k Oct 17, 2025
8cd5921
removed redundant return after skip in tests
ArnabChatterjee20k Oct 17, 2025
117af69
updated array handling for equal and contains in object
ArnabChatterjee20k Oct 17, 2025
a92fd41
Merge remote-tracking branch 'upstream/main' into var_object
ArnabChatterjee20k Oct 17, 2025
979619f
fixed gin index issue
ArnabChatterjee20k Oct 17, 2025
6ba8558
updated validating default types
ArnabChatterjee20k Oct 17, 2025
49139d8
Merge remote-tracking branch 'upstream/main' into var_object
ArnabChatterjee20k Oct 27, 2025
739a4c3
* added support method in the mongodb adapter
ArnabChatterjee20k Oct 27, 2025
2cb3d98
renamed gin to object index to have a general term
ArnabChatterjee20k Oct 27, 2025
e2768d9
updated lock file
ArnabChatterjee20k Oct 27, 2025
f62ff51
Merge remote-tracking branch 'upstream/main' into var_object
ArnabChatterjee20k Oct 28, 2025
f5c0cfd
Refactor object type constants to use VAR_OBJECT for consistency acro…
ArnabChatterjee20k Oct 31, 2025
9a96110
added object validator test
ArnabChatterjee20k Nov 3, 2025
fd74c74
Merge remote-tracking branch 'upstream/3.x' into var_object
ArnabChatterjee20k Nov 6, 2025
1a4151d
fixed count, upsert methods for vector
ArnabChatterjee20k Nov 6, 2025
d4e3876
updated upsert fix, added sum fix
ArnabChatterjee20k Nov 6, 2025
8649aca
update var_object to be a filter similar to other types
ArnabChatterjee20k Nov 6, 2025
cd4e0b5
linting
ArnabChatterjee20k Nov 6, 2025
80b742e
Merge branch 'fix/vector-queries' into var_object
ArnabChatterjee20k Nov 6, 2025
9a0cea6
added test to simulate a vector store
ArnabChatterjee20k Nov 6, 2025
fad8570
removed reduntant comment
ArnabChatterjee20k Nov 11, 2025
29f4cfe
updated the semantics for not equal case
ArnabChatterjee20k Nov 11, 2025
5b34785
index, attribute filters, typo updates
ArnabChatterjee20k Nov 12, 2025
9a01de3
linting
ArnabChatterjee20k Nov 12, 2025
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
7 changes: 7 additions & 0 deletions src/Database/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,13 @@ abstract public function getSupportForBatchCreateAttributes(): bool;
*/
abstract public function getSupportForSpatialAttributes(): bool;

/**
* Are object (JSON) attributes supported?
*
* @return bool
*/
abstract public function getSupportForObject(): bool;

Comment thread
ArnabChatterjee20k marked this conversation as resolved.
/**
* Does the adapter support null values in spatial indexes?
*
Expand Down
5 changes: 5 additions & 0 deletions src/Database/Adapter/MariaDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -1849,6 +1849,11 @@ public function getSupportForSpatialAttributes(): bool
return true;
}

public function getSupportForObject(): bool
{
return false;
}

/**
* Get Support for Null Values in Spatial Indexes
*
Expand Down
5 changes: 5 additions & 0 deletions src/Database/Adapter/Pool.php
Original file line number Diff line number Diff line change
Expand Up @@ -568,4 +568,9 @@ public function decodePolygon(string $wkb): array
{
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function getSupportForObject(): bool
{
return $this->delegate(__FUNCTION__, \func_get_args());
}
}
86 changes: 82 additions & 4 deletions src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -860,14 +860,15 @@ public function createIndex(string $collection, string $id, string $type, array
Database::INDEX_FULLTEXT => 'INDEX',
Database::INDEX_UNIQUE => 'UNIQUE INDEX',
Database::INDEX_SPATIAL => 'INDEX',
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 => 'INDEX',
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),
};

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

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

// Add USING GIN for JSONB indexes
if ($type === Database::INDEX_GIN) {
$sql .= " USING GIN";
}

$sql .= " ({$attributes});";

$sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql);
Expand Down Expand Up @@ -1562,6 +1568,61 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att
}
}

/**
* Handle JSONB queries
*
* @param Query $query
* @param array<string, mixed> $binds
* @param string $attribute
* @param string $alias
* @param string $placeholder
* @return string
*/
protected function handleObjectQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string
{
switch ($query->getMethod()) {
case Query::TYPE_EQUAL:
case Query::TYPE_NOT_EQUAL: {
$isNot = $query->getMethod() === Query::TYPE_NOT_EQUAL;
$conditions = [];
foreach ($query->getValues() as $key => $value) {
$binds[":{$placeholder}_{$key}"] = json_encode($value);
if (is_array($value)) {
$fragment = "{$alias}.{$attribute} @> :{$placeholder}_{$key}::jsonb";
$conditions[] = $isNot ? "NOT (" . $fragment . ")" : $fragment;
} else {
$fragment = "{$alias}.{$attribute} = :{$placeholder}_{$key}::jsonb";
$conditions[] = $isNot ? "{$alias}.{$attribute} <> :{$placeholder}_{$key}::jsonb" : $fragment;
}
}
$separator = $isNot ? ' AND ' : ' OR ';
return empty($conditions) ? '' : '(' . implode($separator, $conditions) . ')';
}

case Query::TYPE_CONTAINS:
case Query::TYPE_NOT_CONTAINS: {
$isNot = $query->getMethod() === Query::TYPE_NOT_CONTAINS;
$conditions = [];
foreach ($query->getValues() as $key => $value) {
if (count($value) === 1) {
$jsonKey = array_key_first($value);
$jsonValue = $value[$jsonKey];
// wrap to represent array; eg: key -> [value]
$value[$jsonKey] = [$jsonValue];
}
$binds[":{$placeholder}_{$key}"] = json_encode($value);
$fragment = "{$alias}.{$attribute} @> :{$placeholder}_{$key}::jsonb";
$conditions[] = $isNot ? "NOT (" . $fragment . ")" : $fragment;
}
$separator = $isNot ? ' AND ' : ' OR ';
return empty($conditions) ? '' : '(' . implode($separator, $conditions) . ')';
}

default:
throw new DatabaseException('Query method ' . $query->getMethod() . ' not supported for object attributes');
}
}
Comment thread
ArnabChatterjee20k marked this conversation as resolved.

/**
* Get SQL Condition
*
Expand All @@ -1585,6 +1646,10 @@ protected function getSQLCondition(Query $query, array &$binds): string
return $this->handleSpatialQueries($query, $binds, $attribute, $alias, $placeholder);
}

if ($query->isObjectAttribute()) {
return $this->handleObjectQueries($query, $binds, $attribute, $alias, $placeholder);
}

switch ($query->getMethod()) {
case Query::TYPE_OR:
case Query::TYPE_AND:
Expand Down Expand Up @@ -1732,6 +1797,9 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
case Database::VAR_DATETIME:
return 'TIMESTAMP(3)';

case Database::TYPE_OBJECT:
return 'JSONB';

// in all other DB engines, 4326 is the default SRID
case Database::VAR_POINT:
return 'GEOMETRY(POINT,' . Database::SRID . ')';
Expand All @@ -1743,7 +1811,7 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
return 'GEOMETRY(POLYGON,' . Database::SRID . ')';

default:
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);
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::TYPE_OBJECT . ', ' . Database::VAR_POINT . ', ' . Database::VAR_LINESTRING . ', ' . Database::VAR_POLYGON);
}
}

Expand Down Expand Up @@ -1951,6 +2019,16 @@ public function getSupportForSpatialAttributes(): bool
return true;
}

/**
* Are object (JSONB) attributes supported?
*
* @return bool
*/
public function getSupportForObject(): bool
{
return true;
}

/**
* Does the adapter support null values in spatial indexes?
*
Expand Down
9 changes: 9 additions & 0 deletions src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,15 @@ public function getAttributeWidth(Document $collection): int
$total += 7;
break;

case Database::TYPE_OBJECT:
/**
* JSONB/JSON type
* Only the pointer contributes 20 bytes to the row size
* Data is stored externally
*/
$total += 20;
break;

case Database::VAR_POINT:
$total += $this->getMaxPointSize();
break;
Expand Down
5 changes: 5 additions & 0 deletions src/Database/Adapter/SQLite.php
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,11 @@ public function getSupportForSpatialAttributes(): bool
return false; // SQLite doesn't have native spatial support
}

public function getSupportForObject(): bool
{
return false;
}

public function getSupportForSpatialIndexNull(): bool
{
return false; // SQLite doesn't have native spatial support
Expand Down
77 changes: 72 additions & 5 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class Database
public const VAR_DATETIME = 'datetime';
public const VAR_ID = 'id';
public const VAR_OBJECT_ID = 'objectId';
public const TYPE_OBJECT = 'object';

public const INT_MAX = 2147483647;
public const BIG_INT_MAX = PHP_INT_MAX;
Expand All @@ -68,6 +69,7 @@ class Database
public const INDEX_FULLTEXT = 'fulltext';
public const INDEX_UNIQUE = 'unique';
public const INDEX_SPATIAL = 'spatial';
public const INDEX_GIN = 'gin';
public const ARRAY_INDEX_LENGTH = 255;

// Relation Types
Expand Down Expand Up @@ -1411,6 +1413,7 @@ public function createCollection(string $id, array $attributes = [], array $inde
$this->adapter->getSupportForSpatialAttributes(),
$this->adapter->getSupportForSpatialIndexNull(),
$this->adapter->getSupportForSpatialIndexOrder(),
$this->adapter->getSupportForObject(),
);
foreach ($indexes as $index) {
if (!$validator->isValid($index)) {
Expand Down Expand Up @@ -1950,6 +1953,17 @@ private function validateAttribute(
case self::VAR_DATETIME:
case self::VAR_RELATIONSHIP:
break;
case self::TYPE_OBJECT:
if (!$this->adapter->getSupportForObject()) {
throw new DatabaseException('Object attributes are not supported');
}
if (!empty($size)) {
throw new DatabaseException('Size must be empty for object attributes');
}
if (!empty($array)) {
throw new DatabaseException('Object attributes cannot be arrays');
}
break;
case self::VAR_POINT:
case self::VAR_LINESTRING:
case self::VAR_POLYGON:
Expand All @@ -1965,7 +1979,7 @@ private function validateAttribute(
}
break;
default:
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::VAR_POINT . ', ' . self::VAR_LINESTRING . ', ' . self::VAR_POLYGON);
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);
}

// Only execute when $default is given
Expand Down Expand Up @@ -2015,7 +2029,7 @@ protected function validateDefaultTypes(string $type, mixed $default): void

if ($defaultType === 'array') {
// spatial types require the array itself
if (!in_array($type, Database::SPATIAL_TYPES)) {
if (!in_array($type, Database::SPATIAL_TYPES) && $type != Database::TYPE_OBJECT) {
foreach ($default as $value) {
$this->validateDefaultTypes($type, $value);
}
Expand All @@ -2037,6 +2051,13 @@ protected function validateDefaultTypes(string $type, mixed $default): void
throw new DatabaseException('Default value ' . $default . ' does not match given type ' . $type);
}
break;
case self::TYPE_OBJECT:
// Object types expect arrays as default values
var_dump($defaultType);
if ($defaultType !== 'array') {
throw new DatabaseException('Default value for object type must be an array');
}
// no break
Comment thread
abnegate marked this conversation as resolved.
Outdated
case self::VAR_POINT:
case self::VAR_LINESTRING:
case self::VAR_POLYGON:
Expand All @@ -2046,7 +2067,7 @@ protected function validateDefaultTypes(string $type, mixed $default): void
}
// no break
default:
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::VAR_POINT . ', ' . self::VAR_LINESTRING . ', ' . self::VAR_POLYGON);
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);
}
}

Expand Down Expand Up @@ -2293,6 +2314,18 @@ public function updateAttribute(string $collection, string $id, ?string $type =
}
break;

case self::TYPE_OBJECT:
if (!$this->adapter->getSupportForObject()) {
throw new DatabaseException('Object attributes are not supported');
}
if (!empty($size)) {
throw new DatabaseException('Size must be empty for object attributes');
}
if (!empty($array)) {
throw new DatabaseException('Object attributes cannot be arrays');
}
break;

case self::VAR_POINT:
case self::VAR_LINESTRING:
case self::VAR_POLYGON:
Expand All @@ -2307,7 +2340,7 @@ public function updateAttribute(string $collection, string $id, ?string $type =
}
break;
default:
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);
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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

/** Ensure required filters for the attribute are passed */
Expand Down Expand Up @@ -3285,8 +3318,14 @@ public function createIndex(string $collection, string $id, string $type, array
}
break;

case self::INDEX_GIN:
if (!$this->adapter->getSupportForObject()) {
throw new DatabaseException('GIN indexes are not supported');
}
break;

default:
throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT . ', ' . Database::INDEX_SPATIAL);
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);
}

/** @var array<Document> $collectionAttributes */
Expand Down Expand Up @@ -3343,6 +3382,27 @@ public function createIndex(string $collection, string $id, string $type, array
}
}

if ($type === self::INDEX_GIN) {
if (count($attributes) !== 1) {
throw new IndexException('GIN index can be created on a single object attribute');
}

foreach ($attributes as $attr) {
if (!isset($indexAttributesWithTypes[$attr])) {
throw new IndexException('Attribute "' . $attr . '" not found in collection');
}

$attributeType = $indexAttributesWithTypes[$attr];
if ($attributeType !== self::TYPE_OBJECT) {
throw new IndexException('GIN index can only be created on object attributes. Attribute "' . $attr . '" is of type "' . $attributeType . '"');
}
}

if (!empty($orders)) {
throw new IndexException('GIN indexes do not support explicit orders. Remove the orders to create this index.');
}
}

$index = new Document([
'$id' => ID::custom($id),
'key' => $id,
Expand All @@ -3363,6 +3423,7 @@ public function createIndex(string $collection, string $id, string $type, array
$this->adapter->getSupportForSpatialAttributes(),
$this->adapter->getSupportForSpatialIndexNull(),
$this->adapter->getSupportForSpatialIndexOrder(),
$this->adapter->getSupportForObject(),
);
if (!$validator->isValid($index)) {
throw new IndexException($validator->getDescription());
Expand Down Expand Up @@ -7287,6 +7348,12 @@ public function casting(Document $collection, Document $document): Document
case self::VAR_FLOAT:
$node = (float)$node;
break;
case self::TYPE_OBJECT:
// Decode JSONB string to array
if (is_string($node)) {
$node = json_decode($node, true);
}
break;
default:
break;
}
Expand Down
8 changes: 8 additions & 0 deletions src/Database/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,14 @@ public function isSpatialAttribute(): bool
return in_array($this->attributeType, Database::SPATIAL_TYPES);
}

/**
* @return bool
*/
public function isObjectAttribute(): bool
{
return $this->attributeType === Database::TYPE_OBJECT;
}

// Spatial query methods

/**
Expand Down
Loading