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 }