Schema¶
Fluent's schema API allows you to create and update your database schema programatically. It is often used in conjunction with migrations to prepare the database for use with models.
// An example of Fluent's schema API
try await database.schema("planets")
.id()
.field("name", .string, .required)
.field("star_id", .uuid, .required, .references("stars", "id"))
.create()
To create a SchemaBuilder
, use the schema
method on database. Pass in the name of the table or collection you want to affect. If you are editing the schema for a model, make sure this name matches the model's schema
.
Actions¶
The schema API supports creating, updating, and deleting schemas. Each action supports a subset of the API's available methods.
Create¶
Calling create()
creates a new table or collection in the database. All methods for defining new fields and constraints are supported. Methods for updates or deletes are ignored.
// An example schema creation.
try await database.schema("planets")
.id()
.field("name", .string, .required)
.create()
If a table or collection with the chosen name already exists, an error will be thrown. To ignore this, use .ignoreExisting()
.
Update¶
Calling update()
updates an existing table or collection in the database. All methods for creating, updating, and deleting fields and constraints are supported.
// An example schema update.
try await database.schema("planets")
.unique(on: "name")
.deleteField("star_id")
.update()
Delete¶
Calling delete()
deletes an existing table or collection from the database. No additional methods are supported.
// An example schema deletion.
database.schema("planets").delete()
Field¶
Fields can be added when creating or updating a schema.
// Adds a new field
.field("name", .string, .required)
The first parameter is the name of the field. This should match the key used on the associated model property. The second parameter is the field's data type. Finally, zero or more constraints can be added.
Data Type¶
Supported field data types are listed below.
DataType | Swift Type |
---|---|
.string |
String |
.int{8,16,32,64} |
Int{8,16,32,64} |
.uint{8,16,32,64} |
UInt{8,16,32,64} |
.bool |
Bool |
.datetime |
Date (recommended) |
.time |
Date (omitting day, month, and year) |
.date |
Date (omitting time of day) |
.float |
Float |
.double |
Double |
.data |
Data |
.uuid |
UUID |
.dictionary |
See dictionary |
.array |
See array |
.enum |
See enum |
Field Constraint¶
Supported field constraints are listed below.
FieldConstraint | Description |
---|---|
.required |
Disallows nil values. |
.references |
Requires that this field's value match a value in the referenced schema. See foreign key |
.identifier |
Denotes the primary key. See identifier |
Identifier¶
If your model uses a standard @ID
property, you can use the id()
helper to create its field. This uses the special .id
field key and UUID
value type.
// Adds field for default identifier.
.id()
For custom identifier types, you will need to specify the field manually.
// Adds field for custom identifier.
.field("id", .int, .identifier(auto: true))
The identifier
constraint may be used on a single field and denotes the primary key. The auto
flag determines whether or not the database should generate this value automatically.
Update Field¶
You can update a field's data type using updateField
.
// Updates the field to `double` data type.
.updateField("age", .double)
See advanced for more information on advanced schema updates.
Delete Field¶
You can remove a field from a schema using deleteField
.
// Deletes the field "age".
.deleteField("age")
Constraint¶
Constraints can be added when creating or updating a schema. Unlike field constraints, top-level constraints can affect multiple fields.
Unique¶
A unique constraint requires that there are no duplicate values in one or more fields.
// Disallow duplicate email addresses.
.unique(on: "email")
If multiple field are constrained, the specific combination of each field's value must be unique.
// Disallow users with the same full name.
.unique(on: "first_name", "last_name")
To delete a unique constraint, use deleteUnique
.
// Removes duplicate email constraint.
.deleteUnique(on: "email")
Constraint Name¶
Fluent will generate unique constraint names by default. However, you may want to pass a custom constraint name. You can do this using the name
parameter.
// Disallow duplicate email addresses.
.unique(on: "email", name: "no_duplicate_emails")
To delete a named constraint, you must use deleteConstraint(name:)
.
// Removes duplicate email constraint.
.deleteConstraint(name: "no_duplicate_emails")
Foreign Key¶
Foreign key constraints require that a field's value match ones of the values in the referenced field. This is useful for preventing invalid data from being saved. Foreign key constraints can be added as either a field or top-level constraint.
To add a foreign key constraint to a field, use .references
.
// Example of adding a field foreign key constraint.
.field("star_id", .uuid, .required, .references("stars", "id"))
The above constraint requires that all values in the "star_id" field must match one of the values in Star's "id" field.
This same constraint could be added as a top-level constraint using foreignKey
.
// Example of adding a top-level foreign key constraint.
.foreignKey("star_id", references: "stars", "id")
Unlike field constraints, top-level constraints can be added in a schema update. They can also be named.
Foreign key constraints support optional onDelete
and onUpdate
actions.
ForeignKeyAction | Description |
---|---|
.noAction |
Prevents foreign key violations (default). |
.restrict |
Same as .noAction . |
.cascade |
Propagates deletes through foreign keys. |
.setNull |
Sets field to null if reference is broken. |
.setDefault |
Sets field to default if reference is broken. |
Below is an example using foreign key actions.
// Example of adding a top-level foreign key constraint.
.foreignKey("star_id", references: "stars", "id", onDelete: .cascade)
Warning
Foreign key actions happen solely in the database, bypassing Fluent. This means things like model middleware and soft-delete may not work correctly.
SQL¶
The .sql
parameter allows you to add arbitrary SQL to your schema. This is useful for adding specific constraints or data types.
A common use case is defining a default value for a field:
.field("active", .bool, .required, .sql(.default(true)))
or even a default value for a timestamp:
.field("created_at", .datetime, .required, .sql(.default(SQLFunction("now"))))
Dictionary¶
The dictionary data type is capable of storing nested dictionary values. This includes structs that conform to Codable
and Swift dictionaries with a Codable
value.
Note
Fluent's SQL database drivers store nested dictionaries in JSON columns.
Take the following Codable
struct.
struct Pet: Codable {
var name: String
var age: Int
}
Since this Pet
struct is Codable
, it can be stored in a @Field
.
@Field(key: "pet")
var pet: Pet
This field can be stored using the .dictionary(of:)
data type.
.field("pet", .dictionary, .required)
Since Codable
types are heterogenous dictionaries, we do not specify the of
parameter.
If the dictionary values were homogenous, for example [String: Int]
, the of
parameter would specify the value type.
.field("numbers", .dictionary(of: .int), .required)
Dictionary keys must always be strings.
Array¶
The array data type is capable of storing nested arrays. This includes Swift arrays that contain Codable
values and Codable
types that use an unkeyed container.
Take the following @Field
that stores an array of strings.
@Field(key: "tags")
var tags: [String]
This field can be stored using the .array(of:)
data type.
.field("tags", .array(of: .string), .required)
Since the array is homogenous, we specify the of
parameter.
Codable Swift Array
s will always have a homogenous value type. Custom Codable
types that serialize heterogenous values to unkeyed containers are the exception and should use the .array
data type.
Enum¶
The enum data type is capable of storing string backed Swift enums natively. Native database enums provide an added layer of type safety to your database and may be more performant than raw enums.
To define a native database enum, use the enum
method on Database
. Use case
to define each case of the enum.
// An example of enum creation.
database.enum("planet_type")
.case("smallRocky")
.case("gasGiant")
.case("dwarf")
.create()
Once an enum has been created, you can use the read()
method to generate a data type for your schema field.
// An example of reading an enum and using it to define a new field.
database.enum("planet_type").read().flatMap { planetType in
database.schema("planets")
.field("type", planetType, .required)
.update()
}
// Or
let planetType = try await database.enum("planet_type").read()
try await database.schema("planets")
.field("type", planetType, .required)
.update()
To update an enum, call update()
. Cases can be deleted from existing enums.
// An example of enum update.
database.enum("planet_type")
.deleteCase("gasGiant")
.update()
To delete an enum, call delete()
.
// An example of enum deletion.
database.enum("planet_type").delete()
Model Coupling¶
Schema building is purposefully decoupled from models. Unlike query building, schema building does not make use of key paths and is completely stringly typed. This is important since schema definitions, especially those written for migrations, may need to reference model properties that no longer exist.
To better understand this, take a look at the following example migration.
struct UserMigration: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema("users")
.field("id", .uuid, .identifier(auto: false))
.field("name", .string, .required)
.create()
}
func revert(on database: Database) async throws {
try await database.schema("users").delete()
}
}
Let's assume that this migration has been has already been pushed to production. Now let's assume we need to make the following change to the User model.
- @Field(key: "name")
- var name: String
+ @Field(key: "first_name")
+ var firstName: String
+
+ @Field(key: "last_name")
+ var lastName: String
We can make the necessary database schema adjustments with the following migration.
struct UserNameMigration: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema("users")
.field("first_name", .string, .required)
.field("last_name", .string, .required)
.update()
// It is not currently possible to express this update without using custom SQL.
// This also doesn't try to deal with splitting the name into first and last,
// as that requires database-specific syntax.
try await User.query(on: database)
.set(["first_name": .sql(embed: "name"))
.run()
try await database.schema("users")
.deleteField("name")
.update()
}
func revert(on database: Database) async throws {
try await database.schema("users")
.field("name", .string, .required)
.update()
try await User.query(on: database)
.set(["name": .sql(embed: "concat(first_name, ' ', last_name)"))
.run()
try await database.schema("users")
.deleteField("first_name")
.deleteField("last_name")
.update()
}
}
Note that for this migration to work, we need to be able to reference both the removed name
field and the new firstName
and lastName
fields at the same time. Furthermore, the original UserMigration
should continue to be valid. This would not be possible to do with key paths.
Setting Model Space¶
To define the space for a model, pass the space to the schema(_:space:)
when creating the table. E.g.
try await db.schema("planets", space: "mirror_universe")
.id()
// ...
.create()