def test_match(self):

        parser = QueryLanguage()

        #------------------------------------------------------------------------------------------------------
        # Check that when field is outside range (less than lower bound), match() returns false
        #------------------------------------------------------------------------------------------------------

        field = 'voltage'
        lower_bound = 5
        upper_bound = 10
        instrument = 'instrument_1'
        search_string1 = "SEARCH '%s' VALUES FROM %s TO %s FROM '%s'"\
        % (field, lower_bound, upper_bound, instrument)
        query = parser.parse(search_string1)

        event = ExampleDetectableEvent('TestEvent', voltage=4)
        self.assertFalse(QueryLanguage.match(event, query['query']))

        #------------------------------------------------------------------------------------------------------
        # Check that when field is outside range (is higher than upper bound), match() returns false
        #------------------------------------------------------------------------------------------------------

        event = ExampleDetectableEvent('TestEvent', voltage=11)
        self.assertFalse(QueryLanguage.match(event, query['query']))

        #------------------------------------------------------------------------------------------------------
        # Check that when field is inside range, match() returns true
        #------------------------------------------------------------------------------------------------------

        event = ExampleDetectableEvent('TestEvent', voltage=6)
        self.assertTrue(QueryLanguage.match(event, query['query']))

        #------------------------------------------------------------------------------------------------------
        # Check that when field is exactly of the value mentioned in a query, match() returns true
        #------------------------------------------------------------------------------------------------------

        value = 15
        search_string2 = "search '%s' is '%s' from '%s'" % (field, value, instrument)
        query = parser.parse(search_string2)

        event = ExampleDetectableEvent('TestEvent', voltage=15)
        self.assertTrue(QueryLanguage.match(event, query['query']))

        #------------------------------------------------------------------------------------------------------
        # Check that when value is not exactly what is mentioned in a query, match() returns false
        #------------------------------------------------------------------------------------------------------

        event = ExampleDetectableEvent('TestEvent', voltage=14)
        self.assertFalse(QueryLanguage.match(event, query['query']))
Exemplo n.º 2
0
    def setUp(self):
        super(QueryLanguageUnitTest,self).setUp()

        self.parser = QueryLanguage()
Exemplo n.º 3
0
class QueryLanguageUnitTest(PyonTestCase):
    def setUp(self):
        super(QueryLanguageUnitTest,self).setUp()

        self.parser = QueryLanguage()

    def test_basic_search(self):
        test_string = 'SEARCH "model" IS "high" FROM "instrument.model"'
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.value == 'high')
        self.assertTrue(struct.query.index == 'instrument.model')
        self.assertTrue(struct.query.field == 'model')

    def test_single_quote_search(self):
        test_string = "SEARCH 'model' IS 'high' FROM 'instrument.model'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.value == 'high')
        self.assertTrue(struct.query.index == 'instrument.model')
        self.assertTrue(struct.query.field == 'model')

    def test_bad_search(self):
        with self.assertRaises(BadRequest):
            self.parser.parse('BAD STRING')

    def test_caseless_literal(self):
        test_string = 'search "cache" is "empty" from "bins"'
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.value == 'empty')
        self.assertTrue(struct.query.field == 'cache')
        self.assertTrue(struct.query.index == 'bins')

    def test_range_query(self):
        test_string = 'SEARCH "model" VALUES FROM 1.14 TO 1024 FROM "instrument.model"'

        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query['range']['from'] == 1.14)
        self.assertTrue(struct.query['range']['to'] == 1024, '%s\n%s' % (self.parser.tokens, struct))

    def test_belongs_to(self):
        test_string = 'BELONGS TO "resourceID"'
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.association == 'resourceID')

    def test_simple_compound(self):
        test_string = "search 'instrument.model' is 'throwable' from 'indexID' and search 'model' values from 3.1415 to 69 from 'weapons'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.value == 'throwable')
        self.assertTrue(struct.query.field == 'instrument.model')
        self.assertTrue(struct.query.index == 'indexID')
        self.assertTrue(struct['and'][0]['range']['from']== 3.1415)
        self.assertTrue(struct['and'][0]['range']['to'] == 69)

    def test_association_search(self):
        test_string = "search 'instrument.model' is 'throwable' from 'indexID' and belongs to 'rsn'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.field == 'instrument.model')
        self.assertTrue(struct.query.value == 'throwable')
        self.assertTrue(struct.query.index == 'indexID')
        self.assertTrue(struct['and'][0]['association'] == 'rsn')

    def test_union_search(self):
        test_string = "search 'instrument.family' is 'submersible' from 'devices' or search 'model' is 'abc1*' from 'instruments'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.field == 'instrument.family')
        self.assertTrue(struct.query.value == 'submersible')
        self.assertTrue(struct.query.index == 'devices')
        self.assertTrue(struct['or'][0].field == 'model')
        self.assertTrue(struct['or'][0].value == 'abc1*')
        self.assertTrue(struct['or'][0].index == 'instruments')

    def test_collection_search(self):
        test_string = "in 'collectionID'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.collection == 'collectionID')

    def test_order_by(self):
        test_string = "search 'field' is 'value' from 'index' order by 'blah' limit 2"
        test_reverse_string = "search 'field' is 'value' from 'index' limit 2 order by 'blah'"

        retval = self.parser.parse(test_string)

        struct = DotDict(retval)
        self.assertTrue(struct.query.field == 'field')
        self.assertTrue(struct.query.value == 'value')
        self.assertTrue(struct.query.index == 'index')
        self.assertTrue(struct.order == {'blah' : 'asc'})
        self.assertTrue(struct.limit == 2)

        retval = self.parser.parse(test_reverse_string)

        struct = DotDict(retval)
        self.assertTrue(struct.query.field == 'field')
        self.assertTrue(struct.query.value == 'value')
        self.assertTrue(struct.query.index == 'index')
        self.assertTrue(struct.order == {'blah' : 'asc'})
        self.assertTrue(struct.limit == 2)

    def test_offset(self):
        test_string = "search 'field' is 'value' from 'index' skip 3"
        retval = self.parser.parse(test_string)
        self.assertTrue(retval == {'and':[], 'or':[], 'query':{'field':'field', 'index':'index', 'value':'value'}, 'offset':3})

    def test_geo_distance(self):
        test_string = "search 'location' geo distance 20 km from lat 20 lon 30.0 from 'index'"
        retval = self.parser.parse(test_string)
        self.assertTrue(retval == {'and':[], 'or':[], 'query':{'lat':20.0, 'lon':30.0, 'units':'km', 'field':'location', 'index':'index', 'dist':20.0}}, '%s' % retval)

    def test_geo_bbox(self):
        test_string = "search 'location' geo box top-left lat 40 lon 0 bottom-right lat 0 lon 40 from 'index'"
        retval = self.parser.parse(test_string)
        self.assertTrue(retval == {'and':[], 'or':[], 'query':{'field':'location', 'top_left':[0.0, 40.0], 'bottom_right': [40.0, 0.0], 'index':'index'}})

    def  test_time_search(self):
        test_string = "search 'ts_timestamp' time from '2012-01-01' to '2012-02-01' from 'index'"
        retval = self.parser.parse(test_string)
        self.assertTrue(retval == {'and':[], 'or':[], 'query':{'field':'ts_timestamp', 'time':{'from':'2012-01-01', 'to':'2012-02-01'}, 'index':'index'}})

    def test_open_range(self):
        test_string = 'SEARCH "model" VALUES FROM 1.14 FROM "instrument.model"'

        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query['range']['from'] == 1.14)
    
    def  test_open_time_search(self):
        test_string = "search 'ts_timestamp' time from '2012-01-01' from 'index'"
        retval = self.parser.parse(test_string)
        self.assertTrue(retval == {'and':[], 'or':[], 'query':{'field':'ts_timestamp', 'time':{'from':'2012-01-01'}, 'index':'index'}})

    def test_extensive(self):

        cases = [
            ( "SEARCH 'model' IS 'abc*' FROM 'models' AND BELONGS TO 'platformDeviceID'", 
                {'and':[{'association':'platformDeviceID'}], 'or':[], 'query':{'field':'model', 'value':'abc*', 'index':'models'}}),
            ( "SEARCH 'model' IS 'sbc*' FROM 'devices' AND BELONGS TO 'platformDeviceID' ORDER BY 'name' LIMIT 30", 
                {'and':[{'association':'platformDeviceID'}],'or':[],'query':{'field':'model', 'value':'sbc*', 'index':'devices'}, 'order':{'name':'asc'}, 'limit':30}),
            ( "SEARCH 'runtime' VALUES FROM 1. TO 100 FROM 'devices' AND BELONGS TO 'RSN'",
                {'and':[{'association':'RSN'}], 'or': [], 'query':{'field':'runtime', 'range':{'from':1, 'to':100}, 'index':'devices'}}),
            ( "BELONGS TO 'org'",
                {'and':[], 'or':[], 'query':{'association':'org'}}),

        ]


        for case in cases:
            retval = self.parser.parse(case[0])
            self.assertTrue(retval == case[1], 'Expected %s, received %s' % (case[1], retval))

    def test_fuzzy_search(self):
        test_string = "search 'description' like 'products' from 'index'"
        retval = self.parser.parse(test_string)
        self.assertEquals(retval, {'and':[], 'or':[], 'query':{'field':'description', 'fuzzy':'products', 'index':'index'}})
    
    def test_match_search(self):
        test_string = "search 'description' match 'products' from 'index'"
        retval = self.parser.parse(test_string)
        self.assertEquals(retval, {'and':[], 'or':[], 'query':{'field':'description', 'match':'products', 'index':'index'}})

    def test_owner_search(self):
        test_string = "search 'description' like 'products' from 'index' and has 'abc123'"
        retval = self.parser.parse(test_string)
        self.assertEquals(retval, {'and':[{'owner':'abc123'}], 'or':[], 'query':{'field':'description', 'fuzzy':'products', 'index':'index'}})

    def test_time_bounds_search(self):
        test_string = "search 'nominal_datetime' timebounds from '2012-01-01' to '2013-04-04' from 'index'"
        retval = self.parser.parse(test_string)
        self.assertEquals(retval, {'and':[], 'or':[], 'query':{'field':'nominal_datetime', 'time_bounds':{'from':'2012-01-01', 'to':'2013-04-04'}, 'index':'index'}})

    def test_vertical_bounds_search(self):
        test_string = "search 'geospatial_bounds' vertical from 0.5 to 10.2 from 'index'"
        retval = self.parser.parse(test_string)
        self.assertEquals(retval, {'and':[], 'or':[], 'query':{'field':'geospatial_bounds', 'vertical_bounds':{'from':0.5, 'to':10.2}, 'index':'index'}})
Exemplo n.º 4
0
    def setUp(self):
        super(QueryLanguageUnitTest, self).setUp()

        self.parser = QueryLanguage()
Exemplo n.º 5
0
class QueryLanguageUnitTest(PyonTestCase):
    def setUp(self):
        super(QueryLanguageUnitTest, self).setUp()

        self.parser = QueryLanguage()

    def test_basic_search(self):
        test_string = 'SEARCH "model" IS "high" FROM "instrument.model"'
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.value == 'high')
        self.assertTrue(struct.query.index == 'instrument.model')
        self.assertTrue(struct.query.field == 'model')

    def test_single_quote_search(self):
        test_string = "SEARCH 'model' IS 'high' FROM 'instrument.model'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.value == 'high')
        self.assertTrue(struct.query.index == 'instrument.model')
        self.assertTrue(struct.query.field == 'model')

    def test_bad_search(self):
        with self.assertRaises(BadRequest):
            self.parser.parse('BAD STRING')

    def test_caseless_literal(self):
        test_string = 'search "cache" is "empty" from "bins"'
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.value == 'empty')
        self.assertTrue(struct.query.field == 'cache')
        self.assertTrue(struct.query.index == 'bins')

    def test_range_query(self):
        test_string = 'SEARCH "model" VALUES FROM 1.14 TO 1024 FROM "instrument.model"'

        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query['range']['from'] == 1.14)
        self.assertTrue(struct.query['range']['to'] == 1024,
                        '%s\n%s' % (self.parser.tokens, struct))

    def test_belongs_to(self):
        test_string = 'BELONGS TO "resourceID"'
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.association == 'resourceID')

    def test_simple_compound(self):
        test_string = "search 'instrument.model' is 'throwable' from 'indexID' and search 'model' values from 3.1415 to 69 from 'weapons'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.value == 'throwable')
        self.assertTrue(struct.query.field == 'instrument.model')
        self.assertTrue(struct.query.index == 'indexID')
        self.assertTrue(struct['and'][0]['range']['from'] == 3.1415)
        self.assertTrue(struct['and'][0]['range']['to'] == 69)

    def test_association_search(self):
        test_string = "search 'instrument.model' is 'throwable' from 'indexID' and belongs to 'rsn'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.field == 'instrument.model')
        self.assertTrue(struct.query.value == 'throwable')
        self.assertTrue(struct.query.index == 'indexID')
        print struct
        self.assertTrue(struct['and'][0]['association'] == 'rsn')

    def test_union_search(self):
        test_string = "search 'instrument.family' is 'submersible' from 'devices' or search 'model' is 'abc1*' from 'instruments'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.field == 'instrument.family')
        self.assertTrue(struct.query.value == 'submersible')
        self.assertTrue(struct.query.index == 'devices')
        self.assertTrue(struct['or'][0].field == 'model')
        self.assertTrue(struct['or'][0].value == 'abc1*')
        self.assertTrue(struct['or'][0].index == 'instruments')

    def test_collection_search(self):
        test_string = "in 'collectionID'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.collection == 'collectionID')

    def test_order_by(self):
        test_string = "search 'field' is 'value' from 'index' order by 'blah' limit 2"
        test_reverse_string = "search 'field' is 'value' from 'index' limit 2 order by 'blah'"

        retval = self.parser.parse(test_string)

        struct = DotDict(retval)
        self.assertTrue(struct.query.field == 'field')
        self.assertTrue(struct.query.value == 'value')
        self.assertTrue(struct.query.index == 'index')
        self.assertTrue(struct.query.order == {'blah': 'asc'})
        self.assertTrue(struct.query.limit == 2)

        retval = self.parser.parse(test_reverse_string)

        struct = DotDict(retval)
        self.assertTrue(struct.query.field == 'field')
        self.assertTrue(struct.query.value == 'value')
        self.assertTrue(struct.query.index == 'index')
        self.assertTrue(struct.query.order == {'blah': 'asc'})
        self.assertTrue(struct.query.limit == 2)

    def test_offset(self):
        test_string = "search 'field' is 'value' from 'index' skip 3"
        retval = self.parser.parse(test_string)
        self.assertTrue(
            retval == {
                'and': [],
                'or': [],
                'query': {
                    'field': 'field',
                    'index': 'index',
                    'value': 'value',
                    'offset': 3
                }
            })

    def test_geo_distance(self):
        test_string = "search 'location' geo distance 20 km from lat 20 lon 30.0 from 'index'"
        retval = self.parser.parse(test_string)
        self.assertTrue(
            retval == {
                'and': [],
                'or': [],
                'query': {
                    'lat': 20.0,
                    'lon': 30.0,
                    'units': 'km',
                    'field': 'location',
                    'index': 'index',
                    'dist': 20.0
                }
            }, '%s' % retval)

    def test_geo_bbox(self):
        test_string = "search 'location' geo box top-left lat 40 lon 0 bottom-right lat 0 lon 40 from 'index'"
        retval = self.parser.parse(test_string)
        self.assertTrue(
            retval == {
                'and': [],
                'or': [],
                'query': {
                    'field': 'location',
                    'top_left': [0.0, 40.0],
                    'bottom_right': [40.0, 0.0],
                    'index': 'index'
                }
            })

    def test_extensive(self):

        cases = [
            ("SEARCH 'model' IS 'abc*' FROM 'models' AND BELONGS TO 'platformDeviceID'",
             {
                 'and': [{
                     'association': 'platformDeviceID'
                 }],
                 'or': [],
                 'query': {
                     'field': 'model',
                     'value': 'abc*',
                     'index': 'models'
                 }
             }),
            ("SEARCH 'model' IS 'sbc*' FROM 'devices' ORDER BY 'name' LIMIT 30 AND BELONGS TO 'platformDeviceID'",
             {
                 'and': [{
                     'association': 'platformDeviceID'
                 }],
                 'or': [],
                 'query': {
                     'field': 'model',
                     'value': 'sbc*',
                     'index': 'devices',
                     'order': {
                         'name': 'asc'
                     },
                     'limit': 30
                 }
             }),
            ("SEARCH 'runtime' VALUES FROM 1. TO 100 FROM 'devices' AND BELONGS TO 'RSN'",
             {
                 'and': [{
                     'association': 'RSN'
                 }],
                 'or': [],
                 'query': {
                     'field': 'runtime',
                     'range': {
                         'from': 1,
                         'to': 100
                     },
                     'index': 'devices'
                 }
             }),
            ("BELONGS TO 'org'", {
                'and': [],
                'or': [],
                'query': {
                    'association': 'org'
                }
            }),
        ]

        for case in cases:
            retval = self.parser.parse(case[0])
            self.assertTrue(retval == case[1],
                            'Expected %s, received %s' % (case[1], retval))
    def test_evaluate_condition(self):

        parser = QueryLanguage()

        #------------------------------------------------------------------------------------------------------
        # Set up the search strings for different queries:
        # These include main query, a list of or queries and a list of and queries
        #------------------------------------------------------------------------------------------------------

        field = 'voltage'
        instrument = 'instrument'

        #------------------------------------------------------------------------------------------------------
        # main query
        #------------------------------------------------------------------------------------------------------

        lower_bound = 5
        upper_bound = 10
        search_string1 = "SEARCH '%s' VALUES FROM %s TO %s FROM '%s'"\
        % (field, lower_bound, upper_bound, instrument)

        #------------------------------------------------------------------------------------------------------
        # or queries
        #------------------------------------------------------------------------------------------------------

        value = 15
        search_string2 = "or search '%s' is '%s' from '%s'" % (field, value, instrument)

        value = 17
        search_string3 = "or search '%s' is '%s' from '%s'" % (field, value, instrument)

        lower_bound = 20
        upper_bound = 30
        search_string4 = "or SEARCH '%s' VALUES FROM %s TO %s FROM '%s'"\
        % (field, lower_bound, upper_bound, instrument)

        #------------------------------------------------------------------------------------------------------
        # and queries
        #------------------------------------------------------------------------------------------------------

        lower_bound = 5
        upper_bound = 6
        search_string5 = "and SEARCH '%s' VALUES FROM %s TO %s FROM '%s'"\
        % (field, lower_bound, upper_bound, instrument)

        lower_bound = 6
        upper_bound = 7
        search_string6 = "and SEARCH '%s' VALUES FROM %s TO %s FROM '%s'"\
        % (field, lower_bound, upper_bound, instrument)

        #------------------------------------------------------------------------------------------------------
        # Construct queries by parsing different search strings and test the evaluate_condition()
        # for each such complex query
        #------------------------------------------------------------------------------------------------------
        search_string = search_string1+search_string2+search_string3+search_string4+search_string5+search_string6
        query = parser.parse(search_string)

        # the main query as well as the 'and' queries pass for this case
        event = ExampleDetectableEvent('TestEvent', voltage=6)
        self.assertTrue(QueryLanguage.evaluate_condition(event, query))

        # check true conditions. If any one of the 'or' conditions passes, evaluate_condition()
        # will return True
        event = ExampleDetectableEvent('TestEvent', voltage=15)
        self.assertTrue(QueryLanguage.evaluate_condition(event, query))

        event = ExampleDetectableEvent('TestEvent', voltage=17)
        self.assertTrue(QueryLanguage.evaluate_condition(event, query))

        event = ExampleDetectableEvent('TestEvent', voltage=25)
        self.assertTrue(QueryLanguage.evaluate_condition(event, query))

        # check fail conditions arising from the 'and' condition (happens if any one of the 'and' conditions fail)
        # note: the 'and' queries are attached to the main query
        event = ExampleDetectableEvent('TestEvent', voltage=5)
        self.assertFalse(QueryLanguage.evaluate_condition(event, query))

        event = ExampleDetectableEvent('TestEvent', voltage=7)
        self.assertFalse(QueryLanguage.evaluate_condition(event, query))

        event = ExampleDetectableEvent('TestEvent', voltage=9)
        self.assertFalse(QueryLanguage.evaluate_condition(event, query))
Exemplo n.º 7
0
class QueryLanguageUnitTest(PyonTestCase):
    def setUp(self):
        super(QueryLanguageUnitTest, self).setUp()

        self.parser = QueryLanguage()

    def test_basic_search(self):
        test_string = 'SEARCH "model" IS "high" FROM "instrument.model"'
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.value == "high")
        self.assertTrue(struct.query.index == "instrument.model")
        self.assertTrue(struct.query.field == "model")

    def test_single_quote_search(self):
        test_string = "SEARCH 'model' IS 'high' FROM 'instrument.model'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.value == "high")
        self.assertTrue(struct.query.index == "instrument.model")
        self.assertTrue(struct.query.field == "model")

    def test_bad_search(self):
        with self.assertRaises(BadRequest):
            self.parser.parse("BAD STRING")

    def test_caseless_literal(self):
        test_string = 'search "cache" is "empty" from "bins"'
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.value == "empty")
        self.assertTrue(struct.query.field == "cache")
        self.assertTrue(struct.query.index == "bins")

    def test_range_query(self):
        test_string = 'SEARCH "model" VALUES FROM 1.14 TO 1024 FROM "instrument.model"'

        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query["range"]["from"] == 1.14)
        self.assertTrue(struct.query["range"]["to"] == 1024, "%s\n%s" % (self.parser.tokens, struct))

    def test_belongs_to(self):
        test_string = 'BELONGS TO "resourceID"'
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.association == "resourceID")

    def test_simple_compound(self):
        test_string = "search 'instrument.model' is 'throwable' from 'indexID' and search 'model' values from 3.1415 to 69 from 'weapons'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.value == "throwable")
        self.assertTrue(struct.query.field == "instrument.model")
        self.assertTrue(struct.query.index == "indexID")
        self.assertTrue(struct["and"][0]["range"]["from"] == 3.1415)
        self.assertTrue(struct["and"][0]["range"]["to"] == 69)

    def test_association_search(self):
        test_string = "search 'instrument.model' is 'throwable' from 'indexID' and belongs to 'rsn'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.field == "instrument.model")
        self.assertTrue(struct.query.value == "throwable")
        self.assertTrue(struct.query.index == "indexID")
        self.assertTrue(struct["and"][0]["association"] == "rsn")

    def test_union_search(self):
        test_string = (
            "search 'instrument.family' is 'submersible' from 'devices' or search 'model' is 'abc1*' from 'instruments'"
        )
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.field == "instrument.family")
        self.assertTrue(struct.query.value == "submersible")
        self.assertTrue(struct.query.index == "devices")
        self.assertTrue(struct["or"][0].field == "model")
        self.assertTrue(struct["or"][0].value == "abc1*")
        self.assertTrue(struct["or"][0].index == "instruments")

    def test_collection_search(self):
        test_string = "in 'collectionID'"
        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query.collection == "collectionID")

    def test_order_by(self):
        test_string = "search 'field' is 'value' from 'index' order by 'blah' limit 2"
        test_reverse_string = "search 'field' is 'value' from 'index' limit 2 order by 'blah'"

        retval = self.parser.parse(test_string)

        struct = DotDict(retval)
        self.assertTrue(struct.query.field == "field")
        self.assertTrue(struct.query.value == "value")
        self.assertTrue(struct.query.index == "index")
        self.assertTrue(struct.query.order == {"blah": "asc"})
        self.assertTrue(struct.query.limit == 2)

        retval = self.parser.parse(test_reverse_string)

        struct = DotDict(retval)
        self.assertTrue(struct.query.field == "field")
        self.assertTrue(struct.query.value == "value")
        self.assertTrue(struct.query.index == "index")
        self.assertTrue(struct.query.order == {"blah": "asc"})
        self.assertTrue(struct.query.limit == 2)

    def test_offset(self):
        test_string = "search 'field' is 'value' from 'index' skip 3"
        retval = self.parser.parse(test_string)
        self.assertTrue(
            retval
            == {"and": [], "or": [], "query": {"field": "field", "index": "index", "value": "value", "offset": 3}}
        )

    def test_geo_distance(self):
        test_string = "search 'location' geo distance 20 km from lat 20 lon 30.0 from 'index'"
        retval = self.parser.parse(test_string)
        self.assertTrue(
            retval
            == {
                "and": [],
                "or": [],
                "query": {"lat": 20.0, "lon": 30.0, "units": "km", "field": "location", "index": "index", "dist": 20.0},
            },
            "%s" % retval,
        )

    def test_geo_bbox(self):
        test_string = "search 'location' geo box top-left lat 40 lon 0 bottom-right lat 0 lon 40 from 'index'"
        retval = self.parser.parse(test_string)
        self.assertTrue(
            retval
            == {
                "and": [],
                "or": [],
                "query": {"field": "location", "top_left": [0.0, 40.0], "bottom_right": [40.0, 0.0], "index": "index"},
            }
        )

    def test_time_search(self):
        test_string = "search 'ts_timestamp' time from '2012-01-01' to '2012-02-01' from 'index'"
        retval = self.parser.parse(test_string)
        self.assertTrue(
            retval
            == {
                "and": [],
                "or": [],
                "query": {
                    "field": "ts_timestamp",
                    "time": {"from": "2012-01-01", "to": "2012-02-01"},
                    "index": "index",
                },
            }
        )

    def test_open_range(self):
        test_string = 'SEARCH "model" VALUES FROM 1.14 FROM "instrument.model"'

        retval = self.parser.parse(test_string)
        struct = DotDict(retval)
        self.assertTrue(struct.query["range"]["from"] == 1.14)

    def test_open_time_search(self):
        test_string = "search 'ts_timestamp' time from '2012-01-01' from 'index'"
        retval = self.parser.parse(test_string)
        self.assertTrue(
            retval
            == {
                "and": [],
                "or": [],
                "query": {"field": "ts_timestamp", "time": {"from": "2012-01-01"}, "index": "index"},
            }
        )

    def test_extensive(self):

        cases = [
            (
                "SEARCH 'model' IS 'abc*' FROM 'models' AND BELONGS TO 'platformDeviceID'",
                {
                    "and": [{"association": "platformDeviceID"}],
                    "or": [],
                    "query": {"field": "model", "value": "abc*", "index": "models"},
                },
            ),
            (
                "SEARCH 'model' IS 'sbc*' FROM 'devices' ORDER BY 'name' LIMIT 30 AND BELONGS TO 'platformDeviceID'",
                {
                    "and": [{"association": "platformDeviceID"}],
                    "or": [],
                    "query": {
                        "field": "model",
                        "value": "sbc*",
                        "index": "devices",
                        "order": {"name": "asc"},
                        "limit": 30,
                    },
                },
            ),
            (
                "SEARCH 'runtime' VALUES FROM 1. TO 100 FROM 'devices' AND BELONGS TO 'RSN'",
                {
                    "and": [{"association": "RSN"}],
                    "or": [],
                    "query": {"field": "runtime", "range": {"from": 1, "to": 100}, "index": "devices"},
                },
            ),
            ("BELONGS TO 'org'", {"and": [], "or": [], "query": {"association": "org"}}),
        ]

        for case in cases:
            retval = self.parser.parse(case[0])
            self.assertTrue(retval == case[1], "Expected %s, received %s" % (case[1], retval))

    def test_fuzzy_search(self):
        test_string = "search 'description' like 'products' from 'index'"
        retval = self.parser.parse(test_string)
        self.assertEquals(
            retval, {"and": [], "or": [], "query": {"field": "description", "fuzzy": "products", "index": "index"}}
        )

    def test_match_search(self):
        test_string = "search 'description' match 'products' from 'index'"
        retval = self.parser.parse(test_string)
        self.assertEquals(
            retval, {"and": [], "or": [], "query": {"field": "description", "match": "products", "index": "index"}}
        )

    def test_owner_search(self):
        test_string = "search 'description' like 'products' from 'index' and has 'abc123'"
        retval = self.parser.parse(test_string)
        self.assertEquals(
            retval,
            {
                "and": [{"owner": "abc123"}],
                "or": [],
                "query": {"field": "description", "fuzzy": "products", "index": "index"},
            },
        )

    def test_time_bounds_search(self):
        test_string = "search 'nominal_datetime' timebounds from '2012-01-01' to '2013-04-04' from 'index'"
        retval = self.parser.parse(test_string)
        self.assertEquals(
            retval,
            {
                "and": [],
                "or": [],
                "query": {
                    "field": "nominal_datetime",
                    "time_bounds": {"from": "2012-01-01", "to": "2013-04-04"},
                    "index": "index",
                },
            },
        )

    def test_vertical_bounds_search(self):
        test_string = "search 'geospatial_bounds' vertical from 0.5 to 10.2 from 'index'"
        retval = self.parser.parse(test_string)
        self.assertEquals(
            retval,
            {
                "and": [],
                "or": [],
                "query": {"field": "geospatial_bounds", "vertical_bounds": {"from": 0.5, "to": 10.2}, "index": "index"},
            },
        )