diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86f74ac..a144af8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,6 +153,10 @@ jobs: fdbcli -C fdb.cluster --exec "status" || true exit 1 + - name: Configure tenant mode + run: | + fdbcli -C fdb.cluster --exec "configure tenant_mode=optional_experimental" + - name: Run E2E tests env: FDB_CLUSTER_FILE: fdb.cluster diff --git a/src/Database.php b/src/Database.php index b7097fd..aaeaa02 100644 --- a/src/Database.php +++ b/src/Database.php @@ -27,6 +27,21 @@ public function createTransaction(): Transaction return new Transaction($trPointer, $this, $this->client); } + public function openTenant(string $name): Tenant + { + $tpointer = $this->client->fdb->new('FDBTenant*'); + $this->client->checkError( + $this->client->fdb->fdb_database_open_tenant( + $this->dpointer, + $name, + strlen($name), + FFI::addr($tpointer), + ), + ); + + return new Tenant($tpointer, $this, $this->client); + } + public function transact(callable $fn): mixed { $tr = $this->createTransaction(); diff --git a/src/NativeClient.php b/src/NativeClient.php index 260bda0..dd06187 100644 --- a/src/NativeClient.php +++ b/src/NativeClient.php @@ -71,6 +71,7 @@ final class NativeClient void fdb_tenant_destroy(FDBTenant* t); fdb_error_t fdb_tenant_create_transaction(FDBTenant* t, FDBTransaction** out_transaction); + FDBFuture* fdb_tenant_get_id(FDBTenant* tenant); void fdb_transaction_destroy(FDBTransaction* tr); void fdb_transaction_cancel(FDBTransaction* tr); diff --git a/src/Tenant.php b/src/Tenant.php index 72fc1fc..7b514eb 100644 --- a/src/Tenant.php +++ b/src/Tenant.php @@ -4,6 +4,7 @@ namespace CrazyGoat\FoundationDB; +use CrazyGoat\FoundationDB\Future\FutureInt64; use FFI; use FFI\CData; @@ -26,6 +27,16 @@ public function createTransaction(): Transaction return new Transaction($trPointer, $this->db, $this->client); } + public function getId(): int + { + $future = new FutureInt64( + $this->client->fdb->fdb_tenant_get_id($this->tpointer), + $this->client, + ); + + return $future->await(); + } + public function __destruct() { $this->client->fdb->fdb_tenant_destroy($this->tpointer); diff --git a/tests/Integration/TenantTest.php b/tests/Integration/TenantTest.php new file mode 100644 index 0000000..10b98a4 --- /dev/null +++ b/tests/Integration/TenantTest.php @@ -0,0 +1,138 @@ +configureTenantMode(); + } + + #[Test] + public function openTenantReturnsTenantInstance(): void + { + $this->createTenantViaFdbcli('test_tenant_open'); + + try { + $tenant = self::$db->openTenant('test_tenant_open'); + self::assertInstanceOf(Tenant::class, $tenant); + } finally { + $this->deleteTenantViaFdbcli('test_tenant_open'); + } + } + + #[Test] + public function tenantGetIdReturnsPositiveInteger(): void + { + $this->createTenantViaFdbcli('test_tenant_id'); + + try { + $tenant = self::$db->openTenant('test_tenant_id'); + $id = $tenant->getId(); + self::assertGreaterThan(0, $id); + } finally { + $this->deleteTenantViaFdbcli('test_tenant_id'); + } + } + + #[Test] + public function tenantGetIdReturnsSameIdForSameTenant(): void + { + $this->createTenantViaFdbcli('test_tenant_same_id'); + + try { + $tenant1 = self::$db->openTenant('test_tenant_same_id'); + $tenant2 = self::$db->openTenant('test_tenant_same_id'); + self::assertSame($tenant1->getId(), $tenant2->getId()); + } finally { + $this->deleteTenantViaFdbcli('test_tenant_same_id'); + } + } + + #[Test] + public function tenantGetIdReturnsDifferentIdsForDifferentTenants(): void + { + $this->createTenantViaFdbcli('test_tenant_diff_a'); + $this->createTenantViaFdbcli('test_tenant_diff_b'); + + try { + $tenantA = self::$db->openTenant('test_tenant_diff_a'); + $tenantB = self::$db->openTenant('test_tenant_diff_b'); + self::assertNotSame($tenantA->getId(), $tenantB->getId()); + } finally { + $this->deleteTenantViaFdbcli('test_tenant_diff_a'); + $this->deleteTenantViaFdbcli('test_tenant_diff_b'); + } + } + + #[Test] + public function tenantCanCreateTransactionAndPerformCrud(): void + { + $this->createTenantViaFdbcli('test_tenant_crud'); + + try { + $tenant = self::$db->openTenant('test_tenant_crud'); + $tr = $tenant->createTransaction(); + $tr->set('tenant_key', 'tenant_value'); + $tr->commit()->await(); + + $tr2 = $tenant->createTransaction(); + $value = $tr2->get('tenant_key')->await(); + self::assertSame('tenant_value', $value); + + $tr3 = $tenant->createTransaction(); + $tr3->clear('tenant_key'); + $tr3->commit()->await(); + } finally { + $this->deleteTenantViaFdbcli('test_tenant_crud'); + } + } + + private function configureTenantMode(): void + { + $clusterFile = getenv('FDB_CLUSTER_FILE') ?: '/etc/foundationdb/fdb.cluster'; + $output = (string) shell_exec( + "fdbcli -C {$clusterFile} --exec 'configure tenant_mode=optional_experimental' 2>&1", + ); + if (!str_contains($output, 'committed') && !str_contains($output, 'already')) { + self::markTestSkipped('Could not configure tenant mode: ' . $output); + } + } + + private function createTenantViaFdbcli(string $name): void + { + $clusterFile = getenv('FDB_CLUSTER_FILE') ?: '/etc/foundationdb/fdb.cluster'; + $output = (string) shell_exec( + "fdbcli -C {$clusterFile} --exec 'createtenant {$name}' 2>&1", + ); + if (!str_contains($output, 'created') && !str_contains($output, 'already exists')) { + self::markTestSkipped('Could not create tenant: ' . $output); + } + } + + private function deleteTenantViaFdbcli(string $name): void + { + $clusterFile = getenv('FDB_CLUSTER_FILE') ?: '/etc/foundationdb/fdb.cluster'; + shell_exec("fdbcli -C {$clusterFile} --exec 'deletetenant {$name}' 2>&1"); + } +}