Repository navigation
Closes #673: Support conditional unique constraints on Custom Object Types - #830
Conversation
…Types
A constraint can now have a condition: a list of {field, lookup, value}
predicates that must all hold for the constraint to apply, enforced as a
partial unique index. Lookups are exact, n, in and isnull; object fields
can only be tested for emptiness. A conditional constraint may cover a
single field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@claude /review and report only on actionable issues. |
|
Claude finished @bctiemann's task in 3m 38s —— View job PR Review: Conditional Unique Constraints
Overall this is a solid, well-tested implementation. Below are the actionable issues found, ordered by severity. 1.
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review. I changed item 4 in 08ef703; the other four don't need changes:
|
jnovinger
left a comment
There was a problem hiding this comment.
A couple of items that I think should be addressed:
-
Text fields saved from the UI store a blank value as
''rather than NULL, so a constraint displayed as "Status is empty" doesn't apply to them: two objects added through the form with the same serial and a blank status both saved. The storage side predates this PR and I'll open a separate issue for it, so should this wait on that fix, or labelisnullas "is null" until then? -
Posting a condition with a non-string
field(e.g.[{"field": ["status"], "lookup": "isnull", "value": true}]) from the constraint form returns a 500 fromforms.py:519rather than a validation error.
Text, long text, URL and select fields store a blank value as NULL or '' depending on how it was saved, so "is empty" matches both. Also reject a non-string field name in the form's condition instead of raising a TypeError, and fix the form's description of the "n" lookup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@jnovinger Thanks. All three are fixed in c866d61:
Both full suites pass: 1465 tests without netbox-branching and 1538 with it, 8 skipped in each. |
Closes: #673
Adds an optional
conditionto Custom Object Type unique constraints. A constraint with a condition becomes a partial unique index in PostgreSQL, so uniqueness applies only to the rows that match the condition.Condition format
The condition is a list of predicates that are all ANDed together:
[{"field": <field>, "lookup": "exact" | "n" | "in" | "isnull", "value": ...}]exactmeans equals.nmeans "does not equal". It also matches empty values, because a plainNOT (col = x)would treat NULL as not matching.inmeans "is one of" and needs a non-empty list.isnullmeans "is empty" and needs a boolean.isnull.to_python(). Select values must be valid choices.Examples
The examples use the UI form's syntax, with fields named. Through the REST API,
fieldis the field's ID instead, e.g.{"field": 43, ...}.Serial numbers must be unique among non-retired devices. Fields:
serial.[{"field": "status", "lookup": "n", "value": "retired"}]The constraint also covers objects whose
statusis empty, and the table shows it as "Status does not equal retired (or is empty)".Only one primary contact per site. Fields:
site.[{"field": "is_primary", "lookup": "exact", "value": true}]Hostnames must be unique among live environments. Fields:
hostname.[{"field": "environment", "lookup": "in", "value": ["production", "staging"]}]Top-level names must be unique; nested ones only within their parent. This takes two constraints.
name, with condition[{"field": "parent", "lookup": "isnull", "value": true}]parent,name, with no conditionCombined predicates (ANDed): the asset tag must be unique among assigned, active assets. Fields:
asset_tag.[ {"field": "assigned_to", "lookup": "isnull", "value": false}, {"field": "status", "lookup": "exact", "value": "active"} ]Things that are rejected:
{"field": "parent", "lookup": "exact", "value": 5}: object fields can only be tested withisnull.{"field": "status", "lookup": "in", "value": []}:inneeds a non-empty list.{"field": "status", "lookup": "exact", "value": "bogus"}: a select field's value must be one of its choices.{"field": "status", "lookup": "isnull"}:isnullneedstrueorfalse, and every other lookup needs a value too.{"field": "tags", "lookup": "isnull", "value": true}, wheretagsis a multi-object field: conditions can only test the field types that constraints can use, and multi-object fields aren't one of them.Integration
CustomObjectTypeField.constraints). The existing protections therefore cover condition fields too:condition.0020adds theconditionJSONField (default[]).Tests
New cases cover:
Full suites pass:
🤖 Generated with Claude Code