diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cc5ecdcf1..2aeae21655 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -480,6 +480,10 @@ jobs: _APP_BROWSER_HOST: http://invalid-browser/v1 _APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }} _APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }} + _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }} + _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }} + _APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }} + _APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }} run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose pull --quiet --ignore-buildable @@ -554,6 +558,10 @@ jobs: _APP_OPTIONS_ABUSE: enabled _APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }} _APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }} + _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }} + _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }} + _APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }} + _APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }} run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose pull --quiet --ignore-buildable @@ -611,6 +619,10 @@ jobs: env: _APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }} _APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }} + _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }} + _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }} + _APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }} + _APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }} run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose pull --quiet --ignore-buildable diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 2bec412882..156e151501 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -1306,11 +1306,12 @@ return function (Container $container): void { $dsn = new DSN('mysql://' . $project->getAttribute('database')); } - $pool = $pools->get($databaseDSN->getHost()); + $databaseHost = $databaseDSN->getHost(); + $pool = $pools->get($databaseHost); $adapter = new DatabasePool($pool); $database = new Database($adapter, $cache); - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + $sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''))); $database ->setDatabase(APP_DATABASE) @@ -1321,10 +1322,31 @@ return function (Container $container): void { ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); // inside pools authorization needs to be set first $database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB); - if (\in_array($dsn->getHost(), $sharedTables)) { + + // For separate pools (documentsdb/vectorsdb), check their own shared tables config. + // If not configured, use dedicated mode to avoid cross-engine tenant type mismatches. + if ($databaseHost !== $dsn->getHost()) { + $dbTypeSharedTables = match ($databaseType) { + DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))), + VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))), + default => [], + }; + + if (\in_array($databaseHost, $dbTypeSharedTables)) { + $database + ->setSharedTables(true) + ->setTenant($project->getSequence()) + ->setNamespace($databaseDSN->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + } + } elseif (\in_array($dsn->getHost(), $sharedTables)) { $database ->setSharedTables(true) - ->setTenant((int) $project->getSequence()) + ->setTenant($project->getSequence()) ->setNamespace($dsn->getParam('namespace')); } else { $database diff --git a/app/init/worker/message.php b/app/init/worker/message.php index a444ada91d..95477088ce 100644 --- a/app/init/worker/message.php +++ b/app/init/worker/message.php @@ -202,7 +202,8 @@ return function (Container $container): void { } $pools = $register->get('pools'); - $pool = $pools->get($databaseDSN->getHost()); + $databaseHost = $databaseDSN->getHost(); + $pool = $pools->get($databaseHost); $adapter = new DatabasePool($pool); $database = new Database($adapter, $cache); @@ -211,12 +212,32 @@ return function (Container $container): void { ->setAuthorization($authorization); $database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB); - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + $sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''))); - if (\in_array($dsn->getHost(), $sharedTables, true)) { + // For separate pools (documentsdb/vectorsdb), check their own shared tables config. + // If not configured, use dedicated mode to avoid cross-engine tenant type mismatches. + if ($databaseHost !== $dsn->getHost()) { + $dbTypeSharedTables = match ($databaseType) { + DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))), + VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))), + default => [], + }; + + if (\in_array($databaseHost, $dbTypeSharedTables)) { + $database + ->setSharedTables(true) + ->setTenant($projectDocument->getSequence()) + ->setNamespace($databaseDSN->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $projectDocument->getSequence()); + } + } elseif (\in_array($dsn->getHost(), $sharedTables, true)) { $database ->setSharedTables(true) - ->setTenant((int) $projectDocument->getSequence()) + ->setTenant($projectDocument->getSequence()) ->setNamespace($dsn->getParam('namespace')); } else { $database diff --git a/composer.lock b/composer.lock index de0628e76c..813dfe3c1d 100644 --- a/composer.lock +++ b/composer.lock @@ -3850,16 +3850,16 @@ }, { "name": "utopia-php/database", - "version": "5.3.17", + "version": "5.3.19", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "cff2b6ed63d3291b74110d086e16ff089fe05993" + "reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993", - "reference": "cff2b6ed63d3291b74110d086e16ff089fe05993", + "url": "https://api.github.com/repos/utopia-php/database/zipball/72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691", + "reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691", "shasum": "" }, "require": { @@ -3903,9 +3903,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/5.3.17" + "source": "https://github.com/utopia-php/database/tree/5.3.19" }, - "time": "2026-03-20T01:18:52+00:00" + "time": "2026-03-31T15:52:08+00:00" }, { "name": "utopia-php/detector", diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php index 3585bc4477..3d07c65250 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php @@ -61,16 +61,16 @@ class Create extends Action $databaseKeys = System::getEnv('_APP_DATABASE_DOCUMENTSDB_KEYS', ''); $databaseOverride = System::getEnv('_APP_DATABASE_DOCUMENTSDB_OVERRIDE'); $dbScheme = System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb'); - $databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', '')); - $databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', '')); + $databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))); + $databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''))); break; case VECTORSDB: $databases = Config::getParam('pools-vectorsdb', []); $databaseKeys = System::getEnv('_APP_DATABASE_VECTORSDB_KEYS', ''); $databaseOverride = System::getEnv('_APP_DATABASE_VECTORSDB_OVERRIDE'); $dbScheme = System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql'); - $databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', '')); - $databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', '')); + $databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))); + $databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''))); break; default: // legacy/tablesdb @@ -108,7 +108,7 @@ class Create extends Action if ($index !== false) { $selectedDsn = $databases[$index]; } else { - if (!empty($dsn)) { + if (!empty($dsn) && !empty($databaseSharedTables)) { $beforeFilter = \array_values($databases); if ($isSharedTablesV1) { $databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV1)); @@ -118,7 +118,10 @@ class Create extends Action $databases = array_filter($databases, fn ($value) => !\in_array($value, $databaseSharedTables)); } } - $selectedDsn = !empty($databases) ? $databases[array_rand($databases)] : ''; + if (empty($databases)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "No {$databasetype} database pool available for the current shared-tables mode"); + } + $selectedDsn = $databases[array_rand($databases)]; } if (\in_array($selectedDsn, $databaseSharedTables)) { diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index f6200ed209..03723bf231 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -3039,8 +3039,21 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals(200, $update['headers']['status-code']); - $event = json_decode($client->receive(), true); + // Drain WebSocket messages until the .update event arrives. + // Earlier events (e.g. a late-arriving .create from the row seed above) are skipped. + $updateEvent = "tablesdb.{$databaseId}.tables.{$tableId}.rows.{$rowId}.update"; + $event = null; + $deadline = \time() + 10; + while (\time() < $deadline) { + $raw = $client->receive(); + $msg = json_decode($raw, true); + if (($msg['type'] ?? '') === 'event' && \in_array($updateEvent, $msg['data']['events'] ?? [])) { + $event = $msg; + break; + } + } + $this->assertNotNull($event, 'Timed out waiting for the row update event'); $this->assertArrayHasKey('type', $event); $this->assertArrayHasKey('data', $event); $this->assertEquals('event', $event['type']);