From 6ed50f34fbf7f5dcdcbd5aa83b898debaea3fc5a Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Aug 2018 20:02:02 +0200 Subject: [PATCH] Add query package with first set of conditions and tests --- database/query/condition-and.go | 31 +++++++ database/query/condition-bool.go | 64 +++++++++++++++ database/query/condition-error.go | 19 +++++ database/query/condition-exists.go | 28 +++++++ database/query/condition-float.go | 90 ++++++++++++++++++++ database/query/condition-int.go | 86 ++++++++++++++++++++ database/query/condition-not.go | 20 +++++ database/query/condition-or.go | 31 +++++++ database/query/condition-regex.go | 57 +++++++++++++ database/query/condition-string.go | 56 +++++++++++++ database/query/condition-stringslice.go | 60 ++++++++++++++ database/query/condition.go | 67 +++++++++++++++ database/query/fetcher.go | 78 ++++++++++++++++++ database/query/parser.go | 44 ++++++++++ database/query/query.go | 98 ++++++++++++++++++++++ database/query/query_test.go | 104 ++++++++++++++++++++++++ 16 files changed, 933 insertions(+) create mode 100644 database/query/condition-and.go create mode 100644 database/query/condition-bool.go create mode 100644 database/query/condition-error.go create mode 100644 database/query/condition-exists.go create mode 100644 database/query/condition-float.go create mode 100644 database/query/condition-int.go create mode 100644 database/query/condition-not.go create mode 100644 database/query/condition-or.go create mode 100644 database/query/condition-regex.go create mode 100644 database/query/condition-string.go create mode 100644 database/query/condition-stringslice.go create mode 100644 database/query/condition.go create mode 100644 database/query/fetcher.go create mode 100644 database/query/parser.go create mode 100644 database/query/query.go create mode 100644 database/query/query_test.go diff --git a/database/query/condition-and.go b/database/query/condition-and.go new file mode 100644 index 0000000..913cbcf --- /dev/null +++ b/database/query/condition-and.go @@ -0,0 +1,31 @@ +package query + +// And combines multiple conditions with a logical _AND_ operator. +func And(conditions ...Condition) Condition { + return &andCond{ + conditions: conditions, + } +} + +type andCond struct { + conditions []Condition +} + +func (c *andCond) complies(f Fetcher) bool { + for _, cond := range c.conditions { + if !cond.complies(f) { + return false + } + } + return true +} + +func (c *andCond) check() (err error) { + for _, cond := range c.conditions { + err = cond.check() + if err != nil { + return err + } + } + return nil +} diff --git a/database/query/condition-bool.go b/database/query/condition-bool.go new file mode 100644 index 0000000..2e6bec8 --- /dev/null +++ b/database/query/condition-bool.go @@ -0,0 +1,64 @@ +package query + +import ( + "errors" + "fmt" + "strconv" +) + +type boolCondition struct { + key string + operator uint8 + value bool +} + +func newBoolCondition(key string, operator uint8, value interface{}) *boolCondition { + + var parsedValue bool + + switch v := value.(type) { + case bool: + parsedValue = v + case string: + var err error + parsedValue, err = strconv.ParseBool(v) + if err != nil { + return &boolCondition{ + key: fmt.Sprintf("could not parse \"%s\" to bool: %s", v, err), + operator: errorPresent, + } + } + default: + return &boolCondition{ + key: fmt.Sprintf("incompatible value %v for int64", value), + operator: errorPresent, + } + } + + return &boolCondition{ + key: key, + operator: operator, + value: parsedValue, + } +} + +func (c *boolCondition) complies(f Fetcher) bool { + comp, ok := f.GetBool(c.key) + if !ok { + return false + } + + switch c.operator { + case Is: + return comp == c.value + default: + return false + } +} + +func (c *boolCondition) check() error { + if c.operator == errorPresent { + return errors.New(c.key) + } + return nil +} diff --git a/database/query/condition-error.go b/database/query/condition-error.go new file mode 100644 index 0000000..a8c7610 --- /dev/null +++ b/database/query/condition-error.go @@ -0,0 +1,19 @@ +package query + +type errorCondition struct { + err error +} + +func newErrorCondition(err error) *errorCondition { + return &errorCondition{ + err: err, + } +} + +func (c *errorCondition) complies(f Fetcher) bool { + return false +} + +func (c *errorCondition) check() error { + return c.err +} diff --git a/database/query/condition-exists.go b/database/query/condition-exists.go new file mode 100644 index 0000000..086f200 --- /dev/null +++ b/database/query/condition-exists.go @@ -0,0 +1,28 @@ +package query + +import ( + "errors" +) + +type existsCondition struct { + key string + operator uint8 +} + +func newExistsCondition(key string, operator uint8) *existsCondition { + return &existsCondition{ + key: key, + operator: operator, + } +} + +func (c *existsCondition) complies(f Fetcher) bool { + return f.Exists(c.key) +} + +func (c *existsCondition) check() error { + if c.operator == errorPresent { + return errors.New(c.key) + } + return nil +} diff --git a/database/query/condition-float.go b/database/query/condition-float.go new file mode 100644 index 0000000..3627b91 --- /dev/null +++ b/database/query/condition-float.go @@ -0,0 +1,90 @@ +package query + +import ( + "errors" + "fmt" + "strconv" +) + +type floatCondition struct { + key string + operator uint8 + value float64 +} + +func newFloatCondition(key string, operator uint8, value interface{}) *floatCondition { + + var parsedValue float64 + + switch v := value.(type) { + case int: + parsedValue = float64(v) + case int8: + parsedValue = float64(v) + case int16: + parsedValue = float64(v) + case int32: + parsedValue = float64(v) + case int64: + parsedValue = float64(v) + case uint: + parsedValue = float64(v) + case uint8: + parsedValue = float64(v) + case uint32: + parsedValue = float64(v) + case float32: + parsedValue = float64(v) + case float64: + parsedValue = v + case string: + var err error + parsedValue, err = strconv.ParseFloat(v, 64) + if err != nil { + return &floatCondition{ + key: fmt.Sprintf("could not parse %s to float64: %s", v, err), + operator: errorPresent, + } + } + default: + return &floatCondition{ + key: fmt.Sprintf("incompatible value %v for float64", value), + operator: errorPresent, + } + } + + return &floatCondition{ + key: key, + operator: operator, + value: parsedValue, + } +} + +func (c *floatCondition) complies(f Fetcher) bool { + comp, ok := f.GetFloat(c.key) + if !ok { + return false + } + + switch c.operator { + case FloatEquals: + return comp == c.value + case FloatGreaterThan: + return comp > c.value + case FloatGreaterThanOrEqual: + return comp >= c.value + case FloatLessThan: + return comp < c.value + case FloatLessThanOrEqual: + return comp <= c.value + default: + return false + } +} + +func (c *floatCondition) check() error { + if c.operator == errorPresent { + return errors.New(c.key) + } + return nil +} diff --git a/database/query/condition-int.go b/database/query/condition-int.go new file mode 100644 index 0000000..b169a12 --- /dev/null +++ b/database/query/condition-int.go @@ -0,0 +1,86 @@ +package query + +import ( + "errors" + "fmt" + "strconv" +) + +type intCondition struct { + key string + operator uint8 + value int64 +} + +func newIntCondition(key string, operator uint8, value interface{}) *intCondition { + + var parsedValue int64 + + switch v := value.(type) { + case int: + parsedValue = int64(v) + case int8: + parsedValue = int64(v) + case int16: + parsedValue = int64(v) + case int32: + parsedValue = int64(v) + case int64: + parsedValue = int64(v) + case uint: + parsedValue = int64(v) + case uint8: + parsedValue = int64(v) + case uint32: + parsedValue = int64(v) + case string: + var err error + parsedValue, err = strconv.ParseInt(v, 10, 64) + if err != nil { + return &intCondition{ + key: fmt.Sprintf("could not parse %s to int64: %s (hint: use \"sameas\" to compare strings)", v, err), + operator: errorPresent, + } + } + default: + return &intCondition{ + key: fmt.Sprintf("incompatible value %v for int64", value), + operator: errorPresent, + } + } + + return &intCondition{ + key: key, + operator: operator, + value: parsedValue, + } +} + +func (c *intCondition) complies(f Fetcher) bool { + comp, ok := f.GetInt(c.key) + if !ok { + return false + } + + switch c.operator { + case Equals: + return comp == c.value + case GreaterThan: + return comp > c.value + case GreaterThanOrEqual: + return comp >= c.value + case LessThan: + return comp < c.value + case LessThanOrEqual: + return comp <= c.value + default: + return false + } +} + +func (c *intCondition) check() error { + if c.operator == errorPresent { + return errors.New(c.key) + } + return nil +} diff --git a/database/query/condition-not.go b/database/query/condition-not.go new file mode 100644 index 0000000..7249568 --- /dev/null +++ b/database/query/condition-not.go @@ -0,0 +1,20 @@ +package query + +// Not negates the supplied condition. +func Not(c Condition) Condition { + return ¬Cond{ + notC: c, + } +} + +type notCond struct { + notC Condition +} + +func (c *notCond) complies(f Fetcher) bool { + return !c.notC.complies(f) +} + +func (c *notCond) check() error { + return c.notC.check() +} diff --git a/database/query/condition-or.go b/database/query/condition-or.go new file mode 100644 index 0000000..e93228b --- /dev/null +++ b/database/query/condition-or.go @@ -0,0 +1,31 @@ +package query + +// Or combines multiple conditions with a logical _OR_ operator. +func Or(conditions ...Condition) Condition { + return &orCond{ + conditions: conditions, + } +} + +type orCond struct { + conditions []Condition +} + +func (c *orCond) complies(f Fetcher) bool { + for _, cond := range c.conditions { + if cond.complies(f) { + return true + } + } + return false +} + +func (c *orCond) check() (err error) { + for _, cond := range c.conditions { + err = cond.check() + if err != nil { + return err + } + } + return nil +} diff --git a/database/query/condition-regex.go b/database/query/condition-regex.go new file mode 100644 index 0000000..b32d212 --- /dev/null +++ b/database/query/condition-regex.go @@ -0,0 +1,57 @@ +package query + +import ( + "errors" + "fmt" + "regexp" +) + +type regexCondition struct { + key string + operator uint8 + regex *regexp.Regexp +} + +func newRegexCondition(key string, operator uint8, value interface{}) *regexCondition { + switch v := value.(type) { + case string: + r, err := regexp.Compile(v) + if err != nil { + return ®exCondition{ + key: fmt.Sprintf("could not compile regex \"%s\": %s", v, err), + operator: errorPresent, + } + } + return ®exCondition{ + key: key, + operator: operator, + regex: r, + } + default: + return ®exCondition{ + key: fmt.Sprintf("incompatible value %v for string", value), + operator: errorPresent, + } + } +} + +func (c *regexCondition) complies(f Fetcher) bool { + comp, ok := f.GetString(c.key) + if !ok { + return false + } + + switch c.operator { + case Matches: + return c.regex.MatchString(comp) + default: + return false + } +} + +func (c *regexCondition) check() error { + if c.operator == errorPresent { + return errors.New(c.key) + } + return nil +} diff --git a/database/query/condition-string.go b/database/query/condition-string.go new file mode 100644 index 0000000..f255693 --- /dev/null +++ b/database/query/condition-string.go @@ -0,0 +1,56 @@ +package query + +import ( + "errors" + "fmt" + "strings" +) + +type stringCondition struct { + key string + operator uint8 + value string +} + +func newStringCondition(key string, operator uint8, value interface{}) *stringCondition { + switch v := value.(type) { + case string: + return &stringCondition{ + key: key, + operator: operator, + value: v, + } + default: + return &stringCondition{ + key: fmt.Sprintf("incompatible value %v for string", value), + operator: errorPresent, + } + } +} + +func (c *stringCondition) complies(f Fetcher) bool { + comp, ok := f.GetString(c.key) + if !ok { + return false + } + + switch c.operator { + case Matches: + return c.value == comp + case Contains: + return strings.Contains(comp, c.value) + case StartsWith: + return strings.HasPrefix(comp, c.value) + case EndsWith: + return strings.HasSuffix(comp, c.value) + default: + return false + } +} + +func (c *stringCondition) check() error { + if c.operator == errorPresent { + return errors.New(c.key) + } + return nil +} diff --git a/database/query/condition-stringslice.go b/database/query/condition-stringslice.go new file mode 100644 index 0000000..cfd04d4 --- /dev/null +++ b/database/query/condition-stringslice.go @@ -0,0 +1,60 @@ +package query + +import ( + "fmt" + "strings" + + "github.com/Safing/portbase/utils" +) + +type stringSliceCondition struct { + key string + operator uint8 + value []string +} + +func newStringSliceCondition(key string, operator uint8, value interface{}) *stringSliceCondition { + + switch v := value.(type) { + case string: + parsedValue := strings.Split(v, ",") + if len(parsedValue) < 2 { + return &stringSliceCondition{ + key: fmt.Sprintf("could not parse \"%s\" to []string", v), + operator: errorPresent, + } + } + return &stringSliceCondition{ + key: key, + operator: operator, + value: parsedValue, + } + default: + return &stringSliceCondition{ + key: fmt.Sprintf("incompatible value %v for []string", value), + operator: errorPresent, + } + } + +} + +func (c *stringSliceCondition) complies(f Fetcher) bool { + comp, ok := f.GetString(c.key) + if !ok { + return false + } + + switch c.operator { + case In: + return utils.StringInSlice(c.value, comp) + default: + return false + } +} + +func (c *stringSliceCondition) check() error { + if c.operator == errorPresent { + return fmt.Errorf("could not parse \"%s\" to []string", c.key) + } + return nil +} diff --git a/database/query/condition.go b/database/query/condition.go new file mode 100644 index 0000000..85ae28b --- /dev/null +++ b/database/query/condition.go @@ -0,0 +1,67 @@ +package query + +import "fmt" + +// Condition is an interface to provide a common api to all condition types. +type Condition interface { + complies(f Fetcher) bool + check() error + // string() string +} + +// Operators +const ( + Equals uint8 = iota // int + GreaterThan // int + GreaterThanOrEqual // int + LessThan // int + LessThanOrEqual // int + FloatEquals // float + FloatGreaterThan // float + FloatGreaterThanOrEqual // float + FloatLessThan // float + FloatLessThanOrEqual // float + SameAs // string + Contains // string + StartsWith // string + EndsWith // string + In // stringSlice + Matches // regex + Is // bool: accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE + Exists // any + + errorPresent uint8 = 255 +) + +// Where returns a condition to add to a query. +func Where(key string, operator uint8, value interface{}) Condition { + switch operator { + case Equals, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual: + return newIntCondition(key, operator, value) + case FloatEquals, + FloatGreaterThan, + FloatGreaterThanOrEqual, + FloatLessThan, + FloatLessThanOrEqual: + return newFloatCondition(key, operator, value) + case SameAs, + Contains, + StartsWith, + EndsWith: + return newStringCondition(key, operator, value) + case In: + return newStringSliceCondition(key, operator, value) + case Matches: + return newRegexCondition(key, operator, value) + case Is: + return newBoolCondition(key, operator, value) + case Exists: + return newExistsCondition(key, operator) + default: + return newErrorCondition(fmt.Errorf("no operator with ID %d", operator)) + } +} diff --git a/database/query/fetcher.go b/database/query/fetcher.go new file mode 100644 index 0000000..c2004bd --- /dev/null +++ b/database/query/fetcher.go @@ -0,0 +1,78 @@ +package query + +import ( + "github.com/tidwall/gjson" +) + +const ( + emptyString = "" +) + +// Fetcher provides an interface to supply the query matcher a method to retrieve values from an object. +type Fetcher interface { + GetString(key string) (value string, ok bool) + GetInt(key string) (value int64, ok bool) + GetFloat(key string) (value float64, ok bool) + GetBool(key string) (value bool, ok bool) + Exists(key string) bool +} + +// JSONFetcher is a json string with get functions. +type JSONFetcher struct { + json string +} + +// NewJSONFetcher adds the Fetcher interface to a JSON string. +func NewJSONFetcher(json string) *JSONFetcher { + return &JSONFetcher{ + json: json, + } +} + +// GetString returns the string found by the given json key and whether it could be successfully extracted. +func (jf *JSONFetcher) GetString(key string) (value string, ok bool) { + result := gjson.Get(jf.json, key) + if !result.Exists() || result.Type != gjson.String { + return emptyString, false + } + return result.String(), true +} + +// GetInt returns the int found by the given json key and whether it could be successfully extracted. +func (jf *JSONFetcher) GetInt(key string) (value int64, ok bool) { + result := gjson.Get(jf.json, key) + if !result.Exists() || result.Type != gjson.Number { + return 0, false + } + return result.Int(), true +} + +// GetFloat returns the float found by the given json key and whether it could be successfully extracted. +func (jf *JSONFetcher) GetFloat(key string) (value float64, ok bool) { + result := gjson.Get(jf.json, key) + if !result.Exists() || result.Type != gjson.Number { + return 0, false + } + return result.Float(), true +} + +// GetBool returns the bool found by the given json key and whether it could be successfully extracted. +func (jf *JSONFetcher) GetBool(key string) (value bool, ok bool) { + result := gjson.Get(jf.json, key) + switch { + case !result.Exists(): + return false, false + case result.Type == gjson.True: + return true, true + case result.Type == gjson.False: + return false, true + default: + return false, false + } +} + +// Exists returns the whether the given key exists. +func (jf *JSONFetcher) Exists(key string) bool { + result := gjson.Get(jf.json, key) + return result.Exists() +} diff --git a/database/query/parser.go b/database/query/parser.go new file mode 100644 index 0000000..3fcf1ae --- /dev/null +++ b/database/query/parser.go @@ -0,0 +1,44 @@ +package query + +var ( + operatorNames = map[string]uint8{ + "==": Equals, + ">": GreaterThan, + ">=": GreaterThanOrEqual, + "<": LessThan, + "<=": LessThanOrEqual, + "f==": FloatEquals, + "f>": FloatGreaterThan, + "f>=": FloatGreaterThanOrEqual, + "f<": FloatLessThan, + "f<=": FloatLessThanOrEqual, + "sameas": SameAs, + "s==": SameAs, + "contains": Contains, + "co": Contains, + "startswith": StartsWith, + "sw": StartsWith, + "endswith": EndsWith, + "ew": EndsWith, + "in": In, + "matches": Matches, + "re": Matches, + "is": Is, + "exists": Exists, + "ex": Exists, + } +) + +func getOpName(operator uint8) string { + for opName, op := range operatorNames { + if op == operator { + return opName + } + } + return "[unknown]" +} + +// ParseQuery parses a plaintext query. +func ParseQuery(query string) (*Query, error) { + return nil, nil +} diff --git a/database/query/query.go b/database/query/query.go new file mode 100644 index 0000000..8924ac7 --- /dev/null +++ b/database/query/query.go @@ -0,0 +1,98 @@ +package query + +import ( + "fmt" + "regexp" +) + +// Example: +// q.New("core:/", +// q.Where("a", q.GreaterThan, 0), +// q.Where("b", q.Equals, 0), +// q.Or( +// q.Where("c", q.StartsWith, "x"), +// q.Where("d", q.Contains, "y") +// ) +// ) + +var ( + prefixExpr = regexp.MustCompile("^[a-z-]+:") +) + +// Query contains a compiled query. +type Query struct { + prefix string + conditions []Condition +} + +// New creates a new query. +func New(prefix string, conditions ...Condition) (*Query, error) { + // check prefix + if !prefixExpr.MatchString(prefix) { + return nil, fmt.Errorf("invalid prefix: %s", prefix) + } + + // check conditions + var err error + for _, cond := range conditions { + err = cond.check() + if err != nil { + return nil, err + } + } + + // return query + return &Query{ + prefix: prefix, + conditions: conditions, + }, nil +} + +// MustCompile creates a new query and panics on an error. +func MustCompile(prefix string, conditions ...Condition) *Query { + q, err := New(prefix, conditions...) + if err != nil { + panic(err) + } + return q +} + +// Prepend prepends (check first) new query conditions to the query. +func (q *Query) Prepend(conditions ...Condition) error { + // check conditions + var err error + for _, cond := range conditions { + err = cond.check() + if err != nil { + return err + } + } + + q.conditions = append(conditions, q.conditions...) + return nil +} + +// Append appends (check last) new query conditions to the query. +func (q *Query) Append(conditions ...Condition) error { + // check conditions + var err error + for _, cond := range conditions { + err = cond.check() + if err != nil { + return err + } + } + + q.conditions = append(q.conditions, conditions...) + return nil +} + +// Matches checks whether the query matches the supplied data object. +func (q *Query) Matches(f Fetcher) bool { + for _, cond := range q.conditions { + if !cond.complies(f) { + return false + } + } + return true +} diff --git a/database/query/query_test.go b/database/query/query_test.go new file mode 100644 index 0000000..a97fe2d --- /dev/null +++ b/database/query/query_test.go @@ -0,0 +1,104 @@ +package query + +import ( + "testing" +) + +var ( + // copied from https://github.com/tidwall/gjson/blob/master/gjson_test.go + testJson = `{"age":100, "name":{"here":"B\\\"R"}, + "noop":{"what is a wren?":"a bird"}, + "happy":true,"immortal":false, + "items":[1,2,3,{"tags":[1,2,3],"points":[[1,2],[3,4]]},4,5,6,7], + "arr":["1",2,"3",{"hello":"world"},"4",5], + "vals":[1,2,3,{"sadf":sdf"asdf"}],"name":{"first":"tom","last":null}, + "created":"2014-05-16T08:28:06.989Z", + "loggy":{ + "programmers": [ + { + "firstName": "Brett", + "lastName": "McLaughlin", + "email": "aaaa", + "tag": "good" + }, + { + "firstName": "Jason", + "lastName": "Hunter", + "email": "bbbb", + "tag": "bad" + }, + { + "firstName": "Elliotte", + "lastName": "Harold", + "email": "cccc", + "tag":, "good" + }, + { + "firstName": 1002.3, + "age": 101 + } + ] + }, + "lastly":{"yay":"final"}, + "temperature": 120.413 +}` +) + +func testQuery(t *testing.T, f Fetcher, shouldMatch bool, condition Condition) { + q := MustCompile("test:") + err := q.Append(condition) + if err != nil { + t.Errorf("append failed: %s", err) + } + + matched := q.Matches(f) + switch { + case matched && !shouldMatch: + t.Errorf("query should match") + case !matched && shouldMatch: + t.Errorf("query should not match") + } +} + +func TestQuery(t *testing.T) { + + // if !gjson.Valid(testJson) { + // t.Fatal("test json is invalid") + // } + f := NewJSONFetcher(testJson) + + testQuery(t, f, true, Where("age", Equals, 100)) + testQuery(t, f, true, Where("age", GreaterThan, uint8(99))) + testQuery(t, f, true, Where("age", GreaterThanOrEqual, 99)) + testQuery(t, f, true, Where("age", GreaterThanOrEqual, 100)) + testQuery(t, f, true, Where("age", LessThan, 101)) + testQuery(t, f, true, Where("age", LessThanOrEqual, "101")) + testQuery(t, f, true, Where("age", LessThanOrEqual, 100)) + + testQuery(t, f, true, Where("temperature", FloatEquals, 120.413)) + testQuery(t, f, true, Where("temperature", FloatGreaterThan, 120)) + testQuery(t, f, true, Where("temperature", FloatGreaterThanOrEqual, 120)) + testQuery(t, f, true, Where("temperature", FloatGreaterThanOrEqual, 120.413)) + testQuery(t, f, true, Where("temperature", FloatLessThan, 121)) + testQuery(t, f, true, Where("temperature", FloatLessThanOrEqual, "121")) + testQuery(t, f, true, Where("temperature", FloatLessThanOrEqual, "120.413")) + + testQuery(t, f, true, Where("lastly.yay", Matches, "final")) + testQuery(t, f, true, Where("lastly.yay", Contains, "ina")) + testQuery(t, f, true, Where("lastly.yay", StartsWith, "fin")) + testQuery(t, f, true, Where("lastly.yay", EndsWith, "nal")) + testQuery(t, f, true, Where("lastly.yay", In, "draft,final")) + testQuery(t, f, true, Where("lastly.yay", In, "final,draft")) + + testQuery(t, f, true, Where("happy", Is, "true")) + testQuery(t, f, true, Where("happy", Is, "t")) + testQuery(t, f, true, Where("happy", Is, "1")) + testQuery(t, f, true, Not(Where("happy", Is, "false"))) + testQuery(t, f, true, Not(Where("happy", Is, "f"))) + testQuery(t, f, true, Not(Where("happy", Is, "0"))) + + testQuery(t, f, true, Where("happy", Exists, nil)) + + testQuery(t, f, true, Where("created", Matches, "^2014-[0-9]{2}-[0-9]{2}T")) + +}