Unique Constraints for School Records
Enforces uniqueness on three optional school identifier fields at both database and application levels with clear error responses.
What this file does
Enforces uniqueness on three optional school identifier fields at both database and application levels with clear error responses.
When to use it
- You need to prevent duplicate school records by recognition, register, or affiliation numbers
- You want partial uniqueness where some fields can be null but non-null values must be unique
- You require a migration script that checks for existing duplicates before applying constraints
- You want consistent 409 Conflict API responses with duplicate field details
Assumes this stack
Unique Constraints for School Records
Overview
This implementation prevents duplicate school records by enforcing:
- At least ONE of these fields must be provided:
school_recognition_no,general_register_no, oraffiliation_no - Whichever field(s) are provided must be unique across all schools
- Each field individually enforces uniqueness (when provided)
Fields:
school_recognition_no- School recognition number (must be unique if provided)general_register_no- General register number (must be unique if provided)affiliation_no- Affiliation number (must be unique if provided)
Implementation Details
1. Database Level (Unique Constraints)
Unique indexes have been added to enforce uniqueness at the database level:
-- school_recognition_no must be unique (allows NULL)
CREATE UNIQUE INDEX idx_schools_recognition_no_unique
ON schools(school_recognition_no)
WHERE school_recognition_no IS NOT NULL;
-- general_register_no must be unique (allows NULL)
CREATE UNIQUE INDEX idx_schools_general_register_no_unique
ON schools(general_register_no)
WHERE general_register_no IS NOT NULL;
-- affiliation_no must be unique (allows NULL)
CREATE UNIQUE INDEX idx_schools_affiliation_no_unique
ON schools(affiliation_no)
WHERE affiliation_no IS NOT NULL;
Note: NULL values are allowed (multiple NULLs won't conflict), but non-NULL values must be unique.
2. Application Level (Validation)
The School model now includes:
checkForDuplicates()- Checks for duplicates before insert/update- Pre-validation in
create()andupdate()methods - Clear error messages identifying which field(s) have duplicates
3. API Error Response
When a duplicate is detected, the API returns:
{
"success": false,
"error": "Duplicate record found",
"duplicates": [
{
"field": "school_recognition_no",
"value": "4260/37",
"message": "School recognition number already exists"
}
]
}
HTTP Status: 409 Conflict
Running the Migration
Step 1: Check for Existing Duplicates
The migration script will automatically check for existing duplicates:
npm run migrate-unique
If duplicates exist, the script will:
- List all duplicate values
- Show which records have duplicates
- Stop the migration (you must resolve duplicates first)
Step 2: Resolve Duplicates (if any)
If duplicates are found, you need to:
-
Review duplicate records:
-- Check duplicates SELECT school_recognition_no, COUNT(*) as cnt, array_agg(id) as ids FROM schools WHERE school_recognition_no IS NOT NULL GROUP BY school_recognition_no HAVING COUNT(*) > 1; -
Update or delete duplicate records:
-- Update duplicate to a unique value UPDATE schools SET school_recognition_no = 'NEW_VALUE' WHERE id = <duplicate_id>; -- OR delete if it's truly a duplicate DELETE FROM schools WHERE id = <duplicate_id>; -
Re-run the migration:
npm run migrate-unique
Step 3: Verify Constraints
After migration, test the constraints:
-- Try to insert a duplicate (should fail)
INSERT INTO schools (name, district, school_recognition_no)
VALUES ('Test School', 'Test District', 'EXISTING_VALUE');
-- Error: duplicate key value violates unique constraint
Testing
Test 0: Create School Without Any Identifier (Should Fail)
curl -X POST http://localhost:3000/api/schools \
-H "Content-Type: application/json" \
-d '{
"name": "Test School",
"district": "Test"
}'
Expected Response (400):
{
"success": false,
"error": "At least one identifier is required",
"validationErrors": [
{
"field": "identifier",
"message": "At least one of school_recognition_no, general_register_no, or affiliation_no must be provided"
}
]
}
Test 1: Create School with Duplicate Recognition Number
curl -X POST http://localhost:3000/api/schools \
-H "Content-Type: application/json" \
-d '{
"name": "Test School",
"district": "Test",
"schoolRecognitionNumber": "EXISTING_VALUE"
}'
Expected Response (409):
{
"success": false,
"error": "Duplicate record found",
"duplicates": [
{
"field": "school_recognition_no",
"value": "EXISTING_VALUE",
"message": "School recognition number already exists"
}
]
}
Test 2: Update School to Duplicate Value
curl -X PUT http://localhost:3000/api/schools/123 \
-H "Content-Type: application/json" \
-d '{
"schoolRecognitionNumber": "EXISTING_VALUE"
}'
Expected Response (409): Same as above
Test 3: Create School with Unique Values
curl -X POST http://localhost:3000/api/schools \
-H "Content-Type: application/json" \
-d '{
"name": "New School",
"district": "Test",
"schoolRecognitionNumber": "UNIQUE_VALUE_123"
}'
Expected Response (201): School created successfully
Behavior
✅ Allowed
- Creating schools with at least one unique identifier field
- Creating schools with one or more identifier fields (not all three required)
- Creating schools where different schools use different identifier fields
- Example: School A uses
school_recognition_no, School B usesgeneral_register_no
- Example: School A uses
- Updating a school without changing identifier fields
- Updating a school to unique identifier values
- Multiple NULL values (allowed - only non-NULL values must be unique)
❌ Prevented
- Creating a school without any identifier (no
school_recognition_no,general_register_no, oraffiliation_no) - Creating a school with a
school_recognition_nothat already exists - Creating a school with a
general_register_nothat already exists - Creating a school with an
affiliation_nothat already exists - Updating a school to remove all identifier fields
- Updating a school to use any duplicate identifier value
Model Methods Added
School.checkForDuplicates(schoolData, excludeId)
Checks for duplicate values before insert/update.
Parameters:
schoolData- Object containing school dataexcludeId- Optional ID to exclude from check (for updates)
Returns: Array of duplicate errors (empty if no duplicates)
Example:
const errors = await School.checkForDuplicates({
school_recognition_no: '4260/37'
});
if (errors.length > 0) {
console.log('Duplicates found:', errors);
}
School.findByGeneralRegisterNo(generalRegisterNo)
Find school by general register number.
School.findByAffiliationNo(affiliationNo)
Find school by affiliation number.
Files Modified
-
database/migrations/007_add_unique_constraints_schools.sql- SQL migration to add unique indexes
-
scripts/migrate-unique-constraints.js- Migration script with duplicate checking
-
models/School.js- Added
checkForDuplicates()method - Added
findByGeneralRegisterNo()method - Added
findByAffiliationNo()method - Updated
create()with duplicate checking - Updated
update()with duplicate checking
- Added
-
routes/schools.js- Added duplicate error handling (409 status)
- Returns detailed error messages
-
package.json- Added
migrate-uniquescript
- Added
Troubleshooting
Error: "duplicate key value violates unique constraint"
This means a duplicate value was inserted despite validation. This can happen if:
- Data was inserted directly via SQL
- Race condition (two requests at the same time)
Solution: Check application logs and ensure proper validation is in place.
Migration fails with existing duplicates
Solution:
- Review duplicate records
- Update or delete duplicates
- Re-run migration
Need to remove unique constraint
-- Remove unique constraints
DROP INDEX IF EXISTS idx_schools_recognition_no_unique;
DROP INDEX IF EXISTS idx_schools_general_register_no_unique;
DROP INDEX IF EXISTS idx_schools_affiliation_no_unique;
Summary
✅ Database-level protection - Unique indexes prevent duplicates ✅ Application-level validation - Pre-checks before insert/update ✅ Clear error messages - API returns specific duplicate fields ✅ Migration safety - Checks for existing duplicates before applying constraints
The system now prevents duplicate school records at both the database and application levels!
What's inside
7 sections, 3 SQL indexes, 2 model methods, 4 test curl commands, 5 modified files list
Change this for your project
- Replace
schoolstable name with your actual table name in all SQL statements - Replace
school_recognition_no,general_register_no,affiliation_nowith your own field names - Replace
http://localhost:3000/api/schoolswith your actual API endpoint - Replace
Schoolmodel references with your own model class name
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- Partial unique indexes with
WHERE... IS NOT NULLto allow multiple nulls while enforcing uniqueness on non-null values - A migration script that proactively checks for existing duplicates and halts if found, preventing silent failures
- Returning a structured
duplicatesarray in the API error response so clients can pinpoint which field conflicts
Related Documents
DunApp PWA - Project Constraints
Defines 14 hard constraints for a Hungarian PWA project, banning Netlify deployment and enforcing local-only testing, Supabase backend, and zero-cost development.
Constraints
Defines a three-tier priority system for design decisions, with conflict resolution examples to guide trade-offs.
Version Constraints Guide
Teaches Composer version constraint syntax for WordPress plugins and themes using a custom shell script wrapper.
Specifying version constraints
Explains how to pin Terraform CLI, provider, and Ansible versions for IBM Cloud Schematics workspaces and actions.