Repository navigation
*!: add JSONB containment predicates usable on postgres and sqlite3 - #49
Conversation
Only sql.DB exposed its driver, so code holding a transaction, such as the x/sqlbuilder upserter, could not tell which dialect to write. Transactions now implement an optional Driver method, read through sql.DriverOf.
Lets query segments write SQL specific to the database they target, the driver being read from the Queryer of the QueryBuilder. Implementations of QueryWriter outside this package have to add the Driver method.
Adds JSONBArrayContainsAny, JSONBArrayContainsAll and JSONBObjectContains, written with the @> operator on PostgreSQL and with the json1 functions on SQLite, so the same predicate can be used by services and their SQLite backed tests. Only strings and numbers are supported as values.
|
|
||
| func (is InsertStatement) buildQueries(vvs []map[string]any, qvs map[string]any) (string, []any, error) { | ||
| var qw queryWriter | ||
| func (is InsertStatement) buildQueries(driver string, vvs []map[string]any, qvs map[string]any) (string, []any, error) { |
There was a problem hiding this comment.
🚫 [golangci] reported by reviewdog 🐶
calculated cyclomatic complexity for function buildQueries is 21, max is 15 (cyclop)
| } | ||
|
|
||
| func (ss SelectStatement) buildQuery(vs map[string]any) (string, []any, []string, error) { | ||
| func (ss SelectStatement) buildQuery(driver string, vs map[string]any) (string, []any, []string, error) { |
There was a problem hiding this comment.
🚫 [golangci] reported by reviewdog 🐶
calculated cyclomatic complexity for function buildQuery is 19, max is 15 (cyclop)
| ) | ||
| } | ||
|
|
||
| func TestJSONBPredicateClause(t *testing.T) { |
There was a problem hiding this comment.
My initial use case is directly a JSON Array like [{"type":"campaign","id":12},{"type":"contribution","id":14}]. Could you add a test that match this please? Just to be sure :)
Thanks for the PR btw, nice feature for the ORM.
There was a problem hiding this comment.
I updated the PR to match your schema and realized the PR was not soliving your problem (it was only handling slices or object of scalars not double nested shapes). It should now solve your use case
|
|
||
| // Driver returns the driver the query is built for, or an empty string | ||
| // when the queryer of the QueryBuilder does not expose it. | ||
| Driver() string |
There was a problem hiding this comment.
While we're here, should we type driver? I think the signature of statement and other functions that take a driver are only clear because it's named driver, but they look really off.
Then ideally we have each impl export it's own typed driver, even if it's a string it makes it easier to know the driver you're comparing against is the correct one, when we need to pull this kind of thing.
There was a problem hiding this comment.
I dont think it is a bad idea, but i really dont want to break interfaces in this PR more than what i already did.
The goal of driver was to piggy back off of the method that was already there in sql.DB.
In this PR we only assert Driver() == "sqlite3", given the fact our postgres driver setup could be more dynamic.
When it comes to typing we have:
- https://github.com/upfluence/sql/blob/am-sqlbuilder-jsonb/backend/postgres/db.go#L124
- https://github.com/upfluence/sql/blob/am-sqlbuilder-jsonb/backend/sqlite3/db.go#L211
But there is two things missing:
- There is no proper scheme to unwrap middleware
- it does not support any queryer (i.e tx)
If you have suggestion on how to acheive that without messing up too many interface it could be a nice abstraction!
There was a problem hiding this comment.
I agree with the breaking concern. I think it's a nice to have and the cost now is too big. If we ever do another big refactor I'd say it would be worth investigating, but now is probably not the time as you mention
The IsXYZ assertions are very much footgun-y, I had never seen them and I'm glad about it, too dangerous.
Relying on string comparison is not the end of the world and we don't have that many. They're also trivially testable so it's not the worst anyhow, we can live with it
Columns holding arrays of objects, such as the bound entities of a conversation, could not be filtered as the predicates only accepted strings and numbers. On SQLite every wanted object or array is now matched with an iteration of its own, as @> does on PostgreSQL.
| } | ||
|
|
||
| if res == nil { | ||
| return nil, nil |
There was a problem hiding this comment.
🚫 [golangci] reported by reviewdog 🐶
return both a nil error and an invalid value: use a sentinel error instead (nilnil)
| ).Query(ctx, nil) | ||
|
|
||
| if err != nil { | ||
| return nil, err |
There was a problem hiding this comment.
🚫 [golangci] reported by reviewdog 🐶
error returned from external package is unwrapped: sig: func (*github.com/upfluence/sql/x/sqlbuilder.SelectQueryer).Query(ctx context.Context, qvs map[string]any) (github.com/upfluence/sql/x/sqlbuilder.Cursor, error) (wrapcheck)
| var id int64 | ||
|
|
||
| if err := sc.Scan(map[string]any{"id": &id}); err != nil { | ||
| return err |
There was a problem hiding this comment.
🚫 [golangci] reported by reviewdog 🐶
error returned from interface method should be wrapped: sig: func (github.com/upfluence/sql/x/sqlbuilder.Scanner).Scan(map[string]any) error (wrapcheck)
| }, | ||
| ) | ||
|
|
||
| return ids, err |
There was a problem hiding this comment.
🚫 [golangci] reported by reviewdog 🐶
error returned from external package is unwrapped: sig: func github.com/upfluence/sql/x/sqlbuilder.ScrollCursor(c github.com/upfluence/sql/x/sqlbuilder.Cursor, fn github.com/upfluence/sql/x/sqlbuilder.ScanFunc) error (wrapcheck)
What does this PR do?
Services write their queries with
x/sqlbuilderand run their tests on SQLite, but anything touching JSONB columns had to be written in raw PostgreSQL SQL and could not be tested there. The builder also had no way to know which database it writes for: onlysql.DBexposed its driver, transactions did not.This PR makes the driver reachable from any
sql.Queryer, hands it to the query segments throughQueryWriter, and uses it to add JSONB containment predicates that write@>on PostgreSQL and the json1 functions on SQLite.No related issue.
What are the observable changes?
sql.DriverOf(q Queryer) stringreturns the driver of a DB or of a transaction, or""whenqdoes not expose one. The transactions ofbackend/{simple,postgres,sqlite3,balancer,static}andmiddleware/loggernow implementDriver() string.sqlbuilder.QueryWritergains aDriver() stringmethod. Implementations ofQueryWriteroutsidex/sqlbuilderhave to add it; callers ofWriteToare unaffected.Static…variant:JSONBArrayContainsAny(col @> $1::jsonb OR col @> $2::jsonb …)EXISTSoverjson_each(col)JSONBArrayContainsAllcol @> $1::jsonbjson_each(col)JSONBObjectContainscol @> $1::jsonbsqlite3driver, the PostgreSQL one for any other driver, including"".nulland arrays of objects return an error when the query is built, on both databases, so a query that cannot be expressed on SQLite also fails against PostgreSQL.Good PR checklist
Additional Notes
integration/jsonb_test.goruns 16 cases, plus one insidesql.ExecuteTx, against SQLite and PostgreSQL 18. Both return the same rows.Driver()as well. Without it, the predicates write the PostgreSQL form, which fails with a syntax error on SQLite instead of returning wrong rows.POSTGRES_URLtest runs: with it set,go test ./...has to run with-p 1.integrationandx/sqlbuilder/upserterboth create afootable in the same database and collide when run in parallel. This was already the case before this PR.cyclopwarnings: they show up onInsertStatement.buildQueriesandSelectStatement.buildQuerybecause their signatures changed. Their complexity is the same as onmaster.