-
Notifications
You must be signed in to change notification settings - Fork 58
feat: drop unknown attributes on schema-managed writes instead of failing #946
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a532485
e0d6ee7
fe0d8cd
a749179
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -428,6 +428,8 @@ class Database | |
|
|
||
| protected bool $validate = true; | ||
|
|
||
| protected bool $dropUnknownAttributes = false; | ||
|
|
||
| protected bool $preserveDates = false; | ||
|
|
||
| protected bool $skipDuplicates = false; | ||
|
|
@@ -1495,6 +1497,25 @@ protected function createDocumentInstance(string $collection, array $data): Docu | |
| return new $className($data); | ||
| } | ||
|
|
||
| public function getDropUnknownAttributes(): bool | ||
| { | ||
| return $this->dropUnknownAttributes; | ||
| } | ||
|
|
||
| /** | ||
| * Drop attributes missing from the collection schema instead of rejecting the write. | ||
| * | ||
| * Enable this where the schema is owned by the application rather than the caller, so a | ||
| * deploy that writes an attribute before its migration has run degrades to a warning | ||
| * instead of failing every write. | ||
| */ | ||
| public function setDropUnknownAttributes(bool $drop): static | ||
| { | ||
| $this->dropUnknownAttributes = $drop; | ||
|
|
||
| return $this; | ||
| } | ||
|
|
||
| public function getPreserveDates(): bool | ||
| { | ||
| return $this->preserveDates; | ||
|
|
@@ -6316,6 +6337,11 @@ public function updateDocument(string $collection, string $id, Document $documen | |
| } | ||
| $document = new Document($document); | ||
|
|
||
| // Ahead of change detection: a dropped attribute is never persisted, so | ||
| // counting it as a change would bump $updatedAt and fire an update event | ||
| // for a write that leaves the stored document identical. | ||
| $document = $this->removeUnknownAttributes($collection, $document); | ||
|
|
||
| $attributes = $collection->getAttribute('attributes', []); | ||
|
|
||
| $relationships = \array_filter($attributes, function ($attribute) { | ||
|
|
@@ -7392,6 +7418,8 @@ public function upsertDocumentsWithIncrease( | |
| foreach ($documents as $key => $document) { | ||
| $old = $existingDocs[$this->tenantKey($document)] ?? new Document(); | ||
|
|
||
| $document = $this->removeUnknownAttributes($collection, $document); | ||
|
|
||
| // Extract operators early to avoid comparison issues | ||
| $documentArray = $document->getArrayCopy(); | ||
| $extracted = Operator::extractOperators($documentArray); | ||
|
|
@@ -9231,9 +9259,57 @@ public static function addFilter(string $name, callable $encode, callable $decod | |
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * Remove attributes the collection schema does not declare. | ||
| * | ||
| * Used ahead of change detection on update/upsert so a dropped key is not | ||
| * counted as a write. Encode also calls this after iterating attributes. | ||
| * | ||
| * @param Document $collection | ||
| * @param Document $document | ||
| * @param array<string, true>|null $known Attribute ids already collected (e.g. during encode) | ||
| * | ||
| * @return Document | ||
| */ | ||
| protected function removeUnknownAttributes(Document $collection, Document $document, ?array $known = null): Document | ||
| { | ||
| if (!$this->dropUnknownAttributes || !$this->adapter->getSupportForAttributes()) { | ||
| return $document; | ||
| } | ||
|
|
||
| if ($known === null) { | ||
| $known = []; | ||
| foreach ($collection->getAttribute('attributes', []) as $attribute) { | ||
| $known[$attribute['$id'] ?? ''] = true; | ||
| } | ||
| } | ||
|
|
||
| $dropped = []; | ||
| foreach (\array_keys($document->getArrayCopy()) as $key) { | ||
| if (\str_starts_with($key, '$') || isset($known[$key])) { | ||
| continue; | ||
| } | ||
|
|
||
| $dropped[] = $key; | ||
| $document->removeAttribute($key); | ||
| } | ||
|
|
||
| if (!empty($dropped)) { | ||
| Console::warning( | ||
| 'Dropped unknown attributes "' . \implode('", "', $dropped) . '" from collection "' . $collection->getId() . '"' | ||
| . ($this->adapter->getTenant() === null ? '' : ' on tenant ' . $this->adapter->getTenant()) | ||
| ); | ||
| } | ||
|
|
||
| return $document; | ||
| } | ||
|
|
||
| /** | ||
| * Encode Document | ||
| * | ||
| * When dropUnknownAttributes is enabled, attributes missing from the | ||
| * collection schema are removed here while the known set is collected. | ||
| * | ||
| * @param Document $collection | ||
| * @param Document $document | ||
| * @param bool $applyDefaults Whether to apply default values to null attributes | ||
|
|
@@ -9249,8 +9325,10 @@ public function encode(Document $collection, Document $document, bool $applyDefa | |
| $attributes[] = $attribute; | ||
| } | ||
|
|
||
| $known = []; | ||
| foreach ($attributes as $attribute) { | ||
| $key = $attribute['$id'] ?? ''; | ||
| $known[$key] = true; | ||
| $array = $attribute['array'] ?? false; | ||
| $default = $attribute['default'] ?? null; | ||
| $filters = $attribute['filters'] ?? []; | ||
|
|
@@ -9303,7 +9381,7 @@ public function encode(Document $collection, Document $document, bool $applyDefa | |
| $document->setAttribute($key, $value); | ||
| } | ||
|
|
||
| return $document; | ||
| return $this->removeUnknownAttributes($collection, $document, $known); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Do not assign
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Gate events on actual changes.
Filtering makes a dropped-only
updateDocumentcall a no-op and removes unchanged documents from the upsert batch. However,updateDocument()still triggersEVENT_DOCUMENT_UPDATEat Line [6559], andupsertDocumentsWithIncrease()still triggersEVENT_DOCUMENTS_UPSERTat Line [7664] when no document changed. Emit each event only when the corresponding write count or change flag is positive.Also applies to: 7421-7422
🤖 Prompt for AI Agents