Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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 @@ -1070,6 +1070,13 @@ abstract public function getSupportForBatchCreateAttributes(): bool;
*/
abstract public function getSupportForSpatialAttributes(): bool;

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

/**
* 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/Mongo.php
Original file line number Diff line number Diff line change
Expand Up @@ -2788,6 +2788,11 @@ public function getSupportForBatchCreateAttributes(): bool
return true;
}

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

/**
* Get current attribute count from collection document
*
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 @@ -580,6 +580,11 @@ public function decodePolygon(string $wkb): array
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function getSupportForObject(): bool
{
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function castingBefore(Document $collection, Document $document): Document
{
return $this->delegate(__FUNCTION__, \func_get_args());
Expand Down
79 changes: 77 additions & 2 deletions src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,8 @@ public function createIndex(string $collection, string $id, string $type, array
Database::INDEX_HNSW_COSINE,
Database::INDEX_HNSW_DOT => 'INDEX',
Database::INDEX_UNIQUE => 'UNIQUE 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_HNSW_EUCLIDEAN . ', ' . Database::INDEX_HNSW_COSINE . ', ' . Database::INDEX_HNSW_DOT),
Database::INDEX_OBJECT => '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_OBJECT . ', ' . Database::INDEX_HNSW_EUCLIDEAN . ', ' . Database::INDEX_HNSW_COSINE . ', ' . Database::INDEX_HNSW_DOT),
};

$key = "\"{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}\"";
Expand All @@ -905,6 +906,7 @@ public function createIndex(string $collection, string $id, string $type, array
Database::INDEX_HNSW_EUCLIDEAN => " USING HNSW ({$attributes} vector_l2_ops)",
Database::INDEX_HNSW_COSINE => " USING HNSW ({$attributes} vector_cosine_ops)",
Database::INDEX_HNSW_DOT => " USING HNSW ({$attributes} vector_ip_ops)",
Database::INDEX_OBJECT => " USING GIN ({$attributes})",
default => " ({$attributes})",
};

Expand Down Expand Up @@ -1589,6 +1591,62 @@ 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);
$fragment = "{$alias}.{$attribute} @> :{$placeholder}_{$key}::jsonb";
$conditions[] = $isNot ? "NOT (" . $fragment . ")" : $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];

// If scalar (e.g. "skills" => "typescript"),
// wrap it to express array containment: {"skills": ["typescript"]}
// If it's already an object/associative array (e.g. "config" => ["lang" => "en"]),
// keep as-is to express object containment.
if (!\is_array($jsonValue)) {
$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');
}
}

/**
* Get SQL Condition
*
Expand All @@ -1612,6 +1670,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 @@ -1793,6 +1855,9 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
case Database::VAR_DATETIME:
return 'TIMESTAMP(3)';

case Database::VAR_OBJECT:
return 'JSONB';

case Database::VAR_POINT:
return 'GEOMETRY(POINT,' . Database::DEFAULT_SRID . ')';

Expand All @@ -1806,7 +1871,7 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
return "VECTOR({$size})";

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

Expand Down Expand Up @@ -2024,6 +2089,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 @@ -1126,6 +1126,15 @@ public function getAttributeWidth(Document $collection): int
$total += 7;
break;

case Database::VAR_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
70 changes: 67 additions & 3 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class Database
public const VAR_ID = 'id';
public const VAR_UUID7 = 'uuid7';

// object type
public const VAR_OBJECT = 'object';

// Vector types
public const VAR_VECTOR = 'vector';

Expand All @@ -70,6 +73,8 @@ class Database
public const INDEX_FULLTEXT = 'fulltext';
public const INDEX_UNIQUE = 'unique';
public const INDEX_SPATIAL = 'spatial';
// keeping
public const INDEX_OBJECT = 'object';
public const INDEX_HNSW_EUCLIDEAN = 'hnsw_euclidean';
public const INDEX_HNSW_COSINE = 'hnsw_cosine';
public const INDEX_HNSW_DOT = 'hnsw_dot';
Expand Down Expand Up @@ -1467,6 +1472,7 @@ public function createCollection(string $id, array $attributes = [], array $inde
$this->adapter->getSupportForAttributes(),
$this->adapter->getSupportForMultipleFulltextIndexes(),
$this->adapter->getSupportForIdenticalIndexes(),
$this->adapter->getSupportForObject(),
);
foreach ($indexes as $index) {
if (!$validator->isValid($index)) {
Expand Down Expand Up @@ -2010,6 +2016,17 @@ private function validateAttribute(
case self::VAR_DATETIME:
case self::VAR_RELATIONSHIP:
break;
case self::VAR_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 Down Expand Up @@ -2118,7 +2135,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::VAR_OBJECT) {
foreach ($default as $value) {
$this->validateDefaultTypes($type, $value);
}
Expand Down Expand Up @@ -2415,6 +2432,18 @@ public function updateAttribute(string $collection, string $id, ?string $type =
}
break;

case self::VAR_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 Down Expand Up @@ -2462,7 +2491,7 @@ public function updateAttribute(string $collection, string $id, ?string $type =
self::VAR_FLOAT,
self::VAR_BOOLEAN,
self::VAR_DATETIME,
self::VAR_RELATIONSHIP
self::VAR_RELATIONSHIP . ', ' . self::VAR_OBJECT
];
if ($this->adapter->getSupportForVectors()) {
$supportedTypes[] = self::VAR_VECTOR;
Expand Down Expand Up @@ -2587,6 +2616,7 @@ public function updateAttribute(string $collection, string $id, ?string $type =
$this->adapter->getSupportForAttributes(),
$this->adapter->getSupportForMultipleFulltextIndexes(),
$this->adapter->getSupportForIdenticalIndexes(),
$this->adapter->getSupportForObject()
);

foreach ($indexes as $index) {
Expand Down Expand Up @@ -3460,8 +3490,14 @@ public function createIndex(string $collection, string $id, string $type, array
}
break;

case self::INDEX_OBJECT:
if (!$this->adapter->getSupportForObject()) {
throw new DatabaseException('Object 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 . ', ' . Database::INDEX_HNSW_EUCLIDEAN . ', ' . Database::INDEX_HNSW_COSINE . ', ' . Database::INDEX_HNSW_DOT);
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);
}

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

if ($type === self::INDEX_OBJECT) {
if (count($attributes) !== 1) {
throw new IndexException('Object 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::VAR_OBJECT) {
throw new IndexException('Object index can only be created on object attributes. Attribute "' . $attr . '" is of type "' . $attributeType . '"');
}
}

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

$index = new Document([
'$id' => ID::custom($id),
'key' => $id,
Expand All @@ -3516,6 +3573,7 @@ public function createIndex(string $collection, string $id, string $type, array
$this->adapter->getSupportForAttributes(),
$this->adapter->getSupportForMultipleFulltextIndexes(),
$this->adapter->getSupportForIdenticalIndexes(),
$this->adapter->getSupportForObject(),
);
if (!$validator->isValid($index)) {
throw new IndexException($validator->getDescription());
Expand Down Expand Up @@ -7503,6 +7561,12 @@ public function casting(Document $collection, Document $document): Document
case self::VAR_FLOAT:
$node = (float)$node;
break;
case self::VAR_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 @@ -977,6 +977,14 @@ public function isSpatialAttribute(): bool
return in_array($this->attributeType, Database::SPATIAL_TYPES);
}

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

// Spatial query methods

/**
Expand Down
Loading