- Track skipped_no_phone and skipped_no_id separately
- Display skip information in admin UI with warning icon
- Clarify that empty phone numbers cannot be imported (PK constraint)
- Update documentation to explain expected skip behavior
- Example: 143 rows without phone numbers is correct, not an error
When importing PHONE.csv with empty phone numbers:
- Rows are properly skipped (cannot have NULL in primary key)
- User sees: '⚠️ Skipped: 143 rows without phone number'
- This is expected behavior, not a bug
3.8 KiB
Phone Import Unique Constraint Fix
Issue
When uploading PHONE.csv, the import would fail with a SQLite integrity error:
UNIQUE constraint failed: phone.id, phone.phone
This error occurred at row 507 and cascaded to subsequent rows due to transaction rollback.
Root Cause
The LegacyPhone model has a composite primary key on (id, phone) to prevent duplicate phone number entries for the same person/entity. The original import_phone() function used bulk inserts without checking for existing records, causing the constraint violation when:
- Re-importing the same CSV file
- The CSV contains duplicate
(id, phone)combinations - Partial imports left some data in the database
Solution
Updated import_phone() in /app/import_legacy.py to implement an upsert strategy:
Changes Made
- Check for duplicates within CSV: Track seen
(id, phone)combinations to skip duplicates in the same import - Check database for existing records: Query for existing
(id, phone)before inserting - Update or Insert:
- If record exists → update the
locationfield - If record doesn't exist → insert new record
- If record exists → update the
- Enhanced error handling: Rollback only the failed row, not the entire batch
- Better logging: Track
inserted,updated, andskippedcounts separately
Code Changes
# Before: Bulk insert without checking
db.bulk_save_objects(batch)
db.commit()
# After: Upsert with duplicate handling
existing = db.query(LegacyPhone).filter(
LegacyPhone.id == rolodex_id,
LegacyPhone.phone == phone
).first()
if existing:
existing.location = clean_string(row.get('Location'))
result['updated'] += 1
else:
record = LegacyPhone(...)
db.add(record)
result['inserted'] += 1
Result Tracking
The function now returns detailed statistics:
success: Total successfully processed rowsinserted: New records addedupdated: Existing records updatedskipped: Duplicate combinations within the CSVskipped_no_phone: Rows without a phone number (cannot import - phone is part of primary key)skipped_no_id: Rows without an ID (cannot import - required field)errors: List of error messages for failed rowstotal_rows: Total rows in CSV
Understanding Skipped Rows
Important: The phone field is part of the composite primary key (id, phone). This means:
- You cannot import a phone record without a phone number
- Empty phone numbers will be skipped (this is expected and correct behavior)
- The web UI will display:
⚠️ Skipped: X rows without phone number
Example: If your CSV has 26,437 rows and 143 have empty phone numbers:
- Total rows: 26,437
- Success: 26,294
- Skipped (no phone): 143
- This is working correctly - those 143 rows don't have phone numbers to import
Testing
After deploying this fix:
- Uploading
PHONE.csvfor the first time will insert all records - Re-uploading the same file will update existing records (no errors)
- Uploading a CSV with internal duplicates will skip duplicates gracefully
Consistency with Other Imports
This fix aligns import_phone() with the upsert pattern already used in:
import_rolodex()- handles duplicates by IDimport_trnstype()- upserts by T_Typeimport_trnslkup()- upserts by T_Codeimport_footers()- upserts by F_Code- And other reference table imports
Related Files
/app/import_legacy.py- Contains the fixedimport_phone()function/app/models.py- DefinesLegacyPhonemodel with composite PK/app/main.py- Routes CSV uploads to import functions
Prevention
To prevent similar issues in future imports:
- Always use upsert logic for tables with unique constraints
- Test re-imports of the same CSV file
- Handle duplicates within the CSV gracefully
- Provide detailed success/error statistics to users