Sangeetha-Grantha

Metadata Value
Status Active
Version 1.1.0
Last Updated 2026-09-10
Author Sangeetha Grantha Team
Document Type Decision record

Exposed DSL Optimization: Fixing DELETE+INSERT Anti-Pattern


[!NOTE] Decision record: preserve the original rationale and check its decision/supersession status. Current runtime guidance is in system architecture and Flyway migrations.


Problem Statement

The KrithiRepository.saveSections() method was using an inefficient DELETE+INSERT pattern that:

  1. Deleted ALL sections for a krithi, even if only one section changed
  2. Re-inserted all sections, losing original created_at timestamps
  3. Generated unnecessary SQL operations
  4. Could break foreign key relationships with krithi_lyric_sections

Example of Inefficient SQL

Before (DELETE+INSERT):

-- Deletes ALL sections
DELETE FROM krithi_sections WHERE krithi_id = '50756508-a56c-4053-b654-f44cc372cb74'

-- Re-inserts ALL sections (even unchanged ones)
INSERT INTO krithi_sections (...) VALUES (...)
INSERT INTO krithi_sections (...) VALUES (...)
INSERT INTO krithi_sections (...) VALUES (...)

After (UPDATE/INSERT/DELETE):

-- Only updates changed section
UPDATE krithi_sections 
SET section_type = 'CHARANAM', updated_at = NOW() 
WHERE id = '1aab9810-fea6-4ef8-920d-a54e4219f6bf'

-- Only inserts new section (if any)
INSERT INTO krithi_sections (...) VALUES (...)

-- Only deletes removed section (if any)
DELETE FROM krithi_sections WHERE id = '...'

Solution Implemented

Changes Made

  1. Updated Repository Method Signature
    • Changed from List<Pair<String, Int>> to List<Triple<String, Int, String?>>
    • Now accepts (sectionType, orderIndex, label) to preserve label information
  2. Implemented Diff Logic
    • Reads existing sections from database
    • Compares existing vs new sections by order_index
    • Determines what needs to be:
      • Updated: Same order_index, different sectionType or label
      • Inserted: New order_index not in existing sections
      • Deleted: Existing order_index not in new sections
  3. Preserved Metadata
    • created_at timestamps are preserved for existing sections
    • notes field is preserved (not in request, so not overwritten)
    • Only updated_at is modified for changed sections

Code Changes

Repository (KrithiRepository.kt)

suspend fun saveSections(krithiId: Uuid, sections: List<Triple<String, Int, String?>>) = DatabaseFactory.dbQuery {
    val now = OffsetDateTime.now(ZoneOffset.UTC)
    val javaKrithiId = krithiId.toJavaUuid()
    
    // Get existing sections indexed by order_index
    val existingSections = KrithiSectionsTable
        .selectAll()
        .where { KrithiSectionsTable.krithiId eq javaKrithiId }
        .associateBy { it[KrithiSectionsTable.orderIndex] }
    
    // Build map of new sections by order_index
    val newSectionsMap = sections.associateBy { it.second }
    
    // Determine what to update, insert, and delete
    val toUpdate = mutableListOf<Triple<UUID, String, String?>>()
    val toInsert = mutableListOf<Triple<String, Int, String?>>()
    val toDelete = mutableListOf<UUID>()
    
    // Process existing sections
    existingSections.forEach { (orderIndex, row) ->
        val existingId = row[KrithiSectionsTable.id].value
        val existingType = row[KrithiSectionsTable.sectionType]
        val existingLabel = row[KrithiSectionsTable.label]
        val newSection = newSectionsMap[orderIndex]
        
        if (newSection != null) {
            val (newType, _, newLabel) = newSection
            if (newType != existingType || newLabel != existingLabel) {
                toUpdate.add(Triple(existingId, newType, newLabel))
            }
        } else {
            toDelete.add(existingId)
        }
    }
    
    // Find sections to insert
    newSectionsMap.forEach { (orderIndex, section) ->
        if (!existingSections.containsKey(orderIndex)) {
            toInsert.add(section)
        }
    }
    
    // Execute updates
    toUpdate.forEach { (id, sectionType, label) ->
        KrithiSectionsTable.update({ KrithiSectionsTable.id eq id }) {
            it[KrithiSectionsTable.sectionType] = sectionType
            it[KrithiSectionsTable.label] = label
            it[KrithiSectionsTable.updatedAt] = now
        }
    }
    
    // Execute inserts
    if (toInsert.isNotEmpty()) {
        KrithiSectionsTable.batchInsert(toInsert) { section ->
            val (sectionType, orderIndex, label) = section
            this[KrithiSectionsTable.id] = UUID.randomUUID()
            this[KrithiSectionsTable.krithiId] = javaKrithiId
            this[KrithiSectionsTable.sectionType] = sectionType
            this[KrithiSectionsTable.orderIndex] = orderIndex
            this[KrithiSectionsTable.label] = label
            this[KrithiSectionsTable.notes] = null
            this[KrithiSectionsTable.createdAt] = now
            this[KrithiSectionsTable.updatedAt] = now
        }
    }
    
    // Execute deletes
    if (toDelete.isNotEmpty()) {
        KrithiSectionsTable.deleteWhere { 
            KrithiSectionsTable.id inList toDelete 
        }
    }
}

Service (KrithiService.kt)

suspend fun saveKrithiSections(id: Uuid, sections: List<KrithiSectionRequest>) {
    // Pass full section data including label for efficient updates
    val sectionsData = sections.map { 
        Triple(it.sectionType, it.orderIndex, it.label) 
    }
    dal.krithis.saveSections(id, sectionsData)
    
    dal.auditLogs.append(
        action = "UPDATE_KRITHI_SECTIONS",
        entityTable = "krithi_sections",
        entityId = id
    )
}

Benefits

Performance Improvements

  1. Reduced Database Operations
    • Before: Always DELETE all + INSERT all (2 operations minimum)
    • After: Only UPDATE/INSERT/DELETE what changed (0-3 operations as needed)
  2. Preserved Metadata
    • created_at timestamps maintained
    • notes field preserved
    • Better audit trail
  3. Foreign Key Safety
    • Existing section IDs are preserved when possible
    • krithi_lyric_sections references remain valid

Example Scenarios

Scenario 1: No Changes

Scenario 2: One Section Changed

Scenario 3: One Section Added

Scenario 4: Complete Replacement

Trade-offs

Advantages

✅ More efficient SQL generation ✅ Preserves metadata (created_at, notes) ✅ Maintains foreign key relationships ✅ Better performance for partial updates

Disadvantages

⚠️ More complex code (diff logic) ⚠️ Requires reading existing data first (one extra query) ⚠️ Slightly more memory usage (loading existing sections)

When DELETE+INSERT is Still Appropriate

The DELETE+INSERT pattern is still acceptable for:

Testing Recommendations

  1. Unit Tests
    • Test with no changes (should do nothing)
    • Test with one section changed
    • Test with sections added/removed
    • Test with label changes
    • Test with complete replacement
  2. Integration Tests
    • Verify created_at is preserved
    • Verify notes field is preserved
    • Verify foreign key relationships remain intact
    • Verify audit logs are correct
  3. Performance Tests
    • Compare SQL query counts before/after
    • Measure execution time for various scenarios
    • Test with large numbers of sections

Future Improvements

Similar Patterns - All Optimized ✅

  1. saveLyricVariantSections()OPTIMIZED
    • Before: DELETE all + INSERT all
    • After: UPDATE changed text, INSERT new sections, DELETE removed sections
    • Key: Matches by (lyric_variant_id, section_id) unique constraint
    • Preserves: created_at, normalized_text metadata
    • Benefit: Only updates sections where text actually changed
  2. updateTags()OPTIMIZED
    • Before: DELETE all + INSERT all
    • After: INSERT new tags, DELETE removed tags
    • Key: Matches by tag_id (composite primary key with krithi_id)
    • Preserves: sourceInfo, confidence for tags that remain
    • Benefit: Tags that stay unchanged preserve their metadata
  3. update() method for ragasOPTIMIZED
    • Before: DELETE all + INSERT all
    • After: INSERT new/changed ragas, DELETE removed/changed ragas
    • Key: Matches by (ragaId, orderIndex) composite key
    • Note: For composite primary keys, position changes require delete+insert
    • Benefit: Only processes ragas that actually changed position or were added/removed

Potential Enhancements

  1. Batch Operations
    • Use batchUpdate() if Exposed supports it
    • Reduce number of UPDATE statements
  2. Transaction Optimization
    • Combine multiple operations into fewer statements
    • Use UPSERT (INSERT … ON CONFLICT UPDATE) if supported
  3. Caching
    • Cache existing sections to avoid extra query
    • Only useful if called multiple times in same transaction

Summary of All Optimizations

Methods Optimized

Method Table Key Strategy Metadata Preserved Status
saveSections() krithi_sections Match by order_index created_at, notes ✅ Optimized
saveLyricVariantSections() krithi_lyric_sections Match by section_id created_at, normalized_text ✅ Optimized
updateTags() krithi_tags Match by tag_id sourceInfo, confidence ✅ Optimized
update() ragas krithi_ragas Match by (ragaId, orderIndex) N/A (composite key) ✅ Optimized

Performance Impact Summary

Before Optimization:

After Optimization:

Key Patterns Applied

  1. Read existing data first - Load current state into memory
  2. Diff logic - Compare existing vs new to determine changes
  3. Selective operations - Only UPDATE/INSERT/DELETE what changed
  4. Metadata preservation - Keep created_at, notes, and other metadata when possible

Conclusion

All identified DELETE+INSERT patterns have been successfully optimized. The implementations:

The codebase now uses efficient UPDATE/INSERT/DELETE operations that only process what actually changed, resulting in significant performance improvements and better data integrity.


Section index · Documentation home · Feature status