Add support for SELECT_JSON statements.

This commit is contained in:
go-jet 2025-02-21 19:55:01 +01:00
parent 7047de44a9
commit 7b16e432ff
46 changed files with 2732 additions and 307 deletions

View file

@ -30,3 +30,8 @@ func (a *alias) serializeForProjection(statement StatementType, out *SQLBuilder)
out.WriteString("AS")
out.WriteAlias(a.alias)
}
func (a *alias) serializeForJsonObj(statement StatementType, out *SQLBuilder) {
out.WriteJsonObjKey(a.alias)
a.expression.serialize(statement, out)
}

View file

@ -52,6 +52,10 @@ func (s *ClauseSelect) Projections() ProjectionList {
// Serialize serializes clause into SQLBuilder
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.WriteString("SELECT")
s.OptimizerHints.Serialize(statementType, out, options...)
@ -66,10 +70,6 @@ func (s *ClauseSelect) Serialize(statementType StatementType, out *SQLBuilder, o
out.WriteByte(')')
}
if len(s.ProjectionList) == 0 {
panic("jet: SELECT clause has to have at least one projection")
}
out.WriteProjections(statementType, s.ProjectionList)
}

View file

@ -2,6 +2,10 @@
package jet
import (
"github.com/go-jet/jet/v2/internal/3rdparty/snaker"
)
// Column is common column interface for all types of columns.
type Column interface {
Name() string
@ -97,7 +101,17 @@ func (c ColumnExpressionImpl) serializeForProjection(statement StatementType, ou
c.serialize(statement, out)
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) {

View file

@ -78,6 +78,12 @@ func (cl ColumnList) serializeForProjection(statement StatementType, out *SQLBui
SerializeProjectionList(statement, projections, out)
}
func (cl ColumnList) serializeForJsonObj(statement StatementType, out *SQLBuilder) {
projections := ColumnListToProjectionList(cl)
SerializeProjectionListJsonObj(statement, projections, out)
}
// dummy column interface implementation
// Name is placeholder for ColumnList to implement Column interface

View file

@ -2,7 +2,7 @@ package jet
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.
type Expression interface {
Serializer
@ -89,9 +89,16 @@ func (e *ExpressionInterfaceImpl) serializeForGroupBy(statement StatementType, o
}
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)
}
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) {
e.Parent.serialize(statement, out, NoWrap)
}

View file

@ -3,6 +3,7 @@ package jet
// Projection is interface for all projection types. Types that can be part of, for instance SELECT clause.
type Projection interface {
serializeForProjection(statement StatementType, out *SQLBuilder)
serializeForJsonObj(statement StatementType, out *SQLBuilder)
fromImpl(subQuery SelectTable) Projection
}
@ -28,6 +29,10 @@ func (pl ProjectionList) serializeForProjection(statement StatementType, out *SQ
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.
// 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
@ -79,3 +84,10 @@ func (pl ProjectionList) Except(toExclude ...Column) ProjectionList {
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)
}

View file

@ -1,7 +1,7 @@
package jet
type rawStatementImpl struct {
serializerStatementInterfaceImpl
statementInterfaceImpl
RawQuery string
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
func RawStatement(dialect Dialect, rawQuery string, namedArgument ...map[string]interface{}) SerializerStatement {
newRawStatement := rawStatementImpl{
serializerStatementInterfaceImpl: serializerStatementInterfaceImpl{
statementInterfaceImpl: statementInterfaceImpl{
dialect: dialect,
statementType: "",
parent: nil,

View file

@ -22,16 +22,22 @@ func (s SerializeOption) WithFallTrough(options []SerializeOption) []SerializeOp
// StatementType is type of the SQL statement
type StatementType string
func (s StatementType) IsSelectJSON() bool {
return s == SelectJsonObjStatementType || s == SelectJsonArrStatementType
}
// Statement types
const (
SelectStatementType StatementType = "SELECT"
InsertStatementType StatementType = "INSERT"
UpdateStatementType StatementType = "UPDATE"
DeleteStatementType StatementType = "DELETE"
SetStatementType StatementType = "SET"
LockStatementType StatementType = "LOCK"
UnLockStatementType StatementType = "UNLOCK"
WithStatementType StatementType = "WITH"
SelectStatementType StatementType = "SELECT"
SelectJsonObjStatementType StatementType = "SELECT_JSON_OBJ"
SelectJsonArrStatementType StatementType = "SELECT_JSON_ARR"
InsertStatementType StatementType = "INSERT"
UpdateStatementType StatementType = "UPDATE"
DeleteStatementType StatementType = "DELETE"
SetStatementType StatementType = "SET"
LockStatementType StatementType = "LOCK"
UnLockStatementType StatementType = "UNLOCK"
WithStatementType StatementType = "WITH"
)
// Serializer interface

View file

@ -99,6 +99,10 @@ func (s *SQLBuilder) WriteString(str string) {
s.write([]byte(str))
}
func (s *SQLBuilder) WriteJsonObjKey(key string) {
s.WriteString(fmt.Sprintf(`'%s', `, key))
}
// WriteIdentifier adds identifier to output SQL
func (s *SQLBuilder) WriteIdentifier(name string, alwaysQuote ...bool) {
if s.shouldQuote(name, alwaysQuote...) {

View file

@ -7,25 +7,49 @@ import (
"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 {
// 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{})
// 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)
// 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.
// If destination is pointer to struct and query result set is empty, method returns qrm.ErrNoRows.
// Query executes statement on the provided database connection or transaction (db),
// 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
// 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.
// If destination is pointer to struct and query result set is empty, method returns qrm.ErrNoRows.
// QueryContext executes statement with a context over database connection/transaction db,
// 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
// 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(db qrm.Executable) (sql.Result, error)
// ExecContext executes statement with context over db connection/transaction without returning any rows.
ExecContext(ctx context.Context, db qrm.Executable) (sql.Result, error)
// Rows executes statements over db connection/transaction and returns rows
Rows(ctx context.Context, db qrm.Queryable) (*Rows, error)
}
@ -60,14 +84,14 @@ type SerializerHasProjections interface {
HasProjections
}
// serializerStatementInterfaceImpl struct
type serializerStatementInterfaceImpl struct {
// statementInterfaceImpl struct
type statementInterfaceImpl struct {
dialect Dialect
statementType StatementType
parent SerializerStatement
}
func (s *serializerStatementInterfaceImpl) Sql() (query string, args []interface{}) {
func (s *statementInterfaceImpl) Sql() (query string, args []interface{}) {
queryData := &SQLBuilder{Dialect: s.dialect}
@ -77,7 +101,7 @@ func (s *serializerStatementInterfaceImpl) Sql() (query string, args []interface
return
}
func (s *serializerStatementInterfaceImpl) DebugSql() (query string) {
func (s *statementInterfaceImpl) DebugSql() (query string) {
sqlBuilder := &SQLBuilder{Dialect: s.dialect, Debug: true}
s.parent.serialize(s.statementType, sqlBuilder, NoWrap)
@ -86,11 +110,29 @@ func (s *serializerStatementInterfaceImpl) DebugSql() (query string) {
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)
}
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()
callLogger(ctx, s)
@ -99,7 +141,7 @@ func (s *serializerStatementInterfaceImpl) QueryContext(ctx context.Context, db
var err error
duration := duration(func() {
rowsProcessed, err = qrm.Query(ctx, db, query, args, destination)
rowsProcessed, err = queryFunc(query, args)
})
callQueryLoggerFunc(ctx, QueryInfo{
@ -112,11 +154,11 @@ func (s *serializerStatementInterfaceImpl) QueryContext(ctx context.Context, db
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)
}
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()
callLogger(ctx, s)
@ -141,7 +183,7 @@ func (s *serializerStatementInterfaceImpl) ExecContext(ctx context.Context, db q
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()
callLogger(ctx, s)
@ -191,11 +233,15 @@ type ExpressionStatement interface {
}
// 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{
ExpressionInterfaceImpl{Parent: parent},
statementImpl{
serializerStatementInterfaceImpl: serializerStatementInterfaceImpl{
statementInterfaceImpl: statementInterfaceImpl{
parent: parent,
dialect: Dialect,
statementType: statementType,
@ -211,13 +257,16 @@ type expressionStatementImpl struct {
}
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)
}
// NewStatementImpl creates new statementImpl
func NewStatementImpl(Dialect Dialect, statementType StatementType, parent SerializerStatement, clauses ...Clause) SerializerStatement {
return &statementImpl{
serializerStatementInterfaceImpl: serializerStatementInterfaceImpl{
statementInterfaceImpl: statementInterfaceImpl{
parent: parent,
dialect: Dialect,
statementType: statementType,
@ -227,7 +276,7 @@ func NewStatementImpl(Dialect Dialect, statementType StatementType, parent Seria
}
type statementImpl struct {
serializerStatementInterfaceImpl
statementInterfaceImpl
Clauses []Clause
}

View file

@ -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
func SerializeColumnNames(columns []Column, out *SQLBuilder) {
for i, col := range columns {

View file

@ -7,7 +7,7 @@ func WITH(dialect Dialect, recursive bool, cte ...*CommonTableExpression) func(s
newWithImpl := &withImpl{
recursive: recursive,
ctes: cte,
serializerStatementInterfaceImpl: serializerStatementInterfaceImpl{
statementInterfaceImpl: statementInterfaceImpl{
dialect: dialect,
statementType: WithStatementType,
},
@ -25,7 +25,7 @@ func WITH(dialect Dialect, recursive bool, cte ...*CommonTableExpression) func(s
}
type withImpl struct {
serializerStatementInterfaceImpl
statementInterfaceImpl
recursive bool
ctes []*CommonTableExpression
primaryStatement SerializerStatement