Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -1072,6 +1072,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 @@ -2135,6 +2135,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 @@ -2790,6 +2790,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 @@ -585,6 +585,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 @@ -889,7 +889,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 @@ -908,6 +909,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 @@ -1656,6 +1658,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 @@ -1679,6 +1737,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 @@ -1860,6 +1922,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 @@ -1873,7 +1938,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 @@ -2106,6 +2171,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
61 changes: 57 additions & 4 deletions src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,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 Expand Up @@ -3188,7 +3197,18 @@ public function count(Document $collection, array $queries = [], ?int $max = nul

$queries = array_map(fn ($query) => clone $query, $queries);

$conditions = $this->getSQLConditions($queries, $binds);
// Extract vector queries (used for ORDER BY) and keep non-vector for WHERE
$vectorQueries = [];
$otherQueries = [];
foreach ($queries as $query) {
if (in_array($query->getMethod(), Query::VECTOR_TYPES)) {
$vectorQueries[] = $query;
} else {
$otherQueries[] = $query;
}
}

$conditions = $this->getSQLConditions($otherQueries, $binds);
if (!empty($conditions)) {
$where[] = $conditions;
}
Expand All @@ -3206,12 +3226,23 @@ public function count(Document $collection, array $queries = [], ?int $max = nul
? 'WHERE ' . \implode(' AND ', $where)
: '';

// Add vector distance calculations to ORDER BY (similarity-aware LIMIT)
$vectorOrders = [];
foreach ($vectorQueries as $query) {
$vectorOrder = $this->getVectorDistanceOrder($query, $binds, $alias);
if ($vectorOrder) {
$vectorOrders[] = $vectorOrder;
}
}
$sqlOrder = !empty($vectorOrders) ? 'ORDER BY ' . implode(', ', $vectorOrders) : '';

$sql = "
SELECT COUNT(1) as sum FROM (
SELECT 1
FROM {$this->getSQLTable($name)} AS {$this->quote($alias)}
{$sqlWhere}
{$limit}
{$sqlWhere}
{$sqlOrder}
{$limit}
) table_count
";

Expand Down Expand Up @@ -3264,7 +3295,18 @@ public function sum(Document $collection, string $attribute, array $queries = []

$queries = array_map(fn ($query) => clone $query, $queries);

$conditions = $this->getSQLConditions($queries, $binds);
// Extract vector queries (used for ORDER BY) and keep non-vector for WHERE
$vectorQueries = [];
$otherQueries = [];
foreach ($queries as $query) {
if (in_array($query->getMethod(), Query::VECTOR_TYPES)) {
$vectorQueries[] = $query;
} else {
$otherQueries[] = $query;
}
}

$conditions = $this->getSQLConditions($otherQueries, $binds);
if (!empty($conditions)) {
$where[] = $conditions;
}
Expand All @@ -3282,11 +3324,22 @@ public function sum(Document $collection, string $attribute, array $queries = []
? 'WHERE ' . \implode(' AND ', $where)
: '';

// Add vector distance calculations to ORDER BY (similarity-aware LIMIT)
$vectorOrders = [];
foreach ($vectorQueries as $query) {
$vectorOrder = $this->getVectorDistanceOrder($query, $binds, $alias);
if ($vectorOrder) {
$vectorOrders[] = $vectorOrder;
}
}
$sqlOrder = !empty($vectorOrders) ? 'ORDER BY ' . implode(', ', $vectorOrders) : '';

$sql = "
SELECT SUM({$this->quote($attribute)}) as sum FROM (
SELECT {$this->quote($attribute)}
FROM {$this->getSQLTable($name)} AS {$this->quote($alias)}
{$sqlWhere}
{$sqlOrder}
{$limit}
) table_count
";
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 @@ -1008,6 +1008,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
Loading