Skip to content

Commit 2e15b29

Browse files
committed
Merge branch 'main' of github.com:utopia-php/database into joins9
# Conflicts: # src/Database/Adapter/Mongo.php
2 parents 35cfaa7 + c2ad814 commit 2e15b29

15 files changed

Lines changed: 1854 additions & 81 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
@@ -1514,4 +1516,14 @@ public function getSupportForRegex(): bool
15141516
{
15151517
return $this->getSupportForPCRERegex() || $this->getSupportForPOSIXRegex();
15161518
}
1519+
1520+
/**
1521+
* Are ttl indexes supported?
1522+
*
1523+
* @return bool
1524+
*/
1525+
public function getSupportForTTLIndexes(): bool
1526+
{
1527+
return false;
1528+
}
15171529
}

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);
@@ -2292,4 +2292,9 @@ public function getSupportForPOSIXRegex(): bool
22922292
{
22932293
return false;
22942294
}
2295+
2296+
public function getSupportForTTLIndexes(): bool
2297+
{
2298+
return false;
2299+
}
22952300
}

src/Database/Adapter/Mongo.php

Lines changed: 187 additions & 28 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

@@ -933,6 +945,8 @@ public function createIndex(string $collection, string $id, string $type, array
933945
case Database::INDEX_UNIQUE:
934946
$indexes['unique'] = true;
935947
break;
948+
case Database::INDEX_TTL:
949+
break;
936950
default:
937951
return false;
938952
}
@@ -961,6 +975,11 @@ public function createIndex(string $collection, string $id, string $type, array
961975
$indexes['default_language'] = 'none';
962976
}
963977

978+
// Handle TTL indexes
979+
if ($type === Database::INDEX_TTL && $ttl > 0) {
980+
$indexes['expireAfterSeconds'] = $ttl;
981+
}
982+
964983
// Add partial filter for indexes to avoid indexing null values
965984
if (in_array($type, [Database::INDEX_UNIQUE, Database::INDEX_KEY])) {
966985
$partialFilter = [];
@@ -1073,7 +1092,7 @@ public function renameIndex(string $collection, string $old, string $new): bool
10731092

10741093
try {
10751094
$deletedindex = $this->deleteIndex($collection, $old);
1076-
$createdindex = $this->createIndex($collection, $new, $index['type'], $index['attributes'], $index['lengths'] ?? [], $index['orders'] ?? [], $indexAttributeTypes);
1095+
$createdindex = $this->createIndex($collection, $new, $index['type'], $index['attributes'], $index['lengths'] ?? [], $index['orders'] ?? [], $indexAttributeTypes, [], $index['ttl'] ?? 0);
10771096
} catch (\Exception $e) {
10781097
throw $this->processException($e);
10791098
}
@@ -1245,30 +1264,7 @@ public function castingAfter(Document $collection, Document $document): Document
12451264
$node = (int)$node;
12461265
break;
12471266
case Database::VAR_DATETIME:
1248-
if ($node instanceof UTCDateTime) {
1249-
// Handle UTCDateTime objects
1250-
$node = DateTime::format($node->toDateTime());
1251-
} elseif (is_array($node) && isset($node['$date'])) {
1252-
// Handle Extended JSON format from (array) cast
1253-
// Format: {"$date":{"$numberLong":"1760405478290"}}
1254-
if (is_array($node['$date']) && isset($node['$date']['$numberLong'])) {
1255-
$milliseconds = (int)$node['$date']['$numberLong'];
1256-
$seconds = intdiv($milliseconds, 1000);
1257-
$microseconds = ($milliseconds % 1000) * 1000;
1258-
$dateTime = \DateTime::createFromFormat('U.u', $seconds . '.' . str_pad((string)$microseconds, 6, '0'));
1259-
if ($dateTime) {
1260-
$dateTime->setTimezone(new \DateTimeZone('UTC'));
1261-
$node = DateTime::format($dateTime);
1262-
}
1263-
}
1264-
} elseif (is_string($node)) {
1265-
// Already a string, validate and pass through
1266-
try {
1267-
new \DateTime($node);
1268-
} catch (\Exception $e) {
1269-
// Invalid date string, skip
1270-
}
1271-
}
1267+
$node = $this->convertUTCDateToString($node);
12721268
break;
12731269
case Database::VAR_OBJECT:
12741270
// Convert stdClass objects to arrays for object attributes
@@ -1289,6 +1285,8 @@ public function castingAfter(Document $collection, Document $document): Document
12891285
// mongodb results out a stdclass for objects
12901286
if (is_object($value) && get_class($value) === stdClass::class) {
12911287
$document->setAttribute($key, $this->convertStdClassToArray($value));
1288+
} elseif ($value instanceof UTCDateTime) {
1289+
$document->setAttribute($key, $this->convertUTCDateToString($value));
12921290
}
12931291
}
12941292
}
@@ -1371,6 +1369,24 @@ public function castingBefore(Document $collection, Document $document): Documen
13711369
unset($node);
13721370
$document->setAttribute($key, ($array) ? $value : $value[0]);
13731371
}
1372+
$indexes = $collection->getAttribute('indexes');
1373+
$ttlIndexes = array_filter($indexes, fn ($index) => $index->getAttribute('type') === Database::INDEX_TTL);
1374+
1375+
if (!$this->getSupportForAttributes()) {
1376+
foreach ($document->getArrayCopy() as $key => $value) {
1377+
if (in_array($this->getInternalKeyForAttribute($key), Database::INTERNAL_ATTRIBUTE_KEYS)) {
1378+
continue;
1379+
}
1380+
if (is_string($value) && (in_array($key, $ttlIndexes) || $this->isExtendedISODatetime($value))) {
1381+
try {
1382+
$newValue = new UTCDateTime(new \DateTime($value));
1383+
$document->setAttribute($key, $newValue);
1384+
} catch (\Throwable $th) {
1385+
// skip -> a valid string
1386+
}
1387+
}
1388+
}
1389+
}
13741390

13751391
return $document;
13761392
}
@@ -2752,6 +2768,25 @@ protected function getOrder(string $order): int
27522768
};
27532769
}
27542770

2771+
/**
2772+
* Check if tenant should be added to index
2773+
*
2774+
* @param Document|string $indexOrType Index document or index type string
2775+
* @return bool
2776+
*/
2777+
protected function shouldAddTenantToIndex(Document|string $indexOrType): bool
2778+
{
2779+
if (!$this->sharedTables) {
2780+
return false;
2781+
}
2782+
2783+
$indexType = $indexOrType instanceof Document
2784+
? $indexOrType->getAttribute('type')
2785+
: $indexOrType;
2786+
2787+
return $indexType !== Database::INDEX_TTL;
2788+
}
2789+
27552790
/**
27562791
* @param array<Query> $selects
27572792
*
@@ -3519,4 +3554,128 @@ public function getSupportForTrigramIndex(): bool
35193554
{
35203555
return false;
35213556
}
3557+
3558+
public function getSupportForTTLIndexes(): bool
3559+
{
3560+
return true;
3561+
}
3562+
3563+
protected function isExtendedISODatetime(string $val): bool
3564+
{
3565+
/**
3566+
* Min:
3567+
* YYYY-MM-DDTHH:mm:ssZ (20)
3568+
* YYYY-MM-DDTHH:mm:ss+HH:MM (25)
3569+
*
3570+
* Max:
3571+
* YYYY-MM-DDTHH:mm:ss.fffffZ (26)
3572+
* YYYY-MM-DDTHH:mm:ss.fffff+HH:MM (31)
3573+
*/
3574+
3575+
$len = strlen($val);
3576+
3577+
// absolute minimum
3578+
if ($len < 20) {
3579+
return false;
3580+
}
3581+
3582+
// fixed datetime fingerprints
3583+
if (
3584+
!isset($val[19]) ||
3585+
$val[4] !== '-' ||
3586+
$val[7] !== '-' ||
3587+
$val[10] !== 'T' ||
3588+
$val[13] !== ':' ||
3589+
$val[16] !== ':'
3590+
) {
3591+
return false;
3592+
}
3593+
3594+
// timezone detection
3595+
$hasZ = ($val[$len - 1] === 'Z');
3596+
3597+
$hasOffset = (
3598+
$len >= 25 &&
3599+
($val[$len - 6] === '+' || $val[$len - 6] === '-') &&
3600+
$val[$len - 3] === ':'
3601+
);
3602+
3603+
if (!$hasZ && !$hasOffset) {
3604+
return false;
3605+
}
3606+
3607+
if ($hasOffset && $len > 31) {
3608+
return false;
3609+
}
3610+
3611+
if ($hasZ && $len > 26) {
3612+
return false;
3613+
}
3614+
3615+
$digitPositions = [
3616+
0,1,2,3,
3617+
5,6,
3618+
8,9,
3619+
11,12,
3620+
14,15,
3621+
17,18
3622+
];
3623+
3624+
$timeEnd = $hasZ ? $len - 1 : $len - 6;
3625+
3626+
// fractional seconds
3627+
if ($timeEnd > 19) {
3628+
if ($val[19] !== '.' || $timeEnd < 21) {
3629+
return false;
3630+
}
3631+
for ($i = 20; $i < $timeEnd; $i++) {
3632+
$digitPositions[] = $i;
3633+
}
3634+
}
3635+
3636+
// timezone offset numeric digits
3637+
if ($hasOffset) {
3638+
foreach ([$len - 5, $len - 4, $len - 2, $len - 1] as $i) {
3639+
$digitPositions[] = $i;
3640+
}
3641+
}
3642+
3643+
foreach ($digitPositions as $i) {
3644+
if (!ctype_digit($val[$i])) {
3645+
return false;
3646+
}
3647+
}
3648+
3649+
return true;
3650+
}
3651+
3652+
protected function convertUTCDateToString(mixed $node): mixed
3653+
{
3654+
if ($node instanceof UTCDateTime) {
3655+
// Handle UTCDateTime objects
3656+
$node = DateTime::format($node->toDateTime());
3657+
} elseif (is_array($node) && isset($node['$date'])) {
3658+
// Handle Extended JSON format from (array) cast
3659+
// Format: {"$date":{"$numberLong":"1760405478290"}}
3660+
if (is_array($node['$date']) && isset($node['$date']['$numberLong'])) {
3661+
$milliseconds = (int)$node['$date']['$numberLong'];
3662+
$seconds = intdiv($milliseconds, 1000);
3663+
$microseconds = ($milliseconds % 1000) * 1000;
3664+
$dateTime = \DateTime::createFromFormat('U.u', $seconds . '.' . str_pad((string)$microseconds, 6, '0'));
3665+
if ($dateTime) {
3666+
$dateTime->setTimezone(new \DateTimeZone('UTC'));
3667+
$node = DateTime::format($dateTime);
3668+
}
3669+
}
3670+
} elseif (is_string($node)) {
3671+
// Already a string, validate and pass through
3672+
try {
3673+
new \DateTime($node);
3674+
} catch (\Exception $e) {
3675+
// Invalid date string, skip
3676+
}
3677+
}
3678+
3679+
return $node;
3680+
}
35223681
}

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
@@ -188,7 +188,7 @@ public function renameIndex(string $collection, string $old, string $new): bool
188188
return $this->delegate(__FUNCTION__, \func_get_args());
189189
}
190190

191-
public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $indexAttributeTypes = []): bool
191+
public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $indexAttributeTypes = [], array $collation = [], int $ttl = 1): bool
192192
{
193193
return $this->delegate(__FUNCTION__, \func_get_args());
194194
}
@@ -658,4 +658,9 @@ public function getSupportNonUtfCharacters(): bool
658658
{
659659
return $this->delegate(__FUNCTION__, \func_get_args());
660660
}
661+
662+
public function getSupportForTTLIndexes(): bool
663+
{
664+
return $this->delegate(__FUNCTION__, \func_get_args());
665+
}
661666
}

0 commit comments

Comments
 (0)