From 307b4588896fafe2aab6f084c1aec6586de47b67 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 4 Sep 2025 23:56:16 +1200 Subject: [PATCH 1/2] Add tests --- composer.lock | 46 +- .../Databases/TablesDB/DatabasesBase.php | 429 ++++++++++++++++++ 2 files changed, 452 insertions(+), 23 deletions(-) diff --git a/composer.lock b/composer.lock index c22b7f4b05..bd1e62dac2 100644 --- a/composer.lock +++ b/composer.lock @@ -1515,16 +1515,16 @@ }, { "name": "open-telemetry/sem-conv", - "version": "1.36.0", + "version": "1.37.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sem-conv.git", - "reference": "60dd18fd21d45e6f4234ecab89c14021b6e3de9a" + "reference": "8da7ec497c881e39afa6657d72586e27efbd29a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/60dd18fd21d45e6f4234ecab89c14021b6e3de9a", - "reference": "60dd18fd21d45e6f4234ecab89c14021b6e3de9a", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/8da7ec497c881e39afa6657d72586e27efbd29a1", + "reference": "8da7ec497c881e39afa6657d72586e27efbd29a1", "shasum": "" }, "require": { @@ -1568,7 +1568,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-08-04T03:22:08+00:00" + "time": "2025-09-03T12:08:10+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -3638,16 +3638,16 @@ }, { "name": "utopia-php/database", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "06ffa2b1c977f5451200a1ee82a500be1390a789" + "reference": "d32bd6160d55cab0cbe4b070e1c56e4c2a03c7a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/06ffa2b1c977f5451200a1ee82a500be1390a789", - "reference": "06ffa2b1c977f5451200a1ee82a500be1390a789", + "url": "https://api.github.com/repos/utopia-php/database/zipball/d32bd6160d55cab0cbe4b070e1c56e4c2a03c7a0", + "reference": "d32bd6160d55cab0cbe4b070e1c56e4c2a03c7a0", "shasum": "" }, "require": { @@ -3688,9 +3688,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/1.3.0" + "source": "https://github.com/utopia-php/database/tree/1.4.0" }, - "time": "2025-09-02T16:20:02+00:00" + "time": "2025-09-04T11:45:26+00:00" }, { "name": "utopia-php/detector", @@ -3942,16 +3942,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.22", + "version": "0.33.24", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "c01a815cb976c9255e045fc3bcc3f5fcf477e0bc" + "reference": "5112b1023342163e3fbedec99f38fc32c8700aa0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/c01a815cb976c9255e045fc3bcc3f5fcf477e0bc", - "reference": "c01a815cb976c9255e045fc3bcc3f5fcf477e0bc", + "url": "https://api.github.com/repos/utopia-php/http/zipball/5112b1023342163e3fbedec99f38fc32c8700aa0", + "reference": "5112b1023342163e3fbedec99f38fc32c8700aa0", "shasum": "" }, "require": { @@ -3983,9 +3983,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.22" + "source": "https://github.com/utopia-php/http/tree/0.33.24" }, - "time": "2025-08-26T10:29:50+00:00" + "time": "2025-09-04T04:18:39+00:00" }, { "name": "utopia-php/image", @@ -5007,16 +5007,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.1.15", + "version": "1.1.16", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "8e8e39634ba7558704522959d88f3542563a5444" + "reference": "f8fbc4b1ba0e918825338f50cbdea4d887389c41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/8e8e39634ba7558704522959d88f3542563a5444", - "reference": "8e8e39634ba7558704522959d88f3542563a5444", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f8fbc4b1ba0e918825338f50cbdea4d887389c41", + "reference": "f8fbc4b1ba0e918825338f50cbdea4d887389c41", "shasum": "" }, "require": { @@ -5052,9 +5052,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.1.15" + "source": "https://github.com/appwrite/sdk-generator/tree/1.1.16" }, - "time": "2025-08-27T04:59:35+00:00" + "time": "2025-09-03T06:50:04+00:00" }, { "name": "doctrine/annotations", diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index ada7a73301..961d0b1a85 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -6343,6 +6343,192 @@ trait DatabasesBase $this->assertEquals('Latest Event', $rows['body']['rows'][0]['name']); } + /** + * @throws \Utopia\Database\Exception + * @throws \Utopia\Database\Exception\Query + */ + public function testCreatedBetween(): void + { + // Create database + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'CreatedBetween test' + ]); + + $this->assertNotEmpty($database['body']['$id']); + $this->assertEquals(201, $database['headers']['status-code']); + $this->assertEquals('CreatedBetween test', $database['body']['name']); + + $databaseId = $database['body']['$id']; + + // Create Collection + $articles = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Articles', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + ], + ]); + + $this->assertEquals(201, $articles['headers']['status-code']); + $this->assertEquals($articles['body']['name'], 'Articles'); + + // Create Attributes + $title = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $articles['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 256, + 'required' => true, + ]); + $this->assertEquals(202, $title['headers']['status-code']); + + $content = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $articles['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'content', + 'size' => 5000, + 'required' => true, + ]); + $this->assertEquals(202, $content['headers']['status-code']); + + // Wait for attributes to be available + sleep(2); + + // Create first article + $row1 = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $articles['body']['$id'] . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'title' => 'First Article', + 'content' => 'This is the first article content', + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $row1['headers']['status-code']); + $firstArticleCreatedAt = $row1['body']['$createdAt']; + + // Sleep to ensure different timestamps + sleep(1); + + // Create second article + $row2 = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $articles['body']['$id'] . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'title' => 'Second Article', + 'content' => 'This is the second article content', + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $row2['headers']['status-code']); + $secondArticleCreatedAt = $row2['body']['$createdAt']; + + // Sleep again + sleep(1); + + // Create third article + $row3 = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $articles['body']['$id'] . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'title' => 'Third Article', + 'content' => 'This is the third article content', + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $row3['headers']['status-code']); + $thirdArticleCreatedAt = $row3['body']['$createdAt']; + + // Sleep again + sleep(1); + + // Create fourth article + $row4 = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $articles['body']['$id'] . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'title' => 'Fourth Article', + 'content' => 'This is the fourth article content', + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $row4['headers']['status-code']); + + // Test createdBetween query - should return articles created between first and third (inclusive) + $rows = $this->client->call( + Client::METHOD_GET, + '/tablesdb/' . $databaseId . '/tables/' . $articles['body']['$id'] . '/rows', + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), + [ + 'queries' => [ + Query::createdBetween($firstArticleCreatedAt, $thirdArticleCreatedAt)->toString(), + ], + ] + ); + + $this->assertEquals(200, $rows['headers']['status-code']); + $this->assertCount(3, $rows['body']['rows']); + + // Verify the returned articles are the correct ones + $titles = array_column($rows['body']['rows'], 'title'); + $this->assertContains('First Article', $titles); + $this->assertContains('Second Article', $titles); + $this->assertContains('Third Article', $titles); + $this->assertNotContains('Fourth Article', $titles); + + // Test createdBetween query - should return only the second article when using its timestamp for both bounds + $rows = $this->client->call( + Client::METHOD_GET, + '/tablesdb/' . $databaseId . '/tables/' . $articles['body']['$id'] . '/rows', + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), + [ + 'queries' => [ + Query::createdBetween($secondArticleCreatedAt, $secondArticleCreatedAt)->toString(), + ], + ] + ); + + $this->assertEquals(200, $rows['headers']['status-code']); + $this->assertCount(1, $rows['body']['rows']); + $this->assertEquals('Second Article', $rows['body']['rows'][0]['title']); + } + /** * @throws \Utopia\Database\Exception * @throws \Utopia\Database\Exception\Query @@ -6689,6 +6875,249 @@ trait DatabasesBase $this->assertEquals('delivered', $rows['body']['rows'][0]['status']); } + /** + * @throws \Utopia\Database\Exception + * @throws \Utopia\Database\Exception\Query + */ + public function testUpdatedBetween(): void + { + // Create database + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'UpdatedBetween test' + ]); + + $this->assertNotEmpty($database['body']['$id']); + $this->assertEquals(201, $database['headers']['status-code']); + $this->assertEquals('UpdatedBetween test', $database['body']['name']); + + $databaseId = $database['body']['$id']; + + // Create Collection + $products = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Products', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + ], + ]); + + $this->assertEquals(201, $products['headers']['status-code']); + $this->assertEquals($products['body']['name'], 'Products'); + + // Create Attributes + $name = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $products['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + $this->assertEquals(202, $name['headers']['status-code']); + + $price = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $products['body']['$id'] . '/columns/float', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'price', + 'required' => true, + ]); + $this->assertEquals(202, $price['headers']['status-code']); + + // Wait for attributes to be available + sleep(2); + + // Create first product + $row1 = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $products['body']['$id'] . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Product A', + 'price' => 99.99, + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $row1['headers']['status-code']); + + // Sleep to ensure different timestamps + sleep(1); + + // Create second product + $row2 = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $products['body']['$id'] . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Product B', + 'price' => 149.99, + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $row2['headers']['status-code']); + + // Sleep again + sleep(1); + + // Create third product + $row3 = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $products['body']['$id'] . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Product C', + 'price' => 199.99, + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $row3['headers']['status-code']); + + // Sleep again + sleep(1); + + // Create fourth product + $row4 = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $products['body']['$id'] . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Product D', + 'price' => 249.99, + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $row4['headers']['status-code']); + + // Now update products in sequence to get different updatedAt timestamps + sleep(1); + + // Update first product + $update1 = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $products['body']['$id'] . '/rows/' . $row1['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'price' => 89.99, + ] + ]); + $this->assertEquals(200, $update1['headers']['status-code']); + $firstProductUpdatedAt = $update1['body']['$updatedAt']; + + sleep(1); + + // Update second product + $update2 = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $products['body']['$id'] . '/rows/' . $row2['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'price' => 139.99, + ] + ]); + $this->assertEquals(200, $update2['headers']['status-code']); + $secondProductUpdatedAt = $update2['body']['$updatedAt']; + + sleep(1); + + // Update third product + $update3 = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $products['body']['$id'] . '/rows/' . $row3['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'price' => 189.99, + ] + ]); + $this->assertEquals(200, $update3['headers']['status-code']); + $thirdProductUpdatedAt = $update3['body']['$updatedAt']; + + sleep(1); + + // Update fourth product + $update4 = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $products['body']['$id'] . '/rows/' . $row4['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'price' => 239.99, + ] + ]); + $this->assertEquals(200, $update4['headers']['status-code']); + + // Test updatedBetween query - should return products updated between first and third (inclusive) + $rows = $this->client->call( + Client::METHOD_GET, + '/tablesdb/' . $databaseId . '/tables/' . $products['body']['$id'] . '/rows', + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), + [ + 'queries' => [ + Query::updatedBetween($firstProductUpdatedAt, $thirdProductUpdatedAt)->toString(), + ], + ] + ); + + $this->assertEquals(200, $rows['headers']['status-code']); + $this->assertCount(3, $rows['body']['rows']); + + // Verify the returned products are the correct ones + $names = array_column($rows['body']['rows'], 'name'); + $this->assertContains('Product A', $names); + $this->assertContains('Product B', $names); + $this->assertContains('Product C', $names); + $this->assertNotContains('Product D', $names); + + // Test updatedBetween query - should return only the second product when using its timestamp for both bounds + $rows = $this->client->call( + Client::METHOD_GET, + '/tablesdb/' . $databaseId . '/tables/' . $products['body']['$id'] . '/rows', + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), + [ + 'queries' => [ + Query::updatedBetween($secondProductUpdatedAt, $secondProductUpdatedAt)->toString(), + ], + ] + ); + + $this->assertEquals(200, $rows['headers']['status-code']); + $this->assertCount(1, $rows['body']['rows']); + $this->assertEquals('Product B', $rows['body']['rows'][0]['name']); + $this->assertEquals(139.99, $rows['body']['rows'][0]['price']); + } + /** * @depends testCreateDatabase * @param array $data From aa51bb505277ccef6835bae0a3ee2d0587cd574e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 5 Sep 2025 00:13:39 +1200 Subject: [PATCH 2/2] Lint --- tests/e2e/Services/Databases/TablesDB/DatabasesBase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index 961d0b1a85..3b220e192e 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -6501,7 +6501,7 @@ trait DatabasesBase $this->assertEquals(200, $rows['headers']['status-code']); $this->assertCount(3, $rows['body']['rows']); - + // Verify the returned articles are the correct ones $titles = array_column($rows['body']['rows'], 'title'); $this->assertContains('First Article', $titles); @@ -7089,7 +7089,7 @@ trait DatabasesBase $this->assertEquals(200, $rows['headers']['status-code']); $this->assertCount(3, $rows['body']['rows']); - + // Verify the returned products are the correct ones $names = array_column($rows['body']['rows'], 'name'); $this->assertContains('Product A', $names);