Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Views #299

Open
grosch opened this issue May 26, 2018 · 13 comments
Open

Views #299

grosch opened this issue May 26, 2018 · 13 comments
Labels
enhancement New feature or request
Projects

Comments

@grosch
Copy link

grosch commented May 26, 2018

Please provide a way to create database views.

@MrMage
Copy link

MrMage commented May 26, 2018

See also vapor/fluent#493.

@grosch
Copy link
Author

grosch commented May 28, 2018

Here's an even better example of why I want this. To get the data for one of my Leaf views I'm currently using this view from the database. Would you want to try and write that in fluent?

CREATE VIEW public.v_orders AS
WITH cte AS (
	SELECT o.id order_id, SUM(w.amount * pt.amount * (CASE WHEN pt.abbrev = 'ud' THEN 1.0 ELSE o.station_multiplier END)) complete
	FROM work_completed w
	INNER JOIN order_points op ON w.order_point_id = op.id
	INNER JOIN points pt ON op.points_id = pt.id
	INNER JOIN orders o ON op.order_id = o.id
	GROUP BY o.id
)
SELECT o.id,
    c.name AS client_name,
    s.name AS station_name,
    o.created,
    o.due,
    o.month,
    o.comments,
    o.name AS order_name,
    o.station_multiplier,
    o.total_points,
    COALESCE(cte.complete, 0) AS produced,
    COALESCE(cte.complete, 0) >= o.total_points AS complete
FROM orders o
LEFT OUTER JOIN cte ON cte.order_id = o.id
JOIN stations s ON o.station_id = s.id
JOIN clients c ON s.client_id = c.id;

@tanner0101
Copy link
Member

@grosch how would you imagine this working through Fluent? If you could share some ideas for what the API might look like that would be great. TBH I haven't really used views much before. Do you need to create them per connection or are they saved to the database?

I imagine we could create a sub-protocol of Model that helps define models that are backed by views instead of tables.

@grosch
Copy link
Author

grosch commented May 30, 2018

Views are saved in the database just like a table. It's basically just a stored "select" statement so you don't have to rewrite it all the time. I think there's a way to insert via a view, but I've never done that, just the query.

On a model you've got the Database.create() call where it'll create the table. We would just need some way to give a view definition (like above) so that it only ran a single time and didn't try to keep recreating it over and over.

For the view example I showed above I have this in my Xcode project:

import FluentPostgreSQL
import Vapor

final class ViewOrder: Codable {
    static let entity = "v_orders"

    var id: Int? = nil
    var clientName = ""
    var stationName = ""
    var created = Date()
    var due = Date()
    var month = Date()
    var comments: String? = nil
    var name = ""
    var stationMultiplier = 0
    var totalPoints = 0.0
    var produced = 0.0
    var complete = false

    enum CodingKeys: String, CodingKey {
        case id, created, due, month, comments, complete, produced
        case clientName = "client_name"
        case stationName = "station_name"
        case name = "order_name"
        case stationMultiplier = "station_multiplier"
        case totalPoints = "total_points"
    }
}

extension ViewOrder: PostgreSQLModel {}

I had to assign default values to everything because of the constructor, which was a bit annoying, but seems to work OK. The only trick, which @MrMage showed me, was that in configure.swift I also had to do this:

    ViewOrder.defaultDatabase = DatabaseIdentifier<PostgreSQLDatabase>.psql

which seemed to be required because there was no Migration to run, and so it didn't associate it with a database. So I'm honestly not seeing any need/value for a new protocol, just a way to tell vapor to create the view if it doesn't exist.

Obviously the person could just go into the database and do it directly, but it's better to have it as part of the Vapor project I think so that if you deploy to a new box down the road you don't have to remember to do it.

@grosch
Copy link
Author

grosch commented May 30, 2018

I guess you could have a DBView type protocol that required a static func viewCreateCommand() -> String and then in that they'd return the string, like I showed above. Then, in configure.swift, you'd do something like:

let db = ....
db.configureViews([ViewOrder.self, ViewSomethingElse.self])

and it would thus expect an array of DBView objects, and would call the viewCreateCommand on them if those names or whatever didn't exist in the database's fluent table.

@grosch
Copy link
Author

grosch commented May 30, 2018

Or, to be more consistent, you could add another method to the builder:

extension ViewOrder: Migration {
    static func prepare(on connection: PostgreSQLConnection) -> Future<Void> {
        return Database.create(self, on: connection) { builder in
            try builder.createView(
                """
                WITH cte AS (
                     ....
                JOIN clients c ON s.client_id = c.id
                """
            )
        }
    }
}

@MrMage
Copy link

MrMage commented May 30, 2018

Or, to be more consistent, you could add another method to the builder:

At that point I guess one could just skip using the builder and execute raw SQL to create the View via the connection instead of using Database.create.

@grosch
Copy link
Author

grosch commented Jun 7, 2018

I think it would have to be inside Database.create so that it only ran the one time. But I'm happy to do it however you guys say is the right way.

@MrMage
Copy link

MrMage commented Jun 7, 2018

AFAIK, Database.create is specifically for creating a schema (currently only tables). The entire migration method will only be called once (I think), so you could just put custom SQL that creates the view into the migration method of your view class.

@foobarzap
Copy link

Just a note: view integration might not be as easy as simply reusing the Model-class. Views may be read only and if they are not, they may demand special treatment when it comes to caching. Moreover, there are temporary views whose lifetime is tied to the the session.

@tanner0101
Copy link
Member

tanner0101 commented Jul 12, 2018

If a view is saved to the database like a table (not just temporary to the session / connection), then I think the migration system would be the best way to create them.

It should be possible to do something like this currently:

final class ViewOrder: Model {
    // set it up just like a normal model
}
struct CreateViewOrderView: Migration {
    static func prepare(on connection: PostgreSQLConnection) -> Future<Void> {
            return connection.raw(
                """
                CREATE VIEW public.v_orders AS
                WITH cte AS (
                     ....
                JOIN clients c ON s.client_id = c.id
                """
            ).run()
    }

    static func revert() { /* drop the view */ }
}
ViewOrder.defaultDatabase = .psql

If that is the case, then we just need some conveniences around building CREATE VIEW queries. If views are standard SQL, then that should be added to https://github.com/vapor/sql.

If views are specific only to certain SQL dialects, then we should add that capability to the drivers themselves.

@vzsg
Copy link
Member

vzsg commented Jul 12, 2018

The CREATE VIEW SQL command has been part of the standard since '89 (at least that's the oldest reference I found to it 😄), and all dialects support it. There are some extra features in some of them, but the core syntax and behavior is the same.

@foobarzap
Copy link

A view is not a table - even not a read-only one. Updating a view might be impossible (forbidden) and changing an underlying table might effect the view (see CHECK OPTION).

@tanner0101 tanner0101 changed the title Allow view creation Views May 29, 2020
@tanner0101 tanner0101 transferred this issue from vapor/fluent May 29, 2020
@tanner0101 tanner0101 added the enhancement New feature or request label May 29, 2020
@tanner0101 tanner0101 added this to To Do in Vapor 4 via automation May 29, 2020
@tanner0101 tanner0101 moved this from To Do to Backlog in Vapor 4 May 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Vapor 4
  
Backlog
Development

No branches or pull requests

5 participants