github.com/dolthub/go-mysql-server@v0.18.0/sql/expression/function/json/json_contains.go (about)

     1  // Copyright 2021 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package json
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  
    21  	"github.com/dolthub/go-mysql-server/sql"
    22  	"github.com/dolthub/go-mysql-server/sql/types"
    23  )
    24  
    25  // JSON_CONTAINS(target, candidate[, path])
    26  //
    27  // JSONContains indicates by returning 1 or 0 whether a given candidate JSON document is contained within a target JSON
    28  // document, or, if a path argument was supplied, whether the candidate is found at a specific path within the target.
    29  // Returns NULL if any argument is NULL, or if the path argument does not identify a section of the target document.
    30  // An error occurs if target or candidate is not a valid JSON document, or if the path argument is not a valid path
    31  // expression or contains a * or ** wildcard. To check only whether any data exists at the path, use
    32  // JSON_CONTAINS_PATH() instead.
    33  //
    34  // The following rules define containment:
    35  //   - A candidate scalar is contained in a target scalar if and only if they are comparable and are equal. Two scalar
    36  //     values are comparable if they have the same JSON_TYPE() types, with the exception that values of types INTEGER
    37  //     and DECIMAL are also comparable to each other.
    38  //   - A candidate array is contained in a target array if and only if every element in the candidate is contained in
    39  //     some element of the target.
    40  //   - A candidate non-array is contained in a target array if and only if the candidate is contained in some element
    41  //     of the target.
    42  //   - A candidate object is contained in a target object if and only if for each key in the candidate there is a key
    43  //     with the same name in the target and the value associated with the candidate key is contained in the value
    44  //     associated with the target key.
    45  //
    46  // Otherwise, the candidate value is not contained in the target document.
    47  //
    48  // https://dev.mysql.com/doc/refman/8.0/en/json-search-functions.html#function_json-contains
    49  // TODO: Add multi index optimization -> https://dev.mysql.com/doc/refman/8.0/en/create-index.html#create-index-multi-valued
    50  type JSONContains struct {
    51  	JSONTarget    sql.Expression
    52  	JSONCandidate sql.Expression
    53  	Path          sql.Expression
    54  }
    55  
    56  var _ sql.FunctionExpression = (*JSONContains)(nil)
    57  var _ sql.CollationCoercible = (*JSONContains)(nil)
    58  
    59  // NewJSONContains creates a new JSONContains function.
    60  func NewJSONContains(args ...sql.Expression) (sql.Expression, error) {
    61  	if len(args) < 2 || len(args) > 3 {
    62  		return nil, sql.ErrInvalidArgumentNumber.New("JSON_CONTAINS", "2 or 3", len(args))
    63  	}
    64  
    65  	if len(args) == 2 {
    66  		return &JSONContains{args[0], args[1], nil}, nil
    67  	}
    68  
    69  	return &JSONContains{args[0], args[1], args[2]}, nil
    70  }
    71  
    72  // FunctionName implements sql.FunctionExpression
    73  func (j *JSONContains) FunctionName() string {
    74  	return "json_contains"
    75  }
    76  
    77  // Description implements sql.FunctionExpression
    78  func (j *JSONContains) Description() string {
    79  	return "returns whether JSON document contains specific object at path."
    80  }
    81  
    82  // IsUnsupported implements sql.UnsupportedFunctionStub
    83  func (j JSONContains) IsUnsupported() bool {
    84  	return false
    85  }
    86  
    87  func (j *JSONContains) Resolved() bool {
    88  	for _, child := range j.Children() {
    89  		if child != nil && !child.Resolved() {
    90  			return false
    91  		}
    92  	}
    93  	return true
    94  }
    95  
    96  func (j *JSONContains) String() string {
    97  	children := j.Children()
    98  	var parts = make([]string, len(children))
    99  
   100  	for i, c := range children {
   101  		parts[i] = c.String()
   102  	}
   103  
   104  	return fmt.Sprintf("%s(%s)", j.FunctionName(), strings.Join(parts, ","))
   105  }
   106  
   107  func (j *JSONContains) Type() sql.Type {
   108  	return types.Boolean
   109  }
   110  
   111  // CollationCoercibility implements the interface sql.CollationCoercible.
   112  func (*JSONContains) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) {
   113  	return sql.Collation_binary, 5
   114  }
   115  
   116  func (j *JSONContains) IsNullable() bool {
   117  	return j.JSONTarget.IsNullable() || j.JSONCandidate.IsNullable() || (j.Path != nil && j.Path.IsNullable())
   118  }
   119  
   120  func (j *JSONContains) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
   121  	target, err := getSearchableJSONVal(ctx, row, j.JSONTarget)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	if target == nil {
   126  		return nil, nil
   127  	}
   128  
   129  	candidate, err := getSearchableJSONVal(ctx, row, j.JSONCandidate)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	if candidate == nil {
   134  		return nil, nil
   135  	}
   136  
   137  	// If there's a path reevaluate target based off of it
   138  	if j.Path != nil {
   139  		// Evaluate the given path if there is one
   140  		path, err := j.Path.Eval(ctx, row)
   141  		if err != nil {
   142  			return nil, err
   143  		}
   144  
   145  		path, _, err = types.LongText.Convert(path)
   146  		if err != nil {
   147  			return nil, err
   148  		}
   149  
   150  		extracted, err := types.LookupJSONValue(target, path.(string))
   151  		if err != nil {
   152  			return nil, err
   153  		}
   154  		var ok bool
   155  		target, ok = extracted.(sql.JSONWrapper)
   156  		if !ok {
   157  			return nil, nil
   158  		}
   159  		if target == nil {
   160  			return nil, nil
   161  		}
   162  	}
   163  
   164  	// Now determine whether the candidate value exists in the target
   165  	return types.ContainsJSON(target.ToInterface(), candidate.ToInterface())
   166  }
   167  
   168  func (j *JSONContains) Children() []sql.Expression {
   169  	if j.Path != nil {
   170  		return []sql.Expression{j.JSONTarget, j.JSONCandidate, j.Path}
   171  	}
   172  
   173  	return []sql.Expression{j.JSONTarget, j.JSONCandidate}
   174  }
   175  
   176  func (j *JSONContains) WithChildren(children ...sql.Expression) (sql.Expression, error) {
   177  	if len(j.Children()) != len(children) {
   178  		return nil, fmt.Errorf("json_contains did not receive the correct amount of args")
   179  	}
   180  
   181  	return NewJSONContains(children...)
   182  }