go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/service/datastore/index.go (about)

     1  // Copyright 2015 The LUCI Authors.
     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 datastore
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"gopkg.in/yaml.v2"
    23  )
    24  
    25  // IndexColumn represents a sort order for a single entity field.
    26  type IndexColumn struct {
    27  	Property   string
    28  	Descending bool
    29  }
    30  
    31  // ParseIndexColumn takes a spec in the form of /\s*-?\s*.+\s*/, and
    32  // returns an IndexColumn. Examples are:
    33  //
    34  //	`- Field `:  IndexColumn{Property: "Field", Descending: true}
    35  //	`Something`: IndexColumn{Property: "Something", Descending: false}
    36  //
    37  // `+Field` is invalid. “ is invalid.
    38  func ParseIndexColumn(spec string) (IndexColumn, error) {
    39  	col := IndexColumn{}
    40  	spec = strings.TrimSpace(spec)
    41  	if strings.HasPrefix(spec, "-") {
    42  		col.Descending = true
    43  		col.Property = strings.TrimSpace(spec[1:])
    44  	} else if strings.HasPrefix(spec, "+") {
    45  		return col, fmt.Errorf("datastore: invalid order: %q", spec)
    46  	} else {
    47  		col.Property = strings.TrimSpace(spec)
    48  	}
    49  	if col.Property == "" {
    50  		return col, fmt.Errorf("datastore: empty order: %q", spec)
    51  	}
    52  	return col, nil
    53  }
    54  
    55  func (i IndexColumn) cmp(o IndexColumn) int {
    56  	// sort ascending first
    57  	if !i.Descending && o.Descending {
    58  		return -1
    59  	} else if i.Descending && !o.Descending {
    60  		return 1
    61  	}
    62  	return cmpString(i.Property, o.Property)()
    63  }
    64  
    65  // UnmarshalYAML deserializes a index.yml `property` into an IndexColumn.
    66  func (i *IndexColumn) UnmarshalYAML(unmarshal func(any) error) error {
    67  	var m map[string]string
    68  	if err := unmarshal(&m); err != nil {
    69  		return err
    70  	}
    71  
    72  	name, ok := m["name"]
    73  	if !ok {
    74  		return fmt.Errorf("datastore: missing required key `name`: %v", m)
    75  	}
    76  	i.Property = name
    77  
    78  	i.Descending = false // default direction is "asc"
    79  	if v, ok := m["direction"]; ok && v == "desc" {
    80  		i.Descending = true
    81  	}
    82  
    83  	return nil
    84  }
    85  
    86  // MarshalYAML serializes an IndexColumn into a index.yml `property`.
    87  func (i *IndexColumn) MarshalYAML() (any, error) {
    88  	direction := "asc"
    89  
    90  	if i.Descending {
    91  		direction = "desc"
    92  	}
    93  
    94  	return yaml.Marshal(map[string]string{
    95  		"name":      i.Property,
    96  		"direction": direction,
    97  	})
    98  }
    99  
   100  // String returns a human-readable version of this IndexColumn which is
   101  // compatible with ParseIndexColumn.
   102  func (i IndexColumn) String() string {
   103  	ret := ""
   104  	if i.Descending {
   105  		ret = "-"
   106  	}
   107  	return ret + i.Property
   108  }
   109  
   110  // GQL returns a correctly formatted Cloud Datastore GQL literal which
   111  // is valid for the `ORDER BY` clause.
   112  //
   113  // The flavor of GQL that this emits is defined here:
   114  //
   115  //	https://cloud.google.com/datastore/docs/apis/gql/gql_reference
   116  func (i IndexColumn) GQL() string {
   117  	if i.Descending {
   118  		return gqlQuoteName(i.Property) + " DESC"
   119  	}
   120  	return gqlQuoteName(i.Property)
   121  }
   122  
   123  // IndexDefinition holds the parsed definition of a datastore index definition.
   124  type IndexDefinition struct {
   125  	Kind     string        `yaml:"kind"`
   126  	Ancestor bool          `yaml:"ancestor"`
   127  	SortBy   []IndexColumn `yaml:"properties"`
   128  }
   129  
   130  // MarshalYAML serializes an IndexDefinition into a index.yml `index`.
   131  func (id *IndexDefinition) MarshalYAML() (any, error) {
   132  	if id.Builtin() || !id.Compound() {
   133  		return nil, fmt.Errorf("cannot generate YAML for %s", id)
   134  	}
   135  
   136  	return yaml.Marshal(map[string]any{
   137  		"kind":       id.Kind,
   138  		"ancestor":   id.Ancestor,
   139  		"properties": id.SortBy,
   140  	})
   141  }
   142  
   143  // Equal returns true if the two IndexDefinitions are equivalent.
   144  func (id *IndexDefinition) Equal(o *IndexDefinition) bool {
   145  	if id.Kind != o.Kind || id.Ancestor != o.Ancestor || len(id.SortBy) != len(o.SortBy) {
   146  		return false
   147  	}
   148  	for i, col := range id.SortBy {
   149  		if col != o.SortBy[i] {
   150  			return false
   151  		}
   152  	}
   153  	return true
   154  }
   155  
   156  // Normalize returns an IndexDefinition which has a normalized SortBy field.
   157  //
   158  // This is just appending __key__ if it's not explicitly the last field in this
   159  // IndexDefinition.
   160  func (id *IndexDefinition) Normalize() *IndexDefinition {
   161  	if len(id.SortBy) > 0 && id.SortBy[len(id.SortBy)-1].Property == "__key__" {
   162  		return id
   163  	}
   164  	ret := *id
   165  	ret.SortBy = make([]IndexColumn, len(id.SortBy), len(id.SortBy)+1)
   166  	copy(ret.SortBy, id.SortBy)
   167  	ret.SortBy = append(ret.SortBy, IndexColumn{Property: "__key__"})
   168  	return &ret
   169  }
   170  
   171  // GetFullSortOrder gets the full sort order for this IndexDefinition,
   172  // including an extra "__ancestor__" column at the front if this index has
   173  // Ancestor set to true.
   174  func (id *IndexDefinition) GetFullSortOrder() []IndexColumn {
   175  	id = id.Normalize()
   176  	if !id.Ancestor {
   177  		return id.SortBy
   178  	}
   179  	ret := make([]IndexColumn, 0, len(id.SortBy)+1)
   180  	ret = append(ret, IndexColumn{Property: "__ancestor__"})
   181  	return append(ret, id.SortBy...)
   182  }
   183  
   184  // PrepForIdxTable normalize and then flips the IndexDefinition.
   185  func (id *IndexDefinition) PrepForIdxTable() *IndexDefinition {
   186  	return id.Normalize().Flip()
   187  }
   188  
   189  // Flip returns an IndexDefinition with its SortBy field in reverse order.
   190  func (id *IndexDefinition) Flip() *IndexDefinition {
   191  	ret := *id
   192  	ret.SortBy = make([]IndexColumn, 0, len(id.SortBy))
   193  	for i := len(id.SortBy) - 1; i >= 0; i-- {
   194  		ret.SortBy = append(ret.SortBy, id.SortBy[i])
   195  	}
   196  	return &ret
   197  }
   198  
   199  // Yeah who needs templates, right?
   200  // <flames>This is fine.</flames>
   201  
   202  func cmpBool(a, b bool) func() int {
   203  	return func() int {
   204  		if a == b {
   205  			return 0
   206  		}
   207  		if a && !b { // >
   208  			return 1
   209  		}
   210  		return -1
   211  	}
   212  }
   213  
   214  func cmpInt(a, b int) func() int {
   215  	return func() int {
   216  		if a == b {
   217  			return 0
   218  		}
   219  		if a > b {
   220  			return 1
   221  		}
   222  		return -1
   223  	}
   224  }
   225  
   226  func cmpString(a, b string) func() int {
   227  	return func() int {
   228  		if a == b {
   229  			return 0
   230  		}
   231  		if a > b {
   232  			return 1
   233  		}
   234  		return -1
   235  	}
   236  }
   237  
   238  // Less returns true iff id is ordered before o.
   239  func (id *IndexDefinition) Less(o *IndexDefinition) bool {
   240  	decide := func(v int) (ret, keepGoing bool) {
   241  		if v > 0 {
   242  			return false, false
   243  		}
   244  		if v < 0 {
   245  			return true, false
   246  		}
   247  		return false, true
   248  	}
   249  
   250  	factors := []func() int{
   251  		cmpBool(id.Builtin(), o.Builtin()),
   252  		cmpString(id.Kind, o.Kind),
   253  		cmpBool(id.Ancestor, o.Ancestor),
   254  		cmpInt(len(id.SortBy), len(o.SortBy)),
   255  	}
   256  	for _, f := range factors {
   257  		ret, keepGoing := decide(f())
   258  		if !keepGoing {
   259  			return ret
   260  		}
   261  	}
   262  	for idx := range id.SortBy {
   263  		ret, keepGoing := decide(id.SortBy[idx].cmp(o.SortBy[idx]))
   264  		if !keepGoing {
   265  			return ret
   266  		}
   267  	}
   268  	return false
   269  }
   270  
   271  // Builtin returns true iff the IndexDefinition is one of the automatic built-in
   272  // indexes.
   273  func (id *IndexDefinition) Builtin() bool {
   274  	return !id.Ancestor && len(id.SortBy) <= 1
   275  }
   276  
   277  // Compound returns true iff this IndexDefinition is a valid compound index
   278  // definition.
   279  //
   280  // NOTE: !Builtin() does not imply Compound().
   281  func (id *IndexDefinition) Compound() bool {
   282  	if id.Kind == "" || id.Builtin() {
   283  		return false
   284  	}
   285  	for _, sb := range id.SortBy {
   286  		if sb.Property == "" || sb.Property == "__ancestor__" {
   287  			return false
   288  		}
   289  	}
   290  	return true
   291  }
   292  
   293  // YAMLString returns the YAML representation of this IndexDefinition.
   294  //
   295  // If the index definition is Builtin() or not Compound(), this will return
   296  // an error.
   297  func (id *IndexDefinition) YAMLString() (string, error) {
   298  	if id.Builtin() || !id.Compound() {
   299  		return "", fmt.Errorf("cannot generate YAML for %s", id)
   300  	}
   301  
   302  	ret := bytes.Buffer{}
   303  
   304  	first := true
   305  	ws := func(s string, indent int) {
   306  		nl := "\n"
   307  		if first {
   308  			nl = ""
   309  			first = false
   310  		}
   311  		fmt.Fprintf(&ret, "%s%s%s", nl, strings.Repeat("  ", indent), s)
   312  	}
   313  
   314  	ws(fmt.Sprintf("- kind: %s", id.Kind), 0)
   315  	if id.Ancestor {
   316  		ws("ancestor: yes", 1)
   317  	}
   318  	ws("properties:", 1)
   319  	for _, o := range id.SortBy {
   320  		ws(fmt.Sprintf("- name: %s", o.Property), 1)
   321  		if o.Descending {
   322  			ws("direction: desc", 2)
   323  		}
   324  	}
   325  	return ret.String(), nil
   326  }
   327  
   328  func (id *IndexDefinition) String() string {
   329  	ret := bytes.Buffer{}
   330  	wr := func(r rune) {
   331  		_, err := ret.WriteRune(r)
   332  		if err != nil {
   333  			panic(err)
   334  		}
   335  	}
   336  
   337  	ws := func(s string) {
   338  		_, err := ret.WriteString(s)
   339  		if err != nil {
   340  			panic(err)
   341  		}
   342  	}
   343  
   344  	if id.Builtin() {
   345  		wr('B')
   346  	} else {
   347  		wr('C')
   348  	}
   349  	wr(':')
   350  	ws(id.Kind)
   351  	if id.Ancestor {
   352  		ws("|A")
   353  	}
   354  	for _, sb := range id.SortBy {
   355  		wr('/')
   356  		if sb.Descending {
   357  			wr('-')
   358  		}
   359  		ws(sb.Property)
   360  	}
   361  	return ret.String()
   362  }