ql icon indicating copy to clipboard operation
ql copied to clipboard

[request]: Build dynamic query builder similar to sequel (rb)

Open mguentner opened this issue 7 years ago • 16 comments

Currently statements need to be written and compiled for each request. A query builder could help doing that by aggregating the values / parameters first and then compiling the statement. I like the interface the Sequel project (ruby) has.

Here is a simple select with chained statements:

ds = DB[:posts]
result = ds.select(:id, :name).order(:name).where{id < 100}.all

http://sequel.jeremyevans.net/rdoc/files/doc/dataset_basics_rdoc.html http://sequel.jeremyevans.net/documentation.html

mguentner avatar Apr 25 '18 20:04 mguentner

I hit the "Comment" button too soon.

mguentner avatar Apr 25 '18 21:04 mguentner

I'm afraid this does not fit the existing QL mechanism. The linked article says this:

One of the core dataset ideas that should be understood is that datasets are frozen and use a functional style of modification, in which methods called on the dataset return modified copies of the dataset, they don't modify the dataset themselves:

The problem is, that in QL a dataset do not exists. Or more precisely, it never materializes in its entirety. QL translates the query to a function that on every call computes and returns just one row of the dataset. The nice property of a "frozen" dataset is that it can be reused and (sub)queried in different ways later, without recomputing it. That is possible to simulate in QL only by INSERT INTO foo SELECT ... to materialize the dataset and subsequently performing the (sub)queries over the foo table.

But maybe I've just misunderstood the concepts used by sequel.

cznic avatar Apr 26 '18 08:04 cznic

@mguentner I'm not sure what can be done with this issue. WDYT?

cznic avatar May 07 '18 09:05 cznic

The initial idea was to have a query builder that helps to construct queries where the arguments can differ. Sequel was just chosen because I like the API of constructing queries. How the actual dataset is filtered is not important.

My current use case is a function that queries a table. Some of the arguments can be nil and thus won't be used in the WHERE clause. I currently have a base query like SELECT * FROM users and maintain a index counter for the arguments.

Example:

count := 1
basequery := "SELECT * FROM users"
query := basequery
if name != nil {
  if count == 1 {
    query += " where"
  } else if count > 1 {
    query += " and"
  }
  query = query + " name == $" + strconv.Itoa(count)
  count++
}
if email != nil {
  if count == 1 {
    query += " where"
  } else if count > 1 {
    query += " and"
  }
  query = query + " email == $" + strconv.Itoa(count)
  count++
}
// further down...
compiledQuery, err := ql.Compile(query + ";")
// even further down...

if name != nil && email != nil {
  rs, _, err = db.Execute(ctx, compiledQuery, name, email)
}
if name == nil && email != nil {
  rs, _, err = db.Execute(ctx, compiledQuery, email)
}
if name == nil && email == nil {
  rs, _, err = db.Execute(ctx, compiledQuery)
}

Instead of concatenating a query string like above, I'd like to be able to construct a builder.


users := ql.NewSelectBuilder("users") // constructs a query for the table "users"
// -> SELECT * from users where email == [email protected] and name == "John"
query, _ = users.Where("email", email).And("name", name).Compile()
//or -> SELECT * from users where name == "John"
query, _ = users.Where("name", name).Compile()
//or -> SELECT * from users
query, _ = users.Compile()

// query is now ql.List

rs, _, err = db.Execute(ctx, query)

I believe that the above is possible with ql. Thanks for your work by the way! You can close the issue if you want or leave it open to document the requirement :)

mguentner avatar May 07 '18 12:05 mguentner

@mguentner I've tried to code something in branch issue198. Please take a look and let me know IIUC the goal. The code is dirty copy pasting and not at all undocumented, but the point of your interest is perhaps mainly the test at https://github.com/cznic/ql/blob/0469cd0e545deca4543e5c709b6edc9c5054a077/all_test.go#L3953.

The new API is mostly ad hoc, so any suggestions are welcome, thanks.

cznic avatar May 07 '18 17:05 cznic

Awesome! This already looks really nice. Thank you.

The expression API should allow for an identity Expression that is ignored and forms the start of a chain when the chain links are not known at compile time or should be constructed imperatively:

expr := NewExpression("")
first := true
if email != nil {
  expr = expr.Start("email").Equal(email)
  first = false
}
if name != nil {
  if first {
    expr = expr.Start("name").Equal(name)
  } else {
    expr = expr.And("name").Equal(name)
  }
  first = false
}
// int16
if age != -1 {
  if first {
    expr = expr.Start("age").Equal(age)
  } else {
    expr = expr.And("age").Equal(age)
  }
  first = false
}
func (e *Expression) Start(f interface{}) *Expression    { return e.binop("", f) }

Could be enough.

Let me know if you see another way of constructing such a query using your current API since my example is just a naive continuation of the count++ example in https://github.com/cznic/ql/issues/198#issuecomment-387047655

mguentner avatar May 07 '18 17:05 mguentner

@mguentner PTAL, thank you.

cznic avatar May 07 '18 19:05 cznic

LGTM ;)

mguentner avatar May 07 '18 21:05 mguentner

Also https://github.com/cznic/ql/commit/52dc064dbc0a58f4941c1456b0896e4671bafe57 updates this issue.

@mguentner I'd like to ask you to now try to use this branch for some time, say a week or so. Let's collect your experiences and iterate over any troubles you may run into. After we'll eventually think it's ready to publish, it'll be committed to the master branch. Thanks.

cznic avatar May 08 '18 08:05 cznic

Will do.

mguentner avatar May 08 '18 15:05 mguentner

@mguentner Have you had a chance to work with the new API? If so, please provide feedback, thank you.

cznic avatar May 16 '18 09:05 cznic

@cznic Not yet. Needed to refactor a lot of stuff before I change the database layer again. Once I've used the new API, I will update this issue with feedback. I promise.

mguentner avatar May 16 '18 10:05 mguentner

No hurry, no problem. Thanks.

cznic avatar May 16 '18 10:05 cznic

ql.NewSelectStmt(ql.NewField("COUNT(*)", "")).From("users").Where(...) does not work.

-> invalid expression COUNT(*)

however ql.NewSelectStmt(ql.NewField("COUNT", "")).From("users").Where(...) does work but does not make sense in SQL.

The COUNT(*) field could also be a const in the ql package as it is quite common.

mguentner avatar May 24 '18 12:05 mguentner

This is working as documented. COUNT(*) is not a field name, it's an expression. This works

NewSelectStmt(NewExpression("count(*)")).From("users").Where(42)

cznic avatar May 24 '18 13:05 cznic

Indeed. Thanks!

mguentner avatar May 24 '18 13:05 mguentner