- Notifications
You must be signed in to change notification settings - Fork975
Support dynamic queries#364
-
Does it make sense to support dynamic queries ?(Dynamic Due to some requirements we have within our team, we need select statements with dynamic filters (a set of unknown numbers of where conditions, or lets say a set of optional where conditions which could be added to the query based on generated function's arguments) and also dynamic columns to order. I know |
BetaWas this translation helpful?Give feedback.
All reactions
👍 116👀 28
Replies: 39 comments 49 replies
-
Hi@mehdijoe, I requested for the same in slack but it's not supported as of now. I was thinking even if this got supported there is another mess of switch case statements for right sqlc method to call based on the dynamics parameters passed. |
BetaWas this translation helpful?Give feedback.
All reactions
👍 3
-
@mehdijoe Can you provide a concrete example of your needs? I'm surprised that the dynamic filters are unknown. Having a concrete example would also make it easier to discuss the best solution.@dharmjit, it sounds like you have the same issue? Could you provide your use case? |
BetaWas this translation helpful?Give feedback.
All reactions
-
Thanks for ur response@kyleconroy
Actually unknownnumbers of conditions ..., OK, here is my use case: createtablet1( field_fk uuid, field1varchar(100), field2varchar(100),); I need to select on select*from t1where field_fk=$1select*from t1where field_fk=$1AND field1= $2select*from t1where field_fk=$1AND field1like $2select*from t1where field_fk=$1AND field2= $2select*from t1where field_fk=$1AND field2like $2select*from t1where field_fk=$1AND field1= $2and field2= $3select*from t1where field_fk=$1AND field1like $2and field2= $3select*from t1where field_fk=$1AND field1= $2and field2like $3select*from t1where field_fk=$1AND field1like $2and field2like $3 Users can also sort on any column they wish (ASC/DESC), it means for each query listed above I need to append following order by field1ASCorder by field1DESCorder by field2ASCorder by field2DESC Which is I wish I could use sqlc with such an input query (this is kind of my proposal for the feature): -- name: GetT1 :manySELECT t1Where field_fk=@field-fk_param::text--optional field1_eq: AND field1=@field1_eq_value::text--optional field1_like: AND field1 like @field1_like_value::text--optional field2_eq: AND field2=@field1_eq_value::text--optional field2_like: AND field2 like @field1_like_value::text--optional order_field1_ASC: ORDER BY field1 ASC--optional order_field1_DESC: ORDER BY field1 DESC--optional order_field2_ASC: ORDER BY field2 ASC--optional order_field2_DESC: ORDER BY field2 DESC--optional_group: order_field1_ASC, order_field1_DESC, order_field2_ASC, order_field2_DESC A select statement with a number of optional lines, if there is any optional line in the input query the whole query should use named parameters, each optional line would start with And Before compiling the query, sqlc needs to un-comment the optional lines |
BetaWas this translation helpful?Give feedback.
All reactions
👍 42
-
@mehdijoe Thank you so much for providing a concrete example. sqlc can handle the use case you outlined with a single query. Just a note, I changed the names of the columns so that the example would be more readable. CREATETABLEfoo ( fk uuid, barvarchar(100), bazvarchar(100));-- name: FilterFoo :manySELECT*FROM fooWHERE fk= @fkAND (CASE WHEN @is_bar::bool THEN bar= @bar ELSE TRUE END)AND (CASE WHEN @lk_bar::bool THEN barLIKE @bar ELSE TRUE END)AND (CASE WHEN @is_baz::bool THEN baz= @baz ELSE TRUE END)AND (CASE WHEN @lk_baz::bool THEN bazLIKE @baz ELSE TRUE END)ORDER BY CASE WHEN @bar_asc::bool THEN bar ENDasc, CASE WHEN @bar_desc::bool THEN bar ENDdesc, CASE WHEN @baz_asc::bool THEN baz ENDasc, CASE WHEN @baz_desc::bool THEN baz ENDdesc; You can see what the generated code lookshere in the playground. Below is an example main function that shows how to use Please let me know if I've missed anything! package mainimport ("context""database/sql""log"_"github.com/lib/pq""github.com/google/uuid")funcrun(ctx context.Context,db*sql.DB)error {q:=New(db)rows,err:=q.Filter(ctx,FilterParams{// Set the values of the two columns that you're comparingFk:uuid.MustParse("example-uuid"),Bar: sql.NullString{Valid:true,String:"foo"},Baz: sql.NullString{Valid:true,String:"bar"},// Set the values of the two columns that you're comparingIsBar:true,// IsBaz: true,// Configured the order of the resultsBarAsc:true,// BarDesc: true,})iferr!=nil {returnerr}for_,row:=rangerows {log.Printf("%d: bar:%s baz:%s\b",row.Sid,row.Bar.String,row.Baz.String)}returnnil}funcmain() {db,err:=sql.Open("postgres","dbname=example sslmode=disable")iferr!=nil {log.Fatal(err)}iferr:=run(context.Background(),db);err!=nil {log.Fatal(err)}} |
BetaWas this translation helpful?Give feedback.
All reactions
👍 43👎 5
-
Could you please provide syntax when using with MySQL? |
BetaWas this translation helpful?Give feedback.
All reactions
-
you just need to remove all "::bool" |
BetaWas this translation helpful?Give feedback.
All reactions
-
This approach breaks down when |
BetaWas this translation helpful?Give feedback.
All reactions
-
@bnllb this doesn't seem to work in mySQL. Eg: |
BetaWas this translation helpful?Give feedback.
All reactions
-
usingnamed parameters: -- name: GetAuthorByUid :oneselect*from authorswhere id=sqlc.arg(id)and (CASE WHENsqlc.arg(is_name) THEN name=sqlc.arg(name) ELSE TRUE END); |
BetaWas this translation helpful?Give feedback.
All reactions
-
@kyleconroy Thank you a lot for the explanation and your time amigo ! |
BetaWas this translation helpful?Give feedback.
All reactions
👍 4
-
Some input to the discussion: This form of static query with dynamic filtering is generally regarded as one of the worst performance With respect to What this would mean in the case of the combination of |
BetaWas this translation helpful?Give feedback.
All reactions
👍 24
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
I am not sure this is the same for postgresql as I think its optimizer optimizes the queries, however ordering with a case statement will not be able to use the indexes which critical. I might be mistaken because I don't usually write my queries with the case statement work around. For example the following where I don't expect baz to ever have a value of an empty string |
BetaWas this translation helpful?Give feedback.
All reactions
-
Hi@kyleconroy , your comment with sql using dynamic filter kind of solve the issue but sometimes its gets messy to prepare/provide these many arguments to generated method. Do you see this being supported in some other way in sqlc evolution. |
BetaWas this translation helpful?Give feedback.
All reactions
-
@arddor Thanks for the great write up about why this pattern can result in sub-optimal query plans.
I think there are a few ways we could better support this pattern in sqlc. Right now I'm leaning toward generating multiple queries for a single input query, and then choosing which query to execute based on one of the arguments. Here's what this could look like in practice -- name: FilterFoo :manySELECT*FROM fooWHERE fk= @fkORDER BY CASE WHENsqlc.switch('sort','bar_asc') THEN bar ENDasc, CASE WHENsqlc.switch('sort','bar_desc') THEN bar ENDdesc, CASE WHENsqlc.switch('sort','baz_asc') THEN baz ENDasc, CASE WHENsqlc.switch('sort','baz_desc') THEN baz ENDdesc; package dbimport ("context""database/sql""log""github.com/google/uuid")typeSortstringconst (SortBarAscSort="bar_asc"SortBarDescSort="bar_desc"SortBazAscSort="baz_asc"SortBazDescSort="baz_desc")typeFilterFooParamsstruct {Fk uuid.UUIDSortSortEnum} While I think this could work, it's a significant amount of work. Right now I'm focusing my time working on better type checking and inference, and adding SQLite and MySQL support. I don't see this being a priority any time soon. Sorry. |
BetaWas this translation helpful?Give feedback.
All reactions
👍 31❤️ 15
-
Is this on the roadmap? |
BetaWas this translation helpful?Give feedback.
All reactions
👀 1
-
The problem with this is it fixes the sort options. For example lets say I had a table like order_item, and It had a price and quantiy column, and I wanted to sort by quantiy * price. |
BetaWas this translation helpful?Give feedback.
All reactions
-
Is this supported using MySQL? Cannot figure out the syntax for this. |
BetaWas this translation helpful?Give feedback.
All reactions
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
I'm running into this issue too. This is currently impossible on MySQL with SQLC. And the behavior is weird. Doing something like Produces this error when running
Except So@kyleconroy it doesn't look like sqlc has any support for dynamic queries, only that postgres does and sqlc lets you pass in variables postgres can use. It's also unclear to me why we can't type query inputs in the msql driver,it's undocumented why mysql uses question mark In MySQL with sqlc making a query like raises the error
I think mysql users are out of luck here and have to deal with combinatorial explosion for now |
BetaWas this translation helpful?Give feedback.
All reactions
-
I think mysql is a little easier. Or you can do it like |
BetaWas this translation helpful?Give feedback.
All reactions
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
Would also be interested in this feature. I think dynamic where/orders etc appear in common use-cases. Would it make sense to break out of SQL syntax here? This feature is a wish to express multiple possible query structures, rather than parameterising a single static query. Using a syntax that's clearly separate would avoid confusion between what's slotting parameters into one SQL text, and what's about expressing multiple SQL texts. A rough example of what this could look like (not thinking too much about the right delimiters to use etc): -- name: FilterFoo :manySELECT*FROM fooWHERE fk= @fkORDER BY [switch sort case'bar_desc': sql("bar desc") case'bar_asc': sql("bar desc") case'baz_desc': sql("baz desc") case'baz_asc': sql("baz asc") ] One approach would be for the compiler to handle this first and generate multiple outputs, which it could then parse (and therefore validate) as SQL. Using the multiple queries approach@kyleconroy described, this final output of this would be 4 queries, one for each of the sort options, and a function that decides which to run. Rough example output: funcFilterFoo(ctx context.Context,paramsFilterFooParams) {// pick the sql to usesql:=""switchparams.Sort {caseSortBarAsc:sql=filterFooSQL_a// ... etc }// run query}constfilterFooSQL_a=`SELECT *WHERE fk = ?ORDER BY bar desc`// other queries (potentially expressed more optimally than consts with complete fragments) Dynamic where clauses for a query are often useful, vs selecting a potentially large result set and filtering in memory. As an example of what would be required to express "filter the results by zero or more of these criteria": SELECT*FROM foo[if any criteria_a critera_b [combine"AND" [if present criteria_a sql("col_a = $criteria_a") ] [if present criteria_b sql("col_b = $criteria_b") ]] ] This would output four queries: with where clauses (), (A), (B), (A,B). The parameters generated for the criteria would need to be nullable to distinguish zero values, e.g A potential challenge: the number of queries generated would double with each new independent parameter. So with 10 parameters there are 1,024 possible SQL texts. It'd need some research/thought to consider how best to mix runtime and compile-time generation to avoid runtime, memory or binary-size overhead. Clearly this would break out of the "only need to know SQL" paradigm (and would confuse IDEs). But that's the status-quo for what developers would have to do instead while using Anyway, I'd be interested in this feature, and may potentially have time to implement it. But before that I wanted to see if the maintainers and community liked the look of the approach! |
BetaWas this translation helpful?Give feedback.
All reactions
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
@timruffles |
BetaWas this translation helpful?Give feedback.
All reactions
-
Hi@kyleconroy, what do you think about this approach to select only necessary columns? As you can see we declare command CREATETABLEusers ( user_id UUIDPRIMARY KEYNOT NULL, emailVARCHAR(256)NOT NULL, first_nameVARCHAR(50)NOT NULL, last_nameVARCHAR(50)NOT NULL, created_atTIMESTAMPTZNOT NULL DEFAULT NOW(), updated_atTIMESTAMPTZNOT NULL DEFAULT NOW()); constuserColumns=`-- name: userColumns :one-columnsSELECT * FROM usersWHERE user_id = $1 LIMIT 1`func (q*Queries)userColumns(ctx context.Context,userID uuid.UUID,columns...string) (User,error) {s:="SELECT "+strings.Join(columns,",")variUserargs:=make([]interface{},len(columns))ifexists(columns,"user_id") {args=append(args,&i.UserID)}ifexists(columns,"email") {args=append(args,&i.Email)}ifexists(columns,"first_name") {args=append(args,&i.FirstName)}ifexists(columns,"last_name") {args=append(args,&i.LastName)}ifexists(columns,"created_at") {args=append(args,&i.CreatedAt)}ifexists(columns,"updated_at") {args=append(args,&i.UpdatedAt)}row:=q.db.QueryRow(ctx,strings.Replace(userColumns,"SELECT *",s,1),userID)err:=row.Scan(args...)returni,err}funcexists(ss []string,sstring)bool {fori:=0;i<len(ss);i++ {ifs==ss[i] {returntrue}}returnfalse} |
BetaWas this translation helpful?Give feedback.
All reactions
-
Is there a PR or Branch that has this behavior? |
BetaWas this translation helpful?Give feedback.
All reactions
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
Hello I just madea sql builder to make sqlc support simple dynamic queries. Not perfect, but enough for me. Any feedback or suggestion is welcome :) |
BetaWas this translation helpful?Give feedback.
All reactions
👍 11
-
I found your query builder while researching dynamic query solutions for sqlc. However, I can' quite seem to figure out how to use it. |
BetaWas this translation helpful?Give feedback.
All reactions
👍 2
-
My new solutionsqlc dynamic query example for SQL builder implemented usingSquirrel |
BetaWas this translation helpful?Give feedback.
All reactions
-
Try this one for dynamic optional query |
BetaWas this translation helpful?Give feedback.
All reactions
👎 8
-
How about a simpler approach for dynamic where clauses and dynamic order by clauses add support to define queries that if containing ($FILTER) will be replaced by an optional string parameter containing the filter clause or clear out ($FILTER) if empty parameter when where is present when where is not present then is dev responsibility to add the correct filter clause string dynamically from their code logic As for the order by clause it can be predefined as currently is but by adding also an optional string parameter that could contain an overridable order by clause SELECT t.* FROM table t WHERE t.id=3 and t.name='test' and ($FILTER) order by t.id, t.name; e.g: const listClasses = func (q *Queries) ListClasses(ctx context.Context, arg ListClassesParams, filterClause, order string) ([]*Class, error) { perform logic to replace filter if filterClause provided or remove ($FILTER) entirely before executing query rows, err := q.db.Query(ctx, listClasses, arg.ID, arg.Name) |
BetaWas this translation helpful?Give feedback.
All reactions
👍 2
-
I don't get it. The queries are written with $1 or ? in them. So this means that the data is bound by the sql driver to the proper place. and the driver knows if the input data is string and calls proper quote function.. Or driver prepares the statement and then binds parameters. |
BetaWas this translation helpful?Give feedback.
All reactions
-
I proposed these approaches (back in Feb 2023 - tickets listed below), for the simple reason that this will solve when the where clause comes predefined as a string, the sql-injection issue will have to be addressed by the implementing person to parse the sql string before sending it as parameter, but if you can address these tickets fairly quick it will at least let us take care of the choking point which is currently not supporting dynamic where clause and/or order by. Please let us know when these could be addressed, as we look forward to at least have these bare minimum enhancements done |
BetaWas this translation helpful?Give feedback.
All reactions
👍 1👎 1
-
I have implemented@go-aegian proposed solution in pull request#2343 |
BetaWas this translation helpful?Give feedback.
All reactions
-
So I think I have a similar solution that prevents sqlc injection. Instead of having a string like Now I do not have any types that meet the definition of the interface, however the mysql one is trivial. The postgresql one could be a struct that accepts a string in a mysql format and a variadic of interfaces. In the ToSql function replace the question marks with the appropriate dollar symbols. |
BetaWas this translation helpful?Give feedback.
All reactions
-
I decided to create a pull request for this#2859 |
BetaWas this translation helpful?Give feedback.
All reactions
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
My use case today was a select where I'm selecting on sometimes one and sometimes two different values of a column (the actual query is almost 20 lines long, so just duplicating it is unpleasant unless I wrote another tool to template the template ... ugh). select barfrom twhere fooin (?,?) My solution for this worked for two values and my particular schema (where foo isn't NULLable): ...from twhere fooin (sqlc.arg('foo'),sqlc.narg('foo_2')) |
BetaWas this translation helpful?Give feedback.
All reactions
-
I think sqlc has a sqlc.slice option for mysql. And you can use arrays for postgresql. Seehttps://docs.sqlc.dev/en/latest/howto/select.html#passing-a-slice-as-a-parameter-to-a-query |
BetaWas this translation helpful?Give feedback.
All reactions
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
I wrote a proposal#2061 (comment) for a This seems like an approachable compromise to remove some of the friction of |
BetaWas this translation helpful?Give feedback.
All reactions
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
dynamic sql is a very important feature for in fact |
BetaWas this translation helpful?Give feedback.
All reactions
-
@andrewmbenton would it be possible to give us an status update on the ideas you are considering or if you already agreed on the path forward? 🙏 Thanks in advance :) |
BetaWas this translation helpful?Give feedback.
All reactions
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
To add to all the above, we would find dynamic filtering / ordering instrumental at the SQLC layer, currently the API is too crufty and potentially non-performant to be considered acceptable above two or three columns, to the point where we must consider abandoning or forking the repository. It would be helpful for the authors to provide some certainty here, it has been four years and from earlier statements it seems the authors were open to this possibility. Can we be given a clear yes/no regarding support for this? |
BetaWas this translation helpful?Give feedback.
All reactions
-
This might be ugly, but why not just slap Go templates on top of it? -- name: GetUsers :manyselect u.*from usersas u{{ if .ManagerName }}join managersas monm.id=u.manager_idandm.name=sqlc.arg(ManagerName){{ end }}where true {{ if .Name }}and name=sqlc.arg(Name) {{ end }} {{ if .Type }}and type=sqlc.arg(Type) {{ end }} Tools like dbt use this approach and it must be more or less trivial to implement since there must be a template language parser somewhere in |
BetaWas this translation helpful?Give feedback.
All reactions
👍 1
-
Posting this here so I can ask for feedback. What we did for now was create the query with all the possible conditions so it will generate all the types needed ( when Here we added The only problem here is when we add other filters, we need to update our own copy of Usage then afterwards is: This works for most of our use-cases but is not full-on dynamic. |
BetaWas this translation helpful?Give feedback.
All reactions
-
this idea is actually workable |
BetaWas this translation helpful?Give feedback.
All reactions
-
Here is one more approach to the issue. We could just add special comments to the lines those need to be removed from the query text if the corresponding arguments are empty. Like this: Then, if one specifies, for example, only This approach has several advantages:
Maybe it does not cover all the cases, I'm not sure, but it's super simple for the most common case and it keeps the best possible performance of the query. What do you think? |
BetaWas this translation helpful?Give feedback.
All reactions
❤️ 21🚀 8👀 5
-
Below is an example of the kind of code that I hope to one day use with SQLC. Right now, it is usinggithub.com/Masterminds/squirrel (an SQL builder), imported as I believe@maxmanuylov'sproposed solution would cover this. The use of func (dDAO)SelectAllAccountsByFilter(ctx context.Context,filters models.Filters) ([]models.Account,error) {query:=sq.Select("id","name","email","active","fav_color","fav_numbers","properties","created_at").From("accounts").OrderBy("id")iflen(filters.Names)>0 {query=query.Where(sq.Eq{"name":filters.Names})}iffilters.Active!=nil {query=query.Where(sq.Eq{"active":*filters.Active})}iflen(filters.FavColors)>0 {query=query.Where(sq.Eq{"fav_color":filters.FavColors})} |
BetaWas this translation helpful?Give feedback.
All reactions
-
I have recently started experimenting with sqlc. Most colleagues who I have shown it to also love it but indeed dynamic queries is where things could be improved. In my specific use case, I often write APIs using Google'sAIP system which allow forfiltering in methods like This means that your SQL query is essentially based on user input. Writing a static query to cater for a situation like this can become quite hairy and somewhat difficult to maintain if you have a large amount of fields to filter on (Seethis article for more details on the subject). What I have personally done to avoid this is relied on usinghttps://github.com/Masterminds/squirrel like@veqryn mentioned for these methods which works but means yet another library to pull in, 2 different ways of handling SQL code in the same code base and also more room for mistakes as the code is written by hand. This is mostly meant to be a "here's the kind of problems I had and solutions I've come up with" post to hopefully allow for a more informed decision on the subject. |
BetaWas this translation helpful?Give feedback.
All reactions
-
Want to share my "solution" to this problem here as well in case it might be useful for anyone else: SELECT ...FROM ...WHERE<fixed condition>AND version= ifnull(sqlc.arg('version'), version)-- if version == NULL, then version = version, making it a NOOP With that, sqlc generates an typeParamsstruct {Versioninterface{}} Later in the code you can set the arg:= internal.Params{Version:storage.Optional(p.Version),} Our small // Optional returns a non-nil pointer to v if v is not its zero value.funcOptional[Tcomparable](vT)*T {varzeroTifv==zero {returnnil}return&v} But would be happy to have native support from sqlc here 🙏 |
BetaWas this translation helpful?Give feedback.
All reactions
-
Perhaps one solution could be to have a which would be rewritten to |
BetaWas this translation helpful?Give feedback.
All reactions
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
If you don't care about null values in filtered fields then you may do something like this Table: CREATETABLEpublic.parcel ( order_idcharacter varying(255) DEFAULT''::character varyingNOT NULL, shop_namecharacter varying(10) DEFAULTNULL::character varying,); Query -- name: ParcelsByFilters :manySELECT*FROM parcelWHERE (sqlc.narg(shopName)::text isnullor shop_name=sqlc.narg(shopName)::text)and (sqlc.narg(orderId)::text isnullor order_id=sqlc.narg(orderId)::text); sqlc.narg means nullable argument. Sqlc will generate structure with pointer fields and you can fill it as you want orderId:="XXX16549356748884"//shopName := "7507"filters:= repo.ParcelsByFiltersParams{Orderid:&orderId,//Shopname: &shopName,}parcels,err:=queries.ParcelsByFilters(context.Background(),filters)iferr!=nil {log.Fatal(err)} |
BetaWas this translation helpful?Give feedback.
All reactions
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
Seems like there hasn't been any discussions on this since last year. Do people have new workarounds or have moved to another library for this problem? |
BetaWas this translation helpful?Give feedback.
All reactions
-
I still don't know how to make dynamic order by, but after reading this thread and some more articles on the topic, I've compiled a list of hackshere. This by no means solves all problems, but that technique and not using queries directly, but instead wrapping into a repository helps me out a lot on all of the projects I use sqlc at. It just gives too many features that I'm not ready to give up and no support for truly dynamic queries doesn't really matter to me personally. |
BetaWas this translation helpful?Give feedback.
All reactions
-
This seems like a lot of code to handwrite. If these simple statements could be auto generated like update by pk with case, it would be a much easier win |
BetaWas this translation helpful?Give feedback.
All reactions
-
I've built an example combining sqlc's type safety with Squirrel for dynamic queries. It intercepts the DB layer to replace static queries while keeping sqlc's models intact. |
BetaWas this translation helpful?Give feedback.
All reactions
This discussion was converted from issue #364 on August 23, 2020 20:05.