github.com/mitranim/sqlb@v0.7.2/sqlb_jel.go (about)

     1  package sqlb
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	r "reflect"
     8  )
     9  
    10  /*
    11  Known SQL operations used in JEL. Serves as a whitelist, allowing us to
    12  differentiate them from casts, and describes how to transform JEL Lisp-style
    13  calls into SQL expressions (prefix, infix, etc.). This is case-sensitive and
    14  whitespace-sensitive.
    15  */
    16  var Ops = map[string]Op{
    17  	`and`:                  OpInfix,
    18  	`or`:                   OpInfix,
    19  	`not`:                  OpPrefix,
    20  	`is null`:              OpPostfix,
    21  	`is not null`:          OpPostfix,
    22  	`is true`:              OpPostfix,
    23  	`is not true`:          OpPostfix,
    24  	`is false`:             OpPostfix,
    25  	`is not false`:         OpPostfix,
    26  	`is unknown`:           OpPostfix,
    27  	`is not unknown`:       OpPostfix,
    28  	`is distinct from`:     OpInfix,
    29  	`is not distinct from`: OpInfix,
    30  	`=`:                    OpInfix,
    31  	`~`:                    OpInfix,
    32  	`~*`:                   OpInfix,
    33  	`~=`:                   OpInfix,
    34  	`<>`:                   OpInfix,
    35  	`<`:                    OpInfix,
    36  	`>`:                    OpInfix,
    37  	`>=`:                   OpInfix,
    38  	`<=`:                   OpInfix,
    39  	`@@`:                   OpInfix,
    40  	`any`:                  OpAny,
    41  	`between`:              OpBetween,
    42  }
    43  
    44  /*
    45  Syntax type of SQL operator expressions used in JEL. Allows us to convert JEL
    46  Lisp-style "calls" into SQL-style operations that use prefix, infix, etc.
    47  */
    48  type Op byte
    49  
    50  const (
    51  	OpPrefix Op = iota + 1
    52  	OpPostfix
    53  	OpInfix
    54  	OpFunc
    55  	OpAny
    56  	OpBetween
    57  )
    58  
    59  /*
    60  Shortcut for instantiating `Jel` with the type of the given value. The input is
    61  used only as a type carrier.
    62  */
    63  func JelFor(typ any) Jel { return Jel{Type: typeElemOf(typ)} }
    64  
    65  /*
    66  Short for "JSON Expression Language". Provides support for expressing a
    67  whitelisted subset of SQL with JSON, as Lisp-style nested lists. Transcodes
    68  JSON to SQL on the fly. Implements `Expr`. Can be transparently used as a
    69  sub-expression in other `sqlb` expressions. See the provided example.
    70  
    71  Expressions are Lisp-style, using nested lists to express "calls". This syntax
    72  is used for all SQL operations. Binary infix operators are considered
    73  variadic.
    74  
    75  Lists are used for calls and casts. The first element must be a string. It may
    76  be one of the whitelisted operators or functions, listed in `Ops`. If not, it
    77  must be a field name or a dot-separated field path. Calls are arbitrarily
    78  nestable.
    79  
    80  	["and", true, ["or", true, ["and", true, false]]]
    81  
    82  	["<=", 10, 20]
    83  
    84  	["=", "someField", "otherField"]
    85  
    86  	["and",
    87  		["=", "someField", "otherField"],
    88  		["<=", "dateField", ["dateField", "9999-01-01T00:00:00Z"]]
    89  	]
    90  
    91  Transcoding from JSON to SQL is done by consulting two things: the built-in
    92  whitelist of SQL operations (`Ops`, shared), and a struct type provided to that
    93  particular decoder. The struct serves as a whitelist of available identifiers,
    94  and allows to determine value types via casting.
    95  
    96  Casting allows to decode arbitrary JSON directly into the corresponding Go type:
    97  
    98  	["someDateField", "9999-01-01T00:00:00Z"]
    99  
   100  	["someGeoField", {"lng": 10, "lat": 20}]
   101  
   102  Such decoded values are substituted with ordinal parameters such as $1, and
   103  appended to the slice of arguments (see below).
   104  
   105  A string not in a call position and not inside a cast is interpreted as an
   106  identifier: field name or nested field path, dot-separated. It must be found on
   107  the reference struct, otherwise transcoding fails with an error.
   108  
   109  	"someField"
   110  
   111  	"outerField.innerField"
   112  
   113  Literal numbers, booleans, and nulls that occur outside of casts are decoded
   114  into their Go equivalents. Like casts, they're substituted with ordinal
   115  parameters and appended to the slice of arguments.
   116  
   117  JSON queries are transcoded against a struct, by matching fields tagged with
   118  `json` against fields tagged with `db`. Literal values are JSON-decoded into
   119  the types of the corresponding struct fields.
   120  
   121  	type Input struct {
   122  		FieldOne string `json:"fieldOne" db:"field_one"`
   123  		FieldTwo struct {
   124  			FieldThree *time.Time `json:"fieldThree" db:"field_three"`
   125  		} `json:"fieldTwo" db:"field_two"`
   126  	}
   127  
   128  	const src = `
   129  		["and",
   130  			["=", "fieldOne", ["fieldOne", "literal string"]],
   131  			["<", "fieldTwo.fieldThree", ["fieldTwo.fieldThree", "9999-01-01T00:00:00Z"]]
   132  		]
   133  	`
   134  
   135  	expr := Jel{Type: reflect.TypeOf((*Input)(nil)).Elem(), Text: src}
   136  	text, args := Reify(expr)
   137  
   138  The result is roughly equivalent to the following (formatted for clarity):
   139  
   140  	text := `
   141  		"field_one" = 'literal string'
   142  		and
   143  		("field_two")."field_three" < '9999-01-01T00:00:00Z'
   144  	`
   145  	args := []any{"literal string", time.Time("9999-01-01T00:00:00Z")}
   146  */
   147  type Jel struct {
   148  	Type r.Type
   149  	Text string
   150  }
   151  
   152  var _ = Expr(Jel{})
   153  
   154  /*
   155  Implement `Expr`, allowing this to be used as a sub-expression in queries built
   156  with "github.com/mitranim/sqlb". Always generates a valid boolean expression,
   157  falling back on "true" if empty.
   158  */
   159  func (self Jel) AppendExpr(text []byte, args []any) ([]byte, []any) {
   160  	bui := Bui{text, args}
   161  
   162  	if len(self.Text) == 0 {
   163  		bui.Str(`true`)
   164  	} else {
   165  		self.decode(&bui, stringToBytesUnsafe(self.Text))
   166  	}
   167  
   168  	return bui.Get()
   169  }
   170  
   171  // Implement the `AppenderTo` interface, sometimes allowing more efficient text
   172  // encoding.
   173  func (self Jel) AppendTo(text []byte) []byte { return exprAppend(&self, text) }
   174  
   175  // Implement the `fmt.Stringer` interface for debug purposes.
   176  func (self Jel) String() string { return exprString(&self) }
   177  
   178  // Stores the input for future use in `.AppendExpr`. Input must be valid JSON.
   179  func (self *Jel) Parse(val string) error {
   180  	self.Text = val
   181  	return nil
   182  }
   183  
   184  // Stores the input for future use in `.AppendExpr`. Input must be valid JSON.
   185  func (self *Jel) UnmarshalText(val []byte) error {
   186  	// TODO consider using unsafe conversions.
   187  	self.Text = string(val)
   188  	return nil
   189  }
   190  
   191  // Stores the input for future use in `.AppendExpr`. Input must be valid JSON.
   192  func (self *Jel) UnmarshalJSON(val []byte) error {
   193  	// TODO consider using unsafe conversions.
   194  	self.Text = string(val)
   195  	return nil
   196  }
   197  
   198  /*
   199  If `.Type` is empty, sets the type of the provided value. Otherwise this is a
   200  nop. The input is used only as a type carrier; its actual value is ignored.
   201  */
   202  func (self *Jel) OrType(typ any) {
   203  	if self.Type == nil {
   204  		self.Type = typeElemOf(typ)
   205  	}
   206  }
   207  
   208  func (self *Jel) decode(bui *Bui, input []byte) {
   209  	input = bytes.TrimSpace(input)
   210  
   211  	if isJsonDict(input) {
   212  		panic(ErrInvalidInput{Err{
   213  			`decoding JEL`,
   214  			errf(`unexpected dict in input: %q`, input),
   215  		}})
   216  	} else if isJsonList(input) {
   217  		self.decodeList(bui, input)
   218  	} else if isJsonString(input) {
   219  		self.decodeString(bui, input)
   220  	} else {
   221  		self.decodeAny(bui, input)
   222  	}
   223  }
   224  
   225  func (self *Jel) decodeList(bui *Bui, input []byte) {
   226  	var list []json.RawMessage
   227  	err := json.Unmarshal(input, &list)
   228  	if err != nil {
   229  		panic(ErrInvalidInput{Err{
   230  			`decoding JEL list`,
   231  			fmt.Errorf(`failed to unmarshal as JSON list: %w`, err),
   232  		}})
   233  	}
   234  
   235  	if !(len(list) > 0) {
   236  		panic(ErrInvalidInput{Err{
   237  			`decoding JEL list`,
   238  			ErrStr(`lists must have at least one element, found empty list`),
   239  		}})
   240  	}
   241  
   242  	head, args := list[0], list[1:]
   243  	if !isJsonString(head) {
   244  		panic(ErrInvalidInput{Err{
   245  			`decoding JEL list`,
   246  			errf(`first list element must be a string, found %q`, head),
   247  		}})
   248  	}
   249  
   250  	var name string
   251  	err = json.Unmarshal(head, &name)
   252  	if err != nil {
   253  		panic(ErrInvalidInput{Err{
   254  			`decoding JEL list`,
   255  			fmt.Errorf(`failed to unmarshal JSON list head as string: %w`, err),
   256  		}})
   257  	}
   258  
   259  	switch Ops[name] {
   260  	case OpPrefix:
   261  		self.decodeOpPrefix(bui, name, args)
   262  	case OpPostfix:
   263  		self.decodeOpPostfix(bui, name, args)
   264  	case OpInfix:
   265  		self.decodeOpInfix(bui, name, args)
   266  	case OpFunc:
   267  		self.decodeOpFunc(bui, name, args)
   268  	case OpAny:
   269  		self.decodeOpAny(bui, name, args)
   270  	case OpBetween:
   271  		self.decodeOpBetween(bui, name, args)
   272  	default:
   273  		self.decodeCast(bui, name, args)
   274  	}
   275  }
   276  
   277  func (self *Jel) decodeOpPrefix(bui *Bui, name string, args []json.RawMessage) {
   278  	if len(args) != 1 {
   279  		panic(ErrInvalidInput{Err{
   280  			`decoding JEL op (prefix)`,
   281  			errf(`prefix operation %q must have exactly 1 argument, found %v`, name, len(args)),
   282  		}})
   283  	}
   284  
   285  	bui.Str(`(`)
   286  	bui.Str(name)
   287  	self.decode(bui, args[0])
   288  	bui.Str(`)`)
   289  }
   290  
   291  func (self *Jel) decodeOpPostfix(bui *Bui, name string, args []json.RawMessage) {
   292  	if len(args) != 1 {
   293  		panic(ErrInvalidInput{Err{
   294  			`decoding JEL op (postfix)`,
   295  			errf(`postfix operation %q must have exactly 1 argument, found %v`, name, len(args)),
   296  		}})
   297  	}
   298  
   299  	bui.Str(`(`)
   300  	self.decode(bui, args[0])
   301  	bui.Str(name)
   302  	bui.Str(`)`)
   303  }
   304  
   305  func (self *Jel) decodeOpInfix(bui *Bui, name string, args []json.RawMessage) {
   306  	if !(len(args) >= 2) {
   307  		panic(ErrInvalidInput{Err{
   308  			`decoding JEL op (infix)`,
   309  			errf(`infix operation %q must have at least 2 arguments, found %v`, name, len(args)),
   310  		}})
   311  	}
   312  
   313  	bui.Str(`(`)
   314  	for ind, arg := range args {
   315  		if ind > 0 {
   316  			bui.Str(name)
   317  		}
   318  		self.decode(bui, arg)
   319  	}
   320  	bui.Str(`)`)
   321  }
   322  
   323  func (self *Jel) decodeOpFunc(bui *Bui, name string, args []json.RawMessage) {
   324  	bui.Str(name)
   325  	bui.Str(`(`)
   326  	for ind, arg := range args {
   327  		if ind > 0 {
   328  			bui.Str(`,`)
   329  		}
   330  		self.decode(bui, arg)
   331  	}
   332  	bui.Str(`)`)
   333  }
   334  
   335  func (self *Jel) decodeOpAny(bui *Bui, name string, args []json.RawMessage) {
   336  	if len(args) != 2 {
   337  		panic(ErrInvalidInput{Err{
   338  			`decoding JEL op`,
   339  			errf(`operation %q must have exactly 2 arguments, found %v`, name, len(args)),
   340  		}})
   341  	}
   342  
   343  	bui.Str(`(`)
   344  	self.decode(bui, args[0])
   345  	bui.Str(`=`)
   346  	bui.Str(name)
   347  	bui.Str(`(`)
   348  	self.decode(bui, args[1])
   349  	bui.Str(`)`)
   350  	bui.Str(`)`)
   351  }
   352  
   353  func (self *Jel) decodeOpBetween(bui *Bui, name string, args []json.RawMessage) {
   354  	if len(args) != 3 {
   355  		panic(ErrInvalidInput{Err{
   356  			`decoding JEL op (between)`,
   357  			errf(`operation %q must have exactly 3 arguments, found %v`, name, len(args)),
   358  		}})
   359  	}
   360  
   361  	bui.Str(`(`)
   362  	self.decode(bui, args[0])
   363  	bui.Str(`between`)
   364  	self.decode(bui, args[1])
   365  	bui.Str(`and`)
   366  	self.decode(bui, args[2])
   367  	bui.Str(`)`)
   368  }
   369  
   370  func (self *Jel) decodeCast(bui *Bui, name string, args []json.RawMessage) {
   371  	if len(args) != 1 {
   372  		panic(ErrInvalidInput{Err{
   373  			`decoding JEL op (cast)`,
   374  			errf(`cast into %q must have exactly 1 argument, found %v`, name, len(args)),
   375  		}})
   376  	}
   377  
   378  	typ := self.Type
   379  	field, ok := loadStructJsonPathToNestedDbFieldMap(typ)[name]
   380  	if !ok {
   381  		panic(errUnknownField(`decoding JEL op (cast)`, name, typeName(typ)))
   382  	}
   383  
   384  	val := r.New(field.Field.Type)
   385  	try(json.Unmarshal(args[0], val.Interface()))
   386  
   387  	bui.Arg(val.Elem().Interface())
   388  }
   389  
   390  func (self *Jel) decodeString(bui *Bui, input []byte) {
   391  	var str string
   392  	try(json.Unmarshal(input, &str))
   393  
   394  	typ := self.Type
   395  	val, ok := loadStructJsonPathToNestedDbFieldMap(typ)[str]
   396  	if !ok {
   397  		panic(errUnknownField(`decoding JEL string`, str, typeName(typ)))
   398  	}
   399  
   400  	bui.Set(Path(val.DbPath).AppendExpr(bui.Get()))
   401  }
   402  
   403  // Should be used only for numbers, bools, nulls.
   404  // TODO: unmarshal integers into `int64` rather than `float64`.
   405  func (self *Jel) decodeAny(bui *Bui, input []byte) {
   406  	var val any
   407  	try(json.Unmarshal(input, &val))
   408  	bui.Arg(val)
   409  }