-
-
Notifications
You must be signed in to change notification settings - Fork 727
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
Ordering of order clauses (was: Expose initializer for _SQLExpressible, _SQLOrdering) #85
Comments
Hello @palpatim. I'm in vacation now, so I won't be able to answer your question until next week. |
I'm not sure I understand your problem. Why don't you just write: // MyTable.Columns.objectKind = SQLColumn("objectKind")
// MyTable.Columns.createdDate = SQLColumn("createdDate")
let baseSearchPredicate = MyTable.Columns.objectKind == 1
let defaultOrder = MyTable.Columns.createdDate.desc
// And there we go:
let records = MyTable.filter(baseSearchPredicate).order(defaultOrder).fetchAll(db) Pretty much Swifty enough to me. Have a detailed look at https://github.com/groue/GRDB.swift#the-query-interface. I advise you not to use the underscore-prefixed types in your code. Values are OK, as in the sample code above. But don't write the following: // WRONG: future versions of GRDB may break this code
let baseSearchPredicate: _SQLExpression = MyTable.Columns.objectKind == 1
let defaultOrder: _SQLSortDescriptor = MyTable.Columns.createdDate.desc
// GOOD
let baseSearchPredicate = MyTable.Columns.objectKind == 1
let defaultOrder = MyTable.Columns.createdDate.desc |
Thanks for the response, Gwendal. Your suggestion works at the declaration/assignment site, but doesn't allow us to declare parameter types so that we can pass the values around outside of their declaration scope. (I apologize for not including that in my original use case statement.) What I was hoping to achieve was something like: let baseSearchPredicate = MyTable.Columns.objectKind == 1 // baseSearchPredicate is _SQLExpression
let defaultOrder = MyTable.Columns.createdDate.desc
func doQuery(predicate: ???, order: ???) -> [MyTable]? {
let request = MyTable.filter(predicate && baseSearchPredicate).order(order, defaultOrder)
return dbQueue.inDatabase { db in
return MyTable.fetchAll(db, request)
}
} But it looks like the right way to do this is to compose class Person : Record {
var id: Int64?
var firstName: String?
var lastName: String?
struct Columns {
static let id = SQLColumn("id")
static let firstName = SQLColumn("firstName")
static let lastName = SQLColumn("lastName")
}
static let baseSearchPredicate = Columns.firstName != nil && Columns.lastName != nil
static let defaultOrder = Columns.id.asc
static func doQuery(dbQueue: DatabaseQueue, withRequest suppliedRequest: QueryInterfaceRequest<Person>) -> [Person]? {
let request = suppliedRequest.filter(baseSearchPredicate).order(defaultOrder)
return dbQueue.inDatabase() {
db in
return Person.fetchAll(db, request)
}
}
...
} And invoke it like: let specificRequest = Person.filter(Person.Columns.firstName == "Arthur" || Person.Columns.firstName == "Cinderella").order(Person.Columns.firstName)
let results = Person.doQuery(dbQueue, withRequest: specificRequest)
// SELECT * FROM "persons" WHERE ((("firstName" = 'Arthur') OR ("firstName" = 'Cinderella')) AND (("firstName" IS NOT NULL) AND ("lastName" IS NOT NULL))) ORDER BY "firstName", "id" ASC Does that seem right? If so, and we should work with let request = Person.filter(Person.Columns.firstName == "Arthur" || Person.Columns.firstName == "Cinderella").filter(getBaseFilter()) Where |
Hello again @palpatim Thanks for the extra details.
Indeed, the types for predicate and order in this function are today semi-private: filter() wants _SQLExpressible, and order() wants _SQLOrdering. So you can't write this method today.
Yes, this is an option that you have today. baseSearchPredicate is added as an AND clause, and ordering of those does not matter. defaultOrder is added last, and thus it applies after the specific ordering, and this sounds sensible. In the above example, results are sorted by first name, as expected. Yet my experience of other ORMs makes me think that this pattern is odd. Something is wrong. Usually, in other ORMs, you have a base request that you extend: // The default request
let defaultPersons = Person.filter(defaultPredicate).order(defaultOrder)
// Later
let request1 = defaultPersons.filter(filter1).order(ordering1)
let results1 = dbQueue.inDatabase { request1.fetchAll($0) }
let request2 = defaultPersons.filter(filter2).order(ordering2)
let results2 = dbQueue.inDatabase { request2.fetchAll($0) } But the pattern above fails with ordering: the default ordering is applied first, unlike your example. The famous Ruby ORM Active Record uses a reorder method just for this purpose: // Some constant
let defaultPersons = Person.filter(defaultPredicate).order(defaultOrder)
// Later
let request1 = defaultPersons.filter(filter1).reorder(ordering1) // reorder
let results1 = dbQueue.inDatabase { request1.fetchAll($0) } I kind of believe that this would be still better than the proposed doQuery() method, even if the default ordering does not automatically apply. Your question is interesting, I'll have to consider this topic a little further. Meanwhile, may I suggest you to change the way you use DatabaseQueue? I know that it's tempting to wrap the
|
Thanks again for your reply, @groue. I agree with you about not being completely happy with the In the meantime, I'll re-evaluate my use of |
First let's make a strong point: it's better to refine existing requests than process them in a non-extensible method. Thus: // Works, but with ad-hoc method that performs hidden request transformation
let persons = Person.doQuery(db, filter, order, ...)
// Better, no ad-hoc method, no hidden request transformation, just request derivation
let baseRequest = Person...
let specificRequest = baseRequest.filter(...)...
let persons = dbQueue.inDatabase { request.fetchAll($0) } Now let's get specific. Here is the Active Record situation: class Person < ActiveRecord::Base
end
let basePersons = Person.order(:last_name, :first_name)
basePersons # ordered by last_name & first_name
basePersons.order(:age) # likely a bug: ordered by age *after* last_name & first_name
basePersons.reorder(:age) # ordered by age I don't like the reorder method at all. It is easily overlooked. When you forget it, the innucuous piece of code Yet it is a working solution for anyone that has enough experience with Active Record. And I could not find any alternative or critic of Could we improve on it? What about changing the // SELECT * FROM persons ORDER BY lastName, firstName
let baseRequest = Person.order(lastNameColumn, firstNameColumn)
// SELECT * FROM persons ORDER BY age, lastName, firstName
let specificRequest = baseRequest.order(ageColumn) This solution has many problems. First, it is surprising, because this is not how ORMs usually work (and one should never surprise a developer). Second, it makes the two following lines of code non-equivalent, when they should be (I see room for personal taste here): let request = Person.order(lastNameColumn).order(ageColumn)
let request = Person.order(lastNameColumn, ageColumn) OK so what about splitting ordering clauses in two sets, along the major/minor axis, or default/specific, or first/last? // SELECT * FROM persons ORDER BY lastName, firstName
let baseRequest = Person.lastOrder(lastNameColumn, firstNameColumn)
// SELECT * FROM persons ORDER BY age, lastName, firstName
let specificRequest = baseRequest.order(ageColumn) This would work, but now I feel like it solves the wrong problem: it is questionable that sorting by lastName and firstName is still relevant after the age ordering. Second try: // SELECT * FROM persons ORDER BY lastName, firstName
let baseRequest = Person.defaultOrder(lastNameColumn, firstNameColumn)
// SELECT * FROM persons ORDER BY age
let specificRequest = baseRequest.order(ageColumn) This second try is more interesting: there is a default ordering, but it is cancelled as soon as a "regular" order clause is added. The Of course, now, it is the preservation of the default ordering which is difficult: // Weird way to get SELECT * FROM persons ORDER BY lastName, firstName, age
let specificRequest = baseRequest.defaultOrder(ageColumn) OK, this was the first exploration of the topic. So far, // SELECT * FROM persons ORDER BY lastName, firstName
let baseRequest = Person.order(lastNameColumn).order(firstNameColumn)
// SELECT * FROM persons ORDER BY lastName, firstName, age (likely a bug)
let specificRequest = baseRequest.order(ageColumn)
// SELECT * FROM persons ORDER BY age (unambiguously the desired request)
let specificRequest = baseRequest.reorder(ageColumn) Thoughts? |
(I have renamed the issue to what I think is the root of your question - feel free to refocus the issue if I was misled) |
It is worth noting that Django ORM does not chain order clauses:
Considering the sane "explicit is better than implicit" rule of the Python ecosystem, it is a serious option to consider as well. |
OK I made up my mind, and chose the Django way: each order call clears any previous ordering. // SELECT * FROM "persons" ORDER BY "name"
Person.order(scoreColumn).order(nameColumn) Shipped in v0.76.0. I believe this closes this issue for good. |
Thanks very much, @groue. I appreciate the careful consideration you gave this. While it doesn't necessarily meet my original goal, I agree this is a simpler, less surprising implementation. |
@palpatim That's why developers who share their questions and goals are awesome! Your input brought a tiny modification with great consequences ! |
Thanks for all your work on this module--it's really great work so far.
One thing that would be handy is to expose an API for constructing
_SQLExpressible
and_SQLOrdering
instances. Right now, if we want to, say, cache a commonly-used predicate for a table, we have to interpolate column names into a String. That loses type safety and adds syntactic cruft, and in general doesn't feel Swifty. In addition, it breaks the feel of the query interface because now instead of crafting a predicate, I'm passing in a String--I might as well use hand-crafted SQL at that point. :)Example:
Preferred:
It's very possible I'm just missing something in the API, in which case, please feel free to disregard this message.
The text was updated successfully, but these errors were encountered: