CASSGO-130 Add Query.Binding() function to override binding#1959
CASSGO-130 Add Query.Binding() function to override binding#1959max-melentyev wants to merge 1 commit into
Conversation
|
Can you show some examples of the problem you're trying to solve? |
|
Query.Release() was removed in #1868, and the PR description recommends users to re-use Query objects instead. So I assume a code should be changed this way: // v1
query := session.Query("UPDATE x SET y = ? WHERE z = ?", y1, z1)
query.Exec()
query.Release()
query := session.Query("UPDATE x SET y = ? WHERE z = ?", y2, z2) // Will reuse query object from memory pool
query.Exec()
// v2
query := session.Query("UPDATE x SET y = ? WHERE z = ?", y1, z1)
query.Exec()
query := query.Bind(y2, z2)
query.Exec()However there's no API for re-using queries created with session.Bind() method. This pr adds this API. My use-case is to add support for queries with named params and binding values from maps similar to gocqlx. Something like namedQuery := parse("UPDATE x SET y = :y WHERE z = :z")
query := session.New(namedQuery.CQL())
namedQuery.BindMap(query, map[string]any{"y": y1, "z": z1})While I can user query.Bind() to set values, i need to set binding to make query fail in the case namedQuery.Bind cannot find necessary arguments: namedQuery.BindMap(query, map[string]any{"y": y1})
// I'd like to call this inside BindMap:
query.Binding(func(_ QueryInfo) ([]any, error) { return nil, fmt.Errorf("z is missing") }) |
|
The goal of #1868 was mostly to move the execution related data from Another goal of that PR was to just get rid of query object pooling altogether because the added complexity is just not worth it (in the rare case that a particular workload does see significant performance impact when objects aren't reused then they can add that pooling/caching on the application side). But this PR does make sense since we expect users to build their own pooling/caching if they truly want to reuse the query objects even with different parameters and currently you can do that for queries built via Note that query objects are not safe for concurrent use so if you plan on making some kind of wrapper API then take this into consideration. |
| } | ||
|
|
||
| // Binding is as function that dynamically generates query arguments. | ||
| type Binding func(q *QueryInfo) ([]interface{}, error) |
There was a problem hiding this comment.
I don't think it's necessary to add this, Session.Bind already uses func(q *QueryInfo) ([]interface{}, error) in its declaration anyway
yep. And with previous pooling option it was also possible to re-use queries between different sessions, and with different statement. I think it would be great to remove reference to a Session from Query object. This way it will be possible to cache fully configured query object (retries, timeouts, etc.) and re-use it even if session needs reconnection, for example if credentials needs to be rotated. |
|
I'm not sure if any sort of memory pooling make sense, as internalQuery and queryOptions are instantiated on every request. Caching pre-configured query probably still make sense, as it allows to skip a bunch of builder methods. Something like this: query := gocql.NewQuery("SELECT ...").Idempotent()...
iter1 := session.BindValues(query, arg1, arg2, ...).Iter(ctx)
iter2 := session.Bind(query, binding).Iter(ctx)
// Where
(*Session) Bind(Query, args...) BoundQuery
type BoundQuery struct {
query *Query
values []any
}
// and it's immutable and only has values to execute it. |
8422eb0 to
bdec34e
Compare
worryg0d
left a comment
There was a problem hiding this comment.
Indeed, queries are currently created by Session.Bind API is not poolable because the binding func cannot be changed.
I'm +1 on this change
|
Can you add an integration test for this use case:
You can use the Also add this to the changelog and format the commit message according to the guidelines outlined in CONTRIBUTING.md |
|
I wrote a simple test locally, and it passes: func TestStaticQueryInfo_OverrideBindingFunction(t *testing.T) {
session := createSession(t)
defer session.Close()
if err := createTable(session, "CREATE TABLE IF NOT EXISTS gocql_test.static_query_info_override (id int, value text, PRIMARY KEY (id))"); err != nil {
t.Fatalf("failed to create table with error '%v'", err)
}
if err := session.Query("INSERT INTO static_query_info_override (id, value) VALUES (?, ?)", 1, "foo").Exec(); err != nil {
t.Fatalf("insert into static_query_info_override failed, err '%v'", err)
}
if err := session.Query("INSERT INTO static_query_info_override (id, value) VALUES (?, ?)", 2, "bar").Exec(); err != nil {
t.Fatalf("insert into static_query_info_override failed, err '%v'", err)
}
qry := session.Bind("SELECT id, value FROM static_query_info_override WHERE id = ?", func(q *QueryInfo) ([]interface{}, error) {
values := make([]interface{}, 1)
values[0] = 1
return values, nil
})
iter := qry.Iter()
var id int
var value string
iter.Scan(&id, &value)
if err := iter.Close(); err != nil {
t.Fatalf("query with exposed info failed, err '%v'", err)
}
if id != 1 {
t.Fatalf("Expected id %d, but got %d", 113, id)
}
if value != "foo" {
t.Fatalf("Expected value %s, but got %s", "foo", value)
}
qry.Binding(func(q *QueryInfo) ([]interface{}, error) {
values := make([]interface{}, 1)
values[0] = 2
return values, nil
})
iter = qry.Iter()
iter.Scan(&id, &value)
if err := iter.Close(); err != nil {
t.Fatalf("query with exposed info failed, err '%v'", err)
}
if id != 2 {
t.Fatalf("Expected id %d, but got %d", 2, id)
}
if value != "bar" {
t.Fatalf("Expected value %s, but got %s", "bar", value)
}
} |
As Query is supposed to be reusable since cqlbp v2, it should be possible to overwrite a binding too.