发布

  • [OPIK-7371] [BE] feat: auto-suffix duplicate automation rule names on create (#7568)

    frostbyte_neo 发布于 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

    下载附件