Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
77 changes: 76 additions & 1 deletion src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -1562,6 +1562,64 @@ 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:
$conditions = [];
foreach ($query->getValues() as $key => $value) {
if (is_array($value)) {
// JSONB containment operator @>
$binds[":{$placeholder}_{$key}"] = json_encode($value);
$conditions[] = "{$alias}.{$attribute} @> :{$placeholder}_{$key}::jsonb";
} else {
// Direct equality
$binds[":{$placeholder}_{$key}"] = json_encode($value);
$conditions[] = "{$alias}.{$attribute} = :{$placeholder}_{$key}::jsonb";
}
}
return empty($conditions) ? '' : '(' . implode(' OR ', $conditions) . ')';

case Query::TYPE_CONTAINS:
$conditions = [];
foreach ($query->getValues() as $key => $value) {
if (is_array($value)) {
// For JSONB contains, we need to check if an array contains a specific element
// The JSONB containment operator @> checks if left contains right
// For array element containment: {"array": ["element"]} means array contains "element"
// For nested array containment: {"matrix": [[4,5,6]]} means matrix contains [4,5,6]

if (count($value) === 1) {
$jsonKey = array_key_first($value);
$jsonValue = $value[$jsonKey];

// Always wrap the value in an array to represent "array contains this element"
// - For scalar: 'react' becomes ["react"]
// - For array: [4,5,6] becomes [[4,5,6]]
$value[$jsonKey] = [$jsonValue];
}

$binds[":{$placeholder}_{$key}"] = json_encode($value);
$conditions[] = "{$alias}.{$attribute} @> :{$placeholder}_{$key}::jsonb";
}
}
return empty($conditions) ? '' : '(' . implode(' OR ', $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 +1643,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 +1794,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 +1808,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 +2016,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
44 changes: 40 additions & 4 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 Down Expand Up @@ -1950,6 +1951,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 +1977,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 @@ -2037,16 +2049,22 @@ 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
if ($defaultType !== 'array') {
throw new DatabaseException('Default value for object type must be an array');
}
break;
case self::VAR_POINT:
case self::VAR_LINESTRING:
case self::VAR_POLYGON:
// Spatial types expect arrays as default values
if ($defaultType !== 'array') {
throw new DatabaseException('Default value for spatial type ' . $type . ' must be an array');
}
// no break
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 +2311,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 +2337,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 @@ -7287,6 +7317,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
12 changes: 11 additions & 1 deletion src/Database/Validator/Query/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s
$validator = new Text(255, 0); // The query is always on uid
break;

case Database::TYPE_OBJECT:
// For JSONB/object queries, value must be an array
if (!is_array($value)) {
$this->message = 'Query value for object type must be an array';
return false;
}
// No further validation needed - JSONB accepts any valid array structure
continue 2;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
case Database::VAR_POINT:
case Database::VAR_LINESTRING:
case Database::VAR_POLYGON:
Expand Down Expand Up @@ -201,10 +210,11 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s
!$array &&
in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_NOT_CONTAINS]) &&
$attributeSchema['type'] !== Database::VAR_STRING &&
$attributeSchema['type'] !== Database::TYPE_OBJECT &&
!in_array($attributeSchema['type'], Database::SPATIAL_TYPES)
) {
$queryType = $method === Query::TYPE_NOT_CONTAINS ? 'notContains' : 'contains';
$this->message = 'Cannot query ' . $queryType . ' on attribute "' . $attribute . '" because it is not an array or string.';
$this->message = 'Cannot query ' . $queryType . ' on attribute "' . $attribute . '" because it is not an array, string, or object.';
return false;
}

Expand Down
9 changes: 9 additions & 0 deletions src/Database/Validator/Structure.php
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,15 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys)
);
break;

case Database::TYPE_OBJECT:
// For JSONB/object types, just validate it's an array (associative or list)
if (!is_array($value)) {
$this->message = 'Attribute "'.$key.'" has invalid type. Value must be an array for object type';
return false;
}
// No additional validators needed - JSONB accepts any valid array structure
continue 2; // Skip to next attribute

Comment thread
ArnabChatterjee20k marked this conversation as resolved.
Outdated
case Database::VAR_POINT:
case Database::VAR_LINESTRING:
case Database::VAR_POLYGON:
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/Adapter/Scopes/AttributeTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -1523,7 +1523,7 @@ public function testArrayAttribute(): void
]);
$this->fail('Failed to throw exception');
} catch (Throwable $e) {
$this->assertEquals('Invalid query: Cannot query contains on attribute "age" because it is not an array or string.', $e->getMessage());
$this->assertEquals('Invalid query: Cannot query contains on attribute "age" because it is not an array, string, or object.', $e->getMessage());
}

$documents = $database->find($collection, [
Expand Down
Loading
Loading