diff --git a/composer.lock b/composer.lock index c3b96bfde..b841ff464 100644 --- a/composer.lock +++ b/composer.lock @@ -539,16 +539,16 @@ }, { "name": "open-telemetry/exporter-otlp", - "version": "1.3.3", + "version": "1.3.4", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "07b02bc71838463f6edcc78d3485c04b48fb263d" + "reference": "62e680d587beb42e5247aa6ecd89ad1ca406e8ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/07b02bc71838463f6edcc78d3485c04b48fb263d", - "reference": "07b02bc71838463f6edcc78d3485c04b48fb263d", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/62e680d587beb42e5247aa6ecd89ad1ca406e8ca", + "reference": "62e680d587beb42e5247aa6ecd89ad1ca406e8ca", "shasum": "" }, "require": { @@ -599,7 +599,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-11-13T08:04:37+00:00" + "time": "2026-01-15T09:31:34+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", @@ -4520,7 +4520,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/src/Database/Database.php b/src/Database/Database.php index a715fd56b..a10d20fcb 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2047,7 +2047,11 @@ public function createAttribute(string $collection, string $id, string $type, in } } - $collection->setAttribute('attributes', $attribute, Document::SET_TYPE_APPEND); + // Append attribute, removing any duplicates + $metadataAttributes = $collection->getAttribute('attributes', []); + $metadataAttributes = \array_filter($metadataAttributes, fn ($attribute) => $attribute['$id'] !== $id); + $metadataAttributes[] = $attribute; + $collection->setAttribute('attributes', \array_values($metadataAttributes)); $this->updateMetadata( collection: $collection, @@ -2169,10 +2173,17 @@ public function createAttributes(string $collection, array $attributes): bool } } + // Append attribute, removing any duplicates + $newAttributeIds = \array_map(fn ($attr) => $attr['$id'], $attributeDocuments); + $metadataAttributes = $collection->getAttribute('attributes', []); + $metadataAttributes = \array_filter($metadataAttributes, fn ($attr) => !\in_array($attr['$id'], $newAttributeIds)); + foreach ($attributeDocuments as $attributeDocument) { - $collection->setAttribute('attributes', $attributeDocument, Document::SET_TYPE_APPEND); + $metadataAttributes[] = $attributeDocument; } + $collection->setAttribute('attributes', \array_values($metadataAttributes)); + $this->updateMetadata( collection: $collection, rollbackOperation: fn () => $this->cleanupAttributes($collection->getId(), $attributeDocuments), @@ -2235,9 +2246,12 @@ private function validateAttribute( // Attribute IDs are case-insensitive $attributes = $collection->getAttribute('attributes', []); + // Loosen verification during migration + $isLoose = $this->adapter->getSharedTables() && $this->isMigrating(); + /** @var array $attributes */ foreach ($attributes as $attribute) { - if (\strtolower($attribute->getId()) === \strtolower($id)) { + if (!$isLoose && \strtolower($attribute->getId()) === \strtolower($id)) { throw new DuplicateException('Attribute already exists in metadata'); } } @@ -2246,7 +2260,7 @@ private function validateAttribute( $schema = $this->getSchemaAttributes($collection->getId()); foreach ($schema as $attribute) { $newId = $this->adapter->filter($attribute->getId()); - if (\strtolower($newId) === \strtolower($id)) { + if (!$isLoose && \strtolower($newId) === \strtolower($id)) { throw new DuplicateException('Attribute already exists in schema'); } } diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index 9f1a4b31f..af091e383 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -2195,4 +2195,156 @@ public function testCreateAttributesDelete(): void $this->assertCount(1, $attrs); $this->assertEquals('b', $attrs[0]['$id']); } + + public function testCreateAttributeWhileMigrating(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + // Skip test if adapter doesn't support shared tables + if (!$database->getAdapter()->getSharedTables()) { + $this->expectNotToPerformAssertions(); + return; + } + + // Prepare collection + $database->createCollection('migration_test'); + $database->setMigrating(true); + + // First creation, as usual + $this->assertTrue($database->createAttribute('migration_test', 'statusColumn', Database::VAR_STRING, 128, true)); + + $collection = $database->getCollection('migration_test'); + $attributes = $collection->getAttribute('attributes'); + $this->assertCount(1, $attributes); + $this->assertSame('statusColumn', $attributes[0]['$id']); + + // Second creation, no exceptions, no duplicates + $result = $database->createAttribute('migration_test', 'statusColumn', Database::VAR_STRING, 128, true); + $this->assertTrue($result); + + $collection = $database->getCollection('migration_test'); + $attributes = $collection->getAttribute('attributes'); + $this->assertCount(1, $attributes); + $this->assertSame('statusColumn', $attributes[0]['$id']); + + // Third creation, same as second, once more, just in case + $result = $database->createAttribute('migration_test', 'statusColumn', Database::VAR_STRING, 128, true); + $this->assertTrue($result); + + $collection = $database->getCollection('migration_test'); + $attributes = $collection->getAttribute('attributes'); + $this->assertCount(1, $attributes); + $this->assertSame('statusColumn', $attributes[0]['$id']); + + // Cleanup + $database->setMigrating(false); + $database->deleteCollection('migration_test'); + } + + public function testCreateAttributesWhileMigrating(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + // Skip test if adapter doesn't support shared tables + if (!$database->getAdapter()->getSharedTables()) { + $this->expectNotToPerformAssertions(); + return; + } + + // Skip test if adapter doesn't support batch create attributes + if (!$database->getAdapter()->getSupportForBatchCreateAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + // Prepare collection + $database->createCollection('migration_test_batch'); + $database->setMigrating(true); + + // First creation, as usual + $attributes = [ + [ + '$id' => 'statusColumn', + 'type' => Database::VAR_STRING, + 'size' => 128, + 'required' => true + ], + [ + '$id' => 'count', + 'type' => Database::VAR_INTEGER, + 'size' => 0, + 'required' => false + ], + ]; + + $result = $database->createAttributes('migration_test_batch', $attributes); + $this->assertTrue($result); + + $collection = $database->getCollection('migration_test_batch'); + $attrs = $collection->getAttribute('attributes'); + $this->assertCount(2, $attrs); + $this->assertSame('statusColumn', $attrs[0]['$id']); + $this->assertSame('count', $attrs[1]['$id']); + + // Second creation, no exceptions, no duplicates + $result = $database->createAttributes('migration_test_batch', $attributes); + $this->assertTrue($result); + + $collection = $database->getCollection('migration_test_batch'); + $attrs = $collection->getAttribute('attributes'); + $this->assertCount(2, $attrs); + $this->assertSame('statusColumn', $attrs[0]['$id']); + $this->assertSame('count', $attrs[1]['$id']); + + // Third creation, same as second, once more, just in case + $result = $database->createAttributes('migration_test_batch', $attributes); + $this->assertTrue($result); + + $collection = $database->getCollection('migration_test_batch'); + $attrs = $collection->getAttribute('attributes'); + $this->assertCount(2, $attrs); + $this->assertSame('statusColumn', $attrs[0]['$id']); + $this->assertSame('count', $attrs[1]['$id']); + + // Test partial overlap - create one new attribute and one existing + $mixedAttributes = [ + [ + '$id' => 'statusColumn', // existing + 'type' => Database::VAR_STRING, + 'size' => 128, + 'required' => true + ], + [ + '$id' => 'active', // new + 'type' => Database::VAR_BOOLEAN, + 'size' => 0, + 'required' => false + ], + ]; + + $result = $database->createAttributes('migration_test_batch', $mixedAttributes); + $this->assertTrue($result); + + // Ensure all attributes exist + $collection = $database->getCollection('migration_test_batch'); + $attrs = $collection->getAttribute('attributes'); + $this->assertCount(3, $attrs); + + // TODO: Eventuelly rewrite to assertSame for each item; currently their order differs between providers + $attributes = [ + $attrs[0]['$id'], + $attrs[1]['$id'], + $attrs[2]['$id'] + ]; + + $this->assertContains('statusColumn', $attributes); + $this->assertContains('count', $attributes); + $this->assertContains('active', $attributes); + + // Cleanup + $database->setMigrating(false); + $database->deleteCollection('migration_test_batch'); + } }