1

I'm writing a web service in Swift using Vapor framework.

I have model named Item. Intially it has only name and id properties.

typealias VaporModel = Content & PostgreSQLModel & Parameter
final class Item: VaporModel {
    var id: Int?
    var name: String
}

After I configure a controller for the model and add the routes, when I hit the post Item request, I get the error as Model.defaultDatabase is required to use as DatabaseConnectable. I think the error is because I have not added Item to Migrations in configure.swift and I do the same after conforming Item to PostgreSQLMigration.

var migrations = MigrationConfig()
migrations.add(model: Item.self, database: .psql)
services.register(migrations)

Now, I am able to hit the post request and create items in the database.

So I understand that Migration protocol creates the default schema for a model and adds a new table to the database with the model's properties as columns.

Now I want to add a property such as price to my Item class. Now when I hit the post request, I get the error as column "price" of relation "Item" does not exist. I assume the Migration protocol will be able to identify the schema changes and the column to my table (that's what I was used to in while using Realm for my iOS apps). But I am wrong and I read through the Migration docs and implement the prepare and revert methods in migration like below.

extension Item: PostgreSQLMigration {
    static func prepare(on conn: PostgreSQLConnection) -> Future<Void> {
        return Database.create(self, on: conn) { creator in
            creator.field(for: \.price)
        }
    }

    static func revert(on connection: PostgreSQLConnection) -> EventLoopFuture<Void> {
        return Future.map(on: connection) { }
    }
} 

I'm still struck with the same error column "price" of relation "Item" does not exist. What am I missing here? Is my migration code correct?

Also, I understand that if am not making any changes to the Model, I can comment out the migration config, because they need not run every time I run the service. Is that correct?

imthath
  • 1,353
  • 1
  • 13
  • 35

1 Answers1

3

With your code you haven't added a new migration. You have implemented a manual initial migration, but the initial migration has run already as requested (migrations.add(model: Item.self, database: .psql). To create a new migration you would need sth like:

struct ItemAddPriceMigration: Migration {
    typealias Database = PostgreSQLDatabase

    static func prepare(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {

        return Database.update(Item.self, on: conn) { builder in
            builder.field(for: \.price)
        }
    }

    static func revert(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
        return conn.future()
    }
}

And then you need to add it in configure:

migrations.add(migration: ItemAddPriceMigration.self, database: .psql)

Maciej Trybiło
  • 1,187
  • 8
  • 20
  • I have already tried this approach and it did not work out. The code you have shared, did not build, and it build when I changed `self` to `Item.self`. Then when I run the service, I'm getting the error `Error raised at top level: ⚠️ PostgreSQL Error: relation "Item" already exists` – imthath May 02 '19 at 08:10
  • Sorry, it's `update`, not `create` of course. Answer updated. – Maciej Trybiło May 02 '19 at 09:05
  • thank you. that solved that error. how should I provide default values for the new column? because i am getting the error `column "price" contains null values` – imthath May 02 '19 at 09:21
  • 1
    Assuming `price` is an `Int` you would do `builder.field(for: \.price, type: .bigint, PostgreSQLColumnConstraint.default(.literal(1)))` for price of 1. – Maciej Trybiło May 02 '19 at 09:30
  • now I want to make price as double/float, i can do the migration as above and set default value but that will set all item prices in the database to the default value. How to migrate each item's price? – imthath Jul 27 '19 at 08:21
  • I'm not sure how I would do that, but I'd discourage you from using a floating point type to represent a price. It's a nightmare with rounding errors. Safe and simple is to use an integer that represents the number of 'cents'. So you would have 120 for $1.20 for instance. – Maciej Trybiło Jul 28 '19 at 10:28
  • That’s actually not my requirement. My requirement is that i am changing an enum of raw type int to string. I just asked with the price example i need not start a new thread. – imthath Jul 28 '19 at 11:24