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 }