From 91e252382b753915e2d165276365d0e2efb9b3e1 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 20 Mar 2026 17:33:15 +1300 Subject: [PATCH 1/6] (test): add strict type assertions and list coverage for V21 $sequence filter --- .../unit/Utopia/Response/Filters/V21Test.php | 73 ++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/tests/unit/Utopia/Response/Filters/V21Test.php b/tests/unit/Utopia/Response/Filters/V21Test.php index fdff7fdf3f..a049b70d84 100644 --- a/tests/unit/Utopia/Response/Filters/V21Test.php +++ b/tests/unit/Utopia/Response/Filters/V21Test.php @@ -430,7 +430,7 @@ class V21Test extends TestCase { $result = $this->filter->parse($content, Response::MODEL_DOCUMENT); - $this->assertEquals($expected, $result); + $this->assertSame($expected, $result); } #[DataProvider('documentProvider')] @@ -438,7 +438,76 @@ class V21Test extends TestCase { $result = $this->filter->parse($content, Response::MODEL_ROW); - $this->assertEquals($expected, $result); + $this->assertSame($expected, $result); + } + + public static function documentListProvider(): array + { + return [ + 'cast $sequence in document list' => [ + [ + 'total' => 2, + 'documents' => [ + [ + '$id' => 'doc1', + '$sequence' => '10', + 'name' => 'first', + ], + [ + '$id' => 'doc2', + '$sequence' => '20', + 'name' => 'second', + ], + ], + ], + [ + 'total' => 2, + 'documents' => [ + [ + '$id' => 'doc1', + '$sequence' => 10, + 'name' => 'first', + ], + [ + '$id' => 'doc2', + '$sequence' => 20, + 'name' => 'second', + ], + ], + ] + ], + 'handle empty document list' => [ + [ + 'total' => 0, + 'documents' => [], + ], + [ + 'total' => 0, + 'documents' => [], + ] + ], + ]; + } + + #[DataProvider('documentListProvider')] + public function testDocumentList(array $content, array $expected): void + { + $result = $this->filter->parse($content, Response::MODEL_DOCUMENT_LIST); + + $this->assertSame($expected, $result); + } + + #[DataProvider('documentListProvider')] + public function testRowList(array $content, array $expected): void + { + $content['rows'] = $content['documents']; + unset($content['documents']); + $expected['rows'] = $expected['documents']; + unset($expected['documents']); + + $result = $this->filter->parse($content, Response::MODEL_ROW_LIST); + + $this->assertSame($expected, $result); } public static function defaultPassthroughProvider(): array From c9d023991de971cc403e289ec2ebb1fec24082d7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 20 Mar 2026 18:03:17 +1300 Subject: [PATCH 2/6] (test): add e2e test for $sequence query type validation per adapter --- .../e2e/Services/Databases/DatabasesBase.php | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index aecad983de..5f8ac7dd94 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -3518,6 +3518,62 @@ trait DatabasesBase $this->assertEquals(200, $response['headers']['status-code']); } + public function testQueryBySequenceType(): void + { + $data = $this->setupDocuments(); + $databaseId = $data['databaseId']; + + $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::equal('$id', $data['documentIds'])->toString(), + ], + ]); + + $this->assertEquals(200, $documents['headers']['status-code']); + $this->assertGreaterThan(0, count($documents['body'][$this->getRecordResource()])); + + $sequence = $documents['body'][$this->getRecordResource()][0]['$sequence']; + $this->assertIsString($sequence); + + // Query with string $sequence value (supported by all adapters) + $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::equal('$sequence', [$sequence])->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body'][$this->getRecordResource()]); + $this->assertIsString($response['body'][$this->getRecordResource()][0]['$sequence']); + $this->assertSame($sequence, $response['body'][$this->getRecordResource()][0]['$sequence']); + + // Query with int $sequence value (supported by SQL adapters, rejected by MongoDB) + $intSequence = (int)$sequence; + $response = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::equal('$sequence', [$intSequence])->toString(), + ], + ]); + + $adapter = getenv('_APP_DB_ADAPTER'); + if ($adapter === 'mongodb') { + $this->assertEquals(400, $response['headers']['status-code']); + } else { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body'][$this->getRecordResource()]); + $this->assertIsString($response['body'][$this->getRecordResource()][0]['$sequence']); + } + } + public function testListDocumentsAfterPagination(): void { $data = $this->setupDocuments(); From 1ad2cd68ef4903cc86172706850d83f85529ce18 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 20 Mar 2026 19:12:47 +1300 Subject: [PATCH 3/6] fix: rewrite compose/env files during upgrade so new image versions are applied useExistingConfig was preventing the compose template from being rewritten on non-local upgrades, leaving old image version tags in place. Also fix Upgrade reading hardcoded .env instead of getEnvFileName(). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Appwrite/Platform/Tasks/Install.php | 2 +- src/Appwrite/Platform/Tasks/Upgrade.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index 59a60062eb..d88f50efe6 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -508,7 +508,7 @@ class Install extends Action $this->applyLocalPaths($isLocalInstall, false); $isCLI = php_sapi_name() === 'cli'; - if ($isLocalInstall) { + if ($isLocalInstall || $isUpgrade) { $useExistingConfig = false; } else { $useExistingConfig = file_exists($this->path . '/' . $this->getComposeFileName()) diff --git a/src/Appwrite/Platform/Tasks/Upgrade.php b/src/Appwrite/Platform/Tasks/Upgrade.php index 7214ca2e8c..960f10f3e6 100644 --- a/src/Appwrite/Platform/Tasks/Upgrade.php +++ b/src/Appwrite/Platform/Tasks/Upgrade.php @@ -72,7 +72,7 @@ class Upgrade extends Install } if ($database === null) { - $envData = @file_get_contents($this->path . '/.env'); + $envData = @file_get_contents($this->path . '/' . $this->getEnvFileName()); if ($envData !== false) { $envFile = new Env($envData); $database = $envFile->list()['_APP_DB_ADAPTER'] ?? null; From d4c9af2eb27e84976373e2e2b0f181c55a1019d0 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 20 Mar 2026 20:21:50 +1300 Subject: [PATCH 4/6] fix: strip surrounding quotes when parsing .env file values Env::__construct() now strips " and ' wrapping from values so that _APP_DB_ADAPTER="mariadb" is read as mariadb, not "mariadb". Without this, the upgrade flow rejected the existing database adapter because the quoted value didn't match the whitelist. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Appwrite/Docker/Env.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Docker/Env.php b/src/Appwrite/Docker/Env.php index 51ec9e167c..26566ec484 100644 --- a/src/Appwrite/Docker/Env.php +++ b/src/Appwrite/Docker/Env.php @@ -19,7 +19,7 @@ class Env foreach ($data as &$row) { $row = explode('=', $row, 2); $key = (isset($row[0])) ? trim($row[0]) : null; - $value = (isset($row[1])) ? trim($row[1]) : null; + $value = (isset($row[1])) ? trim(trim($row[1]), '"\'') : null; if ($key) { $this->vars[$key] = $value; From ef0954cdda55b2fff96a9cd21054454670397b97 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 20 Mar 2026 21:56:06 +1300 Subject: [PATCH 5/6] fix: propagate isUpgrade flag from Upgrade to Install for CLI path Install::action() hardcoded isUpgrade=false, so the CLI upgrade path never rewrote compose/env files. Added a protected property that Upgrade sets before calling parent::action(). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Appwrite/Platform/Tasks/Install.php | 3 ++- src/Appwrite/Platform/Tasks/Upgrade.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index d88f50efe6..af768444f2 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -33,6 +33,7 @@ class Install extends Action private const string APPWRITE_API_URL = 'http://appwrite'; private const string GROWTH_API_URL = 'https://growth.appwrite.io/v1'; + protected bool $isUpgrade = false; protected string $hostPath = ''; protected ?bool $isLocalInstall = null; protected ?array $installerConfig = null; @@ -66,7 +67,7 @@ class Install extends Action bool $noStart, string $database ): void { - $isUpgrade = false; + $isUpgrade = $this->isUpgrade; $defaultHttpPort = '80'; $defaultHttpsPort = '443'; $config = Config::getParam('variables'); diff --git a/src/Appwrite/Platform/Tasks/Upgrade.php b/src/Appwrite/Platform/Tasks/Upgrade.php index 960f10f3e6..1d61180963 100644 --- a/src/Appwrite/Platform/Tasks/Upgrade.php +++ b/src/Appwrite/Platform/Tasks/Upgrade.php @@ -42,6 +42,7 @@ class Upgrade extends Install bool $noStart, string $database ): void { + $this->isUpgrade = true; $isLocalInstall = $this->isLocalInstall(); $this->applyLocalPaths($isLocalInstall, true); From 3a40b6b629d4b0d2b38514df68b53908e08b6267 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 20 Mar 2026 22:18:56 +1300 Subject: [PATCH 6/6] Apply suggestion from @greptile-apps[bot] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/Appwrite/Docker/Env.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Docker/Env.php b/src/Appwrite/Docker/Env.php index 26566ec484..3bf6fb2d50 100644 --- a/src/Appwrite/Docker/Env.php +++ b/src/Appwrite/Docker/Env.php @@ -19,7 +19,16 @@ class Env foreach ($data as &$row) { $row = explode('=', $row, 2); $key = (isset($row[0])) ? trim($row[0]) : null; - $value = (isset($row[1])) ? trim(trim($row[1]), '"\'') : null; + $value = (isset($row[1])) ? (function (string $v): string { + $v = trim($v); + if ( + (\str_starts_with($v, '"') && \str_ends_with($v, '"')) || + (\str_starts_with($v, "'") && \str_ends_with($v, "'")) + ) { + return \substr($v, 1, -1); + } + return $v; + })(trim($row[1])) : null; if ($key) { $this->vars[$key] = $value;