Skip to content

Commit f76a0de

Browse files
committed
(fix): Make array columns NOT NULL with default empty array for index reliability
1 parent 927545f commit f76a0de

4 files changed

Lines changed: 58 additions & 13 deletions

File tree

src/Database/Adapter/MariaDB.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,7 +1684,7 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
16841684
return $this->getSpatialSQLType($type, $required);
16851685
}
16861686
if ($array === true) {
1687-
return 'JSON';
1687+
return 'JSON NOT NULL DEFAULT (JSON_ARRAY())';
16881688
}
16891689

16901690
switch ($type) {
@@ -2082,12 +2082,12 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
20822082
case Operator::TYPE_ARRAY_APPEND:
20832083
$bindKey = "op_{$bindIndex}";
20842084
$bindIndex++;
2085-
return "{$quotedColumn} = JSON_MERGE_PRESERVE(IFNULL({$quotedColumn}, JSON_ARRAY()), :$bindKey)";
2085+
return "{$quotedColumn} = JSON_MERGE_PRESERVE({$quotedColumn}, :$bindKey)";
20862086

20872087
case Operator::TYPE_ARRAY_PREPEND:
20882088
$bindKey = "op_{$bindIndex}";
20892089
$bindIndex++;
2090-
return "{$quotedColumn} = JSON_MERGE_PRESERVE(:$bindKey, IFNULL({$quotedColumn}, JSON_ARRAY()))";
2090+
return "{$quotedColumn} = JSON_MERGE_PRESERVE(:$bindKey, {$quotedColumn})";
20912091

20922092
case Operator::TYPE_ARRAY_INSERT:
20932093
$indexKey = "op_{$bindIndex}";

src/Database/Adapter/Postgres.php

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,26 @@ public function createIndex(string $collection, string $id, string $type, array
892892
$collection = $this->filter($collection);
893893
$id = $this->filter($id);
894894

895+
// JSONB array columns need GIN indexes for containment queries (@>).
896+
$isArrayIndex = false;
897+
if ($type === Database::INDEX_KEY) {
898+
$metadataCollection = new Document(['$id' => Database::METADATA]);
899+
$collectionDoc = $this->getDocument($metadataCollection, $collection);
900+
if (!$collectionDoc->isEmpty()) {
901+
$collectionAttributes = \json_decode($collectionDoc->getAttribute('attributes', '[]'), true);
902+
$arrayCount = 0;
903+
foreach ($attributes as $attr) {
904+
foreach ($collectionAttributes as $collectionAttribute) {
905+
if (\strtolower($collectionAttribute['$id']) === \strtolower($attr) && !empty($collectionAttribute['array'])) {
906+
$arrayCount++;
907+
break;
908+
}
909+
}
910+
}
911+
$isArrayIndex = $arrayCount > 0 && $arrayCount === \count($attributes);
912+
}
913+
}
914+
895915
foreach ($attributes as $i => $attr) {
896916
$order = empty($orders[$i]) || Database::INDEX_FULLTEXT === $type ? '' : $orders[$i];
897917
$isNestedPath = isset($indexAttributeTypes[$attr]) && \str_contains($attr, '.') && $indexAttributeTypes[$attr] === Database::VAR_OBJECT;
@@ -933,13 +953,13 @@ public function createIndex(string $collection, string $id, string $type, array
933953
$sql = "CREATE {$sqlType} \"{$keyName}\" ON {$this->getSQLTable($collection)}";
934954

935955
// Add USING clause for special index types
936-
$sql .= match ($type) {
937-
Database::INDEX_SPATIAL => " USING GIST ({$attributes})",
938-
Database::INDEX_HNSW_EUCLIDEAN => " USING HNSW ({$attributes} vector_l2_ops)",
939-
Database::INDEX_HNSW_COSINE => " USING HNSW ({$attributes} vector_cosine_ops)",
940-
Database::INDEX_HNSW_DOT => " USING HNSW ({$attributes} vector_ip_ops)",
941-
Database::INDEX_OBJECT => " USING GIN ({$attributes})",
942-
Database::INDEX_TRIGRAM =>
956+
$sql .= match (true) {
957+
$type === Database::INDEX_SPATIAL => " USING GIST ({$attributes})",
958+
$type === Database::INDEX_HNSW_EUCLIDEAN => " USING HNSW ({$attributes} vector_l2_ops)",
959+
$type === Database::INDEX_HNSW_COSINE => " USING HNSW ({$attributes} vector_cosine_ops)",
960+
$type === Database::INDEX_HNSW_DOT => " USING HNSW ({$attributes} vector_ip_ops)",
961+
$type === Database::INDEX_OBJECT, $isArrayIndex => " USING GIN ({$attributes})",
962+
$type === Database::INDEX_TRIGRAM =>
943963
" USING GIN (" . implode(', ', array_map(
944964
fn ($attr) => "$attr gin_trgm_ops",
945965
array_map(fn ($attr) => trim($attr), explode(',', $attributes))
@@ -1937,7 +1957,7 @@ protected function getFulltextValue(string $value): string
19371957
protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): string
19381958
{
19391959
if ($array === true) {
1940-
return 'JSONB';
1960+
return "JSONB NOT NULL DEFAULT '[]'::jsonb";
19411961
}
19421962

19431963
switch ($type) {
@@ -2117,6 +2137,11 @@ public function getSupportForTimeouts(): bool
21172137
return true;
21182138
}
21192139

2140+
public function getSupportForCastIndexArray(): bool
2141+
{
2142+
return true;
2143+
}
2144+
21202145
/**
21212146
* Does the adapter handle Query Array Overlaps?
21222147
*
@@ -2688,12 +2713,12 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
26882713
case Operator::TYPE_ARRAY_APPEND:
26892714
$bindKey = "op_{$bindIndex}";
26902715
$bindIndex++;
2691-
return "{$quotedColumn} = COALESCE({$columnRef}, '[]'::jsonb) || :$bindKey::jsonb";
2716+
return "{$quotedColumn} = {$columnRef} || :$bindKey::jsonb";
26922717

26932718
case Operator::TYPE_ARRAY_PREPEND:
26942719
$bindKey = "op_{$bindIndex}";
26952720
$bindIndex++;
2696-
return "{$quotedColumn} = :$bindKey::jsonb || COALESCE({$columnRef}, '[]'::jsonb)";
2721+
return "{$quotedColumn} = :$bindKey::jsonb || {$columnRef}";
26972722

26982723
case Operator::TYPE_ARRAY_UNIQUE:
26992724
return "{$quotedColumn} = COALESCE((

src/Database/Adapter/SQL.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2599,8 +2599,12 @@ public function upsertDocuments(
25992599
$spatialAttributes = $this->getSpatialAttributes($collection);
26002600

26012601
$attributeDefaults = [];
2602+
$arrayAttributes = [];
26022603
foreach ($collection->getAttribute('attributes', []) as $attr) {
26032604
$attributeDefaults[$attr['$id']] = $attr['default'] ?? null;
2605+
if ($attr['array'] ?? false) {
2606+
$arrayAttributes[$attr['$id']] = true;
2607+
}
26042608
}
26052609

26062610
$collection = $collection->getId();
@@ -2671,6 +2675,11 @@ public function upsertDocuments(
26712675
foreach ($allColumnNames as $attributeKey) {
26722676
$attrValue = $currentRegularAttributes[$attributeKey] ?? null;
26732677

2678+
// Array columns are NOT NULL DEFAULT '[]', so coerce null to empty array
2679+
if ($attrValue === null && isset($arrayAttributes[$attributeKey])) {
2680+
$attrValue = '[]';
2681+
}
2682+
26742683
if (\is_array($attrValue)) {
26752684
$attrValue = \json_encode($attrValue);
26762685
}
@@ -2798,6 +2807,11 @@ public function upsertDocuments(
27982807
foreach ($allColumnNames as $attributeKey) {
27992808
$attrValue = $currentRegularAttributes[$attributeKey] ?? null;
28002809

2810+
// Array columns are NOT NULL DEFAULT '[]', so coerce null to empty array
2811+
if ($attrValue === null && isset($arrayAttributes[$attributeKey])) {
2812+
$attrValue = '[]';
2813+
}
2814+
28012815
if (\is_array($attrValue)) {
28022816
$attrValue = \json_encode($attrValue);
28032817
}

src/Database/Database.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8200,6 +8200,12 @@ public function encode(Document $collection, Document $document, bool $applyDefa
82008200

82018201
// Continue on optional param with no default
82028202
if (is_null($value) && is_null($default)) {
8203+
// Non-required array columns are NOT NULL with DEFAULT '[]', so coerce null to empty array.
8204+
// Required arrays must remain null so the Structure validator catches the missing value.
8205+
$required = $attribute['required'] ?? false;
8206+
if ($array && !$required) {
8207+
$document->setAttribute($key, []);
8208+
}
82038209
continue;
82048210
}
82058211

0 commit comments

Comments
 (0)