Skip to content

Commit 535c8c5

Browse files
Merge remote-tracking branch 'origin/main' into nested-object-index
2 parents 798f49e + 4d618c6 commit 535c8c5

16 files changed

Lines changed: 1856 additions & 83 deletions

File tree

composer.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Database/Adapter.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,10 +688,12 @@ abstract public function renameIndex(string $collection, string $old, string $ne
688688
* @param array<int> $lengths
689689
* @param array<string> $orders
690690
* @param array<string,string> $indexAttributeTypes
691+
* @param array<string, mixed> $collation
692+
* @param int $ttl
691693
*
692694
* @return bool
693695
*/
694-
abstract public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $indexAttributeTypes = []): bool;
696+
abstract public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $indexAttributeTypes = [], array $collation = [], int $ttl = 1): bool;
695697

696698
/**
697699
* Delete Index
@@ -1519,4 +1521,14 @@ public function getSupportForRegex(): bool
15191521
{
15201522
return $this->getSupportForPCRERegex() || $this->getSupportForPOSIXRegex();
15211523
}
1524+
1525+
/**
1526+
* Are ttl indexes supported?
1527+
*
1528+
* @return bool
1529+
*/
1530+
public function getSupportForTTLIndexes(): bool
1531+
{
1532+
return false;
1533+
}
15221534
}

src/Database/Adapter/MariaDB.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ public function renameIndex(string $collection, string $old, string $new): bool
716716
* @return bool
717717
* @throws DatabaseException
718718
*/
719-
public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $indexAttributeTypes = []): bool
719+
public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $indexAttributeTypes = [], array $collation = [], int $ttl = 1): bool
720720
{
721721
$metadataCollection = new Document(['$id' => Database::METADATA]);
722722
$collection = $this->getDocument($metadataCollection, $collection);
@@ -2283,4 +2283,9 @@ public function getSupportForPOSIXRegex(): bool
22832283
{
22842284
return false;
22852285
}
2286+
2287+
public function getSupportForTTLIndexes(): bool
2288+
{
2289+
return false;
2290+
}
22862291
}

src/Database/Adapter/Mongo.php

Lines changed: 188 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ public function createCollection(string $name, array $attributes = [], array $in
488488
$orders = $index->getAttribute('orders');
489489

490490
// If sharedTables, always add _tenant as the first key
491-
if ($this->sharedTables) {
491+
if ($this->shouldAddTenantToIndex($index)) {
492492
$key['_tenant'] = $this->getOrder(Database::ORDER_ASC);
493493
}
494494

@@ -508,6 +508,9 @@ public function createCollection(string $name, array $attributes = [], array $in
508508
$order = $this->getOrder($this->filter($orders[$j] ?? Database::ORDER_ASC));
509509
$unique = true;
510510
break;
511+
case Database::INDEX_TTL:
512+
$order = $this->getOrder($this->filter($orders[$j] ?? Database::ORDER_ASC));
513+
break;
511514
default:
512515
// index not supported
513516
return false;
@@ -526,6 +529,14 @@ public function createCollection(string $name, array $attributes = [], array $in
526529
$newIndexes[$i]['default_language'] = 'none';
527530
}
528531

532+
// Handle TTL indexes
533+
if ($index->getAttribute('type') === Database::INDEX_TTL) {
534+
$ttl = $index->getAttribute('ttl', 0);
535+
if ($ttl > 0) {
536+
$newIndexes[$i]['expireAfterSeconds'] = $ttl;
537+
}
538+
}
539+
529540
// Add partial filter for indexes to avoid indexing null values
530541
if (in_array($index->getAttribute('type'), [
531542
Database::INDEX_UNIQUE,
@@ -901,10 +912,11 @@ public function deleteRelationship(
901912
* @param array<string> $orders
902913
* @param array<string, string> $indexAttributeTypes
903914
* @param array<string, mixed> $collation
915+
* @param int $ttl
904916
* @return bool
905917
* @throws Exception
906918
*/
907-
public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $indexAttributeTypes = [], array $collation = []): bool
919+
public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $indexAttributeTypes = [], array $collation = [], int $ttl = 1): bool
908920
{
909921
$name = $this->getNamespace() . '_' . $this->filter($collection);
910922
$id = $this->filter($id);
@@ -913,7 +925,7 @@ public function createIndex(string $collection, string $id, string $type, array
913925
$indexes['name'] = $id;
914926

915927
// If sharedTables, always add _tenant as the first key
916-
if ($this->sharedTables) {
928+
if ($this->shouldAddTenantToIndex($type)) {
917929
$indexes['key']['_tenant'] = $this->getOrder(Database::ORDER_ASC);
918930
}
919931

@@ -939,6 +951,8 @@ public function createIndex(string $collection, string $id, string $type, array
939951
case Database::INDEX_UNIQUE:
940952
$indexes['unique'] = true;
941953
break;
954+
case Database::INDEX_TTL:
955+
break;
942956
default:
943957
return false;
944958
}
@@ -967,6 +981,11 @@ public function createIndex(string $collection, string $id, string $type, array
967981
$indexes['default_language'] = 'none';
968982
}
969983

984+
// Handle TTL indexes
985+
if ($type === Database::INDEX_TTL && $ttl > 0) {
986+
$indexes['expireAfterSeconds'] = $ttl;
987+
}
988+
970989
// Add partial filter for indexes to avoid indexing null values
971990
if (in_array($type, [Database::INDEX_UNIQUE, Database::INDEX_KEY])) {
972991
$partialFilter = [];
@@ -1079,7 +1098,7 @@ public function renameIndex(string $collection, string $old, string $new): bool
10791098

10801099
try {
10811100
$deletedindex = $this->deleteIndex($collection, $old);
1082-
$createdindex = $this->createIndex($collection, $new, $index['type'], $index['attributes'], $index['lengths'] ?? [], $index['orders'] ?? [], $indexAttributeTypes);
1101+
$createdindex = $this->createIndex($collection, $new, $index['type'], $index['attributes'], $index['lengths'] ?? [], $index['orders'] ?? [], $indexAttributeTypes, [], $index['ttl'] ?? 0);
10831102
} catch (\Exception $e) {
10841103
throw $this->processException($e);
10851104
}
@@ -1241,30 +1260,7 @@ public function castingAfter(Document $collection, Document $document): Document
12411260
$node = (int)$node;
12421261
break;
12431262
case Database::VAR_DATETIME:
1244-
if ($node instanceof UTCDateTime) {
1245-
// Handle UTCDateTime objects
1246-
$node = DateTime::format($node->toDateTime());
1247-
} elseif (is_array($node) && isset($node['$date'])) {
1248-
// Handle Extended JSON format from (array) cast
1249-
// Format: {"$date":{"$numberLong":"1760405478290"}}
1250-
if (is_array($node['$date']) && isset($node['$date']['$numberLong'])) {
1251-
$milliseconds = (int)$node['$date']['$numberLong'];
1252-
$seconds = intdiv($milliseconds, 1000);
1253-
$microseconds = ($milliseconds % 1000) * 1000;
1254-
$dateTime = \DateTime::createFromFormat('U.u', $seconds . '.' . str_pad((string)$microseconds, 6, '0'));
1255-
if ($dateTime) {
1256-
$dateTime->setTimezone(new \DateTimeZone('UTC'));
1257-
$node = DateTime::format($dateTime);
1258-
}
1259-
}
1260-
} elseif (is_string($node)) {
1261-
// Already a string, validate and pass through
1262-
try {
1263-
new \DateTime($node);
1264-
} catch (\Exception $e) {
1265-
// Invalid date string, skip
1266-
}
1267-
}
1263+
$node = $this->convertUTCDateToString($node);
12681264
break;
12691265
case Database::VAR_OBJECT:
12701266
// Convert stdClass objects to arrays for object attributes
@@ -1285,6 +1281,8 @@ public function castingAfter(Document $collection, Document $document): Document
12851281
// mongodb results out a stdclass for objects
12861282
if (is_object($value) && get_class($value) === stdClass::class) {
12871283
$document->setAttribute($key, $this->convertStdClassToArray($value));
1284+
} elseif ($value instanceof UTCDateTime) {
1285+
$document->setAttribute($key, $this->convertUTCDateToString($value));
12881286
}
12891287
}
12901288
}
@@ -1367,6 +1365,24 @@ public function castingBefore(Document $collection, Document $document): Documen
13671365
unset($node);
13681366
$document->setAttribute($key, ($array) ? $value : $value[0]);
13691367
}
1368+
$indexes = $collection->getAttribute('indexes');
1369+
$ttlIndexes = array_filter($indexes, fn ($index) => $index->getAttribute('type') === Database::INDEX_TTL);
1370+
1371+
if (!$this->getSupportForAttributes()) {
1372+
foreach ($document->getArrayCopy() as $key => $value) {
1373+
if (in_array($this->getInternalKeyForAttribute($key), Database::INTERNAL_ATTRIBUTE_KEYS)) {
1374+
continue;
1375+
}
1376+
if (is_string($value) && (in_array($key, $ttlIndexes) || $this->isExtendedISODatetime($value))) {
1377+
try {
1378+
$newValue = new UTCDateTime(new \DateTime($value));
1379+
$document->setAttribute($key, $newValue);
1380+
} catch (\Throwable $th) {
1381+
// skip -> a valid string
1382+
}
1383+
}
1384+
}
1385+
}
13701386

13711387
return $document;
13721388
}
@@ -2086,7 +2102,7 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
20862102

20872103
foreach ($moreResults as $result) {
20882104
$record = $this->replaceChars('_', '$', (array)$result);
2089-
$found[] = new Document($record);
2105+
$found[] = new Document($this->convertStdClassToArray($record));
20902106
}
20912107

20922108
$cursorId = (int)($moreResponse->cursor->id ?? 0);
@@ -2716,6 +2732,25 @@ protected function getOrder(string $order): int
27162732
};
27172733
}
27182734

2735+
/**
2736+
* Check if tenant should be added to index
2737+
*
2738+
* @param Document|string $indexOrType Index document or index type string
2739+
* @return bool
2740+
*/
2741+
protected function shouldAddTenantToIndex(Document|string $indexOrType): bool
2742+
{
2743+
if (!$this->sharedTables) {
2744+
return false;
2745+
}
2746+
2747+
$indexType = $indexOrType instanceof Document
2748+
? $indexOrType->getAttribute('type')
2749+
: $indexOrType;
2750+
2751+
return $indexType !== Database::INDEX_TTL;
2752+
}
2753+
27192754
/**
27202755
* @param array<string> $selections
27212756
* @param string $prefix
@@ -3475,4 +3510,128 @@ public function getSupportForTrigramIndex(): bool
34753510
{
34763511
return false;
34773512
}
3513+
3514+
public function getSupportForTTLIndexes(): bool
3515+
{
3516+
return true;
3517+
}
3518+
3519+
protected function isExtendedISODatetime(string $val): bool
3520+
{
3521+
/**
3522+
* Min:
3523+
* YYYY-MM-DDTHH:mm:ssZ (20)
3524+
* YYYY-MM-DDTHH:mm:ss+HH:MM (25)
3525+
*
3526+
* Max:
3527+
* YYYY-MM-DDTHH:mm:ss.fffffZ (26)
3528+
* YYYY-MM-DDTHH:mm:ss.fffff+HH:MM (31)
3529+
*/
3530+
3531+
$len = strlen($val);
3532+
3533+
// absolute minimum
3534+
if ($len < 20) {
3535+
return false;
3536+
}
3537+
3538+
// fixed datetime fingerprints
3539+
if (
3540+
!isset($val[19]) ||
3541+
$val[4] !== '-' ||
3542+
$val[7] !== '-' ||
3543+
$val[10] !== 'T' ||
3544+
$val[13] !== ':' ||
3545+
$val[16] !== ':'
3546+
) {
3547+
return false;
3548+
}
3549+
3550+
// timezone detection
3551+
$hasZ = ($val[$len - 1] === 'Z');
3552+
3553+
$hasOffset = (
3554+
$len >= 25 &&
3555+
($val[$len - 6] === '+' || $val[$len - 6] === '-') &&
3556+
$val[$len - 3] === ':'
3557+
);
3558+
3559+
if (!$hasZ && !$hasOffset) {
3560+
return false;
3561+
}
3562+
3563+
if ($hasOffset && $len > 31) {
3564+
return false;
3565+
}
3566+
3567+
if ($hasZ && $len > 26) {
3568+
return false;
3569+
}
3570+
3571+
$digitPositions = [
3572+
0,1,2,3,
3573+
5,6,
3574+
8,9,
3575+
11,12,
3576+
14,15,
3577+
17,18
3578+
];
3579+
3580+
$timeEnd = $hasZ ? $len - 1 : $len - 6;
3581+
3582+
// fractional seconds
3583+
if ($timeEnd > 19) {
3584+
if ($val[19] !== '.' || $timeEnd < 21) {
3585+
return false;
3586+
}
3587+
for ($i = 20; $i < $timeEnd; $i++) {
3588+
$digitPositions[] = $i;
3589+
}
3590+
}
3591+
3592+
// timezone offset numeric digits
3593+
if ($hasOffset) {
3594+
foreach ([$len - 5, $len - 4, $len - 2, $len - 1] as $i) {
3595+
$digitPositions[] = $i;
3596+
}
3597+
}
3598+
3599+
foreach ($digitPositions as $i) {
3600+
if (!ctype_digit($val[$i])) {
3601+
return false;
3602+
}
3603+
}
3604+
3605+
return true;
3606+
}
3607+
3608+
protected function convertUTCDateToString(mixed $node): mixed
3609+
{
3610+
if ($node instanceof UTCDateTime) {
3611+
// Handle UTCDateTime objects
3612+
$node = DateTime::format($node->toDateTime());
3613+
} elseif (is_array($node) && isset($node['$date'])) {
3614+
// Handle Extended JSON format from (array) cast
3615+
// Format: {"$date":{"$numberLong":"1760405478290"}}
3616+
if (is_array($node['$date']) && isset($node['$date']['$numberLong'])) {
3617+
$milliseconds = (int)$node['$date']['$numberLong'];
3618+
$seconds = intdiv($milliseconds, 1000);
3619+
$microseconds = ($milliseconds % 1000) * 1000;
3620+
$dateTime = \DateTime::createFromFormat('U.u', $seconds . '.' . str_pad((string)$microseconds, 6, '0'));
3621+
if ($dateTime) {
3622+
$dateTime->setTimezone(new \DateTimeZone('UTC'));
3623+
$node = DateTime::format($dateTime);
3624+
}
3625+
}
3626+
} elseif (is_string($node)) {
3627+
// Already a string, validate and pass through
3628+
try {
3629+
new \DateTime($node);
3630+
} catch (\Exception $e) {
3631+
// Invalid date string, skip
3632+
}
3633+
}
3634+
3635+
return $node;
3636+
}
34783637
}

src/Database/Adapter/MySQL.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,9 @@ protected function getOperatorSQL(string $column, \Utopia\Database\Operator $ope
319319
// For all other operators, use parent implementation
320320
return parent::getOperatorSQL($column, $operator, $bindIndex);
321321
}
322+
323+
public function getSupportForTTLIndexes(): bool
324+
{
325+
return false;
326+
}
322327
}

src/Database/Adapter/Pool.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public function renameIndex(string $collection, string $old, string $new): bool
187187
return $this->delegate(__FUNCTION__, \func_get_args());
188188
}
189189

190-
public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $indexAttributeTypes = []): bool
190+
public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $indexAttributeTypes = [], array $collation = [], int $ttl = 1): bool
191191
{
192192
return $this->delegate(__FUNCTION__, \func_get_args());
193193
}
@@ -642,4 +642,9 @@ public function getSupportNonUtfCharacters(): bool
642642
{
643643
return $this->delegate(__FUNCTION__, \func_get_args());
644644
}
645+
646+
public function getSupportForTTLIndexes(): bool
647+
{
648+
return $this->delegate(__FUNCTION__, \func_get_args());
649+
}
645650
}

0 commit comments

Comments
 (0)