Add support for SELECT_JSON statements.
This commit is contained in:
parent
7047de44a9
commit
7b16e432ff
46 changed files with 2732 additions and 307 deletions
|
|
@ -8,7 +8,7 @@ jobs:
|
||||||
build_and_tests:
|
build_and_tests:
|
||||||
docker:
|
docker:
|
||||||
# specify the version
|
# specify the version
|
||||||
- image: cimg/go:1.22.8
|
- image: cimg/go:1.23.6
|
||||||
|
|
||||||
# Please keep the version in sync with test/docker-compose.yaml
|
# Please keep the version in sync with test/docker-compose.yaml
|
||||||
- image: cimg/postgres:14.10
|
- image: cimg/postgres:14.10
|
||||||
|
|
@ -29,7 +29,7 @@ jobs:
|
||||||
MYSQL_TCP_PORT: 50902
|
MYSQL_TCP_PORT: 50902
|
||||||
|
|
||||||
# Please keep the version in sync with test/docker-compose.yaml
|
# Please keep the version in sync with test/docker-compose.yaml
|
||||||
- image: circleci/mariadb:10.3
|
- image: circleci/mariadb:11.7
|
||||||
command: [ '--default-authentication-plugin=mysql_native_password', '--port=50903' ]
|
command: [ '--default-authentication-plugin=mysql_native_password', '--port=50903' ]
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: jet
|
MYSQL_ROOT_PASSWORD: jet
|
||||||
|
|
|
||||||
|
|
@ -579,5 +579,5 @@ To run the tests, additional dependencies are required:
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright 2019-2024 Goran Bjelanovic
|
Copyright 2019-2025 Goran Bjelanovic
|
||||||
Licensed under the Apache License, Version 2.0.
|
Licensed under the Apache License, Version 2.0.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
const version = "v2.11.1"
|
const version = "v2.12.0"
|
||||||
|
|
|
||||||
13
internal/3rdparty/snaker/snaker.go
vendored
13
internal/3rdparty/snaker/snaker.go
vendored
|
|
@ -40,14 +40,23 @@ func snakeToCamel(s string, upperCase bool) string {
|
||||||
|
|
||||||
if upperCase || i > 0 {
|
if upperCase || i > 0 {
|
||||||
result += camelizeWord(word, len(words) > 1)
|
result += camelizeWord(word, len(words) > 1)
|
||||||
} else {
|
} else { // lowerCase and i == 0
|
||||||
result += word
|
result += toLowerFirstLetter(word)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toLowerFirstLetter(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
runes := []rune(s)
|
||||||
|
runes[0] = unicode.ToLower(runes[0])
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
|
||||||
func camelizeWord(word string, force bool) string {
|
func camelizeWord(word string, force bool) string {
|
||||||
runes := []rune(word)
|
runes := []rune(word)
|
||||||
|
|
||||||
|
|
|
||||||
2
internal/3rdparty/snaker/snaker_test.go
vendored
2
internal/3rdparty/snaker/snaker_test.go
vendored
|
|
@ -8,6 +8,8 @@ import (
|
||||||
func TestSnakeToCamel(t *testing.T) {
|
func TestSnakeToCamel(t *testing.T) {
|
||||||
require.Equal(t, SnakeToCamel(""), "")
|
require.Equal(t, SnakeToCamel(""), "")
|
||||||
require.Equal(t, SnakeToCamel("potato_"), "Potato")
|
require.Equal(t, SnakeToCamel("potato_"), "Potato")
|
||||||
|
require.Equal(t, SnakeToCamel("potato_", false), "potato")
|
||||||
|
require.Equal(t, SnakeToCamel("Potato_", false), "potato")
|
||||||
require.Equal(t, SnakeToCamel("this_has_to_be_uppercased"), "ThisHasToBeUppercased")
|
require.Equal(t, SnakeToCamel("this_has_to_be_uppercased"), "ThisHasToBeUppercased")
|
||||||
require.Equal(t, SnakeToCamel("this_is_an_id"), "ThisIsAnID")
|
require.Equal(t, SnakeToCamel("this_is_an_id"), "ThisIsAnID")
|
||||||
require.Equal(t, SnakeToCamel("this_is_an_identifier"), "ThisIsAnIdentifier")
|
require.Equal(t, SnakeToCamel("this_is_an_identifier"), "ThisIsAnIdentifier")
|
||||||
|
|
|
||||||
|
|
@ -30,3 +30,8 @@ func (a *alias) serializeForProjection(statement StatementType, out *SQLBuilder)
|
||||||
out.WriteString("AS")
|
out.WriteString("AS")
|
||||||
out.WriteAlias(a.alias)
|
out.WriteAlias(a.alias)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *alias) serializeForJsonObj(statement StatementType, out *SQLBuilder) {
|
||||||
|
out.WriteJsonObjKey(a.alias)
|
||||||
|
a.expression.serialize(statement, out)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ func (s *ClauseSelect) Projections() ProjectionList {
|
||||||
|
|
||||||
// Serialize serializes clause into SQLBuilder
|
// Serialize serializes clause into SQLBuilder
|
||||||
func (s *ClauseSelect) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
func (s *ClauseSelect) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if len(s.ProjectionList) == 0 {
|
||||||
|
panic("jet: SELECT clause has to have at least one projection")
|
||||||
|
}
|
||||||
|
|
||||||
out.NewLine()
|
out.NewLine()
|
||||||
out.WriteString("SELECT")
|
out.WriteString("SELECT")
|
||||||
s.OptimizerHints.Serialize(statementType, out, options...)
|
s.OptimizerHints.Serialize(statementType, out, options...)
|
||||||
|
|
@ -66,10 +70,6 @@ func (s *ClauseSelect) Serialize(statementType StatementType, out *SQLBuilder, o
|
||||||
out.WriteByte(')')
|
out.WriteByte(')')
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(s.ProjectionList) == 0 {
|
|
||||||
panic("jet: SELECT clause has to have at least one projection")
|
|
||||||
}
|
|
||||||
|
|
||||||
out.WriteProjections(statementType, s.ProjectionList)
|
out.WriteProjections(statementType, s.ProjectionList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
package jet
|
package jet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-jet/jet/v2/internal/3rdparty/snaker"
|
||||||
|
)
|
||||||
|
|
||||||
// Column is common column interface for all types of columns.
|
// Column is common column interface for all types of columns.
|
||||||
type Column interface {
|
type Column interface {
|
||||||
Name() string
|
Name() string
|
||||||
|
|
@ -97,7 +101,17 @@ func (c ColumnExpressionImpl) serializeForProjection(statement StatementType, ou
|
||||||
c.serialize(statement, out)
|
c.serialize(statement, out)
|
||||||
|
|
||||||
out.WriteString("AS")
|
out.WriteString("AS")
|
||||||
out.WriteAlias(c.defaultAlias())
|
|
||||||
|
if statement.IsSelectJSON() {
|
||||||
|
out.WriteAlias(snaker.SnakeToCamel(c.name, false))
|
||||||
|
} else {
|
||||||
|
out.WriteAlias(c.defaultAlias())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ColumnExpressionImpl) serializeForJsonObj(statement StatementType, out *SQLBuilder) {
|
||||||
|
out.WriteJsonObjKey(snaker.SnakeToCamel(c.name, false))
|
||||||
|
c.serialize(statement, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c ColumnExpressionImpl) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) {
|
func (c ColumnExpressionImpl) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,12 @@ func (cl ColumnList) serializeForProjection(statement StatementType, out *SQLBui
|
||||||
SerializeProjectionList(statement, projections, out)
|
SerializeProjectionList(statement, projections, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cl ColumnList) serializeForJsonObj(statement StatementType, out *SQLBuilder) {
|
||||||
|
projections := ColumnListToProjectionList(cl)
|
||||||
|
|
||||||
|
SerializeProjectionListJsonObj(statement, projections, out)
|
||||||
|
}
|
||||||
|
|
||||||
// dummy column interface implementation
|
// dummy column interface implementation
|
||||||
|
|
||||||
// Name is placeholder for ColumnList to implement Column interface
|
// Name is placeholder for ColumnList to implement Column interface
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package jet
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
// Expression is common interface for all expressions.
|
// Expression is a common interface for all expressions.
|
||||||
// Can be Bool, Int, Float, String, Date, Time, Timez, Timestamp or Timestampz expressions.
|
// Can be Bool, Int, Float, String, Date, Time, Timez, Timestamp or Timestampz expressions.
|
||||||
type Expression interface {
|
type Expression interface {
|
||||||
Serializer
|
Serializer
|
||||||
|
|
@ -89,9 +89,16 @@ func (e *ExpressionInterfaceImpl) serializeForGroupBy(statement StatementType, o
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ExpressionInterfaceImpl) serializeForProjection(statement StatementType, out *SQLBuilder) {
|
func (e *ExpressionInterfaceImpl) serializeForProjection(statement StatementType, out *SQLBuilder) {
|
||||||
|
if statement.IsSelectJSON() {
|
||||||
|
panic("jet: expression need to be aliased when used as SELECT JSON projection.")
|
||||||
|
}
|
||||||
e.Parent.serialize(statement, out, NoWrap)
|
e.Parent.serialize(statement, out, NoWrap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *ExpressionInterfaceImpl) serializeForJsonObj(statement StatementType, out *SQLBuilder) {
|
||||||
|
panic("jet: expression need to be aliased when used as SELECT JSON projection.")
|
||||||
|
}
|
||||||
|
|
||||||
func (e *ExpressionInterfaceImpl) serializeForOrderBy(statement StatementType, out *SQLBuilder) {
|
func (e *ExpressionInterfaceImpl) serializeForOrderBy(statement StatementType, out *SQLBuilder) {
|
||||||
e.Parent.serialize(statement, out, NoWrap)
|
e.Parent.serialize(statement, out, NoWrap)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package jet
|
||||||
// Projection is interface for all projection types. Types that can be part of, for instance SELECT clause.
|
// Projection is interface for all projection types. Types that can be part of, for instance SELECT clause.
|
||||||
type Projection interface {
|
type Projection interface {
|
||||||
serializeForProjection(statement StatementType, out *SQLBuilder)
|
serializeForProjection(statement StatementType, out *SQLBuilder)
|
||||||
|
serializeForJsonObj(statement StatementType, out *SQLBuilder)
|
||||||
fromImpl(subQuery SelectTable) Projection
|
fromImpl(subQuery SelectTable) Projection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,6 +29,10 @@ func (pl ProjectionList) serializeForProjection(statement StatementType, out *SQ
|
||||||
SerializeProjectionList(statement, pl, out)
|
SerializeProjectionList(statement, pl, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pl ProjectionList) serializeForJsonObj(statement StatementType, out *SQLBuilder) {
|
||||||
|
SerializeProjectionListJsonObj(statement, pl, out)
|
||||||
|
}
|
||||||
|
|
||||||
// As will create new projection list where each column is wrapped with a new table alias.
|
// As will create new projection list where each column is wrapped with a new table alias.
|
||||||
// tableAlias should be in the form 'name' or 'name.*', or it can be an empty string, which will remove existing table alias.
|
// tableAlias should be in the form 'name' or 'name.*', or it can be an empty string, which will remove existing table alias.
|
||||||
// For instance: If projection list has a column 'Artist.Name', and tableAlias is 'Musician.*', returned projection list will
|
// For instance: If projection list has a column 'Artist.Name', and tableAlias is 'Musician.*', returned projection list will
|
||||||
|
|
@ -79,3 +84,10 @@ func (pl ProjectionList) Except(toExclude ...Column) ProjectionList {
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JsonProjectionList redefines []Projection so projections can be serialized as json object key/values
|
||||||
|
type JsonProjectionList []Projection
|
||||||
|
|
||||||
|
func (j JsonProjectionList) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
SerializeProjectionListJsonObj(statement, j, out)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package jet
|
package jet
|
||||||
|
|
||||||
type rawStatementImpl struct {
|
type rawStatementImpl struct {
|
||||||
serializerStatementInterfaceImpl
|
statementInterfaceImpl
|
||||||
|
|
||||||
RawQuery string
|
RawQuery string
|
||||||
NamedArguments map[string]interface{}
|
NamedArguments map[string]interface{}
|
||||||
|
|
@ -10,7 +10,7 @@ type rawStatementImpl struct {
|
||||||
// RawStatement creates new sql statements from raw query and optional map of named arguments
|
// RawStatement creates new sql statements from raw query and optional map of named arguments
|
||||||
func RawStatement(dialect Dialect, rawQuery string, namedArgument ...map[string]interface{}) SerializerStatement {
|
func RawStatement(dialect Dialect, rawQuery string, namedArgument ...map[string]interface{}) SerializerStatement {
|
||||||
newRawStatement := rawStatementImpl{
|
newRawStatement := rawStatementImpl{
|
||||||
serializerStatementInterfaceImpl: serializerStatementInterfaceImpl{
|
statementInterfaceImpl: statementInterfaceImpl{
|
||||||
dialect: dialect,
|
dialect: dialect,
|
||||||
statementType: "",
|
statementType: "",
|
||||||
parent: nil,
|
parent: nil,
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,22 @@ func (s SerializeOption) WithFallTrough(options []SerializeOption) []SerializeOp
|
||||||
// StatementType is type of the SQL statement
|
// StatementType is type of the SQL statement
|
||||||
type StatementType string
|
type StatementType string
|
||||||
|
|
||||||
|
func (s StatementType) IsSelectJSON() bool {
|
||||||
|
return s == SelectJsonObjStatementType || s == SelectJsonArrStatementType
|
||||||
|
}
|
||||||
|
|
||||||
// Statement types
|
// Statement types
|
||||||
const (
|
const (
|
||||||
SelectStatementType StatementType = "SELECT"
|
SelectStatementType StatementType = "SELECT"
|
||||||
InsertStatementType StatementType = "INSERT"
|
SelectJsonObjStatementType StatementType = "SELECT_JSON_OBJ"
|
||||||
UpdateStatementType StatementType = "UPDATE"
|
SelectJsonArrStatementType StatementType = "SELECT_JSON_ARR"
|
||||||
DeleteStatementType StatementType = "DELETE"
|
InsertStatementType StatementType = "INSERT"
|
||||||
SetStatementType StatementType = "SET"
|
UpdateStatementType StatementType = "UPDATE"
|
||||||
LockStatementType StatementType = "LOCK"
|
DeleteStatementType StatementType = "DELETE"
|
||||||
UnLockStatementType StatementType = "UNLOCK"
|
SetStatementType StatementType = "SET"
|
||||||
WithStatementType StatementType = "WITH"
|
LockStatementType StatementType = "LOCK"
|
||||||
|
UnLockStatementType StatementType = "UNLOCK"
|
||||||
|
WithStatementType StatementType = "WITH"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Serializer interface
|
// Serializer interface
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,10 @@ func (s *SQLBuilder) WriteString(str string) {
|
||||||
s.write([]byte(str))
|
s.write([]byte(str))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLBuilder) WriteJsonObjKey(key string) {
|
||||||
|
s.WriteString(fmt.Sprintf(`'%s', `, key))
|
||||||
|
}
|
||||||
|
|
||||||
// WriteIdentifier adds identifier to output SQL
|
// WriteIdentifier adds identifier to output SQL
|
||||||
func (s *SQLBuilder) WriteIdentifier(name string, alwaysQuote ...bool) {
|
func (s *SQLBuilder) WriteIdentifier(name string, alwaysQuote ...bool) {
|
||||||
if s.shouldQuote(name, alwaysQuote...) {
|
if s.shouldQuote(name, alwaysQuote...) {
|
||||||
|
|
|
||||||
|
|
@ -7,25 +7,49 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Statement is common interface for all statements(SELECT, INSERT, UPDATE, DELETE, LOCK)
|
// Statement is a common interface for all SQL statements, including SELECT, SELECT_JSON_ARR, SELECT_JSON_OBJ, INSERT,
|
||||||
|
// UPDATE, DELETE, and LOCK.
|
||||||
type Statement interface {
|
type Statement interface {
|
||||||
// Sql returns parametrized sql query with list of arguments.
|
// Sql returns a parameterized SQL query along with its list of arguments.
|
||||||
Sql() (query string, args []interface{})
|
Sql() (query string, args []interface{})
|
||||||
// DebugSql returns debug query where every parametrized placeholder is replaced with its argument string representation.
|
|
||||||
// Do not use it in production. Use it only for debug purposes.
|
// DebugSql returns a debug-friendly SQL query where all parameterized placeholders
|
||||||
|
// are replaced with their respective argument string representations.
|
||||||
|
//
|
||||||
|
// Warning: This method should only be used for debugging purposes.
|
||||||
|
// Do not use it in production, as it may lead to security risks such as SQL injection.
|
||||||
DebugSql() (query string)
|
DebugSql() (query string)
|
||||||
// Query executes statement over database connection/transaction db and stores row results in destination.
|
|
||||||
// Destination can be either pointer to struct or pointer to a slice.
|
// Query executes statement on the provided database connection or transaction (db),
|
||||||
// If destination is pointer to struct and query result set is empty, method returns qrm.ErrNoRows.
|
// storing the retrieved row results in the given destination.
|
||||||
|
// Destination must be a pointer to either a struct or a slice.
|
||||||
|
// If the destination is a pointer to a struct and the query returns no rows, Query returns qrm.ErrNoRows.
|
||||||
Query(db qrm.Queryable, destination interface{}) error
|
Query(db qrm.Queryable, destination interface{}) error
|
||||||
// QueryContext executes statement with a context over database connection/transaction db and stores row result in destination.
|
|
||||||
// Destination can be either pointer to struct or pointer to a slice.
|
// QueryContext executes statement with a context over database connection/transaction db,
|
||||||
// If destination is pointer to struct and query result set is empty, method returns qrm.ErrNoRows.
|
// storing the retrieved row results in the given destination.
|
||||||
|
// Destination must be a pointer to either a struct or a slice.
|
||||||
|
// If the destination is a pointer to a struct and the query returns no rows, Query returns qrm.ErrNoRows.
|
||||||
QueryContext(ctx context.Context, db qrm.Queryable, destination interface{}) error
|
QueryContext(ctx context.Context, db qrm.Queryable, destination interface{}) error
|
||||||
|
|
||||||
|
// QueryJSON executes the given statement within the provided context on the database connection/transaction (db)
|
||||||
|
// and unmarshals the JSON result into the destination.
|
||||||
|
// If the statement is created as SELECT_JSON_ARR, the destination must be a pointer to a slice of structs or a
|
||||||
|
// pointer to []map[string]any.
|
||||||
|
// If the statement is created as SELECT_JSON_OBJ, the destination must be a pointer to a struct or a pointer to
|
||||||
|
// map[string]any.
|
||||||
|
// QueryJSON can also be used by other SQL statements that generate JSON on the server. The only requirement is
|
||||||
|
// that the query must return exactly one row with a single column; otherwise, an error is returned.
|
||||||
|
// If the destination is a pointer to a struct (or []map[string]any) and the query result set is empty, the method
|
||||||
|
// returns qrm.ErrNoRows.
|
||||||
|
QueryJSON(ctx context.Context, db qrm.Queryable, destination interface{}) error
|
||||||
|
|
||||||
// Exec executes statement over db connection/transaction without returning any rows.
|
// Exec executes statement over db connection/transaction without returning any rows.
|
||||||
Exec(db qrm.Executable) (sql.Result, error)
|
Exec(db qrm.Executable) (sql.Result, error)
|
||||||
|
|
||||||
// ExecContext executes statement with context over db connection/transaction without returning any rows.
|
// ExecContext executes statement with context over db connection/transaction without returning any rows.
|
||||||
ExecContext(ctx context.Context, db qrm.Executable) (sql.Result, error)
|
ExecContext(ctx context.Context, db qrm.Executable) (sql.Result, error)
|
||||||
|
|
||||||
// Rows executes statements over db connection/transaction and returns rows
|
// Rows executes statements over db connection/transaction and returns rows
|
||||||
Rows(ctx context.Context, db qrm.Queryable) (*Rows, error)
|
Rows(ctx context.Context, db qrm.Queryable) (*Rows, error)
|
||||||
}
|
}
|
||||||
|
|
@ -60,14 +84,14 @@ type SerializerHasProjections interface {
|
||||||
HasProjections
|
HasProjections
|
||||||
}
|
}
|
||||||
|
|
||||||
// serializerStatementInterfaceImpl struct
|
// statementInterfaceImpl struct
|
||||||
type serializerStatementInterfaceImpl struct {
|
type statementInterfaceImpl struct {
|
||||||
dialect Dialect
|
dialect Dialect
|
||||||
statementType StatementType
|
statementType StatementType
|
||||||
parent SerializerStatement
|
parent SerializerStatement
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serializerStatementInterfaceImpl) Sql() (query string, args []interface{}) {
|
func (s *statementInterfaceImpl) Sql() (query string, args []interface{}) {
|
||||||
|
|
||||||
queryData := &SQLBuilder{Dialect: s.dialect}
|
queryData := &SQLBuilder{Dialect: s.dialect}
|
||||||
|
|
||||||
|
|
@ -77,7 +101,7 @@ func (s *serializerStatementInterfaceImpl) Sql() (query string, args []interface
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serializerStatementInterfaceImpl) DebugSql() (query string) {
|
func (s *statementInterfaceImpl) DebugSql() (query string) {
|
||||||
sqlBuilder := &SQLBuilder{Dialect: s.dialect, Debug: true}
|
sqlBuilder := &SQLBuilder{Dialect: s.dialect, Debug: true}
|
||||||
|
|
||||||
s.parent.serialize(s.statementType, sqlBuilder, NoWrap)
|
s.parent.serialize(s.statementType, sqlBuilder, NoWrap)
|
||||||
|
|
@ -86,11 +110,29 @@ func (s *serializerStatementInterfaceImpl) DebugSql() (query string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serializerStatementInterfaceImpl) Query(db qrm.Queryable, destination interface{}) error {
|
func (s *statementInterfaceImpl) Query(db qrm.Queryable, destination interface{}) error {
|
||||||
return s.QueryContext(context.Background(), db, destination)
|
return s.QueryContext(context.Background(), db, destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serializerStatementInterfaceImpl) QueryContext(ctx context.Context, db qrm.Queryable, destination interface{}) error {
|
func (s *statementInterfaceImpl) QueryContext(ctx context.Context, db qrm.Queryable, destination interface{}) error {
|
||||||
|
return s.query(ctx, func(query string, args []interface{}) (int64, error) {
|
||||||
|
return qrm.Query(ctx, db, query, args, destination)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *statementInterfaceImpl) QueryJSON(ctx context.Context, db qrm.Queryable, destination interface{}) error {
|
||||||
|
return s.query(ctx, func(query string, args []interface{}) (int64, error) {
|
||||||
|
if s.statementType == SelectJsonObjStatementType {
|
||||||
|
return qrm.QueryJsonObj(ctx, db, query, args, destination)
|
||||||
|
}
|
||||||
|
return qrm.QueryJsonArr(ctx, db, query, args, destination)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *statementInterfaceImpl) query(
|
||||||
|
ctx context.Context,
|
||||||
|
queryFunc func(query string, args []interface{}) (int64, error),
|
||||||
|
) error {
|
||||||
query, args := s.Sql()
|
query, args := s.Sql()
|
||||||
|
|
||||||
callLogger(ctx, s)
|
callLogger(ctx, s)
|
||||||
|
|
@ -99,7 +141,7 @@ func (s *serializerStatementInterfaceImpl) QueryContext(ctx context.Context, db
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
duration := duration(func() {
|
duration := duration(func() {
|
||||||
rowsProcessed, err = qrm.Query(ctx, db, query, args, destination)
|
rowsProcessed, err = queryFunc(query, args)
|
||||||
})
|
})
|
||||||
|
|
||||||
callQueryLoggerFunc(ctx, QueryInfo{
|
callQueryLoggerFunc(ctx, QueryInfo{
|
||||||
|
|
@ -112,11 +154,11 @@ func (s *serializerStatementInterfaceImpl) QueryContext(ctx context.Context, db
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serializerStatementInterfaceImpl) Exec(db qrm.Executable) (res sql.Result, err error) {
|
func (s *statementInterfaceImpl) Exec(db qrm.Executable) (res sql.Result, err error) {
|
||||||
return s.ExecContext(context.Background(), db)
|
return s.ExecContext(context.Background(), db)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serializerStatementInterfaceImpl) ExecContext(ctx context.Context, db qrm.Executable) (res sql.Result, err error) {
|
func (s *statementInterfaceImpl) ExecContext(ctx context.Context, db qrm.Executable) (res sql.Result, err error) {
|
||||||
query, args := s.Sql()
|
query, args := s.Sql()
|
||||||
|
|
||||||
callLogger(ctx, s)
|
callLogger(ctx, s)
|
||||||
|
|
@ -141,7 +183,7 @@ func (s *serializerStatementInterfaceImpl) ExecContext(ctx context.Context, db q
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serializerStatementInterfaceImpl) Rows(ctx context.Context, db qrm.Queryable) (*Rows, error) {
|
func (s *statementInterfaceImpl) Rows(ctx context.Context, db qrm.Queryable) (*Rows, error) {
|
||||||
query, args := s.Sql()
|
query, args := s.Sql()
|
||||||
|
|
||||||
callLogger(ctx, s)
|
callLogger(ctx, s)
|
||||||
|
|
@ -191,11 +233,15 @@ type ExpressionStatement interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewExpressionStatementImpl creates new expression statement
|
// NewExpressionStatementImpl creates new expression statement
|
||||||
func NewExpressionStatementImpl(Dialect Dialect, statementType StatementType, parent ExpressionStatement, clauses ...Clause) ExpressionStatement {
|
func NewExpressionStatementImpl(Dialect Dialect,
|
||||||
|
statementType StatementType,
|
||||||
|
parent ExpressionStatement,
|
||||||
|
clauses ...Clause) ExpressionStatement {
|
||||||
|
|
||||||
return &expressionStatementImpl{
|
return &expressionStatementImpl{
|
||||||
ExpressionInterfaceImpl{Parent: parent},
|
ExpressionInterfaceImpl{Parent: parent},
|
||||||
statementImpl{
|
statementImpl{
|
||||||
serializerStatementInterfaceImpl: serializerStatementInterfaceImpl{
|
statementInterfaceImpl: statementInterfaceImpl{
|
||||||
parent: parent,
|
parent: parent,
|
||||||
dialect: Dialect,
|
dialect: Dialect,
|
||||||
statementType: statementType,
|
statementType: statementType,
|
||||||
|
|
@ -211,13 +257,16 @@ type expressionStatementImpl struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *expressionStatementImpl) serializeForProjection(statement StatementType, out *SQLBuilder) {
|
func (s *expressionStatementImpl) serializeForProjection(statement StatementType, out *SQLBuilder) {
|
||||||
|
if statement.IsSelectJSON() {
|
||||||
|
panic("jet: SELECT JSON statements need to be aliased when used as a projection.")
|
||||||
|
}
|
||||||
s.serialize(statement, out)
|
s.serialize(statement, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStatementImpl creates new statementImpl
|
// NewStatementImpl creates new statementImpl
|
||||||
func NewStatementImpl(Dialect Dialect, statementType StatementType, parent SerializerStatement, clauses ...Clause) SerializerStatement {
|
func NewStatementImpl(Dialect Dialect, statementType StatementType, parent SerializerStatement, clauses ...Clause) SerializerStatement {
|
||||||
return &statementImpl{
|
return &statementImpl{
|
||||||
serializerStatementInterfaceImpl: serializerStatementInterfaceImpl{
|
statementInterfaceImpl: statementInterfaceImpl{
|
||||||
parent: parent,
|
parent: parent,
|
||||||
dialect: Dialect,
|
dialect: Dialect,
|
||||||
statementType: statementType,
|
statementType: statementType,
|
||||||
|
|
@ -227,7 +276,7 @@ func NewStatementImpl(Dialect Dialect, statementType StatementType, parent Seria
|
||||||
}
|
}
|
||||||
|
|
||||||
type statementImpl struct {
|
type statementImpl struct {
|
||||||
serializerStatementInterfaceImpl
|
statementInterfaceImpl
|
||||||
|
|
||||||
Clauses []Clause
|
Clauses []Clause
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,23 @@ func SerializeProjectionList(statement StatementType, projections []Projection,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SerializeProjectionListJsonObj serializes a list of projections for JSON object
|
||||||
|
func SerializeProjectionListJsonObj(statement StatementType, projections []Projection, out *SQLBuilder) {
|
||||||
|
|
||||||
|
for i, p := range projections {
|
||||||
|
if i > 0 {
|
||||||
|
out.WriteString(",")
|
||||||
|
out.NewLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p == nil {
|
||||||
|
panic("jet: Projection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.serializeForJsonObj(statement, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SerializeColumnNames func
|
// SerializeColumnNames func
|
||||||
func SerializeColumnNames(columns []Column, out *SQLBuilder) {
|
func SerializeColumnNames(columns []Column, out *SQLBuilder) {
|
||||||
for i, col := range columns {
|
for i, col := range columns {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ func WITH(dialect Dialect, recursive bool, cte ...*CommonTableExpression) func(s
|
||||||
newWithImpl := &withImpl{
|
newWithImpl := &withImpl{
|
||||||
recursive: recursive,
|
recursive: recursive,
|
||||||
ctes: cte,
|
ctes: cte,
|
||||||
serializerStatementInterfaceImpl: serializerStatementInterfaceImpl{
|
statementInterfaceImpl: statementInterfaceImpl{
|
||||||
dialect: dialect,
|
dialect: dialect,
|
||||||
statementType: WithStatementType,
|
statementType: WithStatementType,
|
||||||
},
|
},
|
||||||
|
|
@ -25,7 +25,7 @@ func WITH(dialect Dialect, recursive bool, cte ...*CommonTableExpression) func(s
|
||||||
}
|
}
|
||||||
|
|
||||||
type withImpl struct {
|
type withImpl struct {
|
||||||
serializerStatementInterfaceImpl
|
statementInterfaceImpl
|
||||||
recursive bool
|
recursive bool
|
||||||
ctes []*CommonTableExpression
|
ctes []*CommonTableExpression
|
||||||
primaryStatement SerializerStatement
|
primaryStatement SerializerStatement
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,16 @@ func AssertJSON(t *testing.T, data interface{}, expectedJSON string) {
|
||||||
require.Equal(t, dataJson, expectedJSON)
|
require.Equal(t, dataJson, expectedJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssertJsonEqual checks if actual and expected json representation are the same
|
||||||
|
func AssertJsonEqual(t require.TestingT, actual, expected interface{}, option ...cmp.Option) {
|
||||||
|
actualJsonData, err := json.MarshalIndent(actual, "", "\t")
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectedJsonData, err := json.MarshalIndent(expected, "", "\t")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, actualJsonData, expectedJsonData)
|
||||||
|
}
|
||||||
|
|
||||||
// SaveJSONFile saves v as json at testRelativePath
|
// SaveJSONFile saves v as json at testRelativePath
|
||||||
// nolint:unused
|
// nolint:unused
|
||||||
func SaveJSONFile(v interface{}, testRelativePath string) {
|
func SaveJSONFile(v interface{}, testRelativePath string) {
|
||||||
|
|
@ -127,7 +137,10 @@ func SaveJSONFile(v interface{}, testRelativePath string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertJSONFile check if data json representation is the same as json at testRelativePath
|
// AssertJSONFile check if data json representation is the same as json at testRelativePath
|
||||||
func AssertJSONFile(t *testing.T, data interface{}, testRelativePath string) {
|
func AssertJSONFile(t require.TestingT, data interface{}, testRelativePath string) {
|
||||||
|
if _, ok := t.(*testing.B); ok {
|
||||||
|
return // skip assert for benchmarks
|
||||||
|
}
|
||||||
|
|
||||||
filePath := getFullPath(testRelativePath)
|
filePath := getFullPath(testRelativePath)
|
||||||
fileJSONData, err := os.ReadFile(filePath) // #nosec G304
|
fileJSONData, err := os.ReadFile(filePath) // #nosec G304
|
||||||
|
|
@ -145,7 +158,11 @@ func AssertJSONFile(t *testing.T, data interface{}, testRelativePath string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertStatementSql check if statement Sql() is the same as expectedQuery and expectedArgs
|
// AssertStatementSql check if statement Sql() is the same as expectedQuery and expectedArgs
|
||||||
func AssertStatementSql(t *testing.T, query jet.PrintableStatement, expectedQuery string, expectedArgs ...interface{}) {
|
func AssertStatementSql(t require.TestingT, query jet.PrintableStatement, expectedQuery string, expectedArgs ...interface{}) {
|
||||||
|
if _, ok := t.(*testing.B); ok {
|
||||||
|
return // skip assert for benchmarks
|
||||||
|
}
|
||||||
|
|
||||||
queryStr, args := query.Sql()
|
queryStr, args := query.Sql()
|
||||||
assertQueryString(t, queryStr, expectedQuery)
|
assertQueryString(t, queryStr, expectedQuery)
|
||||||
|
|
||||||
|
|
@ -255,6 +272,16 @@ func AssertQueryPanicErr(t *testing.T, stmt jet.Statement, db qrm.DB, dest inter
|
||||||
_ = stmt.Query(db, dest)
|
_ = stmt.Query(db, dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssertQueryJsonPanicErr check if statement QueryJSON execution panics with error errString
|
||||||
|
func AssertQueryJsonPanicErr(t *testing.T, stmt jet.Statement, db qrm.DB, dest interface{}, errString string) {
|
||||||
|
defer func() {
|
||||||
|
r := recover()
|
||||||
|
require.Equal(t, r, errString)
|
||||||
|
}()
|
||||||
|
|
||||||
|
_ = stmt.QueryJSON(context.Background(), db, dest)
|
||||||
|
}
|
||||||
|
|
||||||
// AssertFileContent check if file content at filePath contains expectedContent text.
|
// AssertFileContent check if file content at filePath contains expectedContent text.
|
||||||
func AssertFileContent(t *testing.T, filePath string, expectedContent string) {
|
func AssertFileContent(t *testing.T, filePath string, expectedContent string) {
|
||||||
enumFileData, err := os.ReadFile(filePath) // #nosec G304
|
enumFileData, err := os.ReadFile(filePath) // #nosec G304
|
||||||
|
|
@ -283,14 +310,14 @@ func AssertFileNamesEqual(t *testing.T, dirPath string, fileNames ...string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertDeepEqual checks if actual and expected objects are deeply equal.
|
// AssertDeepEqual checks if actual and expected objects are deeply equal.
|
||||||
func AssertDeepEqual(t *testing.T, actual, expected interface{}, option ...cmp.Option) {
|
func AssertDeepEqual(t require.TestingT, actual, expected interface{}, option ...cmp.Option) {
|
||||||
if !assert.True(t, cmp.Equal(actual, expected, option...)) {
|
if !assert.True(t, cmp.Equal(actual, expected, option...)) {
|
||||||
printDiff(actual, expected, option...)
|
printDiff(actual, expected, option...)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertQueryString(t *testing.T, actual, expected string) {
|
func assertQueryString(t require.TestingT, actual, expected string) {
|
||||||
if !assert.Equal(t, actual, expected) {
|
if !assert.Equal(t, actual, expected) {
|
||||||
printDiff(actual, expected)
|
printDiff(actual, expected)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package datetime
|
package datetime
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
//"github.com/go-jet/jet/v2/internal/utils/min"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// ExtractTimeComponents extracts number of days, hours, minutes, seconds, microseconds from duration
|
// ExtractTimeComponents extracts number of days, hours, minutes, seconds, microseconds from duration
|
||||||
func ExtractTimeComponents(duration time.Duration) (days, hours, minutes, seconds, microseconds int64) {
|
func ExtractTimeComponents(duration time.Duration) (days, hours, minutes, seconds, microseconds int64) {
|
||||||
|
|
@ -20,3 +23,36 @@ func ExtractTimeComponents(duration time.Duration) (days, hours, minutes, second
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TryParseAsTime attempts to parse the provided value as a time using one of the given formats.
|
||||||
|
//
|
||||||
|
// The function iterates over the provided formats and tries to parse the value into a time.Time object.
|
||||||
|
// It returns the parsed time and a boolean indicating whether the parsing was successful.
|
||||||
|
func TryParseAsTime(value interface{}, formats []string) (time.Time, bool) {
|
||||||
|
|
||||||
|
var timeStr string
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
timeStr = v
|
||||||
|
case []byte:
|
||||||
|
timeStr = string(v)
|
||||||
|
case int64:
|
||||||
|
return time.Unix(v, 0), true // sqlite
|
||||||
|
default:
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
formatLen := min(len(format), len(timeStr))
|
||||||
|
t, err := time.Parse(format[:formatLen], timeStr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package min
|
|
||||||
|
|
||||||
// Int returns minimum of two int values
|
|
||||||
func Int(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
@ -148,6 +148,11 @@ var NTH_VALUE = jet.NTH_VALUE
|
||||||
|
|
||||||
//--------------------- String functions ------------------//
|
//--------------------- String functions ------------------//
|
||||||
|
|
||||||
|
// HEX function in MySQL takes an input and returns its equivalent hexadecimal representation
|
||||||
|
func HEX(expression Expression) StringExpression {
|
||||||
|
return StringExp(Func("HEX", expression))
|
||||||
|
}
|
||||||
|
|
||||||
// BIT_LENGTH returns number of bits in string expression
|
// BIT_LENGTH returns number of bits in string expression
|
||||||
var BIT_LENGTH = jet.BIT_LENGTH
|
var BIT_LENGTH = jet.BIT_LENGTH
|
||||||
|
|
||||||
|
|
|
||||||
79
mysql/select_json.go
Normal file
79
mysql/select_json.go
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-jet/jet/v2/internal/jet"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SelectJsonStatement is an interface for MySQL statements that generate JSON on the server.
|
||||||
|
type SelectJsonStatement interface {
|
||||||
|
Statement
|
||||||
|
jet.Serializer
|
||||||
|
|
||||||
|
AS(alias string) Projection
|
||||||
|
|
||||||
|
FROM(table ReadableTable) SelectJsonStatement
|
||||||
|
WHERE(condition BoolExpression) SelectJsonStatement
|
||||||
|
ORDER_BY(orderByClauses ...OrderByClause) SelectJsonStatement
|
||||||
|
LIMIT(limit int64) SelectJsonStatement
|
||||||
|
OFFSET(offset int64) SelectJsonStatement
|
||||||
|
}
|
||||||
|
|
||||||
|
// SELECT_JSON_ARR creates a new SelectJsonStatement with a list of projections.
|
||||||
|
func SELECT_JSON_ARR(projections ...Projection) SelectJsonStatement {
|
||||||
|
return newSelectStatementJson(projections, jet.SelectJsonArrStatementType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SELECT_JSON_OBJ creates a new SelectJsonStatement with a list of projections.
|
||||||
|
func SELECT_JSON_OBJ(projections ...Projection) SelectJsonStatement {
|
||||||
|
return newSelectStatementJson(projections, jet.SelectJsonObjStatementType)
|
||||||
|
}
|
||||||
|
|
||||||
|
type selectJsonStatement struct {
|
||||||
|
*selectStatementImpl
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSelectStatementJson(projections []Projection, statementType jet.StatementType) SelectJsonStatement {
|
||||||
|
newSelect := &selectJsonStatement{
|
||||||
|
selectStatementImpl: newSelectStatement(statementType, nil, nil),
|
||||||
|
}
|
||||||
|
|
||||||
|
newSelect.Select.ProjectionList = ProjectionList{constructJsonFunc(projections, statementType).AS("json")}
|
||||||
|
|
||||||
|
return newSelect
|
||||||
|
}
|
||||||
|
|
||||||
|
func constructJsonFunc(projections []Projection, statementType jet.StatementType) Expression {
|
||||||
|
jsonObj := Func("JSON_OBJECT", CustomExpression(jet.JsonProjectionList(projections)))
|
||||||
|
|
||||||
|
if statementType == jet.SelectJsonArrStatementType {
|
||||||
|
return Func("JSON_ARRAYAGG", jsonObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonObj
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) FROM(table ReadableTable) SelectJsonStatement {
|
||||||
|
s.From.Tables = []jet.Serializer{table}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) WHERE(condition BoolExpression) SelectJsonStatement {
|
||||||
|
s.Where.Condition = condition
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) ORDER_BY(orderByClauses ...OrderByClause) SelectJsonStatement {
|
||||||
|
s.OrderBy.List = orderByClauses
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) LIMIT(limit int64) SelectJsonStatement {
|
||||||
|
s.Limit.Count = limit
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) OFFSET(offset int64) SelectJsonStatement {
|
||||||
|
s.Offset.Count = Int(offset)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
@ -62,12 +62,12 @@ type SelectStatement interface {
|
||||||
|
|
||||||
// SELECT creates new SelectStatement with list of projections
|
// SELECT creates new SelectStatement with list of projections
|
||||||
func SELECT(projection Projection, projections ...Projection) SelectStatement {
|
func SELECT(projection Projection, projections ...Projection) SelectStatement {
|
||||||
return newSelectStatement(nil, append([]Projection{projection}, projections...))
|
return newSelectStatement(jet.SelectStatementType, nil, append([]Projection{projection}, projections...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSelectStatement(table ReadableTable, projections []Projection) SelectStatement {
|
func newSelectStatement(stmtType jet.StatementType, table ReadableTable, projections []Projection) *selectStatementImpl {
|
||||||
newSelect := &selectStatementImpl{}
|
newSelect := &selectStatementImpl{}
|
||||||
newSelect.ExpressionStatement = jet.NewExpressionStatementImpl(Dialect, jet.SelectStatementType, newSelect,
|
newSelect.ExpressionStatement = jet.NewExpressionStatementImpl(Dialect, stmtType, newSelect,
|
||||||
&newSelect.Select,
|
&newSelect.Select,
|
||||||
&newSelect.From,
|
&newSelect.From,
|
||||||
&newSelect.Where,
|
&newSelect.Where,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ type readableTableInterfaceImpl struct {
|
||||||
|
|
||||||
// Generates a select query on the current tableName.
|
// Generates a select query on the current tableName.
|
||||||
func (r readableTableInterfaceImpl) SELECT(projection1 Projection, projections ...Projection) SelectStatement {
|
func (r readableTableInterfaceImpl) SELECT(projection1 Projection, projections ...Projection) SelectStatement {
|
||||||
return newSelectStatement(r.parent, append([]Projection{projection1}, projections...))
|
return newSelectStatement(jet.SelectStatementType, r.parent, append([]Projection{projection1}, projections...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a inner join tableName Expression using onCondition.
|
// Creates a inner join tableName Expression using onCondition.
|
||||||
|
|
|
||||||
131
postgres/select_json.go
Normal file
131
postgres/select_json.go
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-jet/jet/v2/internal/jet"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SELECT_JSON_ARR creates a new SelectJsonStatement with a list of projections.
|
||||||
|
func SELECT_JSON_ARR(projections ...Projection) SelectStatement {
|
||||||
|
return newSelectStatementJson(projections, jet.SelectJsonArrStatementType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SELECT_JSON_OBJ creates a new SelectJsonStatement with a list of projections.
|
||||||
|
func SELECT_JSON_OBJ(projections ...Projection) SelectStatement {
|
||||||
|
return newSelectStatementJson(projections, jet.SelectJsonObjStatementType)
|
||||||
|
}
|
||||||
|
|
||||||
|
type selectJsonStatement struct {
|
||||||
|
*selectStatementImpl
|
||||||
|
|
||||||
|
subQuery *selectStatementImpl
|
||||||
|
statementType jet.StatementType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) AS(alias string) Projection {
|
||||||
|
s.setSubQueryAlias(strings.ToLower(alias) + "_")
|
||||||
|
|
||||||
|
return s.selectStatementImpl.AS(alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) FROM(table ...ReadableTable) SelectStatement {
|
||||||
|
s.subQuery.From.Tables = readableTablesToSerializerList(table)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) DISTINCT(on ...jet.ColumnExpression) SelectStatement {
|
||||||
|
s.subQuery.Select.Distinct = true
|
||||||
|
s.subQuery.Select.DistinctOnColumns = on
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) WHERE(condition BoolExpression) SelectStatement {
|
||||||
|
s.subQuery.Where.Condition = condition
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) GROUP_BY(groupByClauses ...GroupByClause) SelectStatement {
|
||||||
|
s.subQuery.GroupBy.List = groupByClauses
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) HAVING(boolExpression BoolExpression) SelectStatement {
|
||||||
|
s.subQuery.Having.Condition = boolExpression
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) WINDOW(name string) windowExpand {
|
||||||
|
s.subQuery.Window.Definitions = append(s.subQuery.Window.Definitions, jet.WindowDefinition{Name: name})
|
||||||
|
return windowExpand{
|
||||||
|
selectStatement: s.subQuery,
|
||||||
|
rootStmt: s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) ORDER_BY(orderByClauses ...OrderByClause) SelectStatement {
|
||||||
|
s.subQuery.OrderBy.List = orderByClauses
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) LIMIT(limit int64) SelectStatement {
|
||||||
|
s.subQuery.Limit.Count = limit
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) OFFSET(offset int64) SelectStatement {
|
||||||
|
s.subQuery.Offset.Count = Int(offset)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) OFFSET_e(offset IntegerExpression) SelectStatement {
|
||||||
|
s.subQuery.Offset.Count = offset
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) FETCH_FIRST(count IntegerExpression) fetchExpand {
|
||||||
|
s.subQuery.Fetch.Count = count
|
||||||
|
|
||||||
|
return fetchExpand{
|
||||||
|
selectStatement: s.subQuery,
|
||||||
|
rootStmt: s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) FOR(lock RowLock) SelectStatement {
|
||||||
|
s.subQuery.For.Lock = lock
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSelectStatementJson(projections []Projection, statementType jet.StatementType) SelectStatement {
|
||||||
|
newSelectJson := &selectJsonStatement{
|
||||||
|
selectStatementImpl: newSelectStatement(statementType, nil, nil),
|
||||||
|
subQuery: newSelectStatement(statementType, nil, projections),
|
||||||
|
statementType: statementType,
|
||||||
|
}
|
||||||
|
|
||||||
|
newSelectJson.setOperatorsImpl.stmtRoot = newSelectJson
|
||||||
|
|
||||||
|
newSelectJson.setSubQueryAlias("")
|
||||||
|
|
||||||
|
return newSelectJson
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *selectJsonStatement) setSubQueryAlias(alias string) {
|
||||||
|
subQueryAlias := alias + "records"
|
||||||
|
jsonAlias := alias + "json"
|
||||||
|
|
||||||
|
s.Select.ProjectionList = ProjectionList{constructJsonFunc(s.statementType, subQueryAlias).AS(jsonAlias)}
|
||||||
|
|
||||||
|
s.From.Tables = []jet.Serializer{newSelectTable(s.subQuery, subQueryAlias, nil)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func constructJsonFunc(statementType jet.StatementType, subQueryAlias string) Expression {
|
||||||
|
rowToJson := Func("row_to_json", CustomExpression(Token(subQueryAlias)))
|
||||||
|
|
||||||
|
if statementType == jet.SelectJsonArrStatementType {
|
||||||
|
return Func("json_agg", rowToJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowToJson
|
||||||
|
}
|
||||||
|
|
@ -70,12 +70,12 @@ type SelectStatement interface {
|
||||||
|
|
||||||
// SELECT creates new SelectStatement with list of projections
|
// SELECT creates new SelectStatement with list of projections
|
||||||
func SELECT(projection Projection, projections ...Projection) SelectStatement {
|
func SELECT(projection Projection, projections ...Projection) SelectStatement {
|
||||||
return newSelectStatement(nil, append([]Projection{projection}, projections...))
|
return newSelectStatement(jet.SelectStatementType, nil, append([]Projection{projection}, projections...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSelectStatement(table ReadableTable, projections []Projection) SelectStatement {
|
func newSelectStatement(stmtType jet.StatementType, table ReadableTable, projections []Projection) *selectStatementImpl {
|
||||||
newSelect := &selectStatementImpl{}
|
newSelect := &selectStatementImpl{}
|
||||||
newSelect.ExpressionStatement = jet.NewExpressionStatementImpl(Dialect, jet.SelectStatementType, newSelect,
|
newSelect.ExpressionStatement = jet.NewExpressionStatementImpl(Dialect, stmtType, newSelect,
|
||||||
&newSelect.Select,
|
&newSelect.Select,
|
||||||
&newSelect.From,
|
&newSelect.From,
|
||||||
&newSelect.Where,
|
&newSelect.Where,
|
||||||
|
|
@ -94,7 +94,7 @@ func newSelectStatement(table ReadableTable, projections []Projection) SelectSta
|
||||||
}
|
}
|
||||||
newSelect.Limit.Count = -1
|
newSelect.Limit.Count = -1
|
||||||
|
|
||||||
newSelect.setOperatorsImpl.parent = newSelect
|
newSelect.setOperatorsImpl.stmtRoot = newSelect
|
||||||
|
|
||||||
return newSelect
|
return newSelect
|
||||||
}
|
}
|
||||||
|
|
@ -144,7 +144,10 @@ func (s *selectStatementImpl) HAVING(boolExpression BoolExpression) SelectStatem
|
||||||
|
|
||||||
func (s *selectStatementImpl) WINDOW(name string) windowExpand {
|
func (s *selectStatementImpl) WINDOW(name string) windowExpand {
|
||||||
s.Window.Definitions = append(s.Window.Definitions, jet.WindowDefinition{Name: name})
|
s.Window.Definitions = append(s.Window.Definitions, jet.WindowDefinition{Name: name})
|
||||||
return windowExpand{selectStatement: s}
|
return windowExpand{
|
||||||
|
selectStatement: s,
|
||||||
|
rootStmt: s,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *selectStatementImpl) ORDER_BY(orderByClauses ...OrderByClause) SelectStatement {
|
func (s *selectStatementImpl) ORDER_BY(orderByClauses ...OrderByClause) SelectStatement {
|
||||||
|
|
@ -172,6 +175,7 @@ func (s *selectStatementImpl) FETCH_FIRST(count IntegerExpression) fetchExpand {
|
||||||
|
|
||||||
return fetchExpand{
|
return fetchExpand{
|
||||||
selectStatement: s,
|
selectStatement: s,
|
||||||
|
rootStmt: s,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,6 +192,7 @@ func (s *selectStatementImpl) AsTable(alias string) SelectTable {
|
||||||
|
|
||||||
type windowExpand struct {
|
type windowExpand struct {
|
||||||
selectStatement *selectStatementImpl
|
selectStatement *selectStatementImpl
|
||||||
|
rootStmt SelectStatement
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w windowExpand) AS(window ...jet.Window) SelectStatement {
|
func (w windowExpand) AS(window ...jet.Window) SelectStatement {
|
||||||
|
|
@ -196,7 +201,7 @@ func (w windowExpand) AS(window ...jet.Window) SelectStatement {
|
||||||
}
|
}
|
||||||
windowsDefinition := w.selectStatement.Window.Definitions
|
windowsDefinition := w.selectStatement.Window.Definitions
|
||||||
windowsDefinition[len(windowsDefinition)-1].Window = window[0]
|
windowsDefinition[len(windowsDefinition)-1].Window = window[0]
|
||||||
return w.selectStatement
|
return w.rootStmt
|
||||||
}
|
}
|
||||||
|
|
||||||
func toJetFrameOffset(offset int64) jet.Serializer {
|
func toJetFrameOffset(offset int64) jet.Serializer {
|
||||||
|
|
@ -216,16 +221,17 @@ func readableTablesToSerializerList(tables []ReadableTable) []jet.Serializer {
|
||||||
|
|
||||||
type fetchExpand struct {
|
type fetchExpand struct {
|
||||||
selectStatement *selectStatementImpl
|
selectStatement *selectStatementImpl
|
||||||
|
rootStmt SelectStatement
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fetchExpand) ROWS_ONLY() SelectStatement {
|
func (f fetchExpand) ROWS_ONLY() SelectStatement {
|
||||||
f.selectStatement.Fetch.WithTies = false
|
f.selectStatement.Fetch.WithTies = false
|
||||||
|
|
||||||
return f.selectStatement
|
return f.rootStmt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fetchExpand) ROWS_WITH_TIES() SelectStatement {
|
func (f fetchExpand) ROWS_WITH_TIES() SelectStatement {
|
||||||
f.selectStatement.Fetch.WithTies = true
|
f.selectStatement.Fetch.WithTies = true
|
||||||
|
|
||||||
return f.selectStatement
|
return f.rootStmt
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,31 +65,31 @@ type setOperators interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type setOperatorsImpl struct {
|
type setOperatorsImpl struct {
|
||||||
parent setOperators
|
stmtRoot setOperators
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *setOperatorsImpl) UNION(rhs SelectStatement) setStatement {
|
func (s *setOperatorsImpl) UNION(rhs SelectStatement) setStatement {
|
||||||
return UNION(s.parent, rhs)
|
return UNION(s.stmtRoot, rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *setOperatorsImpl) UNION_ALL(rhs SelectStatement) setStatement {
|
func (s *setOperatorsImpl) UNION_ALL(rhs SelectStatement) setStatement {
|
||||||
return UNION_ALL(s.parent, rhs)
|
return UNION_ALL(s.stmtRoot, rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *setOperatorsImpl) INTERSECT(rhs SelectStatement) setStatement {
|
func (s *setOperatorsImpl) INTERSECT(rhs SelectStatement) setStatement {
|
||||||
return INTERSECT(s.parent, rhs)
|
return INTERSECT(s.stmtRoot, rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *setOperatorsImpl) INTERSECT_ALL(rhs SelectStatement) setStatement {
|
func (s *setOperatorsImpl) INTERSECT_ALL(rhs SelectStatement) setStatement {
|
||||||
return INTERSECT_ALL(s.parent, rhs)
|
return INTERSECT_ALL(s.stmtRoot, rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *setOperatorsImpl) EXCEPT(rhs SelectStatement) setStatement {
|
func (s *setOperatorsImpl) EXCEPT(rhs SelectStatement) setStatement {
|
||||||
return EXCEPT(s.parent, rhs)
|
return EXCEPT(s.stmtRoot, rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *setOperatorsImpl) EXCEPT_ALL(rhs SelectStatement) setStatement {
|
func (s *setOperatorsImpl) EXCEPT_ALL(rhs SelectStatement) setStatement {
|
||||||
return EXCEPT_ALL(s.parent, rhs)
|
return EXCEPT_ALL(s.stmtRoot, rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
type setStatementImpl struct {
|
type setStatementImpl struct {
|
||||||
|
|
@ -110,7 +110,7 @@ func newSetStatementImpl(operator string, all bool, selects []jet.SerializerStat
|
||||||
newSetStatement.setOperator.Selects = selects
|
newSetStatement.setOperator.Selects = selects
|
||||||
newSetStatement.setOperator.Limit.Count = -1
|
newSetStatement.setOperator.Limit.Count = -1
|
||||||
|
|
||||||
newSetStatement.setOperatorsImpl.parent = newSetStatement
|
newSetStatement.setOperatorsImpl.stmtRoot = newSetStatement
|
||||||
|
|
||||||
return newSetStatement
|
return newSetStatement
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ type readableTableInterfaceImpl struct {
|
||||||
|
|
||||||
// Generates a select query on the current tableName.
|
// Generates a select query on the current tableName.
|
||||||
func (r readableTableInterfaceImpl) SELECT(projection1 Projection, projections ...Projection) SelectStatement {
|
func (r readableTableInterfaceImpl) SELECT(projection1 Projection, projections ...Projection) SelectStatement {
|
||||||
return newSelectStatement(r.parent, append([]Projection{projection1}, projections...))
|
return newSelectStatement(jet.SelectStatementType, r.parent, append([]Projection{projection1}, projections...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a inner join tableName Expression using onCondition.
|
// Creates a inner join tableName Expression using onCondition.
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,9 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/go-jet/jet/v2/internal/utils/min"
|
"github.com/go-jet/jet/v2/internal/utils/datetime"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -64,7 +63,12 @@ func (nt *NullTime) Scan(value interface{}) error {
|
||||||
|
|
||||||
// Some of the drivers (pgx, mysql) are not parsing all of the time formats(date, time with time zone,...) and are just forwarding string value.
|
// Some of the drivers (pgx, mysql) are not parsing all of the time formats(date, time with time zone,...) and are just forwarding string value.
|
||||||
// At this point we try to parse those values using some of the predefined formats
|
// At this point we try to parse those values using some of the predefined formats
|
||||||
nt.Time, nt.Valid = tryParseAsTime(value)
|
nt.Time, nt.Valid = datetime.TryParseAsTime(value, []string{
|
||||||
|
"2006-01-02 15:04:05-07:00", // sqlite
|
||||||
|
"2006-01-02 15:04:05.999999", // go-sql-driver/mysql
|
||||||
|
"15:04:05-07", // pgx
|
||||||
|
"15:04:05.999999", // pgx
|
||||||
|
})
|
||||||
|
|
||||||
if !nt.Valid {
|
if !nt.Valid {
|
||||||
return fmt.Errorf("can't scan time.Time from %q", value)
|
return fmt.Errorf("can't scan time.Time from %q", value)
|
||||||
|
|
@ -73,42 +77,6 @@ func (nt *NullTime) Scan(value interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var formats = []string{
|
|
||||||
"2006-01-02 15:04:05-07:00", // sqlite
|
|
||||||
"2006-01-02 15:04:05.999999", // go-sql-driver/mysql
|
|
||||||
"15:04:05-07", // pgx
|
|
||||||
"15:04:05.999999", // pgx
|
|
||||||
}
|
|
||||||
|
|
||||||
func tryParseAsTime(value interface{}) (time.Time, bool) {
|
|
||||||
|
|
||||||
var timeStr string
|
|
||||||
|
|
||||||
switch v := value.(type) {
|
|
||||||
case string:
|
|
||||||
timeStr = v
|
|
||||||
case []byte:
|
|
||||||
timeStr = string(v)
|
|
||||||
case int64:
|
|
||||||
return time.Unix(v, 0), true // sqlite
|
|
||||||
default:
|
|
||||||
return time.Time{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, format := range formats {
|
|
||||||
formatLen := min.Int(len(format), len(timeStr))
|
|
||||||
t, err := time.Parse(format[:formatLen], timeStr)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return t, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return time.Time{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// NullUInt64 struct
|
// NullUInt64 struct
|
||||||
type NullUInt64 struct {
|
type NullUInt64 struct {
|
||||||
UInt64 uint64
|
UInt64 uint64
|
||||||
|
|
|
||||||
173
qrm/qrm.go
173
qrm/qrm.go
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/go-jet/jet/v2/internal/3rdparty/json"
|
||||||
"github.com/go-jet/jet/v2/internal/utils/must"
|
"github.com/go-jet/jet/v2/internal/utils/must"
|
||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
@ -12,10 +13,130 @@ import (
|
||||||
// ErrNoRows is returned by Query when query result set is empty
|
// ErrNoRows is returned by Query when query result set is empty
|
||||||
var ErrNoRows = errors.New("qrm: no rows in result set")
|
var ErrNoRows = errors.New("qrm: no rows in result set")
|
||||||
|
|
||||||
// Query executes Query Result Mapping (QRM) of `query` with list of parametrized arguments `arg` over database connection `db`
|
// QueryJsonObj executes a SQL query that returns a JSON object, unmarshals the result into the provided destination,
|
||||||
// using context `ctx` into destination `destPtr`.
|
// and returns the number of rows processed.
|
||||||
// Destination can be either pointer to struct or pointer to slice of structs.
|
//
|
||||||
// If destination is pointer to struct and query result set is empty, method returns qrm.ErrNoRows.
|
// The query must return exactly one row with a single column; otherwise, an error is returned.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// ctx - The context for managing query execution (timeouts, cancellations).
|
||||||
|
// db - The database connection or transaction that implements the Queryable interface.
|
||||||
|
// query - The SQL query string to be executed.
|
||||||
|
// args - A slice of arguments to be used with the query.
|
||||||
|
// destPtr - A pointer to the variable where the unmarshaled JSON result will be stored.
|
||||||
|
// The destination should be a pointer to a struct or map[string]any.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
//
|
||||||
|
// rowsProcessed - The number of rows processed by the query execution.
|
||||||
|
// err - An error if query execution or unmarshaling fails.
|
||||||
|
func QueryJsonObj(ctx context.Context, db Queryable, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) {
|
||||||
|
must.BeInitializedPtr(destPtr, "jet: destination is nil")
|
||||||
|
must.BeTypeKind(destPtr, reflect.Ptr, jsonDestObjErr)
|
||||||
|
destType := reflect.TypeOf(destPtr).Elem()
|
||||||
|
must.BeTrue(destType.Kind() == reflect.Struct || destType.Kind() == reflect.Map, jsonDestObjErr)
|
||||||
|
|
||||||
|
return queryJson(ctx, db, query, args, destPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryJsonArr executes a SQL query that returns a JSON array, unmarshals the result into the provided destination,
|
||||||
|
// and returns the number of rows processed.
|
||||||
|
//
|
||||||
|
// The query must return exactly one row with a single column; otherwise, an error is returned.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// ctx - The context for managing query execution (timeouts, cancellations).
|
||||||
|
// db - The database connection or transaction that implements the Queryable interface.
|
||||||
|
// query - The SQL query string to be executed.
|
||||||
|
// args - A slice of arguments to be used with the query.
|
||||||
|
// destPtr - A pointer to the variable where the unmarshaled JSON array will be stored.
|
||||||
|
// The destination should be a pointer to a slice of structs or []map[string]any.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
//
|
||||||
|
// rowsProcessed - The number of rows processed by the query execution.
|
||||||
|
// err - An error if query execution or unmarshaling fails.
|
||||||
|
func QueryJsonArr(ctx context.Context, db Queryable, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) {
|
||||||
|
must.BeInitializedPtr(destPtr, "jet: destination is nil")
|
||||||
|
must.BeTypeKind(destPtr, reflect.Ptr, jsonDestArrErr)
|
||||||
|
destType := reflect.TypeOf(destPtr).Elem()
|
||||||
|
must.BeTrue(destType.Kind() == reflect.Slice, jsonDestArrErr)
|
||||||
|
|
||||||
|
return queryJson(ctx, db, query, args, destPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonDestObjErr = "jet: destination has to be a pointer to struct or pointer to map[string]any"
|
||||||
|
var jsonDestArrErr = "jet: destination has to be a pointer to slice of struct or pointer to []map[string]any"
|
||||||
|
|
||||||
|
func queryJson(ctx context.Context, db Queryable, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) {
|
||||||
|
must.BeInitializedPtr(db, "jet: db is nil")
|
||||||
|
|
||||||
|
var rows *sql.Rows
|
||||||
|
rows, err = db.QueryContext(ctx, query, args...)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return 0, ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonData []byte
|
||||||
|
err = rows.Scan(&jsonData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonData == nil {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(jsonData, &destPtr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows.Next() {
|
||||||
|
return 1, fmt.Errorf("jet: query returned more then one row")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rows.Close()
|
||||||
|
if err != nil {
|
||||||
|
return 1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query executes a Query Result Mapping (QRM) of the provided SQL `query` with a list of parameterized arguments `args`
|
||||||
|
// over the database connection `db` using the provided context `ctx` and stores the result in the destination `destPtr`.
|
||||||
|
//
|
||||||
|
// The destination must be a pointer to either a struct or a slice of structs
|
||||||
|
// If the destination is a pointer to a struct and no rows are returned, the method returns qrm.ErrNoRows.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// ctx - The context for managing query execution (timeouts, cancellations).
|
||||||
|
// db - The database connection or transaction implementing the Queryable interface.
|
||||||
|
// query - The SQL query string to be executed.
|
||||||
|
// args - A slice of arguments to be used with the query.
|
||||||
|
// destPtr - A pointer to the variable where the query result will be stored. This can be a pointer to a struct or a slice of structs.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
//
|
||||||
|
// rowsProcessed - The number of rows processed by the query execution.
|
||||||
|
// err - An error if query execution or result mapping fails, or if no rows are found when a struct is expected.
|
||||||
func Query(ctx context.Context, db Queryable, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) {
|
func Query(ctx context.Context, db Queryable, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) {
|
||||||
|
|
||||||
must.BeInitializedPtr(db, "jet: db is nil")
|
must.BeInitializedPtr(db, "jet: db is nil")
|
||||||
|
|
@ -185,7 +306,7 @@ func mapRowToSlice(
|
||||||
func mapRowToBaseTypeSlice(scanContext *ScanContext, slicePtrValue reflect.Value, field *reflect.StructField) (updated bool, err error) {
|
func mapRowToBaseTypeSlice(scanContext *ScanContext, slicePtrValue reflect.Value, field *reflect.StructField) (updated bool, err error) {
|
||||||
index := 0
|
index := 0
|
||||||
if field != nil {
|
if field != nil {
|
||||||
typeName, columnName := getTypeAndFieldName("", *field)
|
typeName, columnName, _ := getTypeAndFieldName("", *field)
|
||||||
if index = scanContext.typeToColumnIndex(typeName, columnName); index < 0 {
|
if index = scanContext.typeToColumnIndex(typeName, columnName); index < 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -233,9 +354,11 @@ func mapRowToStruct(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldMap := typeInf.fieldMappings[i]
|
fieldMappingInfo := typeInf.fieldMappings[i]
|
||||||
|
|
||||||
if fieldMap.complexType {
|
switch fieldMappingInfo.Type {
|
||||||
|
|
||||||
|
case complexType:
|
||||||
var changed bool
|
var changed bool
|
||||||
changed, err = mapRowToDestinationValue(scanContext, concat(groupKey, ":", field.Name), fieldValue, &field)
|
changed, err = mapRowToDestinationValue(scanContext, concat(groupKey, ":", field.Name), fieldValue, &field)
|
||||||
|
|
||||||
|
|
@ -246,13 +369,12 @@ func mapRowToStruct(
|
||||||
if changed {
|
if changed {
|
||||||
updated = true
|
updated = true
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
} else {
|
if mapOnlySlices || fieldMappingInfo.rowIndex == -1 {
|
||||||
if mapOnlySlices || fieldMap.rowIndex == -1 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
scannedValue := scanContext.rowElemValue(fieldMap.rowIndex)
|
scannedValue := scanContext.rowElemValue(fieldMappingInfo.rowIndex)
|
||||||
|
|
||||||
if !scannedValue.IsValid() {
|
if !scannedValue.IsValid() {
|
||||||
setZeroValue(fieldValue) // scannedValue is nil, destination should be set to zero value
|
setZeroValue(fieldValue) // scannedValue is nil, destination should be set to zero value
|
||||||
|
|
@ -261,7 +383,8 @@ func mapRowToStruct(
|
||||||
|
|
||||||
updated = true
|
updated = true
|
||||||
|
|
||||||
if fieldMap.implementsScanner {
|
switch fieldMappingInfo.Type {
|
||||||
|
case implementsScanner:
|
||||||
initializeValueIfNilPtr(fieldValue)
|
initializeValueIfNilPtr(fieldValue)
|
||||||
fieldScanner := getScanner(fieldValue)
|
fieldScanner := getScanner(fieldValue)
|
||||||
|
|
||||||
|
|
@ -270,14 +393,27 @@ func mapRowToStruct(
|
||||||
err := fieldScanner.Scan(value)
|
err := fieldScanner.Scan(value)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return updated, fmt.Errorf(`can't scan %T(%q) to '%s %s': %w`, value, value, field.Name, field.Type.String(), err)
|
return updated, qrmAssignError(scannedValue, field, err)
|
||||||
}
|
}
|
||||||
} else {
|
case jsonUnmarshal:
|
||||||
|
value, ok := scannedValue.Interface().([]byte)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return updated, qrmAssignError(scannedValue, field, fmt.Errorf("value not convertable to []byte"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldInterface := fieldValue.Addr().Interface()
|
||||||
|
|
||||||
|
err := json.Unmarshal(value, fieldInterface)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return updated, qrmAssignError(scannedValue, field, err)
|
||||||
|
}
|
||||||
|
default: // simple type
|
||||||
err := assign(scannedValue, fieldValue)
|
err := assign(scannedValue, fieldValue)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return updated, fmt.Errorf(`can't assign %T(%q) to '%s %s': %w`, scannedValue.Interface(), scannedValue.Interface(),
|
return updated, qrmAssignError(scannedValue, field, err)
|
||||||
field.Name, field.Type.String(), err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -286,6 +422,11 @@ func mapRowToStruct(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func qrmAssignError(scannedValue reflect.Value, field reflect.StructField, err error) error {
|
||||||
|
return fmt.Errorf(`can't assign %T(%q) to '%s %s': %w`, scannedValue.Interface(), scannedValue.Interface(),
|
||||||
|
field.Name, field.Type.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
func mapRowToDestinationValue(
|
func mapRowToDestinationValue(
|
||||||
scanContext *ScanContext,
|
scanContext *ScanContext,
|
||||||
groupKey string,
|
groupKey string,
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,18 @@ type typeInfo struct {
|
||||||
fieldMappings []fieldMapping
|
fieldMappings []fieldMapping
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fieldMappingType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
simpleType fieldMappingType = iota
|
||||||
|
complexType // slice and struct are complex types supported
|
||||||
|
implementsScanner
|
||||||
|
jsonUnmarshal
|
||||||
|
)
|
||||||
|
|
||||||
type fieldMapping struct {
|
type fieldMapping struct {
|
||||||
complexType bool // slice and struct are complex types
|
rowIndex int // index in ScanContext.row
|
||||||
rowIndex int // index in ScanContext.row
|
Type fieldMappingType
|
||||||
implementsScanner bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ScanContext) getTypeInfo(structType reflect.Type, parentField *reflect.StructField) typeInfo {
|
func (s *ScanContext) getTypeInfo(structType reflect.Type, parentField *reflect.StructField) typeInfo {
|
||||||
|
|
@ -100,17 +108,21 @@ func (s *ScanContext) getTypeInfo(structType reflect.Type, parentField *reflect.
|
||||||
for i := 0; i < structType.NumField(); i++ {
|
for i := 0; i < structType.NumField(); i++ {
|
||||||
field := structType.Field(i)
|
field := structType.Field(i)
|
||||||
|
|
||||||
newTypeName, fieldName := getTypeAndFieldName(typeName, field)
|
newTypeName, fieldName, jsonUnmarshaler := getTypeAndFieldName(typeName, field)
|
||||||
columnIndex := s.typeToColumnIndex(newTypeName, fieldName)
|
columnIndex := s.typeToColumnIndex(newTypeName, fieldName)
|
||||||
|
|
||||||
fieldMap := fieldMapping{
|
fieldMap := fieldMapping{
|
||||||
rowIndex: columnIndex,
|
rowIndex: columnIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
if implementsScannerType(field.Type) {
|
if jsonUnmarshaler {
|
||||||
fieldMap.implementsScanner = true
|
fieldMap.Type = jsonUnmarshal
|
||||||
|
} else if implementsScannerType(field.Type) {
|
||||||
|
fieldMap.Type = implementsScanner
|
||||||
} else if !isSimpleModelType(field.Type) {
|
} else if !isSimpleModelType(field.Type) {
|
||||||
fieldMap.complexType = true
|
fieldMap.Type = complexType
|
||||||
|
} else {
|
||||||
|
fieldMap.Type = simpleType
|
||||||
}
|
}
|
||||||
|
|
||||||
newTypeInfo.fieldMappings = append(newTypeInfo.fieldMappings, fieldMap)
|
newTypeInfo.fieldMappings = append(newTypeInfo.fieldMappings, fieldMap)
|
||||||
|
|
@ -188,7 +200,7 @@ func (s *ScanContext) getGroupKeyInfo(
|
||||||
fieldType := indirectType(field.Type)
|
fieldType := indirectType(field.Type)
|
||||||
|
|
||||||
if isPrimaryKey(field, primaryKeyOverwrites) {
|
if isPrimaryKey(field, primaryKeyOverwrites) {
|
||||||
newTypeName, fieldName := getTypeAndFieldName(typeName, field)
|
newTypeName, fieldName, _ := getTypeAndFieldName(typeName, field)
|
||||||
|
|
||||||
pkIndex := s.typeToColumnIndex(newTypeName, fieldName)
|
pkIndex := s.typeToColumnIndex(newTypeName, fieldName)
|
||||||
|
|
||||||
|
|
|
||||||
20
qrm/utill.go
20
qrm/utill.go
|
|
@ -107,20 +107,26 @@ func getTypeName(structType reflect.Type, parentField *reflect.StructField) stri
|
||||||
return toCommonIdentifier(aliasParts[0])
|
return toCommonIdentifier(aliasParts[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTypeAndFieldName(structType string, field reflect.StructField) (string, string) {
|
func getTypeAndFieldName(structType string, field reflect.StructField) (string, string, bool) {
|
||||||
aliasTag := field.Tag.Get("alias")
|
aliasTag := field.Tag.Get("alias")
|
||||||
|
|
||||||
if aliasTag == "" {
|
if aliasTag != "" {
|
||||||
return structType, field.Name
|
aliasParts := strings.Split(aliasTag, ".")
|
||||||
|
|
||||||
|
if len(aliasParts) == 1 {
|
||||||
|
return structType, toCommonIdentifier(aliasParts[0]), false
|
||||||
|
}
|
||||||
|
|
||||||
|
return toCommonIdentifier(aliasParts[0]), toCommonIdentifier(aliasParts[1]), false
|
||||||
}
|
}
|
||||||
|
|
||||||
aliasParts := strings.Split(aliasTag, ".")
|
jsonColumnTag := field.Tag.Get("json_column")
|
||||||
|
|
||||||
if len(aliasParts) == 1 {
|
if jsonColumnTag != "" {
|
||||||
return structType, toCommonIdentifier(aliasParts[0])
|
return "", toCommonIdentifier(jsonColumnTag), true
|
||||||
}
|
}
|
||||||
|
|
||||||
return toCommonIdentifier(aliasParts[0]), toCommonIdentifier(aliasParts[1])
|
return structType, field.Name, false
|
||||||
}
|
}
|
||||||
|
|
||||||
var replacer = strings.NewReplacer(" ", "", "-", "", "_", "")
|
var replacer = strings.NewReplacer(" ", "", "-", "", "_", "")
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ services:
|
||||||
- ./testdata/init/mysql:/docker-entrypoint-initdb.d
|
- ./testdata/init/mysql:/docker-entrypoint-initdb.d
|
||||||
|
|
||||||
mariadb:
|
mariadb:
|
||||||
image: mariadb:10.3
|
image: mariadb:11.7
|
||||||
command: ['--default-authentication-plugin=mysql_native_password', '--log_bin_trust_function_creators=1']
|
command: ['--default-authentication-plugin=mysql_native_password', '--log_bin_trust_function_creators=1']
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
|
|
|
||||||
|
|
@ -23,19 +23,63 @@ func TestAllTypes(t *testing.T) {
|
||||||
|
|
||||||
var dest []model.AllTypes
|
var dest []model.AllTypes
|
||||||
|
|
||||||
err := AllTypes.
|
err := SELECT(AllTypes.AllColumns).
|
||||||
SELECT(AllTypes.AllColumns).
|
FROM(AllTypes).
|
||||||
LIMIT(2).
|
LIMIT(2).
|
||||||
Query(db, &dest)
|
Query(db, &dest)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, len(dest), 2)
|
require.Equal(t, len(dest), 2)
|
||||||
|
|
||||||
//testutils.PrintJson(dest)
|
//testutils.PrintJson(dest)
|
||||||
testutils.AssertJSON(t, dest, allTypesJson)
|
testutils.AssertJSON(t, dest, allTypesJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAllTypesJSON(t *testing.T) {
|
||||||
|
|
||||||
|
stmt := SELECT_JSON_ARR(
|
||||||
|
AllTypes.AllColumns.Except(
|
||||||
|
AllTypes.JSON,
|
||||||
|
AllTypes.JSONPtr,
|
||||||
|
AllTypes.Bit,
|
||||||
|
AllTypes.BitPtr,
|
||||||
|
AllTypes.Blob,
|
||||||
|
AllTypes.BlobPtr,
|
||||||
|
AllTypes.Binary,
|
||||||
|
AllTypes.BinaryPtr,
|
||||||
|
AllTypes.VarBinary,
|
||||||
|
AllTypes.VarBinaryPtr,
|
||||||
|
),
|
||||||
|
CAST(AllTypes.JSON).AS_CHAR().AS("Json"),
|
||||||
|
CAST(AllTypes.JSONPtr).AS_CHAR().AS("JsonPtr"),
|
||||||
|
CAST(AllTypes.Bit).AS_CHAR().AS("Bit"),
|
||||||
|
CAST(AllTypes.BitPtr).AS_CHAR().AS("BitPtr"),
|
||||||
|
|
||||||
|
// TODO: remove when binary string is implemented
|
||||||
|
CONCAT(String("\\x"), HEX(AllTypes.Blob)).AS("Blob"),
|
||||||
|
CONCAT(String("\\x"), HEX(AllTypes.BlobPtr)).AS("BlobPtr"),
|
||||||
|
|
||||||
|
CONCAT(String("\\x"), HEX(AllTypes.Binary)).AS("Binary"),
|
||||||
|
CONCAT(String("\\x"), HEX(AllTypes.BinaryPtr)).AS("BinaryPtr"),
|
||||||
|
|
||||||
|
CONCAT(String("\\x"), HEX(AllTypes.VarBinary)).AS("VarBinary"),
|
||||||
|
CONCAT(String("\\x"), HEX(AllTypes.VarBinaryPtr)).AS("VarBinaryPtr"),
|
||||||
|
).FROM(AllTypes)
|
||||||
|
|
||||||
|
var dest []model.AllTypes
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// fix float rounding lost before comparison
|
||||||
|
dest[0].Float = 3.33
|
||||||
|
dest[0].FloatPtr = ptr.Of(3.33)
|
||||||
|
dest[1].Float = 3.33
|
||||||
|
|
||||||
|
//fmt.Println(allTypesJson)
|
||||||
|
testutils.AssertJSON(t, dest, allTypesJson)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAllTypesViewSelect(t *testing.T) {
|
func TestAllTypesViewSelect(t *testing.T) {
|
||||||
|
|
||||||
type AllTypesView model.AllTypes
|
type AllTypesView model.AllTypes
|
||||||
|
|
|
||||||
138
tests/mysql/bench_test.go
Normal file
138
tests/mysql/bench_test.go
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
//go:build bench
|
||||||
|
// +build bench
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-jet/jet/v2/internal/testutils"
|
||||||
|
. "github.com/go-jet/jet/v2/mysql"
|
||||||
|
"github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/model"
|
||||||
|
. "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/table"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type allInfo []struct {
|
||||||
|
model.Actor
|
||||||
|
|
||||||
|
Films []struct {
|
||||||
|
model.Film
|
||||||
|
|
||||||
|
Language model.Language
|
||||||
|
Categories []model.Category
|
||||||
|
|
||||||
|
Inventories []struct {
|
||||||
|
model.Inventory
|
||||||
|
|
||||||
|
Rentals []struct {
|
||||||
|
model.Rental
|
||||||
|
|
||||||
|
Customer model.Customer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkTestDVDsJoinEverything(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
testDVDsJoinEverything(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDVDsJoinEverything(t *testing.T) {
|
||||||
|
testDVDsJoinEverything(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDVDsJoinEverything(t require.TestingT) {
|
||||||
|
stmt := SELECT(
|
||||||
|
Actor.AllColumns,
|
||||||
|
Film.AllColumns,
|
||||||
|
Language.AllColumns,
|
||||||
|
Category.AllColumns,
|
||||||
|
Inventory.AllColumns,
|
||||||
|
Rental.AllColumns,
|
||||||
|
Customer.AllColumns,
|
||||||
|
).FROM(
|
||||||
|
Actor.
|
||||||
|
LEFT_JOIN(FilmActor, Actor.ActorID.EQ(FilmActor.ActorID)).
|
||||||
|
LEFT_JOIN(Film, Film.FilmID.EQ(FilmActor.FilmID)).
|
||||||
|
LEFT_JOIN(Language, Language.LanguageID.EQ(Film.LanguageID)).
|
||||||
|
LEFT_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)).
|
||||||
|
LEFT_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)).
|
||||||
|
LEFT_JOIN(Inventory, Inventory.FilmID.EQ(Film.FilmID)).
|
||||||
|
LEFT_JOIN(Rental, Rental.InventoryID.EQ(Inventory.InventoryID)).
|
||||||
|
LEFT_JOIN(Customer, Customer.CustomerID.EQ(Rental.CustomerID)),
|
||||||
|
).ORDER_BY(
|
||||||
|
Actor.ActorID.ASC(),
|
||||||
|
Film.FilmID.ASC(),
|
||||||
|
Category.CategoryID.ASC(),
|
||||||
|
Inventory.InventoryID.ASC(),
|
||||||
|
Rental.RentalID.ASC(),
|
||||||
|
)
|
||||||
|
|
||||||
|
var dest allInfo
|
||||||
|
|
||||||
|
err := stmt.Query(db, &dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
//testutils.SaveJSONFile(dest, "./testdata/results/mysql/dvds_join_everything.json")
|
||||||
|
testutils.AssertJSONFile(t, dest, "./testdata/results/mysql/dvds_join_everything.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkTestDVDsJoinEverythingJSON(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
testDVDsJoinEverythingJSON(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDVDsJoinEverythingJSON(t *testing.T) {
|
||||||
|
testDVDsJoinEverythingJSON(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDVDsJoinEverythingJSON(t require.TestingT) {
|
||||||
|
stmt := SELECT_JSON_ARR(
|
||||||
|
Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate,
|
||||||
|
|
||||||
|
SELECT_JSON_ARR(
|
||||||
|
Film.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(Language.AllColumns).
|
||||||
|
FROM(Language).
|
||||||
|
WHERE(Language.LanguageID.EQ(Film.LanguageID)).AS("Language"),
|
||||||
|
|
||||||
|
SELECT_JSON_ARR(Category.AllColumns).
|
||||||
|
FROM(Category.INNER_JOIN(FilmCategory, FilmCategory.CategoryID.EQ(Category.CategoryID))).
|
||||||
|
WHERE(FilmCategory.FilmID.EQ(Film.FilmID)).AS("Categories"),
|
||||||
|
|
||||||
|
SELECT_JSON_ARR(
|
||||||
|
Inventory.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_ARR(
|
||||||
|
Rental.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(Customer.AllColumns).
|
||||||
|
FROM(Customer).
|
||||||
|
WHERE(Customer.CustomerID.EQ(Rental.CustomerID)).AS("Customer"),
|
||||||
|
).FROM(Rental).
|
||||||
|
WHERE(Rental.InventoryID.EQ(Inventory.InventoryID)).
|
||||||
|
ORDER_BY(Rental.RentalID).AS("Rentals"),
|
||||||
|
).FROM(Inventory).
|
||||||
|
WHERE(Inventory.FilmID.EQ(Film.FilmID)).
|
||||||
|
ORDER_BY(Inventory.InventoryID).AS("Inventories"),
|
||||||
|
).FROM(Film.
|
||||||
|
INNER_JOIN(FilmActor, FilmActor.FilmID.EQ(Film.FilmID)),
|
||||||
|
).WHERE(FilmActor.ActorID.EQ(Actor.ActorID)).
|
||||||
|
ORDER_BY(Film.FilmID.ASC()).AS("Films"),
|
||||||
|
).FROM(Actor).
|
||||||
|
ORDER_BY(Actor.ActorID.ASC())
|
||||||
|
|
||||||
|
//fmt.Println(stmt.DebugSql())
|
||||||
|
|
||||||
|
var dest allInfo
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
//testutils.SaveJSONFile(dest, "./testdata/results/mysql/dvds_join_everything2.json")
|
||||||
|
testutils.AssertJSONFile(t, dest, "./testdata/results/mysql/dvds_join_everything.json")
|
||||||
|
}
|
||||||
437
tests/mysql/select_json_test.go
Normal file
437
tests/mysql/select_json_test.go
Normal file
|
|
@ -0,0 +1,437 @@
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-jet/jet/v2/qrm"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-jet/jet/v2/internal/testutils"
|
||||||
|
. "github.com/go-jet/jet/v2/mysql"
|
||||||
|
"github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/model"
|
||||||
|
. "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/table"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ctx = context.Background()
|
||||||
|
|
||||||
|
func TestSelectJsonObj(t *testing.T) {
|
||||||
|
stmt := SELECT_JSON_OBJ(Actor.AllColumns).
|
||||||
|
FROM(Actor).
|
||||||
|
WHERE(Actor.ActorID.EQ(Int(2)))
|
||||||
|
|
||||||
|
testutils.AssertStatementSql(t, stmt, `
|
||||||
|
SELECT JSON_OBJECT('actorID', actor.actor_id,
|
||||||
|
'firstName', actor.first_name,
|
||||||
|
'lastName', actor.last_name,
|
||||||
|
'lastUpdate', actor.last_update) AS "json"
|
||||||
|
FROM dvds.actor
|
||||||
|
WHERE actor.actor_id = ?;
|
||||||
|
`, int64(2))
|
||||||
|
|
||||||
|
var dest model.Actor
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
testutils.AssertDeepEqual(t, dest, actor2)
|
||||||
|
requireLogged(t, stmt)
|
||||||
|
requireQueryLogged(t, stmt, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectJsonObj_NestedObj(t *testing.T) {
|
||||||
|
stmt := SELECT_JSON_OBJ(
|
||||||
|
Actor.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(Film.AllColumns).
|
||||||
|
FROM(FilmActor.INNER_JOIN(Film, Film.FilmID.EQ(FilmActor.FilmID))).
|
||||||
|
WHERE(Actor.ActorID.EQ(FilmActor.ActorID)).
|
||||||
|
ORDER_BY(Film.Length.DESC()).
|
||||||
|
LIMIT(1).AS("LongestFilm"),
|
||||||
|
).FROM(
|
||||||
|
Actor,
|
||||||
|
).WHERE(
|
||||||
|
Actor.ActorID.EQ(Int(2)),
|
||||||
|
)
|
||||||
|
|
||||||
|
testutils.AssertStatementSql(t, stmt, `
|
||||||
|
SELECT JSON_OBJECT('actorID', actor.actor_id,
|
||||||
|
'firstName', actor.first_name,
|
||||||
|
'lastName', actor.last_name,
|
||||||
|
'lastUpdate', actor.last_update,
|
||||||
|
'LongestFilm', (
|
||||||
|
SELECT JSON_OBJECT('filmID', film.film_id,
|
||||||
|
'title', film.title,
|
||||||
|
'description', film.description,
|
||||||
|
'releaseYear', film.release_year,
|
||||||
|
'languageID', film.language_id,
|
||||||
|
'originalLanguageID', film.original_language_id,
|
||||||
|
'rentalDuration', film.rental_duration,
|
||||||
|
'rentalRate', film.rental_rate,
|
||||||
|
'length', film.length,
|
||||||
|
'replacementCost', film.replacement_cost,
|
||||||
|
'rating', film.rating,
|
||||||
|
'specialFeatures', film.special_features,
|
||||||
|
'lastUpdate', film.last_update) AS "json"
|
||||||
|
FROM dvds.film_actor
|
||||||
|
INNER JOIN dvds.film ON (film.film_id = film_actor.film_id)
|
||||||
|
WHERE actor.actor_id = film_actor.actor_id
|
||||||
|
ORDER BY film.length DESC
|
||||||
|
LIMIT ?
|
||||||
|
)) AS "json"
|
||||||
|
FROM dvds.actor
|
||||||
|
WHERE actor.actor_id = ?;
|
||||||
|
`)
|
||||||
|
|
||||||
|
var dest struct {
|
||||||
|
model.Actor
|
||||||
|
|
||||||
|
LongestFilm model.Film
|
||||||
|
}
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
require.Nil(t, err)
|
||||||
|
testutils.AssertJSON(t, dest, `
|
||||||
|
{
|
||||||
|
"ActorID": 2,
|
||||||
|
"FirstName": "NICK",
|
||||||
|
"LastName": "WAHLBERG",
|
||||||
|
"LastUpdate": "2006-02-15T04:34:33Z",
|
||||||
|
"LongestFilm": {
|
||||||
|
"FilmID": 958,
|
||||||
|
"Title": "WARDROBE PHANTOM",
|
||||||
|
"Description": "A Action-Packed Display of a Mad Cow And a Astronaut who must Kill a Car in Ancient India",
|
||||||
|
"ReleaseYear": 2006,
|
||||||
|
"LanguageID": 1,
|
||||||
|
"OriginalLanguageID": null,
|
||||||
|
"RentalDuration": 6,
|
||||||
|
"RentalRate": 2.99,
|
||||||
|
"Length": 178,
|
||||||
|
"ReplacementCost": 19.99,
|
||||||
|
"Rating": "G",
|
||||||
|
"SpecialFeatures": "Trailers,Commentaries",
|
||||||
|
"LastUpdate": "2006-02-15T05:03:42Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectJsonArr(t *testing.T) {
|
||||||
|
stmt := SELECT_JSON_ARR(Actor.AllColumns).
|
||||||
|
FROM(Actor).
|
||||||
|
ORDER_BY(Actor.ActorID)
|
||||||
|
|
||||||
|
testutils.AssertDebugStatementSql(t, stmt, `
|
||||||
|
SELECT JSON_ARRAYAGG(JSON_OBJECT('actorID', actor.actor_id,
|
||||||
|
'firstName', actor.first_name,
|
||||||
|
'lastName', actor.last_name,
|
||||||
|
'lastUpdate', actor.last_update)) AS "json"
|
||||||
|
FROM dvds.actor
|
||||||
|
ORDER BY actor.actor_id;
|
||||||
|
`)
|
||||||
|
|
||||||
|
var dest []model.Actor
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
testutils.AssertJSONFile(t, dest, "./testdata/results/mysql/all_actors.json")
|
||||||
|
requireLogged(t, stmt)
|
||||||
|
requireQueryLogged(t, stmt, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectJsonArr_NestedArr(t *testing.T) {
|
||||||
|
stmt := SELECT_JSON_ARR(
|
||||||
|
Actor.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_ARR(
|
||||||
|
Film.AllColumns,
|
||||||
|
).FROM(
|
||||||
|
FilmActor.INNER_JOIN(
|
||||||
|
Film,
|
||||||
|
Film.FilmID.EQ(FilmActor.FilmID).AND(
|
||||||
|
Actor.ActorID.EQ(FilmActor.ActorID)),
|
||||||
|
),
|
||||||
|
).WHERE(
|
||||||
|
Film.FilmID.MOD(Int(17)).EQ(Int(0)),
|
||||||
|
).ORDER_BY(
|
||||||
|
Film.Length.DESC(),
|
||||||
|
).AS("Films"),
|
||||||
|
).FROM(
|
||||||
|
Actor,
|
||||||
|
).WHERE(
|
||||||
|
Actor.ActorID.BETWEEN(Int(1), Int(3)),
|
||||||
|
).ORDER_BY(
|
||||||
|
Actor.ActorID,
|
||||||
|
)
|
||||||
|
|
||||||
|
testutils.AssertDebugStatementSql(t, stmt, `
|
||||||
|
SELECT JSON_ARRAYAGG(JSON_OBJECT('actorID', actor.actor_id,
|
||||||
|
'firstName', actor.first_name,
|
||||||
|
'lastName', actor.last_name,
|
||||||
|
'lastUpdate', actor.last_update,
|
||||||
|
'Films', (
|
||||||
|
SELECT JSON_ARRAYAGG(JSON_OBJECT('filmID', film.film_id,
|
||||||
|
'title', film.title,
|
||||||
|
'description', film.description,
|
||||||
|
'releaseYear', film.release_year,
|
||||||
|
'languageID', film.language_id,
|
||||||
|
'originalLanguageID', film.original_language_id,
|
||||||
|
'rentalDuration', film.rental_duration,
|
||||||
|
'rentalRate', film.rental_rate,
|
||||||
|
'length', film.length,
|
||||||
|
'replacementCost', film.replacement_cost,
|
||||||
|
'rating', film.rating,
|
||||||
|
'specialFeatures', film.special_features,
|
||||||
|
'lastUpdate', film.last_update)) AS "json"
|
||||||
|
FROM dvds.film_actor
|
||||||
|
INNER JOIN dvds.film ON ((film.film_id = film_actor.film_id) AND (actor.actor_id = film_actor.actor_id))
|
||||||
|
WHERE (film.film_id % 17) = 0
|
||||||
|
ORDER BY film.length DESC
|
||||||
|
))) AS "json"
|
||||||
|
FROM dvds.actor
|
||||||
|
WHERE actor.actor_id BETWEEN 1 AND 3
|
||||||
|
ORDER BY actor.actor_id;
|
||||||
|
`)
|
||||||
|
|
||||||
|
var dest []struct {
|
||||||
|
model.Actor
|
||||||
|
|
||||||
|
Films []model.Film
|
||||||
|
}
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
fmt.Println(err)
|
||||||
|
require.Nil(t, err)
|
||||||
|
testutils.AssertJSON(t, dest, `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"ActorID": 1,
|
||||||
|
"FirstName": "PENELOPE",
|
||||||
|
"LastName": "GUINESS",
|
||||||
|
"LastUpdate": "2006-02-15T04:34:33Z",
|
||||||
|
"Films": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActorID": 2,
|
||||||
|
"FirstName": "NICK",
|
||||||
|
"LastName": "WAHLBERG",
|
||||||
|
"LastUpdate": "2006-02-15T04:34:33Z",
|
||||||
|
"Films": [
|
||||||
|
{
|
||||||
|
"FilmID": 357,
|
||||||
|
"Title": "GILBERT PELICAN",
|
||||||
|
"Description": "A Fateful Tale of a Man And a Feminist who must Conquer a Crocodile in A Manhattan Penthouse",
|
||||||
|
"ReleaseYear": 2006,
|
||||||
|
"LanguageID": 1,
|
||||||
|
"OriginalLanguageID": null,
|
||||||
|
"RentalDuration": 7,
|
||||||
|
"RentalRate": 0.99,
|
||||||
|
"Length": 114,
|
||||||
|
"ReplacementCost": 13.99,
|
||||||
|
"Rating": "G",
|
||||||
|
"SpecialFeatures": "Trailers,Commentaries",
|
||||||
|
"LastUpdate": "2006-02-15T05:03:42Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"FilmID": 561,
|
||||||
|
"Title": "MASK PEACH",
|
||||||
|
"Description": "A Boring Character Study of a Student And a Robot who must Meet a Woman in California",
|
||||||
|
"ReleaseYear": 2006,
|
||||||
|
"LanguageID": 1,
|
||||||
|
"OriginalLanguageID": null,
|
||||||
|
"RentalDuration": 6,
|
||||||
|
"RentalRate": 2.99,
|
||||||
|
"Length": 123,
|
||||||
|
"ReplacementCost": 26.99,
|
||||||
|
"Rating": "NC-17",
|
||||||
|
"SpecialFeatures": "Commentaries,Deleted Scenes",
|
||||||
|
"LastUpdate": "2006-02-15T05:03:42Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActorID": 3,
|
||||||
|
"FirstName": "ED",
|
||||||
|
"LastName": "CHASE",
|
||||||
|
"LastUpdate": "2006-02-15T04:34:33Z",
|
||||||
|
"Films": [
|
||||||
|
{
|
||||||
|
"FilmID": 17,
|
||||||
|
"Title": "ALONE TRIP",
|
||||||
|
"Description": "A Fast-Paced Character Study of a Composer And a Dog who must Outgun a Boat in An Abandoned Fun House",
|
||||||
|
"ReleaseYear": 2006,
|
||||||
|
"LanguageID": 1,
|
||||||
|
"OriginalLanguageID": null,
|
||||||
|
"RentalDuration": 3,
|
||||||
|
"RentalRate": 0.99,
|
||||||
|
"Length": 82,
|
||||||
|
"ReplacementCost": 14.99,
|
||||||
|
"Rating": "R",
|
||||||
|
"SpecialFeatures": "Trailers,Behind the Scenes",
|
||||||
|
"LastUpdate": "2006-02-15T05:03:42Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"FilmID": 289,
|
||||||
|
"Title": "EVE RESURRECTION",
|
||||||
|
"Description": "A Awe-Inspiring Yarn of a Pastry Chef And a Database Administrator who must Challenge a Teacher in A Baloon",
|
||||||
|
"ReleaseYear": 2006,
|
||||||
|
"LanguageID": 1,
|
||||||
|
"OriginalLanguageID": null,
|
||||||
|
"RentalDuration": 5,
|
||||||
|
"RentalRate": 4.99,
|
||||||
|
"Length": 66,
|
||||||
|
"ReplacementCost": 25.99,
|
||||||
|
"Rating": "G",
|
||||||
|
"SpecialFeatures": "Trailers,Commentaries,Deleted Scenes",
|
||||||
|
"LastUpdate": "2006-02-15T05:03:42Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectJson_GroupBy(t *testing.T) {
|
||||||
|
skipForMariaDB(t) // scope issues with select without FROM
|
||||||
|
|
||||||
|
subQuery := SELECT(
|
||||||
|
Customer.AllColumns,
|
||||||
|
|
||||||
|
SUM(Payment.Amount).AS("sum"),
|
||||||
|
AVG(Payment.Amount).AS("avg"),
|
||||||
|
MAX(Payment.Amount).AS("max"),
|
||||||
|
MIN(Payment.Amount).AS("min"),
|
||||||
|
COUNT(Payment.Amount).AS("count"),
|
||||||
|
).FROM(
|
||||||
|
Payment.
|
||||||
|
INNER_JOIN(Customer, Customer.CustomerID.EQ(Payment.CustomerID)),
|
||||||
|
).GROUP_BY(
|
||||||
|
Customer.CustomerID,
|
||||||
|
).HAVING(
|
||||||
|
SUMf(Payment.Amount).GT(Float(125)),
|
||||||
|
).ORDER_BY(
|
||||||
|
Customer.CustomerID, SUM(Payment.Amount).ASC(),
|
||||||
|
).AsTable("customers_info")
|
||||||
|
|
||||||
|
stmt := SELECT_JSON_ARR(
|
||||||
|
subQuery.AllColumns().Except( // TODO: remove when ColumnList.From() is implemented
|
||||||
|
FloatColumn("sum"),
|
||||||
|
FloatColumn("avg"),
|
||||||
|
FloatColumn("max"),
|
||||||
|
FloatColumn("min"),
|
||||||
|
FloatColumn("count"),
|
||||||
|
),
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(
|
||||||
|
FloatColumn("sum").From(subQuery),
|
||||||
|
FloatColumn("avg").From(subQuery),
|
||||||
|
FloatColumn("max").From(subQuery),
|
||||||
|
FloatColumn("min").From(subQuery),
|
||||||
|
FloatColumn("count").From(subQuery),
|
||||||
|
).AS("amount"),
|
||||||
|
).FROM(subQuery)
|
||||||
|
|
||||||
|
testutils.AssertDebugStatementSql(t, stmt, strings.ReplaceAll(`
|
||||||
|
SELECT JSON_ARRAYAGG(JSON_OBJECT('customerID', customers_info.""customer.customer_id"",
|
||||||
|
'storeID', customers_info.""customer.store_id"",
|
||||||
|
'firstName', customers_info.""customer.first_name"",
|
||||||
|
'lastName', customers_info.""customer.last_name"",
|
||||||
|
'email', customers_info.""customer.email"",
|
||||||
|
'addressID', customers_info.""customer.address_id"",
|
||||||
|
'active', customers_info.""customer.active"",
|
||||||
|
'createDate', customers_info.""customer.create_date"",
|
||||||
|
'lastUpdate', customers_info.""customer.last_update"",
|
||||||
|
'amount', (
|
||||||
|
SELECT JSON_OBJECT('sum', customers_info.sum,
|
||||||
|
'avg', customers_info.avg,
|
||||||
|
'max', customers_info.max,
|
||||||
|
'min', customers_info.min,
|
||||||
|
'count', customers_info.count) AS "json"
|
||||||
|
))) AS "json"
|
||||||
|
FROM (
|
||||||
|
SELECT customer.customer_id AS "customer.customer_id",
|
||||||
|
customer.store_id AS "customer.store_id",
|
||||||
|
customer.first_name AS "customer.first_name",
|
||||||
|
customer.last_name AS "customer.last_name",
|
||||||
|
customer.email AS "customer.email",
|
||||||
|
customer.address_id AS "customer.address_id",
|
||||||
|
customer.active AS "customer.active",
|
||||||
|
customer.create_date AS "customer.create_date",
|
||||||
|
customer.last_update AS "customer.last_update",
|
||||||
|
SUM(payment.amount) AS "sum",
|
||||||
|
AVG(payment.amount) AS "avg",
|
||||||
|
MAX(payment.amount) AS "max",
|
||||||
|
MIN(payment.amount) AS "min",
|
||||||
|
COUNT(payment.amount) AS "count"
|
||||||
|
FROM dvds.payment
|
||||||
|
INNER JOIN dvds.customer ON (customer.customer_id = payment.customer_id)
|
||||||
|
GROUP BY customer.customer_id
|
||||||
|
HAVING SUM(payment.amount) > 125
|
||||||
|
ORDER BY customer.customer_id, SUM(payment.amount) ASC
|
||||||
|
) AS customers_info;
|
||||||
|
`, `""`, "`"))
|
||||||
|
|
||||||
|
var dest []struct {
|
||||||
|
model.Customer
|
||||||
|
|
||||||
|
Amount struct {
|
||||||
|
Sum float64
|
||||||
|
Avg float64
|
||||||
|
Max float64
|
||||||
|
Min float64
|
||||||
|
Count int64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
fmt.Println(err)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
testutils.AssertJSONFile(t, dest, "./testdata/results/mysql/customer_payment_sum.json")
|
||||||
|
requireLogged(t, stmt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectJsonObject_EmptyResult(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("json obj", func(t *testing.T) {
|
||||||
|
stmt := SELECT_JSON_OBJ(Actor.AllColumns).
|
||||||
|
FROM(Actor).
|
||||||
|
WHERE(Actor.FirstName.EQ(String("Kowalski")))
|
||||||
|
|
||||||
|
var dest model.Actor
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
require.ErrorIs(t, err, qrm.ErrNoRows)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json arr", func(t *testing.T) {
|
||||||
|
stmt := SELECT_JSON_ARR(Actor.AllColumns).
|
||||||
|
FROM(Actor).
|
||||||
|
WHERE(Actor.FirstName.EQ(String("Kowalski")))
|
||||||
|
|
||||||
|
var dest []model.Actor
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, dest)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectJson_ProjectionNotAliased(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("expression not aliased", func(t *testing.T) {
|
||||||
|
testutils.AssertPanicErr(t, func() {
|
||||||
|
stmt := SELECT_JSON_ARR(
|
||||||
|
Int(2).ADD(Customer.CustomerID),
|
||||||
|
).FROM(Customer)
|
||||||
|
|
||||||
|
stmt.DebugSql()
|
||||||
|
|
||||||
|
}, "jet: expression need to be aliased when used as SELECT JSON projection.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -19,9 +19,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSelect_ScanToStruct(t *testing.T) {
|
func TestSelect_ScanToStruct(t *testing.T) {
|
||||||
query := Actor.
|
query := SELECT(Actor.AllColumns).
|
||||||
SELECT(Actor.AllColumns).
|
|
||||||
DISTINCT().
|
DISTINCT().
|
||||||
|
FROM(Actor).
|
||||||
WHERE(Actor.ActorID.EQ(Int(2)))
|
WHERE(Actor.ActorID.EQ(Int(2)))
|
||||||
|
|
||||||
testutils.AssertStatementSql(t, query, `
|
testutils.AssertStatementSql(t, query, `
|
||||||
|
|
@ -50,9 +50,56 @@ var actor2 = model.Actor{
|
||||||
LastUpdate: *testutils.TimestampWithoutTimeZone("2006-02-15 04:34:33", 2),
|
LastUpdate: *testutils.TimestampWithoutTimeZone("2006-02-15 04:34:33", 2),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSelect_NestedObject(t *testing.T) {
|
||||||
|
stmt := SELECT(
|
||||||
|
Actor.AllColumns,
|
||||||
|
Film.AllColumns,
|
||||||
|
).FROM(
|
||||||
|
Actor.
|
||||||
|
LEFT_JOIN(FilmActor, FilmActor.ActorID.EQ(Actor.ActorID)).
|
||||||
|
LEFT_JOIN(Film, Film.FilmID.EQ(FilmActor.FilmID)),
|
||||||
|
).WHERE(
|
||||||
|
Actor.ActorID.EQ(Int(2)),
|
||||||
|
).ORDER_BY(
|
||||||
|
Film.LastUpdate.DESC(),
|
||||||
|
).LIMIT(1)
|
||||||
|
|
||||||
|
var dest struct {
|
||||||
|
model.Actor
|
||||||
|
|
||||||
|
LatestFilm model.Film
|
||||||
|
}
|
||||||
|
|
||||||
|
err := stmt.Query(db, &dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
testutils.AssertJSON(t, dest, `
|
||||||
|
{
|
||||||
|
"ActorID": 2,
|
||||||
|
"FirstName": "NICK",
|
||||||
|
"LastName": "WAHLBERG",
|
||||||
|
"LastUpdate": "2006-02-15T04:34:33Z",
|
||||||
|
"LatestFilm": {
|
||||||
|
"FilmID": 3,
|
||||||
|
"Title": "ADAPTATION HOLES",
|
||||||
|
"Description": "A Astounding Reflection of a Lumberjack And a Car who must Sink a Lumberjack in A Baloon Factory",
|
||||||
|
"ReleaseYear": 2006,
|
||||||
|
"LanguageID": 1,
|
||||||
|
"OriginalLanguageID": null,
|
||||||
|
"RentalDuration": 7,
|
||||||
|
"RentalRate": 2.99,
|
||||||
|
"Length": 50,
|
||||||
|
"ReplacementCost": 18.99,
|
||||||
|
"Rating": "NC-17",
|
||||||
|
"SpecialFeatures": "Trailers,Deleted Scenes",
|
||||||
|
"LastUpdate": "2006-02-15T05:03:42Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSelect_ScanToSlice(t *testing.T) {
|
func TestSelect_ScanToSlice(t *testing.T) {
|
||||||
query := Actor.
|
query := SELECT(Actor.AllColumns).
|
||||||
SELECT(Actor.AllColumns).
|
FROM(Actor).
|
||||||
ORDER_BY(Actor.ActorID)
|
ORDER_BY(Actor.ActorID)
|
||||||
|
|
||||||
testutils.AssertStatementSql(t, query, `
|
testutils.AssertStatementSql(t, query, `
|
||||||
|
|
@ -107,19 +154,20 @@ GROUP BY payment.customer_id
|
||||||
HAVING SUM(payment.amount) > 125.6
|
HAVING SUM(payment.amount) > 125.6
|
||||||
ORDER BY payment.customer_id, SUM(payment.amount) ASC;
|
ORDER BY payment.customer_id, SUM(payment.amount) ASC;
|
||||||
`
|
`
|
||||||
query := Payment.
|
query := SELECT(
|
||||||
INNER_JOIN(Customer, Customer.CustomerID.EQ(Payment.CustomerID)).
|
Customer.AllColumns,
|
||||||
SELECT(
|
|
||||||
Customer.AllColumns,
|
|
||||||
|
|
||||||
SUMf(Payment.Amount).AS("amount.sum"),
|
SUMf(Payment.Amount).AS("amount.sum"),
|
||||||
AVG(Payment.Amount).AS("amount.avg"),
|
AVG(Payment.Amount).AS("amount.avg"),
|
||||||
MAX(Payment.PaymentDate).AS("amount.max_date"),
|
MAX(Payment.PaymentDate).AS("amount.max_date"),
|
||||||
MAXf(Payment.Amount).AS("amount.max"),
|
MAXf(Payment.Amount).AS("amount.max"),
|
||||||
MIN(Payment.PaymentDate).AS("amount.min_date"),
|
MIN(Payment.PaymentDate).AS("amount.min_date"),
|
||||||
MINf(Payment.Amount).AS("amount.min"),
|
MINf(Payment.Amount).AS("amount.min"),
|
||||||
COUNT(Payment.Amount).AS("amount.count"),
|
COUNT(Payment.Amount).AS("amount.count"),
|
||||||
).
|
).FROM(
|
||||||
|
Payment.
|
||||||
|
INNER_JOIN(Customer, Customer.CustomerID.EQ(Payment.CustomerID)),
|
||||||
|
).
|
||||||
GROUP_BY(Payment.CustomerID).
|
GROUP_BY(Payment.CustomerID).
|
||||||
HAVING(
|
HAVING(
|
||||||
SUMf(Payment.Amount).GT(Float(125.6)),
|
SUMf(Payment.Amount).GT(Float(125.6)),
|
||||||
|
|
@ -1122,7 +1170,7 @@ WHERE payment.payment_id < ?
|
||||||
WINDOW w1 AS (PARTITION BY payment.payment_date), w2 AS (w1), w3 AS (w2 ORDER BY payment.customer_id)
|
WINDOW w1 AS (PARTITION BY payment.payment_date), w2 AS (w1), w3 AS (w2 ORDER BY payment.customer_id)
|
||||||
ORDER BY payment.customer_id;
|
ORDER BY payment.customer_id;
|
||||||
`
|
`
|
||||||
query := Payment.SELECT(
|
query := SELECT(
|
||||||
AVG(Payment.Amount).OVER(),
|
AVG(Payment.Amount).OVER(),
|
||||||
AVG(Payment.Amount).OVER(Window("w1")),
|
AVG(Payment.Amount).OVER(Window("w1")),
|
||||||
AVG(Payment.Amount).OVER(
|
AVG(Payment.Amount).OVER(
|
||||||
|
|
@ -1131,7 +1179,7 @@ ORDER BY payment.customer_id;
|
||||||
RANGE(PRECEDING(UNBOUNDED), FOLLOWING(UNBOUNDED)),
|
RANGE(PRECEDING(UNBOUNDED), FOLLOWING(UNBOUNDED)),
|
||||||
),
|
),
|
||||||
AVG(Payment.Amount).OVER(Window("w3").RANGE(PRECEDING(UNBOUNDED), FOLLOWING(UNBOUNDED))),
|
AVG(Payment.Amount).OVER(Window("w3").RANGE(PRECEDING(UNBOUNDED), FOLLOWING(UNBOUNDED))),
|
||||||
).
|
).FROM(Payment).
|
||||||
WHERE(Payment.PaymentID.LT(Int(10))).
|
WHERE(Payment.PaymentID.LT(Int(10))).
|
||||||
WINDOW("w1").AS(PARTITION_BY(Payment.PaymentDate)).
|
WINDOW("w1").AS(PARTITION_BY(Payment.PaymentDate)).
|
||||||
WINDOW("w2").AS(Window("w1")).
|
WINDOW("w2").AS(Window("w1")).
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,58 @@ func TestAllTypesSelect(t *testing.T) {
|
||||||
testutils.AssertDeepEqual(t, dest[1], allTypesRow1)
|
testutils.AssertDeepEqual(t, dest[1], allTypesRow1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAllTypesSelectJson(t *testing.T) {
|
||||||
|
|
||||||
|
stmt := SELECT_JSON_ARR(
|
||||||
|
AllTypesAllColumns.Except(
|
||||||
|
AllTypes.JSON, AllTypes.JSONPtr,
|
||||||
|
AllTypes.Jsonb, AllTypes.JsonbPtr,
|
||||||
|
AllTypes.TextArray, AllTypes.TextArrayPtr,
|
||||||
|
AllTypes.JsonbArray, AllTypes.IntegerArray, AllTypes.IntegerArrayPtr,
|
||||||
|
AllTypes.TextMultiDimArray, AllTypes.TextMultiDimArrayPtr,
|
||||||
|
),
|
||||||
|
CAST(AllTypes.JSONPtr).AS_TEXT().AS("jsonPtr"),
|
||||||
|
CAST(AllTypes.JSON).AS_TEXT().AS("JSON"),
|
||||||
|
CAST(AllTypes.JsonbPtr).AS_TEXT().AS("jsonbPtr"),
|
||||||
|
CAST(AllTypes.Jsonb).AS_TEXT().AS("Jsonb"),
|
||||||
|
CAST(AllTypes.TextArrayPtr).AS_TEXT().AS("TextArrayPtr"),
|
||||||
|
CAST(AllTypes.TextArray).AS_TEXT().AS("TextArray"),
|
||||||
|
CAST(AllTypes.JsonbArray).AS_TEXT().AS("JsonbArray"),
|
||||||
|
CAST(AllTypes.IntegerArray).AS_TEXT().AS("IntegerArray"),
|
||||||
|
CAST(AllTypes.IntegerArrayPtr).AS_TEXT().AS("IntegerArrayPtr"),
|
||||||
|
CAST(AllTypes.TextMultiDimArray).AS_TEXT().AS("TextMultiDimArray"),
|
||||||
|
CAST(AllTypes.TextMultiDimArrayPtr).AS_TEXT().AS("TextMultiDimArrayPtr"),
|
||||||
|
).FROM(AllTypes)
|
||||||
|
|
||||||
|
//fmt.Println(stmt.DebugSql())
|
||||||
|
|
||||||
|
var dest []model.AllTypes
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// fix inconsistencies between postgres and cockroachdb.
|
||||||
|
// cockroachdb returns char[N] columns with trailing whitespaces trimmed
|
||||||
|
if sourceIsCockroachDB() {
|
||||||
|
dest[0].Char = allTypesRow0.Char
|
||||||
|
dest[0].CharPtr = allTypesRow0.CharPtr
|
||||||
|
|
||||||
|
dest[1].Char = allTypesRow1.Char
|
||||||
|
dest[1].CharPtr = allTypesRow1.CharPtr
|
||||||
|
}
|
||||||
|
|
||||||
|
// set time local before comparison
|
||||||
|
dest[0].Timestampz = dest[0].Timestampz.Local()
|
||||||
|
|
||||||
|
if dest[0].TimestampzPtr != nil {
|
||||||
|
dest[0].TimestampzPtr = ptr.Of(dest[0].TimestampzPtr.Local())
|
||||||
|
}
|
||||||
|
dest[1].Timestampz = dest[1].Timestampz.Local()
|
||||||
|
|
||||||
|
require.Equal(t, dest[0], allTypesRow0)
|
||||||
|
require.Equal(t, dest[1], allTypesRow1)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAllTypesViewSelect(t *testing.T) {
|
func TestAllTypesViewSelect(t *testing.T) {
|
||||||
type AllTypesView model.AllTypes
|
type AllTypesView model.AllTypes
|
||||||
var dest []AllTypesView
|
var dest []AllTypesView
|
||||||
|
|
@ -146,40 +198,42 @@ RETURNING all_types.bytea AS "all_types.bytea",
|
||||||
all_types.bytea_ptr AS "all_types.bytea_ptr";
|
all_types.bytea_ptr AS "all_types.bytea_ptr";
|
||||||
`, byteArrHex, byteArrBin)
|
`, byteArrHex, byteArrBin)
|
||||||
|
|
||||||
var inserted model.AllTypes
|
testutils.ExecuteInTxAndRollback(t, db, func(tx qrm.DB) {
|
||||||
err := insertStmt.Query(db, &inserted)
|
var inserted model.AllTypes
|
||||||
require.NoError(t, err)
|
err := insertStmt.Query(tx, &inserted)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, string(*inserted.ByteaPtr), "Hello Gopher!")
|
require.Equal(t, string(*inserted.ByteaPtr), "Hello Gopher!")
|
||||||
// It is not possible to initiate bytea column using hex format '\xDEADBEEF' with pq driver.
|
// It is not possible to initiate bytea column using hex format '\xDEADBEEF' with pq driver.
|
||||||
// pq driver always encodes parameter string if destination column is of type bytea.
|
// pq driver always encodes parameter string if destination column is of type bytea.
|
||||||
// Probably pq driver error.
|
// Probably pq driver error.
|
||||||
// require.Equal(t, string(inserted.Bytea), "Hello Gopher!")
|
// require.Equal(t, string(inserted.Bytea), "Hello Gopher!")
|
||||||
|
|
||||||
stmt := SELECT(
|
stmt := SELECT(
|
||||||
AllTypes.Bytea,
|
AllTypes.Bytea,
|
||||||
AllTypes.ByteaPtr,
|
AllTypes.ByteaPtr,
|
||||||
).FROM(
|
).FROM(
|
||||||
AllTypes,
|
AllTypes,
|
||||||
).WHERE(
|
).WHERE(
|
||||||
AllTypes.ByteaPtr.EQ(Bytea(byteArrBin)),
|
AllTypes.ByteaPtr.EQ(Bytea(byteArrBin)),
|
||||||
)
|
)
|
||||||
|
|
||||||
testutils.AssertStatementSql(t, stmt, `
|
testutils.AssertStatementSql(t, stmt, `
|
||||||
SELECT all_types.bytea AS "all_types.bytea",
|
SELECT all_types.bytea AS "all_types.bytea",
|
||||||
all_types.bytea_ptr AS "all_types.bytea_ptr"
|
all_types.bytea_ptr AS "all_types.bytea_ptr"
|
||||||
FROM test_sample.all_types
|
FROM test_sample.all_types
|
||||||
WHERE all_types.bytea_ptr = $1::bytea;
|
WHERE all_types.bytea_ptr = $1::bytea;
|
||||||
`, byteArrBin)
|
`, byteArrBin)
|
||||||
|
|
||||||
var dest model.AllTypes
|
var dest model.AllTypes
|
||||||
|
|
||||||
err = stmt.Query(db, &dest)
|
err = stmt.Query(tx, &dest)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, string(*dest.ByteaPtr), "Hello Gopher!")
|
require.Equal(t, string(*dest.ByteaPtr), "Hello Gopher!")
|
||||||
// Probably pq driver error.
|
// Probably pq driver error.
|
||||||
// require.Equal(t, string(dest.Bytea), "Hello Gopher!")
|
// require.Equal(t, string(dest.Bytea), "Hello Gopher!")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllTypesFromSubQuery(t *testing.T) {
|
func TestAllTypesFromSubQuery(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,130 @@ ORDER BY "Artist"."ArtistId", "Album"."AlbumId", "Track"."TrackId";
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AllArtistDetails []struct { //list of all artist
|
||||||
|
model.Artist
|
||||||
|
|
||||||
|
Albums []struct { // list of albums per artist
|
||||||
|
model.Album
|
||||||
|
|
||||||
|
Tracks []struct { // list of tracks per album
|
||||||
|
model.Track
|
||||||
|
|
||||||
|
Genre model.Genre // track genre
|
||||||
|
MediaType model.MediaType // track media type
|
||||||
|
|
||||||
|
Playlists []model.Playlist // list of playlist where track is used
|
||||||
|
|
||||||
|
Invoices []struct { // list of invoices where track occurs
|
||||||
|
model.Invoice
|
||||||
|
|
||||||
|
Customer struct { // customer data for invoice
|
||||||
|
model.Customer
|
||||||
|
|
||||||
|
Employee *struct { // employee data for customer if exists
|
||||||
|
model.Employee
|
||||||
|
|
||||||
|
Manager *model.Employee `alias:"Manager"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkJoinEverythingJSON(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
testJoinEverythingJSON(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJoinEverythingJSON(t *testing.T) {
|
||||||
|
testJoinEverythingJSON(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testJoinEverythingJSON(t require.TestingT) {
|
||||||
|
|
||||||
|
manager := Employee.AS("Manager")
|
||||||
|
|
||||||
|
stmt := SELECT_JSON_ARR(
|
||||||
|
Artist.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_ARR(
|
||||||
|
Album.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_ARR(
|
||||||
|
Track.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(Genre.AllColumns).
|
||||||
|
FROM(Genre).
|
||||||
|
WHERE(Genre.GenreId.EQ(Track.GenreId)).AS("Genre"),
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(MediaType.AllColumns).
|
||||||
|
FROM(MediaType).
|
||||||
|
WHERE(MediaType.MediaTypeId.EQ(Track.MediaTypeId)).AS("MediaType"),
|
||||||
|
|
||||||
|
SELECT_JSON_ARR(Playlist.AllColumns).
|
||||||
|
FROM(Playlist.INNER_JOIN(
|
||||||
|
PlaylistTrack,
|
||||||
|
Playlist.PlaylistId.EQ(PlaylistTrack.PlaylistId))).
|
||||||
|
WHERE(PlaylistTrack.TrackId.EQ(Track.TrackId)).
|
||||||
|
ORDER_BY(Playlist.PlaylistId).AS("Playlists"),
|
||||||
|
|
||||||
|
SELECT_JSON_ARR(
|
||||||
|
Invoice.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(
|
||||||
|
Customer.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(
|
||||||
|
Employee.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(manager.AllColumns).
|
||||||
|
FROM(manager).
|
||||||
|
WHERE(manager.EmployeeId.EQ(Employee.ReportsTo)).AS("Manager"),
|
||||||
|
).FROM(Employee).
|
||||||
|
WHERE(Employee.EmployeeId.EQ(Customer.SupportRepId)).AS("Employee"),
|
||||||
|
).FROM(Customer).
|
||||||
|
WHERE(Customer.CustomerId.EQ(Invoice.CustomerId)).AS("Customer"),
|
||||||
|
).FROM(Invoice.INNER_JOIN(
|
||||||
|
InvoiceLine,
|
||||||
|
InvoiceLine.InvoiceId.EQ(Invoice.InvoiceId)),
|
||||||
|
).WHERE(InvoiceLine.TrackId.EQ(Track.TrackId)).
|
||||||
|
ORDER_BY(Invoice.InvoiceId).AS("Invoices"),
|
||||||
|
).FROM(Track).
|
||||||
|
WHERE(Track.AlbumId.EQ(Album.AlbumId)).
|
||||||
|
ORDER_BY(Track.TrackId).AS("Tracks"),
|
||||||
|
).FROM(Album).
|
||||||
|
WHERE(Album.ArtistId.EQ(Artist.ArtistId)).
|
||||||
|
ORDER_BY(Album.AlbumId).AS("Albums"),
|
||||||
|
).FROM(Artist).
|
||||||
|
ORDER_BY(Artist.ArtistId)
|
||||||
|
|
||||||
|
//fmt.Println(stmt.DebugSql())
|
||||||
|
|
||||||
|
var dest AllArtistDetails
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, len(dest), 275)
|
||||||
|
//testutils.SaveJSONFile(dest, "./testdata/results/postgres/joined_everything2.json")
|
||||||
|
testutils.AssertJSONFile(t, dest, "./testdata/results/postgres/joined_everything.json")
|
||||||
|
requireLogged(t, stmt)
|
||||||
|
requireQueryLogged(t, stmt, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkJoinEverything(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
testJoinEverything(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestJoinEverything(t *testing.T) {
|
func TestJoinEverything(t *testing.T) {
|
||||||
|
testJoinEverything(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testJoinEverything(t require.TestingT) {
|
||||||
|
|
||||||
manager := Employee.AS("Manager")
|
manager := Employee.AS("Manager")
|
||||||
|
|
||||||
|
|
@ -223,37 +346,6 @@ func TestJoinEverything(t *testing.T) {
|
||||||
Invoice.InvoiceId, Customer.CustomerId,
|
Invoice.InvoiceId, Customer.CustomerId,
|
||||||
)
|
)
|
||||||
|
|
||||||
var dest []struct { //list of all artist
|
|
||||||
model.Artist
|
|
||||||
|
|
||||||
Albums []struct { // list of albums per artist
|
|
||||||
model.Album
|
|
||||||
|
|
||||||
Tracks []struct { // list of tracks per album
|
|
||||||
model.Track
|
|
||||||
|
|
||||||
Genre model.Genre // track genre
|
|
||||||
MediaType model.MediaType // track media type
|
|
||||||
|
|
||||||
Playlists []model.Playlist // list of playlist where track is used
|
|
||||||
|
|
||||||
Invoices []struct { // list of invoices where track occurs
|
|
||||||
model.Invoice
|
|
||||||
|
|
||||||
Customer struct { // customer data for invoice
|
|
||||||
model.Customer
|
|
||||||
|
|
||||||
Employee *struct { // employee data for customer if exists
|
|
||||||
model.Employee
|
|
||||||
|
|
||||||
Manager *model.Employee `alias:"Manager"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.AssertStatementSql(t, stmt, `
|
testutils.AssertStatementSql(t, stmt, `
|
||||||
SELECT "Artist"."ArtistId" AS "Artist.ArtistId",
|
SELECT "Artist"."ArtistId" AS "Artist.ArtistId",
|
||||||
"Artist"."Name" AS "Artist.Name",
|
"Artist"."Name" AS "Artist.Name",
|
||||||
|
|
@ -344,7 +436,7 @@ FROM chinook."Artist"
|
||||||
LEFT JOIN chinook."Employee" AS "Manager" ON ("Manager"."EmployeeId" = "Employee"."ReportsTo")
|
LEFT JOIN chinook."Employee" AS "Manager" ON ("Manager"."EmployeeId" = "Employee"."ReportsTo")
|
||||||
ORDER BY "Artist"."ArtistId", "Album"."AlbumId", "Track"."TrackId", "Genre"."GenreId", "MediaType"."MediaTypeId", "Playlist"."PlaylistId", "Invoice"."InvoiceId", "Customer"."CustomerId";
|
ORDER BY "Artist"."ArtistId", "Album"."AlbumId", "Track"."TrackId", "Genre"."GenreId", "MediaType"."MediaTypeId", "Playlist"."PlaylistId", "Invoice"."InvoiceId", "Customer"."CustomerId";
|
||||||
`)
|
`)
|
||||||
|
var dest AllArtistDetails
|
||||||
err := stmt.QueryContext(context.Background(), db, &dest)
|
err := stmt.QueryContext(context.Background(), db, &dest)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
||||||
|
|
@ -119,14 +119,22 @@ func init() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func requireLogged(t *testing.T, statement postgres.Statement) {
|
func requireLogged(t require.TestingT, statement postgres.Statement) {
|
||||||
|
if _, ok := t.(*testing.B); ok {
|
||||||
|
return // skip assert for benchmarks
|
||||||
|
}
|
||||||
|
|
||||||
query, args := statement.Sql()
|
query, args := statement.Sql()
|
||||||
require.Equal(t, loggedSQL, query)
|
require.Equal(t, loggedSQL, query)
|
||||||
require.Equal(t, loggedSQLArgs, args)
|
require.Equal(t, loggedSQLArgs, args)
|
||||||
require.Equal(t, loggedDebugSQL, statement.DebugSql())
|
require.Equal(t, loggedDebugSQL, statement.DebugSql())
|
||||||
}
|
}
|
||||||
|
|
||||||
func requireQueryLogged(t *testing.T, statement postgres.Statement, rowsProcessed int64) {
|
func requireQueryLogged(t require.TestingT, statement postgres.Statement, rowsProcessed int64) {
|
||||||
|
if _, ok := t.(*testing.B); ok {
|
||||||
|
return // skip assert for benchmarks
|
||||||
|
}
|
||||||
|
|
||||||
query, args := statement.Sql()
|
query, args := statement.Sql()
|
||||||
queryLogged, argsLogged := queryInfo.Statement.Sql()
|
queryLogged, argsLogged := queryInfo.Statement.Sql()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,50 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNorthwindJoinEverything(t *testing.T) {
|
type Dest []struct {
|
||||||
|
model.Customers
|
||||||
|
|
||||||
|
Demographics model.CustomerDemographics
|
||||||
|
|
||||||
|
Orders []struct {
|
||||||
|
model.Orders
|
||||||
|
|
||||||
|
Shipper model.Shippers
|
||||||
|
|
||||||
|
Employee struct {
|
||||||
|
model.Employees
|
||||||
|
|
||||||
|
Territories []struct {
|
||||||
|
model.Territories
|
||||||
|
|
||||||
|
Region model.Region
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Details []struct {
|
||||||
|
model.OrderDetails
|
||||||
|
|
||||||
|
Products struct {
|
||||||
|
model.Products
|
||||||
|
|
||||||
|
Category model.Categories
|
||||||
|
Supplier model.Suppliers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkTestNorthwindJoinEverything(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
testNorthwindJoinEverything(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTestNorthwindJoinEverything(t *testing.T) {
|
||||||
|
testNorthwindJoinEverything(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNorthwindJoinEverything(t require.TestingT) {
|
||||||
|
|
||||||
stmt :=
|
stmt :=
|
||||||
SELECT(
|
SELECT(
|
||||||
|
|
@ -21,6 +64,9 @@ func TestNorthwindJoinEverything(t *testing.T) {
|
||||||
Products.AllColumns,
|
Products.AllColumns,
|
||||||
Categories.AllColumns,
|
Categories.AllColumns,
|
||||||
Suppliers.AllColumns,
|
Suppliers.AllColumns,
|
||||||
|
Employees.AllColumns,
|
||||||
|
Territories.AllColumns,
|
||||||
|
Region.AllColumns,
|
||||||
).FROM(
|
).FROM(
|
||||||
Customers.
|
Customers.
|
||||||
LEFT_JOIN(CustomerCustomerDemo, Customers.CustomerID.EQ(CustomerCustomerDemo.CustomerID)).
|
LEFT_JOIN(CustomerCustomerDemo, Customers.CustomerID.EQ(CustomerCustomerDemo.CustomerID)).
|
||||||
|
|
@ -35,35 +81,110 @@ func TestNorthwindJoinEverything(t *testing.T) {
|
||||||
LEFT_JOIN(EmployeeTerritories, EmployeeTerritories.EmployeeID.EQ(Employees.EmployeeID)).
|
LEFT_JOIN(EmployeeTerritories, EmployeeTerritories.EmployeeID.EQ(Employees.EmployeeID)).
|
||||||
LEFT_JOIN(Territories, EmployeeTerritories.TerritoryID.EQ(Territories.TerritoryID)).
|
LEFT_JOIN(Territories, EmployeeTerritories.TerritoryID.EQ(Territories.TerritoryID)).
|
||||||
LEFT_JOIN(Region, Territories.RegionID.EQ(Region.RegionID)),
|
LEFT_JOIN(Region, Territories.RegionID.EQ(Region.RegionID)),
|
||||||
).ORDER_BY(Customers.CustomerID, Orders.OrderID, Products.ProductID)
|
).ORDER_BY(
|
||||||
|
Customers.CustomerID,
|
||||||
|
Orders.OrderID,
|
||||||
|
Products.ProductID,
|
||||||
|
Territories.TerritoryID,
|
||||||
|
)
|
||||||
|
|
||||||
var dest []struct {
|
//fmt.Println(stmt.DebugSql())
|
||||||
model.Customers
|
|
||||||
|
|
||||||
Demographics model.CustomerDemographics
|
var dest Dest
|
||||||
|
|
||||||
Orders []struct {
|
|
||||||
model.Orders
|
|
||||||
|
|
||||||
Shipper model.Shippers
|
|
||||||
|
|
||||||
Details struct {
|
|
||||||
model.OrderDetails
|
|
||||||
|
|
||||||
Products []struct {
|
|
||||||
model.Products
|
|
||||||
|
|
||||||
Category model.Categories
|
|
||||||
Supplier model.Suppliers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := stmt.Query(db, &dest)
|
err := stmt.Query(db, &dest)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
//jsonSave("./testdata/northwind-all.json", dest)
|
//testutils.SaveJSONFile(dest, "./testdata/results/postgres/northwind-all.json")
|
||||||
testutils.AssertJSONFile(t, dest, "./testdata/results/postgres/northwind-all.json")
|
testutils.AssertJSONFile(t, dest, "./testdata/results/postgres/northwind-all.json")
|
||||||
requireLogged(t, stmt)
|
requireLogged(t, stmt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkTestNorthwindJoinEverythingJson(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
testNorthwindJoinEverythingJson(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNorthwindJoinEverythingJson(t *testing.T) {
|
||||||
|
testNorthwindJoinEverythingJson(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNorthwindJoinEverythingJson(t require.TestingT) {
|
||||||
|
|
||||||
|
stmt := SELECT_JSON_ARR(
|
||||||
|
Customers.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(CustomerDemographics.AllColumns).
|
||||||
|
FROM(CustomerDemographics.INNER_JOIN(CustomerCustomerDemo, CustomerCustomerDemo.CustomerTypeID.EQ(CustomerDemographics.CustomerTypeID))).
|
||||||
|
WHERE(CustomerCustomerDemo.CustomerID.EQ(Customers.CustomerID)).AS("Demographics"),
|
||||||
|
|
||||||
|
SELECT_JSON_ARR(
|
||||||
|
Orders.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(Shippers.AllColumns).
|
||||||
|
FROM(Shippers).
|
||||||
|
WHERE(Shippers.ShipperID.EQ(Orders.ShipVia)).AS("Shipper"),
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(
|
||||||
|
Employees.AllColumns,
|
||||||
|
SELECT_JSON_ARR(
|
||||||
|
Territories.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(Region.AllColumns).
|
||||||
|
FROM(Region).
|
||||||
|
WHERE(Region.RegionID.EQ(Territories.RegionID)).AS("Region"),
|
||||||
|
).FROM(
|
||||||
|
EmployeeTerritories.LEFT_JOIN(
|
||||||
|
Territories,
|
||||||
|
EmployeeTerritories.TerritoryID.EQ(Territories.TerritoryID)),
|
||||||
|
).WHERE(
|
||||||
|
EmployeeTerritories.EmployeeID.EQ(Employees.EmployeeID), // TODO: move to join
|
||||||
|
).AS("Territories"),
|
||||||
|
).FROM(Employees).
|
||||||
|
WHERE(Orders.EmployeeID.EQ(Employees.EmployeeID)).AS("Employee"),
|
||||||
|
|
||||||
|
SELECT_JSON_ARR(
|
||||||
|
OrderDetails.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(
|
||||||
|
Products.AllColumns,
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(
|
||||||
|
Categories.AllColumns,
|
||||||
|
).FROM(Categories).
|
||||||
|
WHERE(Categories.CategoryID.EQ(Products.CategoryID)).AS("Category"),
|
||||||
|
|
||||||
|
SELECT_JSON_OBJ(Suppliers.AllColumns).
|
||||||
|
FROM(Suppliers).
|
||||||
|
WHERE(Suppliers.SupplierID.EQ(Products.SupplierID)).AS("Supplier"),
|
||||||
|
).FROM(Products).
|
||||||
|
WHERE(Products.ProductID.EQ(OrderDetails.ProductID)).AS("Products"),
|
||||||
|
).FROM(
|
||||||
|
OrderDetails,
|
||||||
|
).WHERE(
|
||||||
|
OrderDetails.OrderID.EQ(Orders.OrderID),
|
||||||
|
).AS("Details"),
|
||||||
|
).FROM(
|
||||||
|
Orders,
|
||||||
|
).WHERE(
|
||||||
|
Orders.CustomerID.EQ(Customers.CustomerID),
|
||||||
|
).ORDER_BY(
|
||||||
|
Orders.OrderID,
|
||||||
|
).AS("Orders"),
|
||||||
|
).FROM(
|
||||||
|
Customers,
|
||||||
|
).ORDER_BY(
|
||||||
|
Customers.CustomerID,
|
||||||
|
)
|
||||||
|
|
||||||
|
//fmt.Println(stmt.DebugSql())
|
||||||
|
|
||||||
|
var dest Dest
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
//testutils.SaveJSONFile(dest, "./testdata/results/postgres/northwind-all2.json")
|
||||||
|
testutils.AssertJSONFile(t, dest, "./testdata/results/postgres/northwind-all.json")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -220,20 +220,7 @@ func TestUUIDComplex(t *testing.T) {
|
||||||
requireLogged(t, query)
|
requireLogged(t, query)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("slice of structs left join", func(t *testing.T) {
|
var expectedSliceOfStructsLeftJoin = `
|
||||||
leftQuery := Person.LEFT_JOIN(PersonPhone, PersonPhone.PersonID.EQ(Person.PersonID)).
|
|
||||||
SELECT(Person.AllColumns, PersonPhone.AllColumns).
|
|
||||||
ORDER_BY(Person.PersonID.ASC(), PersonPhone.PhoneID.ASC())
|
|
||||||
var dest []struct {
|
|
||||||
model.Person
|
|
||||||
Phones []struct {
|
|
||||||
model.PersonPhone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err := leftQuery.Query(db, &dest)
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
testutils.AssertJSON(t, dest, `
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"PersonID": "b68dbff4-a87d-11e9-a7f2-98ded00c39c6",
|
"PersonID": "b68dbff4-a87d-11e9-a7f2-98ded00c39c6",
|
||||||
|
|
@ -274,10 +261,50 @@ func TestUUIDComplex(t *testing.T) {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
`)
|
`
|
||||||
|
|
||||||
|
t.Run("slice of structs left join", func(t *testing.T) {
|
||||||
|
leftQuery := Person.LEFT_JOIN(PersonPhone, PersonPhone.PersonID.EQ(Person.PersonID)).
|
||||||
|
SELECT(Person.AllColumns, PersonPhone.AllColumns).
|
||||||
|
ORDER_BY(Person.PersonID.ASC(), PersonPhone.PhoneID.ASC())
|
||||||
|
var dest []struct {
|
||||||
|
model.Person
|
||||||
|
Phones []struct {
|
||||||
|
model.PersonPhone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := leftQuery.Query(db, &dest)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
testutils.AssertJSON(t, dest, expectedSliceOfStructsLeftJoin)
|
||||||
requireLogged(t, leftQuery)
|
requireLogged(t, leftQuery)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("select json", func(t *testing.T) {
|
||||||
|
jsonQuery := SELECT_JSON_ARR(
|
||||||
|
Person.AllColumns,
|
||||||
|
SELECT_JSON_ARR(PersonPhone.AllColumns).
|
||||||
|
FROM(PersonPhone).
|
||||||
|
WHERE(PersonPhone.PersonID.EQ(Person.PersonID)).
|
||||||
|
ORDER_BY(PersonPhone.PhoneID).AS("Phones"),
|
||||||
|
).FROM(
|
||||||
|
Person,
|
||||||
|
).ORDER_BY(
|
||||||
|
Person.PersonID.ASC(),
|
||||||
|
)
|
||||||
|
|
||||||
|
var dest []struct {
|
||||||
|
model.Person
|
||||||
|
Phones []struct {
|
||||||
|
model.PersonPhone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := jsonQuery.QueryJSON(ctx, db, &dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
testutils.AssertJSON(t, dest, expectedSliceOfStructsLeftJoin)
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
func TestEnumType(t *testing.T) {
|
func TestEnumType(t *testing.T) {
|
||||||
query := Person.
|
query := Person.
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ func TestScanToStruct(t *testing.T) {
|
||||||
|
|
||||||
err := query.Query(db, &dest)
|
err := query.Query(db, &dest)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.EqualError(t, err, "jet: can't scan int64('\\x01') to 'InventoryID uuid.UUID': Scan: unable to scan type int64 into UUID")
|
require.EqualError(t, err, "jet: can't assign int64('\\x01') to 'InventoryID uuid.UUID': Scan: unable to scan type int64 into UUID")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("type mismatch base type", func(t *testing.T) {
|
t.Run("type mismatch base type", func(t *testing.T) {
|
||||||
|
|
|
||||||
908
tests/postgres/select_json_test.go
Normal file
908
tests/postgres/select_json_test.go
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -21,36 +21,36 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSelect_ScanToStruct(t *testing.T) {
|
func TestSelect_ScanToStruct(t *testing.T) {
|
||||||
expectedSQL := `
|
|
||||||
|
t.Run("standard", func(t *testing.T) {
|
||||||
|
stmt := SELECT(Actor.AllColumns).
|
||||||
|
DISTINCT().
|
||||||
|
FROM(Actor).
|
||||||
|
WHERE(Actor.ActorID.EQ(Int(2)))
|
||||||
|
|
||||||
|
testutils.AssertDebugStatementSql(t, stmt, `
|
||||||
SELECT DISTINCT actor.actor_id AS "actor.actor_id",
|
SELECT DISTINCT actor.actor_id AS "actor.actor_id",
|
||||||
actor.first_name AS "actor.first_name",
|
actor.first_name AS "actor.first_name",
|
||||||
actor.last_name AS "actor.last_name",
|
actor.last_name AS "actor.last_name",
|
||||||
actor.last_update AS "actor.last_update"
|
actor.last_update AS "actor.last_update"
|
||||||
FROM dvds.actor
|
FROM dvds.actor
|
||||||
WHERE actor.actor_id = 2;
|
WHERE actor.actor_id = 2;
|
||||||
`
|
`, int64(2))
|
||||||
|
|
||||||
query := SELECT(Actor.AllColumns).
|
var dest model.Actor
|
||||||
DISTINCT().
|
err := stmt.Query(db, &dest)
|
||||||
FROM(Actor).
|
|
||||||
WHERE(Actor.ActorID.EQ(Int(2)))
|
|
||||||
|
|
||||||
testutils.AssertDebugStatementSql(t, query, expectedSQL, int64(2))
|
require.NoError(t, err)
|
||||||
|
testutils.AssertDeepEqual(t, dest, actor2)
|
||||||
|
requireLogged(t, stmt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
actor := model.Actor{}
|
var actor2 = model.Actor{
|
||||||
err := query.Query(db, &actor)
|
ActorID: 2,
|
||||||
|
FirstName: "Nick",
|
||||||
require.NoError(t, err)
|
LastName: "Wahlberg",
|
||||||
|
LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:47:57.62", 2),
|
||||||
expectedActor := model.Actor{
|
|
||||||
ActorID: 2,
|
|
||||||
FirstName: "Nick",
|
|
||||||
LastName: "Wahlberg",
|
|
||||||
LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:47:57.62", 2),
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.AssertDeepEqual(t, actor, expectedActor)
|
|
||||||
requireLogged(t, query)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectDistinctOn(t *testing.T) {
|
func TestSelectDistinctOn(t *testing.T) {
|
||||||
|
|
@ -85,7 +85,6 @@ ORDER BY rental.staff_id ASC, rental.customer_id ASC, rental.rental_id ASC;
|
||||||
|
|
||||||
err := stmt.Query(db, &dest)
|
err := stmt.Query(db, &dest)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testutils.AssertJSON(t, dest, `
|
testutils.AssertJSON(t, dest, `
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -187,6 +186,21 @@ ORDER BY customer.customer_id ASC;
|
||||||
testutils.AssertDeepEqual(t, lastCustomer, customers[598])
|
testutils.AssertDeepEqual(t, lastCustomer, customers[598])
|
||||||
|
|
||||||
requireLogged(t, query)
|
requireLogged(t, query)
|
||||||
|
|
||||||
|
t.Run("select json", func(t *testing.T) {
|
||||||
|
stmt := SELECT_JSON_ARR(
|
||||||
|
Customer.AllColumns,
|
||||||
|
).FROM(
|
||||||
|
Customer,
|
||||||
|
).ORDER_BY(Customer.CustomerID.ASC())
|
||||||
|
|
||||||
|
var dest []model.Customer
|
||||||
|
|
||||||
|
err := stmt.QueryJSON(ctx, db, &dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
testutils.AssertDeepEqual(t, customers, dest)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectAndUnionInProjection(t *testing.T) {
|
func TestSelectAndUnionInProjection(t *testing.T) {
|
||||||
|
|
@ -217,15 +231,14 @@ FROM dvds.payment
|
||||||
LIMIT 12;
|
LIMIT 12;
|
||||||
`
|
`
|
||||||
|
|
||||||
query := Payment.
|
query := SELECT(
|
||||||
SELECT(
|
Payment.PaymentID,
|
||||||
Payment.PaymentID,
|
Customer.SELECT(Customer.CustomerID).LIMIT(1),
|
||||||
Customer.SELECT(Customer.CustomerID).LIMIT(1),
|
UNION(
|
||||||
UNION(
|
Payment.SELECT(Payment.PaymentID).LIMIT(1).OFFSET(10),
|
||||||
Payment.SELECT(Payment.PaymentID).LIMIT(1).OFFSET(10),
|
Payment.SELECT(Payment.PaymentID).LIMIT(1).OFFSET(2),
|
||||||
Payment.SELECT(Payment.PaymentID).LIMIT(1).OFFSET(2),
|
).LIMIT(1),
|
||||||
).LIMIT(1),
|
).FROM(Payment).
|
||||||
).
|
|
||||||
LIMIT(12)
|
LIMIT(12)
|
||||||
|
|
||||||
//fmt.Println(query.DebugSql())
|
//fmt.Println(query.DebugSql())
|
||||||
|
|
@ -2771,7 +2784,8 @@ ORDER BY actor.actor_id ASC, film.film_id ASC;
|
||||||
err := stmt.Query(db, &dest)
|
err := stmt.Query(db, &dest)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
//jsonSave("./testdata/quick-start-dest.json", dest)
|
//testutils.SaveJSONFile(dest, "./testdata/results/postgres/quick-start-dest.json")
|
||||||
|
|
||||||
testutils.AssertJSONFile(t, dest, "./testdata/results/postgres/quick-start-dest.json")
|
testutils.AssertJSONFile(t, dest, "./testdata/results/postgres/quick-start-dest.json")
|
||||||
|
|
||||||
var dest2 []struct {
|
var dest2 []struct {
|
||||||
|
|
@ -2784,7 +2798,7 @@ ORDER BY actor.actor_id ASC, film.film_id ASC;
|
||||||
err = stmt.Query(db, &dest2)
|
err = stmt.Query(db, &dest2)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
//jsonSave("./testdata/quick-start-dest2.json", dest2)
|
//testutils.SaveJSONFile(dest2, "./testdata/results/postgres/quick-start-dest2.json")
|
||||||
testutils.AssertJSONFile(t, dest2, "./testdata/results/postgres/quick-start-dest2.json")
|
testutils.AssertJSONFile(t, dest2, "./testdata/results/postgres/quick-start-dest2.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2966,7 +2980,7 @@ WHERE payment.payment_id < $1
|
||||||
WINDOW w1 AS (PARTITION BY payment.payment_date), w2 AS (w1), w3 AS (w2 ORDER BY payment.customer_id)
|
WINDOW w1 AS (PARTITION BY payment.payment_date), w2 AS (w1), w3 AS (w2 ORDER BY payment.customer_id)
|
||||||
ORDER BY payment.customer_id;
|
ORDER BY payment.customer_id;
|
||||||
`
|
`
|
||||||
query := Payment.SELECT(
|
query := SELECT(
|
||||||
AVG(Payment.Amount).OVER(),
|
AVG(Payment.Amount).OVER(),
|
||||||
AVG(Payment.Amount).OVER(Window("w1")),
|
AVG(Payment.Amount).OVER(Window("w1")),
|
||||||
AVG(Payment.Amount).OVER(
|
AVG(Payment.Amount).OVER(
|
||||||
|
|
@ -2976,6 +2990,7 @@ ORDER BY payment.customer_id;
|
||||||
),
|
),
|
||||||
AVG(Payment.Amount).OVER(Window("w3").RANGE(PRECEDING(UNBOUNDED), FOLLOWING(UNBOUNDED))),
|
AVG(Payment.Amount).OVER(Window("w3").RANGE(PRECEDING(UNBOUNDED), FOLLOWING(UNBOUNDED))),
|
||||||
).
|
).
|
||||||
|
FROM(Payment).
|
||||||
WHERE(Payment.PaymentID.LT(Int(10))).
|
WHERE(Payment.PaymentID.LT(Int(10))).
|
||||||
WINDOW("w1").AS(PARTITION_BY(Payment.PaymentDate)).
|
WINDOW("w1").AS(PARTITION_BY(Payment.PaymentDate)).
|
||||||
WINDOW("w2").AS(Window("w1")).
|
WINDOW("w2").AS(Window("w1")).
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1c501acb72bea389788404988ef0130b733f9cee
|
Subproject commit 0997c825e6569fc49b69ffbef959eadab9013e00
|
||||||
Loading…
Add table
Add a link
Reference in a new issue