Skip to content
Closed

3.x #759

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
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
8062cfc
Merge pull request #741 from utopia-php/var_object
abnegate 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) . ')';
Comment on lines +1688 to +1709
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix nested array containment support.

Try inserting a document with meta.profile.skills = ['php', 'go'] and running Query::contains('meta', [['profile' => ['skills' => 'php']]]). The SQL we emit becomes meta @> '{"profile":{"skills":"php"}}'::jsonb, which PostgreSQL will never match because "skills" is stored as an array, not a scalar. The helper only wraps scalars when the fragment consists of a single top-level key, so any nested array (and even top-level arrays combined with other keys) silently stops matching. As a result contains() can no longer express “profile.skills contains X”, which is a correctness regression for realistic payloads. Please normalize these fragments recursively (or drop the partial scalar-to-array heuristic and require callers to supply the proper JSON shape) so nested array lookups continue to work. Happy to help iterate on the fix.

🤖 Prompt for AI Agents
In src/Database/Adapter/Postgres.php around lines 1688-1709, the current
scalar-to-array wrapping only handles single top-level keys and fails to
normalize nested values (so queries like contains('meta',
[['profile'=>['skills'=>'php']]]) emit fragments that won't match stored
arrays). Replace the partial heuristic with a recursive normalizer: traverse the
value tree; for associative arrays (array with string keys) iterate each key and
if the value is scalar wrap it into a single-element array, otherwise recurse;
for numeric-indexed lists (array_is_list) treat them as explicit arrays (do not
convert them to objects) but recurse into their elements if those elements are
associative; after normalization json_encode the resulting structure for the
bind. Implement this recursion and use it in place of the current top-level-only
wrapping so nested array containment queries produce correct JSONB fragments.

}

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
9 changes: 9 additions & 0 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
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