| // Copyright 2017 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package db |
| |
| import ( |
| "fmt" |
| "io" |
| "strings" |
| "unicode" |
| ) |
| |
| // operation is the enum for possible query operations. |
| type operation rune |
| |
| // Query operations. |
| // Only equals, lt, and gt can be specified in a query. |
| // The order of these operations is used by merge. |
| const ( |
| equals operation = iota |
| ltgt |
| lt |
| gt |
| ) |
| |
| // A part is a single query part with a key, operator, and value. |
| type part struct { |
| key string |
| operator operation |
| // value and value2 hold the values to compare against. |
| value, value2 string |
| } |
| |
| // sepToOperation maps runes to operation values. |
| var sepToOperation = map[byte]operation{ |
| ':': equals, |
| '<': lt, |
| '>': gt, |
| } |
| |
| // parseWord parse a single query part (as returned by SplitWords with quoting and escaping already removed) into a part struct. |
| func parseWord(word string) (part, error) { |
| sepIndex := strings.IndexFunc(word, func(r rune) bool { |
| return r == ':' || r == '>' || r == '<' || unicode.IsSpace(r) || unicode.IsUpper(r) |
| }) |
| if sepIndex < 0 { |
| return part{}, fmt.Errorf("query part %q is missing operator", word) |
| } |
| key, sep, value := word[:sepIndex], word[sepIndex], word[sepIndex+1:] |
| if oper, ok := sepToOperation[sep]; ok { |
| return part{key, oper, value, ""}, nil |
| } |
| return part{}, fmt.Errorf("query part %q has invalid key", word) |
| } |
| |
| // merge merges two query parts together into a single query part. |
| // The keys of the two parts must be equal. |
| // If the result is a query part that can never match, io.EOF is returned as the error. |
| func (p part) merge(p2 part) (part, error) { |
| if p2.operator < p.operator { |
| // Sort the parts so we only need half the table below. |
| p, p2 = p2, p |
| } |
| switch p.operator { |
| case equals: |
| switch p2.operator { |
| case equals: |
| if p.value == p2.value { |
| return p, nil |
| } |
| return part{}, io.EOF |
| case lt: |
| if p.value < p2.value { |
| return p, nil |
| } |
| return part{}, io.EOF |
| case gt: |
| if p.value > p2.value { |
| return p, nil |
| } |
| return part{}, io.EOF |
| case ltgt: |
| if p.value < p2.value && p.value > p2.value2 { |
| return p, nil |
| } |
| return part{}, io.EOF |
| } |
| case ltgt: |
| switch p2.operator { |
| case ltgt: |
| if p2.value < p.value { |
| p.value = p2.value |
| } |
| if p2.value2 > p.value2 { |
| p.value2 = p2.value2 |
| } |
| case lt: |
| if p2.value < p.value { |
| p.value = p2.value |
| } |
| case gt: |
| if p2.value > p.value2 { |
| p.value2 = p2.value |
| } |
| } |
| case lt: |
| switch p2.operator { |
| case lt: |
| if p2.value < p.value { |
| return p2, nil |
| } |
| return p, nil |
| case gt: |
| p = part{p.key, ltgt, p.value, p2.value} |
| } |
| case gt: |
| // p2.operator == gt |
| if p2.value > p.value { |
| return p2, nil |
| } |
| return p, nil |
| } |
| // p.operator == ltgt |
| if p.value <= p.value2 || p.value == "" { |
| return part{}, io.EOF |
| } |
| if p.value2 == "" { |
| return part{p.key, lt, p.value, ""}, nil |
| } |
| return p, nil |
| } |
| |
| // sql returns a SQL expression and a list of arguments for finding records matching p. |
| func (p part) sql() (sql string, args []interface{}, err error) { |
| if p.key == "upload" { |
| switch p.operator { |
| case equals: |
| return "SELECT UploadID, RecordID FROM Records WHERE UploadID = ?", []interface{}{p.value}, nil |
| case lt: |
| return "SELECT UploadID, RecordID FROM Records WHERE UploadID < ?", []interface{}{p.value}, nil |
| case gt: |
| return "SELECT UploadID, RecordID FROM Records WHERE UploadID > ?", []interface{}{p.value}, nil |
| case ltgt: |
| return "SELECT UploadID, RecordID FROM Records WHERE UploadID < ? AND UploadID > ?", []interface{}{p.value, p.value2}, nil |
| } |
| } |
| switch p.operator { |
| case equals: |
| if p.value == "" { |
| // TODO(quentin): Implement support for searching for missing labels. |
| return "", nil, fmt.Errorf("missing value for key %q", p.key) |
| } |
| return "SELECT UploadID, RecordID FROM RecordLabels WHERE Name = ? AND Value = ?", []interface{}{p.key, p.value}, nil |
| case lt: |
| return "SELECT UploadID, RecordID FROM RecordLabels WHERE Name = ? AND Value < ?", []interface{}{p.key, p.value}, nil |
| case gt: |
| if p.value == "" { |
| // Simplify queries for any value. |
| return "SELECT UploadID, RecordID FROM RecordLabels WHERE Name = ?", []interface{}{p.key}, nil |
| } |
| return "SELECT UploadID, RecordID FROM RecordLabels WHERE Name = ? AND Value > ?", []interface{}{p.key, p.value}, nil |
| case ltgt: |
| return "SELECT UploadID, RecordID FROM RecordLabels WHERE Name = ? AND Value < ? AND Value > ?", []interface{}{p.key, p.value, p.value2}, nil |
| default: |
| panic("unknown operator " + string(p.operator)) |
| } |
| } |