fix(orphan): handle prefixes with dots in orphan detection (GH#508)

The orphan detection was incorrectly flagging issues with dots in their
prefix (e.g., "my.project-abc123") as orphans because it was looking for
any dot in the ID, treating everything before the first dot as the
parent ID.

The fix:
- Add IsHierarchicalID() helper that correctly detects hierarchical IDs
  by checking if the ID ends with .{digits} (e.g., "bd-abc.1")
- Update SQL query in orphan detection migration to use GLOB patterns
  that only match IDs ending with numeric suffixes
- Update all Go code that checks for hierarchical IDs to use the new
  helper function

Test cases added:
- Unit tests for IsHierarchicalID covering normal, dotted prefix, and
  edge cases
- Integration test verifying dotted prefixes do not trigger false
  positives

Fixes: #508
This commit is contained in:
Steve Yegge
2025-12-14 17:23:46 -08:00
parent 768db19635
commit fb20e43f5f
6 changed files with 270 additions and 17 deletions

View File

@@ -7,23 +7,36 @@ import (
)
// MigrateOrphanDetection detects orphaned child issues and logs them for user action
// Orphaned children are issues with hierarchical IDs (e.g., "parent.child") where the
// Orphaned children are issues with hierarchical IDs (e.g., "parent.1") where the
// parent issue no longer exists in the database.
//
// Hierarchical IDs have the format {parentID}.{N} where N is a numeric child suffix.
// This correctly handles prefixes that contain dots (e.g., "my.project-abc123" is NOT
// hierarchical, but "my.project-abc123.1" IS hierarchical). See GH#508.
//
// This migration does NOT automatically delete or convert orphans - it only logs them
// so the user can decide whether to:
// - Delete the orphans if they're no longer needed
// - Convert them to top-level issues by renaming them
// - Restore the missing parent issues
func MigrateOrphanDetection(db *sql.DB) error {
// Query for orphaned children using the pattern from the issue description:
// SELECT id FROM issues WHERE id LIKE '%.%'
// AND substr(id, 1, instr(id || '.', '.') - 1) NOT IN (SELECT id FROM issues)
// Query for orphaned children:
// - Must end with .N where N is 1-4 digits (covers child numbers 0-9999)
// - Parent (everything before the last .N) must not exist in issues table
// - Uses GLOB patterns to ensure suffix is purely numeric
// - rtrim removes trailing digits, then trailing dot, to get parent ID
//
// GH#508: The old query used instr() to find the first dot, which incorrectly
// flagged IDs with dots in the prefix (e.g., "my.project-abc") as orphans.
// The fix uses GLOB patterns to only match IDs ending with .{digits}.
rows, err := db.Query(`
SELECT id
FROM issues
WHERE id LIKE '%.%'
AND substr(id, 1, instr(id || '.', '.') - 1) NOT IN (SELECT id FROM issues)
WHERE
-- Must end with .N where N is 1-4 digits (child number suffix)
(id GLOB '*.[0-9]' OR id GLOB '*.[0-9][0-9]' OR id GLOB '*.[0-9][0-9][0-9]' OR id GLOB '*.[0-9][0-9][0-9][0-9]')
-- Parent (remove trailing digits then dot) must not exist
AND rtrim(rtrim(id, '0123456789'), '.') NOT IN (SELECT id FROM issues)
ORDER BY id
`)
if err != nil {