-
[OPIK-7371] [BE] feat: auto-suffix duplicate automation rule names on create (#7568)
发布于
2026-07-28 15:57:21 +00:00 - [OPIK-7371] [BE] feat: auto-suffix duplicate automation rule names on create
Rule names are not unique, so re-running an SDK script that creates the
same rule produced multiple rules that look identical in the UI. On
create, when a name already exists within the target project(s), the new
rule is now stored as name-1, name-2, ... so rules stay distinguishable
without a DB uniqueness constraint or data migration.Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- [OPIK-7371] [BE] fix: accent-insensitive + code-point-safe rule name suffixing
Address Baz review: collision comparison now strips diacritics (approximating
utf8mb4_unicode_ci) so accented duplicates collide, and truncateToFit no longer
splits a surrogate pair on non-BMP names. Split the blank-name parameterized test
into explicit cases and added accent/surrogate coverage.Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- [OPIK-7371] [BE] chore: structure collision log message for grep-ability
Address Baz review: stable literal prefix first, values grouped at the end.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- [OPIK-7371] [BE] fix: cover legacy project_id + trailing-space collisions
- Collision query now also considers the legacy automation_rules.project_id
column (LEFT JOIN + OR), so pre-junction rules on older installs are deduped. - canonicalKey strips trailing spaces to match MySQL PAD SPACE comparison.
- Add unit (trailing space) and integration (multi-project shared collision,
cross-evaluator-type collision) coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- [OPIK-7371] [BE] fix: also dedup rule names on update/rename (excluding self)
Final review flagged the uniqueness invariant as half-enforced: create()
auto-suffixed but update() wrote the name verbatim, so a PATCH rename could
re-introduce a collision. update() now resolves a unique name against the
target project(s) excluding the rule itself (via findNamesByProjectsExcludingRule),
so an unchanged name or a non-name edit is never spuriously suffixed. Adds an
integration test for rename-collision and the self-exclusion no-op.Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- [OPIK-7371] [BE] fix: don't rename rule on non-name updates; log after commit
Baz review round 3:
- update() only resolves a unique name when the name actually changes vs the
stored name (findNameById); a non-name edit (sampling rate, enabled, filters)
no longer silently renames a rule to name-1 when a same-named rule already
exists in the project scope (e.g. legacy duplicates or a shared-project edit). - Move the "applied suffix" log out of the write transaction so a rolled-back
write can't leave a misleading success line. - Add integration test: a non-name edit that introduces a scope collision keeps
the name unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- [OPIK-7371] [BE] fix: findNameById returns Optional; guard update-of-missing 404
Self-review: findNameById returned a bare String, deviating from the codebase's
universal Optional<...> lookup convention and relying on JDBI's ambiguous
zero-row behavior. Updating a non-existent rule would call it before
updateBaseRule; return Optional so the missing row is handled cleanly
and the update still 404s (not 500). Add a regression test for that path.Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- [OPIK-7371] [BE] refactor: scalable per-project indexed prefix name lookup
Address @andrescrz review:
- Replace the fetch-all-names-in-workspace + Java-match approach with a bounded,
index-backed prefix query (name LIKE 'base%'), so it no longer loads every rule
name into memory. - Add index automation_rules(workspace_id, name) (migration 000091).
- Scope collisions per project via the junction table only (authoritative after the
AutomationRuleProjectMigration backfill); drop the legacy project_id OR branch. - Collapse the two near-identical DAO queries into one StringTemplate query with an
optional <if(excludeRuleId)> clause. - Escape LIKE metacharacters (\ % _) in the prefix so free-form names match literally.
- update(): return the resolved name from the transaction (drop the String[] holder);
keep the non-name-edit short-circuit; log after commit, no longer dumping projectIds.
Optimistic by design (no DB unique constraint; rare un-backfilled legacy rule or
concurrent create may duplicate) per ticket scope. Tests: unit 15, integration 10.Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- [OPIK-7371] [BE] chore: renumber name-index migration to 000092 after main merge
main introduced its own 000091 (000091_add_filter_guardrail_type_config_type);
renumber the automation-rules name index migration to 000092 to avoid a
duplicate changeset number.Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- [OPIK-7371] [BE] chore: rename findCollidingNames to findCandidateNames
The query returns prefix-filtered candidates; the precise collision check
happens in AutomationRuleNames.generateUniqueName. Name now says what it does.Co-Authored-By: Claude Fable 5 noreply@anthropic.com
- [OPIK-7371] [BE] fix: address review round 3 — length validation, LIKE escaping, shared resolution
Review fixes:
- Reject names >150 chars with 422 at the API boundary (@Size on both the
create and update models); previously an over-long name reached the insert
and failed with a 500 (pre-existing, surfaced by review) - Declare the LIKE escape character explicitly and switch it from '' to '!':
the query passes through three escaping layers (Java text block ->
StringTemplate -> MySQL), so ESCAPE '' silently rendered as a syntax error;
'!' is inert in all three - Fix duplicate suffixes for max-length names on re-run: suffixing a 150-char
name truncates its base, so the stored "name-1" no longer shared the full
search prefix and the next run regenerated it; the LIKE prefix is now capped
at 138 chars (150 minus the worst-case suffix reserve) - Extract resolveUniqueName() shared by save() and update() so the suffixing
rules cannot drift between them - Hoist the \p{M} diacritics Pattern to a static final; use
CollectionUtils.isEmpty for the null-safe check - Rework the suffix log line: stable greppable prefix, all values trailing,
no longer claims to print a "suffix" when it prints the full name - Correct the "index-backed" claim in findCandidateNames docs: measured on
MySQL 8.4 with 50k rules, the (workspace_id, name) index from migration
000092 is never selected; the project filter is what bounds the query
Tests:
- DuplicateNameHandling: centralise helpers into two primitives with
delegating overloads; add 422 length tests (create + update), wildcard
'%'-named rule non-overmatch, SQL-payload literal round-trips, and the
max-length re-run regression test - AutomationRuleNamesTest: parameterise the (requested, existing) -> expected
cases into one @MethodSource test (18 named cases); keep truncation and
likePrefix cases standalone; cover the new prefix cap incl. surrogate-pair
boundary
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
- [OPIK-7371] [BE] test: close review-identified coverage gaps
Two claims previously rested on unit tests or shared code paths alone:
- rename-to-metacharacter-name: the update path shares resolveUniqueName
with create, but escaping on rename had no end-to-end lock - accent-insensitive collision (Café/Cafe): the in-Java canonical-key fold
was unit-tested, but nothing proved the LIKE prefix fetch (running under
utf8mb4_unicode_ci) actually returns the accented stored name as a
candidate
Both are now integration-tested in DuplicateNameHandling (21 tests).
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Co-authored-by: Claude Opus 4.8 (1M context) noreply@anthropic.com
下载附件