Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 package datastore | |
| 6 | |
| 7 import ( | |
| 8 "bytes" | |
| 9 "fmt" | |
| 10 "sort" | |
| 11 "strings" | |
| 12 | |
| 13 "github.com/luci/luci-go/common/errors" | |
| 14 "github.com/luci/luci-go/common/stringset" | |
| 15 ) | |
| 16 | |
| 17 var ErrMultipleInequalityFilter = errors.New( | |
| 18 "inequality filters on multiple properties in the same Query is not allo wed") | |
| 19 | |
| 20 var ErrNullQuery = errors.New( | |
| 21 "the query is overconstraind and can never have results") | |
| 22 | |
| 23 type Query struct { | |
| 24 kind string | |
| 25 | |
| 26 eventualConsistency bool | |
| 27 keysOnly bool | |
| 28 distinct bool | |
| 29 | |
| 30 limit *int32 | |
| 31 offset *int32 | |
| 32 | |
| 33 order []IndexColumn | |
| 34 project stringset.Set | |
| 35 | |
| 36 eqFilts map[string]PropertySlice | |
| 37 | |
| 38 ineqFiltProp string | |
| 39 ineqFiltLow Property | |
| 40 ineqFiltLowIncl bool | |
| 41 ineqFiltLowSet bool | |
| 42 ineqFiltHigh Property | |
| 43 ineqFiltHighIncl bool | |
| 44 ineqFiltHighSet bool | |
| 45 | |
| 46 start Cursor | |
| 47 end Cursor | |
| 48 | |
| 49 // These are set by Finalize as a way to cache the 1-1 correspondence of | |
| 50 // a Query to its FinalizedQuery form. err may also be set by intermedia te | |
| 51 // Query functions if there's a problem before finalization. | |
| 52 finalized *FinalizedQuery | |
| 53 err error | |
| 54 } | |
| 55 | |
| 56 func NewQuery(kind string) *Query { | |
| 57 return &Query{kind: kind} | |
| 58 } | |
| 59 | |
| 60 func (q *Query) mod(cb func(*Query)) *Query { | |
| 61 if q.err != nil { | |
| 62 return q | |
| 63 } | |
| 64 | |
| 65 ret := *q | |
| 66 ret.finalized = nil | |
| 67 if len(q.order) > 0 { | |
| 68 ret.order = make([]IndexColumn, len(q.order)) | |
| 69 copy(ret.order, q.order) | |
| 70 } | |
| 71 if q.project != nil { | |
| 72 ret.project = q.project.Dup() | |
| 73 } | |
| 74 if q.eqFilts != nil { | |
|
dnj
2015/09/18 16:47:58
len(q.eqFilts) > 0
iannucci
2015/09/18 22:25:49
done
| |
| 75 ret.eqFilts = make(map[string]PropertySlice, len(q.eqFilts)) | |
| 76 for k, v := range q.eqFilts { | |
| 77 newV := make(PropertySlice, len(v)) | |
| 78 copy(newV, v) | |
| 79 ret.eqFilts[k] = newV | |
| 80 } | |
| 81 } | |
| 82 cb(&ret) | |
| 83 return &ret | |
| 84 } | |
| 85 | |
| 86 func (q *Query) Kind(kind string) *Query { | |
|
dnj
2015/09/18 16:47:58
Worth doing pre-mod identity checks for these? e.g
iannucci
2015/09/18 22:25:48
Not really. If you have allocation bottlenecks in
| |
| 87 return q.mod(func(q *Query) { | |
| 88 q.kind = kind | |
| 89 }) | |
| 90 } | |
| 91 | |
| 92 func (q *Query) Ancestor(ancestor *Key) *Query { | |
| 93 return q.mod(func(q *Query) { | |
| 94 if q.eqFilts == nil { | |
| 95 q.eqFilts = map[string]PropertySlice{} | |
| 96 } | |
| 97 if ancestor == nil { | |
| 98 delete(q.eqFilts, "__ancestor__") | |
| 99 if len(q.eqFilts) == 0 { | |
| 100 q.eqFilts = nil | |
| 101 } | |
| 102 } else { | |
| 103 q.eqFilts["__ancestor__"] = PropertySlice{MkProperty(anc estor)} | |
| 104 } | |
| 105 }) | |
| 106 } | |
| 107 | |
| 108 func (q *Query) EventualConsistency(on bool) *Query { | |
| 109 return q.mod(func(q *Query) { | |
| 110 q.eventualConsistency = on | |
| 111 }) | |
| 112 } | |
| 113 | |
| 114 func (q *Query) Limit(limit int32) *Query { | |
| 115 return q.mod(func(q *Query) { | |
| 116 if limit < 0 { | |
| 117 q.limit = nil | |
| 118 } else { | |
| 119 q.limit = &limit | |
| 120 } | |
| 121 }) | |
| 122 } | |
| 123 | |
| 124 func (q *Query) Offset(offset int32) *Query { | |
| 125 return q.mod(func(q *Query) { | |
| 126 if offset < 0 { | |
| 127 q.offset = nil | |
| 128 } else { | |
| 129 q.offset = &offset | |
| 130 } | |
| 131 }) | |
| 132 } | |
| 133 | |
| 134 func (q *Query) KeysOnly(on bool) *Query { | |
| 135 return q.mod(func(q *Query) { | |
| 136 q.keysOnly = on | |
| 137 }) | |
| 138 } | |
| 139 | |
| 140 func (q *Query) Order(fieldNames ...string) *Query { | |
| 141 return q.mod(func(q *Query) { | |
| 142 for _, fn := range fieldNames { | |
| 143 ic, err := ParseIndexColumn(fn) | |
| 144 if err != nil { | |
| 145 q.err = err | |
| 146 return | |
| 147 } | |
| 148 if q.reserved(ic.Property) { | |
| 149 return | |
| 150 } | |
| 151 q.order = append(q.order, ic) | |
| 152 } | |
| 153 }) | |
| 154 } | |
| 155 | |
| 156 func (q *Query) RevertOrder() *Query { | |
|
dnj
2015/09/18 16:47:59
ClearOrder()
| |
| 157 return q.mod(func(q *Query) { | |
| 158 q.order = nil | |
| 159 }) | |
| 160 } | |
| 161 | |
| 162 func (q *Query) Project(fieldNames ...string) *Query { | |
| 163 return q.mod(func(q *Query) { | |
| 164 for _, f := range fieldNames { | |
| 165 if q.reserved(f) { | |
| 166 return | |
| 167 } | |
| 168 if f == "__key__" { | |
| 169 q.err = fmt.Errorf("cannot project on %q", f) | |
| 170 return | |
| 171 } | |
| 172 if q.project == nil { | |
| 173 q.project = stringset.New(1) | |
| 174 } | |
| 175 q.project.Add(f) | |
| 176 } | |
| 177 }) | |
| 178 } | |
| 179 | |
| 180 func (q *Query) Distinct(on bool) *Query { | |
| 181 return q.mod(func(q *Query) { | |
| 182 q.distinct = on | |
| 183 }) | |
| 184 } | |
| 185 | |
| 186 func (q *Query) RevertProject() *Query { | |
|
dnj
2015/09/18 16:47:59
ClearProject
| |
| 187 return q.mod(func(q *Query) { | |
| 188 q.project = nil | |
| 189 }) | |
| 190 } | |
| 191 | |
| 192 func (q *Query) Start(c Cursor) *Query { | |
| 193 return q.mod(func(q *Query) { | |
| 194 q.start = c | |
| 195 }) | |
| 196 } | |
| 197 | |
| 198 func (q *Query) End(c Cursor) *Query { | |
| 199 return q.mod(func(q *Query) { | |
| 200 q.end = c | |
| 201 }) | |
| 202 } | |
| 203 | |
| 204 func (q *Query) Eq(field string, values ...interface{}) *Query { | |
| 205 return q.mod(func(q *Query) { | |
| 206 if !q.reserved(field) { | |
| 207 if q.eqFilts == nil { | |
| 208 q.eqFilts = map[string]PropertySlice{} | |
|
dnj
2015/09/18 16:47:58
make(..., 1)
iannucci
2015/09/18 22:25:48
has anyone told you that you're the king of warran
| |
| 209 } | |
| 210 s := q.eqFilts[field] | |
| 211 for _, value := range values { | |
| 212 p := Property{} | |
| 213 if q.err = p.SetValue(value, ShouldIndex); q.err != nil { | |
| 214 return | |
| 215 } | |
| 216 idx := sort.Search(len(s), func(i int) bool { | |
| 217 return s[i].Equal(&p) | |
| 218 }) | |
| 219 if idx == len(s) { | |
| 220 s = append(s, p) | |
| 221 sort.Sort(s) | |
| 222 } | |
| 223 } | |
| 224 q.eqFilts[field] = s | |
| 225 } | |
| 226 }) | |
| 227 } | |
| 228 | |
| 229 func (q *Query) reserved(field string) bool { | |
| 230 if field == "__key__" { | |
| 231 return false | |
| 232 } | |
| 233 if field == "" { | |
| 234 q.err = fmt.Errorf( | |
| 235 "cannot filter/project on: %q", field) | |
| 236 return true | |
| 237 } | |
| 238 if strings.HasPrefix(field, "__") && strings.HasSuffix(field, "__") { | |
|
dnj
2015/09/18 16:47:58
Is "_ _ _" (without spaces) reserved? Granted it's
iannucci
2015/09/18 22:25:49
They're not actually reserved in the underlying th
| |
| 239 q.err = fmt.Errorf( | |
| 240 "cannot filter/project on reserved property: %q", field) | |
| 241 return true | |
| 242 } | |
| 243 return false | |
| 244 } | |
| 245 | |
| 246 func (q *Query) ineqOK(field string, value Property) bool { | |
| 247 if q.reserved(field) { | |
| 248 return false | |
| 249 } | |
| 250 if field == "__key__" && value.Type() != PTKey { | |
| 251 q.err = fmt.Errorf( | |
| 252 "filters on %q must have type *Key (got %s)", field, val ue.Type()) | |
| 253 return false | |
| 254 } | |
| 255 if q.ineqFiltProp != "" && q.ineqFiltProp != field { | |
| 256 q.err = ErrMultipleInequalityFilter | |
| 257 return false | |
| 258 } | |
| 259 return true | |
| 260 } | |
| 261 | |
| 262 func (q *Query) Lt(field string, value interface{}) *Query { | |
| 263 p := Property{} | |
| 264 err := p.SetValue(value, ShouldIndex) | |
| 265 | |
| 266 if err == nil && q.ineqFiltHighSet { | |
|
dnj
2015/09/18 16:47:59
IMO the auto-adjustment is convenient, but it shou
iannucci
2015/09/18 22:25:49
yep, it does in q.ineqOK (which modifies q.err as
| |
| 267 if q.ineqFiltHigh.Less(&p) { | |
| 268 return q | |
| 269 } else if q.ineqFiltHigh.Equal(&p) && !q.ineqFiltHighIncl { | |
| 270 return q | |
| 271 } | |
| 272 } | |
| 273 | |
| 274 return q.mod(func(q *Query) { | |
| 275 if q.err = err; err != nil { | |
| 276 return | |
| 277 } | |
| 278 if q.ineqOK(field, p) { | |
| 279 q.ineqFiltProp = field | |
| 280 q.ineqFiltHighSet = true | |
| 281 q.ineqFiltHigh = p | |
| 282 q.ineqFiltHighIncl = false | |
| 283 } | |
| 284 }) | |
| 285 } | |
| 286 | |
| 287 func (q *Query) Lte(field string, value interface{}) *Query { | |
| 288 p := Property{} | |
| 289 err := p.SetValue(value, ShouldIndex) | |
| 290 | |
| 291 if err == nil && q.ineqFiltHighSet { | |
| 292 if q.ineqFiltHigh.Less(&p) { | |
| 293 return q | |
| 294 } else if q.ineqFiltHigh.Equal(&p) { | |
| 295 return q | |
| 296 } | |
| 297 } | |
| 298 | |
| 299 return q.mod(func(q *Query) { | |
| 300 if q.err = err; err != nil { | |
| 301 return | |
| 302 } | |
| 303 if q.ineqOK(field, p) { | |
| 304 q.ineqFiltProp = field | |
| 305 q.ineqFiltHighSet = true | |
| 306 q.ineqFiltHigh = p | |
| 307 q.ineqFiltHighIncl = true | |
| 308 } | |
| 309 }) | |
| 310 } | |
| 311 | |
| 312 func (q *Query) Gt(field string, value interface{}) *Query { | |
| 313 p := Property{} | |
| 314 err := p.SetValue(value, ShouldIndex) | |
| 315 | |
| 316 if err == nil && q.ineqFiltLowSet { | |
| 317 if p.Less(&q.ineqFiltLow) { | |
| 318 return q | |
| 319 } else if p.Equal(&q.ineqFiltLow) && !q.ineqFiltLowIncl { | |
| 320 return q | |
| 321 } | |
| 322 } | |
| 323 | |
| 324 return q.mod(func(q *Query) { | |
| 325 if q.err = err; err != nil { | |
| 326 return | |
| 327 } | |
| 328 if q.ineqOK(field, p) { | |
| 329 q.ineqFiltProp = field | |
| 330 q.ineqFiltLowSet = true | |
| 331 q.ineqFiltLow = p | |
| 332 q.ineqFiltLowIncl = false | |
| 333 } | |
| 334 }) | |
| 335 } | |
| 336 | |
| 337 func (q *Query) Gte(field string, value interface{}) *Query { | |
| 338 p := Property{} | |
| 339 err := p.SetValue(value, ShouldIndex) | |
| 340 | |
| 341 if err == nil && q.ineqFiltLowSet { | |
| 342 if p.Less(&q.ineqFiltLow) { | |
| 343 return q | |
| 344 } else if p.Equal(&q.ineqFiltLow) { | |
| 345 return q | |
| 346 } | |
| 347 } | |
| 348 | |
| 349 return q.mod(func(q *Query) { | |
| 350 if q.err = err; err != nil { | |
| 351 return | |
| 352 } | |
| 353 if q.ineqOK(field, p) { | |
| 354 q.ineqFiltProp = field | |
| 355 q.ineqFiltLowSet = true | |
| 356 q.ineqFiltLow = p | |
| 357 q.ineqFiltLowIncl = true | |
| 358 } | |
| 359 }) | |
| 360 } | |
| 361 | |
| 362 func (q *Query) RevertFilters() *Query { | |
|
dnj
2015/09/18 16:47:58
ClearFilters
iannucci
2015/09/18 22:25:48
done for all
| |
| 363 return q.mod(func(q *Query) { | |
| 364 anc := q.eqFilts["__ancestor__"] | |
| 365 if anc != nil { | |
| 366 q.eqFilts = map[string]PropertySlice{"__ancestor__": anc } | |
| 367 } else { | |
| 368 q.eqFilts = nil | |
| 369 } | |
| 370 q.ineqFiltLowSet = false | |
| 371 q.ineqFiltHighSet = false | |
| 372 }) | |
| 373 } | |
| 374 | |
| 375 func (q *Query) Finalize() (*FinalizedQuery, error) { | |
| 376 if q.err != nil || q.finalized != nil { | |
| 377 return q.finalized, q.err | |
| 378 } | |
| 379 | |
| 380 err := func() error { | |
| 381 if q.kind == "" { // kindless query checks | |
| 382 if q.ineqFiltProp != "" && q.ineqFiltProp != "__key__" { | |
| 383 return fmt.Errorf( | |
| 384 "kindless queries can only filter on __k ey__, got %q", q.ineqFiltProp) | |
| 385 } | |
| 386 if len(q.eqFilts) > 0 { | |
| 387 return fmt.Errorf("kindless queries not have any equality filters") | |
| 388 } | |
| 389 for _, o := range q.order { | |
| 390 if o.Property != "__key__" || o.Direction != ASC ENDING { | |
| 391 return fmt.Errorf("invalid order for kin dless query: %#v", o) | |
| 392 } | |
| 393 } | |
| 394 } | |
| 395 | |
| 396 if q.keysOnly && q.project != nil && q.project.Len() > 0 { | |
| 397 return errors.New("cannot project a keysOnly query") | |
| 398 } | |
| 399 | |
| 400 if q.ineqFiltProp != "" { | |
| 401 if len(q.order) > 0 && q.order[0].Property != q.ineqFilt Prop { | |
| 402 return fmt.Errorf( | |
| 403 "first sort order must match inequality filter: %q v %q", | |
| 404 q.order[0].Property, q.ineqFiltProp) | |
| 405 } | |
| 406 if q.ineqFiltLowSet && q.ineqFiltHighSet { | |
| 407 if q.ineqFiltHigh.Less(&q.ineqFiltLow) && | |
| 408 (q.ineqFiltHigh.Equal(&q.ineqFiltLow) && | |
| 409 (!q.ineqFiltLowIncl || !q.ineqFi ltHighIncl)) { | |
| 410 return ErrNullQuery | |
| 411 } | |
| 412 } | |
| 413 } | |
| 414 | |
| 415 err := error(nil) | |
| 416 if q.project != nil { | |
| 417 q.project.Iter(func(p string) bool { | |
| 418 if _, iseq := q.eqFilts[p]; iseq { | |
| 419 err = fmt.Errorf("cannot project on equa lity filter field: %s", p) | |
| 420 return false | |
| 421 } | |
| 422 return true | |
| 423 }) | |
| 424 } | |
| 425 return err | |
| 426 }() | |
| 427 if err != nil { | |
| 428 q.err = err | |
| 429 return nil, err | |
| 430 } | |
| 431 | |
| 432 ret := &FinalizedQuery{ | |
| 433 original: q, | |
| 434 kind: q.kind, | |
| 435 | |
| 436 keysOnly: q.keysOnly, | |
| 437 eventuallyConsistent: q.eventualConsistency || q.eqFilts["__ance stor__"] == nil, | |
| 438 limit: q.limit, | |
| 439 offset: q.offset, | |
| 440 start: q.start, | |
| 441 end: q.end, | |
| 442 | |
| 443 eqFilts: q.eqFilts, | |
| 444 | |
| 445 ineqFiltProp: q.ineqFiltProp, | |
| 446 ineqFiltLow: q.ineqFiltLow, | |
| 447 ineqFiltLowIncl: q.ineqFiltLowIncl, | |
| 448 ineqFiltLowSet: q.ineqFiltLowSet, | |
| 449 ineqFiltHigh: q.ineqFiltHigh, | |
| 450 ineqFiltHighIncl: q.ineqFiltHighIncl, | |
| 451 ineqFiltHighSet: q.ineqFiltHighSet, | |
| 452 } | |
| 453 | |
| 454 if q.project != nil { | |
| 455 ret.project = q.project.ToSlice() | |
| 456 ret.distinct = q.distinct && q.project.Len() > 0 | |
| 457 } | |
| 458 | |
| 459 // if len(q.order) > 0, we already enforce that the first order | |
| 460 // is the same as the inequality above. Otherwise we need to add it. | |
| 461 if len(q.order) == 0 && q.ineqFiltProp != "" { | |
| 462 ret.orders = []IndexColumn{{q.ineqFiltProp, ASCENDING}} | |
| 463 } | |
| 464 | |
| 465 seenOrders := stringset.New(len(q.order)) | |
| 466 | |
| 467 // drop orders where there's an equality filter | |
| 468 // https://cloud.google.com/appengine/docs/go/datastore/queries#sort_o rders_are_ignored_on_properties_with_equality_filters | |
| 469 // Deduplicate orders | |
| 470 for _, o := range q.order { | |
| 471 if _, iseq := q.eqFilts[o.Property]; !iseq { | |
| 472 if seenOrders.Add(o.Property) { | |
| 473 ret.orders = append(ret.orders, o) | |
| 474 } | |
| 475 } | |
| 476 } | |
| 477 | |
| 478 // Add any projection columns not mentioned in the user-defined order as | |
| 479 // ASCENDING orders. Technically we could be smart and automatically use | |
| 480 // a DESCENDING ordered index, if it fit, but the logic gets insane, sin ce all | |
| 481 // suffixes of all used indexes need to be PRECISELY equal (and so you'd have | |
| 482 // to hunt/invalidate/something to find the combination of indexes that are | |
| 483 // compatible with each other as well as the query). If you want to use | |
| 484 // a DESCENDING column, just add it to the user sort order, and this loo p will | |
| 485 // not synthesize a new suffix entry for it. | |
| 486 // | |
| 487 // NOTE: if you want to use an index that sorts by -__key__, you MUST | |
| 488 // include all of the projected fields for that index in the order expli citly. | |
| 489 // Otherwise the generated orders will be wacky. So: | |
| 490 // Query("Foo").Project("A", "B").Order("A").Order("-__key__") | |
| 491 // | |
| 492 // will turn into a orders of: | |
| 493 // A, ASCENDING | |
| 494 // __key__, DESCENDING | |
| 495 // B, ASCENDING | |
| 496 // __key__, ASCENDING | |
| 497 // | |
| 498 // To prevent this, your query should have another Order("B") clause bef ore | |
| 499 // the -__key__ clause. | |
| 500 if len(ret.project) > 0 { | |
| 501 sort.Strings(ret.project) | |
| 502 for _, p := range ret.project { | |
| 503 if !seenOrders.Has(p) { | |
| 504 ret.orders = append(ret.orders, IndexColumn{p, A SCENDING}) | |
| 505 } | |
| 506 } | |
| 507 } | |
| 508 | |
| 509 // If the suffix format ends with __key__ already (e.g. .Order("__key__" )), | |
| 510 // then we're good to go. Otherwise we need to add it as the last bit of the | |
| 511 // suffix, since all indexes implicitly have it as the last column. | |
| 512 if len(ret.orders) == 0 || ret.orders[len(ret.orders)-1].Property != "__ key__" { | |
| 513 ret.orders = append(ret.orders, IndexColumn{"__key__", ASCENDING }) | |
| 514 } | |
| 515 | |
| 516 q.finalized = ret | |
| 517 return ret, nil | |
| 518 } | |
| 519 | |
| 520 func (q *Query) String() string { | |
| 521 ret := &bytes.Buffer{} | |
| 522 needComma := false | |
| 523 p := func(fmtStr string, stuff ...interface{}) { | |
| 524 if needComma { | |
| 525 ret.WriteString(", ") | |
| 526 } | |
| 527 needComma = true | |
| 528 fmt.Fprintf(ret, fmtStr, stuff...) | |
| 529 } | |
| 530 ret.WriteString("Query(") | |
| 531 if q.err != nil { | |
| 532 p("ERROR=%q", q.err.Error()) | |
| 533 } | |
| 534 | |
| 535 // Filters | |
| 536 if q.kind != "" { | |
| 537 p("Kind=%q", q.kind) | |
| 538 } | |
| 539 if q.eqFilts["__ancestor__"] != nil { | |
| 540 p("Ancestor=%s", q.eqFilts["__ancestor__"][0].Value().(*Key).Str ing()) | |
| 541 } | |
| 542 for prop, vals := range q.eqFilts { | |
| 543 if prop == "__ancestor__" { | |
| 544 continue | |
| 545 } | |
| 546 for _, v := range vals { | |
| 547 p("Filter(%q == %s)", prop, v.GQL()) | |
| 548 } | |
| 549 } | |
| 550 if q.ineqFiltProp != "" { | |
| 551 if q.ineqFiltLowSet { | |
| 552 op := ">" | |
| 553 if q.ineqFiltLowIncl { | |
| 554 op = ">=" | |
| 555 } | |
| 556 p("Filter(%q %s %s)", q.ineqFiltProp, op, q.ineqFiltLow. GQL()) | |
| 557 } | |
|
dnj
2015/09/18 16:47:58
ineqFiltHighSet?
iannucci
2015/09/18 22:25:48
oops
| |
| 558 } | |
| 559 | |
| 560 // Order | |
| 561 if len(q.order) > 0 { | |
| 562 orders := make([]string, len(q.order)) | |
| 563 for i, o := range q.order { | |
| 564 orders[i] = o.String() | |
| 565 } | |
| 566 p("Order(%s)", strings.Join(orders, ", ")) | |
| 567 } | |
| 568 | |
| 569 // Projection | |
| 570 if q.project != nil && q.project.Len() > 0 { | |
| 571 f := "Project(%s)" | |
| 572 if q.distinct { | |
| 573 f = "Project[DISTINCT](%s)" | |
| 574 } | |
| 575 p(f, strings.Join(q.project.ToSlice(), ", ")) | |
| 576 } | |
| 577 | |
| 578 // Cursors | |
| 579 if q.start != nil { | |
| 580 p("Start(%q)", q.start.String()) | |
| 581 } | |
| 582 if q.end != nil { | |
| 583 p("End(%q)", q.end.String()) | |
| 584 } | |
| 585 | |
| 586 // Modifiiers | |
| 587 if q.limit != nil { | |
| 588 p("Limit=%d", *q.limit) | |
| 589 } | |
| 590 if q.offset != nil { | |
| 591 p("Offset=%d", *q.offset) | |
| 592 } | |
| 593 if q.eventualConsistency { | |
| 594 p("EventualConsistency") | |
| 595 } | |
| 596 if q.keysOnly { | |
| 597 p("KeysOnly") | |
| 598 } | |
| 599 | |
| 600 ret.WriteRune(')') | |
| 601 | |
| 602 return ret.String() | |
| 603 } | |
| OLD | NEW |