@@unique
Create unique composite indexes spanning multiple fields for data integrity.
Creates a unique composite index spanning multiple fields. The database rejects any two records that share the same combination of values for all listed fields.
Syntax
model Employee {
id Record @id
department String
badgeNumber Int
@@unique(deptBadge, [department, badgeNumber])
}Composite directives are placed at the end of the model block:
@@unique(name, [field1, field2, ...])- name — A unique identifier for this composite. Must be globally unique across all models.
- fields — At least 2 field references. Supports dot notation for object subfields.
Behavior
// First record — OK
await db.Employee.create({
data: { department: 'Engineering', badgeNumber: 42 },
});
// Same department + badge → rejected by DB
await db.Employee.create({
data: { department: 'Engineering', badgeNumber: 42 },
});
// Same badge, different department → OK
await db.Employee.create({
data: { department: 'Marketing', badgeNumber: 42 },
});Unique Lookups
Composite unique keys appear as a named variant in the FindUniqueWhere type, alongside single-field unique variants:
// By composite key
const emp = await db.Employee.findUnique({
where: { deptBadge: { department: 'Engineering', badgeNumber: 42 } },
});
// By id (still works)
const emp2 = await db.Employee.findUnique({
where: { id: someId },
});The composite key works with all unique operations:
// updateUnique
await db.Employee.updateUnique({
where: { deptBadge: { department: 'Engineering', badgeNumber: 42 } },
data: { department: 'Management' },
});
// deleteUnique
await db.Employee.deleteUnique({
where: { deptBadge: { department: 'Engineering', badgeNumber: 42 } },
});
// upsert
await db.Employee.upsert({
where: { deptBadge: { department: 'Engineering', badgeNumber: 42 } },
create: { department: 'Engineering', badgeNumber: 42 },
update: { department: 'Management' },
});Additional Filters
You can combine a composite key with extra where filters:
const emp = await db.Employee.findUnique({
where: {
deptBadge: { department: 'Engineering', badgeNumber: 42 },
// Additional filter — record must also match this
isActive: true,
},
});Object Subfields (Dot Notation)
Composite directives support dot notation to reference subfields of object-typed fields:
object Address {
city String
zip String
}
model Store {
id Record @id
name String
address Address
@@unique(cityZip, [address.city, address.zip])
@@unique(nameCity, [name, address.city])
}The generated FindUniqueWhere type uses nested objects to match the schema structure:
const store = await db.Store.findUnique({
where: {
cityZip: { address: { city: 'NYC', zip: '10001' } },
},
});
const store2 = await db.Store.findUnique({
where: {
nameCity: { name: 'Downtown Hub', address: { city: 'NYC' } },
},
});Record Fields
Record-typed fields (foreign keys) can be used in composites:
model Registration {
id Record @id
studentId Record
courseId Record
student Relation @field(studentId) @model(Student)
course Relation @field(courseId) @model(Course)
@@unique(enrollment, [studentId, courseId])
}const reg = await db.Registration.findUnique({
where: {
enrollment: { studentId: student.id, courseId: course.id },
},
});Null Behavior on Optional Fields
When optional fields participate in a @@unique composite, SurrealDB applies these rules:
| Values | Example | Allowed? |
|---|---|---|
| All null/NONE | (null, null) + (null, null) | Yes |
| Mixed with data | (null, 'val') + (null, 'val') | No |
| Mixed with data | ('val', null) + ('val', null) | No |
If at least one field has a concrete value, the combination is enforced as unique. If all fields are null/NONE, duplicates are allowed.
This matches the single-field behavior — see @unique — Null Behavior.
Rules
- Requires at least 2 fields.
- Composite names must be globally unique across all models.
- @id fields cannot be part of a composite (they are already unique).
- Relation fields (virtual) cannot be indexed — use the underlying Record field instead.
- Array fields (
String[],Record[]) cannot be part of a composite. - An object field and its own subfield cannot both appear in the same composite (e.g.,
[address, address.city]is rejected as redundant).
Use @@unique to enforce data integrity constraints on field combinations. For example, ensure no two employees share the same department + badge number combination.