diff --git a/QueryableCache.cfc b/QueryableCache.cfc index 9cd8b7f..ac023f5 100644 --- a/QueryableCache.cfc +++ b/QueryableCache.cfc @@ -1,4 +1,4 @@ -component accessors = "true" extends = "lib.sql.QueryableCache" { +component accessors = "true" extends = "lib.sql.QueryableCache" implements = "lib.sql.IWritable" { property name = "importBatchSize" type = "numeric" default = "1000"; property name = "language" type = "string" default = "english"; @@ -29,7 +29,7 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { local.schema = createObject("java", "io.redisearch.Schema"); for(local.field in getQueryable().getFieldList()) { - if(getQueryable().fieldIsFilterable(local.field)) { + if(getQueryable().fieldIsFilterable(local.field) || local.field == getIdentifierField()) { if(isRedisNumeric(local.field)) { local.schema.addSortableNumericField(local.field); } else { @@ -38,7 +38,8 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { } } - local.indexOptions = createObject("java", "io.redisearch.client.Client$IndexOptions").init(0); + local.indexOptions = createObject("java", "io.redisearch.client.Client$IndexOptions"); + local.indexOptions.init(local.indexOptions.KEEP_FIELD_FLAGS + local.indexOptions.USE_TERM_OFFSETS); if(len(variables.stopWords) == 0) { local.indexOptions.setNoStopwords(); @@ -49,14 +50,51 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { local.indexOptions.setStopwords(listToArray(variables.stopWords, " ")); } + try { + local.indexDefinition = createObject("java", "io.redisearch.client.IndexDefinition") + .setPrefixes([ getName() ]); + + local.indexOptions.setDefinition(local.indexDefinition); + } catch(Object e) { + // IndexDefinition isn't included in the version of JRediSearch being used + } + getClient().createIndex( local.schema, local.indexOptions ); } + lib.sql.DeleteStatement function delete() { + return new lib.sql.DeleteStatement(this); + } + void function dropIndex() { - getClient().dropIndex(); + getClient().dropIndex(true); + } + + void function executeDelete(required lib.sql.DeleteStatement deleteStatement) { + if(len(arguments.deleteStatement.getWhere()) == 0) { + dropIndex(true); + createIndex(); + } else { + do { + local.targetRecords = this.select(getIdentifierField()).where(arguments.deleteStatement.getWhere()).execute(); + local.keys = []; + + for(local.row in local.targetRecords) { + arrayAppend(local.keys, getRowKey(argumentCollection = local.row)); + } + + if(arrayLen(local.keys)) { + getClient().deleteDocuments(true, local.keys); + } + } while(local.targetRecords.recordCount > 0); + } + } + + void function executeInsert(required lib.sql.InsertStatement insertStatement) { + throw(type = "lib.redis.UnsupportedOperationException", message = "Insert is not supported in this implementation"); } query function executeSelect(required lib.sql.SelectStatement selectStatement, required numeric limit, required numeric offset) { @@ -141,11 +179,11 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { switch(local.criteria[local.i].operator) { case "LIKE": - // strip out stop words and format the value + // fuzzy operate our normalized values local.value = listReduce( local.value, function(result, item) { - if(!listFindNoCase(variables.stopWords, arguments.item, " ") && len(arguments.item) >= getMinPrefixLength()) { + if(len(arguments.item) >= getMinPrefixLength()) { arguments.result = listAppend(arguments.result, clause & arguments.item & "*", " "); } @@ -167,20 +205,21 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { break; case "IN": case "NOT IN": - local.clause = listReduce( - local.value, - function(result, item) { - if(find(" ", arguments.item)) { - arguments.item = '"' & arguments.item & '"'; - } - - return listAppend(arguments.result, arguments.item, "|"); - }, - "", - chr(31) - ); - - local.clause = "(" & local.clause & ")"; + local.clause = local.clause + & "(" + & listReduce( + local.value, + function(result, item) { + if(find(" ", arguments.item)) { + arguments.item = '"' & arguments.item & '"'; + } + + return listAppend(arguments.result, arguments.item, "|"); + }, + "", + chr(31) + ) + & ")"; if(local.criteria[local.i].operator == "NOT IN") { local.clause = "(-" & local.clause & ")"; @@ -196,12 +235,10 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { } // replace our placeholders w/ the cache-friendly values - local.queryString = replace(local.queryString, local.criteria[local.i].statement, local.clause, "one"); + local.queryString = replace(local.queryString, local.criteria[local.i].statement, "(" & local.clause & ")", "one"); } } - local.docIDs = []; - if(!structKeyExists(local, "searchException")) { try { local.queryString = local.queryString @@ -209,15 +246,15 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { .replace(" OR ", " | ", "all") .trim(); - // why aggregation? redisearch only supports multiple sorts via the `aggregate` command - local.ab = createObject("java", "io.redisearch.aggregation.AggregationBuilder") - .init("'" & local.queryString & "'") - .apply((isRedisNumeric(getIdentifierField()) ? "@" : "@_NFD_") & getIdentifierField(), "id"); + if(arrayLen(arguments.selectStatement.getOrderCriteria()) > 1) { + // why aggregation? redisearch only supports multiple sorts via the `aggregate` command + local.ab = createObject("java", "io.redisearch.aggregation.AggregationBuilder") + .init("'" & local.queryString & "'") + .apply(variables.aggregateApply, "id"); - // sort - local.sortedField = createObject("java", "io.redisearch.aggregation.SortedField"); + // sort + local.sortedField = createObject("java", "io.redisearch.aggregation.SortedField"); - if(arrayLen(arguments.selectStatement.getOrderCriteria()) > 0) { local.sortFields = arrayReduce( arguments.selectStatement.getOrderCriteria(), function(result, item) { @@ -241,56 +278,80 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { }, [] ); - } else { - local.sortFields = [ local.sortedField.asc("@id") ]; - } - local.maxResults = getMaxResults(); + local.maxResults = getMaxResults(); - if(arguments.limit > 0) { - if(arguments.offset > 0) { - local.maxResults = arguments.offset + arguments.limit; - } else { - local.maxResults = arguments.limit; + if(arguments.limit > 0) { + if(arguments.offset > 0) { + local.maxResults = arguments.offset + arguments.limit; + } else { + local.maxResults = arguments.limit; + } } - } - local.ab.sortBy(javaCast("int", local.maxResults), local.sortFields); + local.ab.sortBy(javaCast("int", local.maxResults), local.sortFields); - // pagination - // look familiar? - this is the same logic as above... just not good way to re-use the condition, as the syntax must be assembled in order - if(arguments.limit > 0) { - if(arguments.offset > 0) { - local.ab.limit(arguments.offset, arguments.limit); - } else { - local.ab.limit(arguments.limit); + // pagination + // look familiar? - this is the same logic as above... just not good way to re-use the condition, as the syntax must be assembled in order + if(arguments.limit > 0) { + if(arguments.offset > 0) { + local.ab.limit(arguments.offset, arguments.limit); + } else { + local.ab.limit(arguments.limit); + } } - } - // convert the command to a string for debugging purposes - if(structKeyExists(arguments, "debug") && arguments.debug == "query") { - local.byteArray = [ createObject("java", "java.lang.String").init(javaCast("string", "")).getBytes() ]; - local.ab.serializeRedisArgs(local.byteArray); - local.string = arrayReduce( - local.byteArray, - function(result, item) { - return listAppend(arguments.result, charsetEncode(arguments.item, "UTF-8"), " "); - }, - "" - ); + // convert the command to a string for debugging purposes + if(structKeyExists(arguments, "debug") && arguments.debug == "query") { + local.byteArray = [ createObject("java", "java.lang.String").init(javaCast("string", "")).getBytes() ]; + local.ab.serializeRedisArgs(local.byteArray); + local.string = arrayReduce( + local.byteArray, + function(result, item) { + return listAppend(arguments.result, charsetEncode(arguments.item, "UTF-8"), " "); + }, + "" + ); + + throw(type = "lib.redis.DebugException", message = "ft.aggregate " & getName() & " " & local.string); + } - throw(type = "lib.redis.DebugException", message = "ft.aggregate " & getName() & " " & local.string); - } + local.docIDs = []; + local.ids = getClient().aggregate(local.ab); + local.resultsLength = local.ids.totalResults; - local.ids = getClient().aggregate(local.ab); - local.resultsLength = local.ids.totalResults; + for(local.i = 0; local.i < local.ids.getResults().size(); local.i++) { + arrayAppend( + local.docIDs, + // GUID/UUID get their dashes stripped during indexing + replace(local.ids.getRow(javaCast("int", local.i)).getString("id"), " ", "", "all") + ); + } - for(local.i = 0; local.i < local.ids.getResults().size(); local.i++) { - arrayAppend( - local.docIDs, - // strip escape characters back out to get the ID - getRowKey(argumentCollection = { "#getIdentifierField()#": replace(local.ids.getRow(javaCast("int", local.i)).getString("id"), "\-", "-", "all") }) - ); + local.documents = getClient().getDocuments(local.docIDs); + } else { + local.q = createObject("java", "io.redisearch.Query") + .init("'" & local.queryString & "'") + .returnFields(listToArray(arguments.selectStatement.getSelect())); + + if(arrayLen(arguments.selectStatement.getOrderCriteria()) == 0) { + local.q.setSortBy(getIdentifierField(), true); + } else { + local.q.setSortBy( + listFirst(arguments.selectStatement.getOrderBy(), " "), + find("ASC", arguments.selectStatement.getOrderBy()) + ); + } + + if(arguments.limit > 0) { + local.q.limit(arguments.offset, arguments.limit); + } else { + local.q.limit(0, getMaxResults()); + } + + local.results = getClient().search(local.q); + local.resultsLength = local.results.totalResults; + local.documents = local.results.docs; } } catch(redis.clients.jedis.exceptions.JedisDataException e) { // nada @@ -326,9 +387,7 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { local.query = queryNew(arguments.selectStatement.getSelect(), local.fieldSQLTypes); - if(arrayLen(local.docIDs) > 0) { - local.documents = getClient().getDocuments(local.docIDs); - + if(structKeyExists(local, "documents") && local.documents.size() > 0) { for(local.i = 0; local.i < local.documents.size(); local.i++) { local.document = local.documents.get(local.i); @@ -346,13 +405,21 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { .setExtendedMetadata({ cached: !structKeyExists(local, "searchException"), engine: "redis", - recordCount: arrayLen(local.docIDs), + recordCount: structKeyExists(local, "documents") ? local.documents.size() : 0, totalRecordCount: (structKeyExists(local, "resultsLength") ? local.resultsLength : 0) }); return local.query; } + void function executeUpdate(required lib.sql.UpdateStatement updateStatement) { + throw(type = "lib.redis.UnsupportedOperationException", message = "Update is not supported in this implementation"); + } + + void function executeUpsert(required lib.sql.UpsertStatement upsertStatement) { + throw(type = "lib.redis.UnsupportedOperationException", message = "Upsert is not supported in this implementation"); + } + struct function fromRediSearchDocument(required any document, string fieldFilter = "") { if(arguments.fieldFilter == "") { arguments.fieldFilter = getFieldList(); @@ -363,12 +430,19 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { arguments.fieldFilter, function(result, item) { if(document.hasProperty(arguments.item)) { - local.value = document.getString(arguments.item); + local.value = document.get(arguments.item); + if(!structKeyExists(local, "value")) { + local.value = ""; + } if(isRedisDate(arguments.item) && isNumeric(val(local.value))) { arguments.result[arguments.item] = createObject("java", "java.util.Date").init(javaCast("long", val(local.value))); } else if(isRedisNumeric(arguments.item) && isNumeric(val(local.value))) { - arguments.result[arguments.item] = val(local.value); + if(listFindNoCase("bigint,bit,integer,smallint,tinyint", getQueryable().getFieldSQLType(arguments.item))) { + arguments.result[arguments.item] = val(local.value); + } else { + arguments.result[arguments.item] = javaCast("double", local.value); + } } else if(len(local.value) > 0) { arguments.result[arguments.item] = local.value; } @@ -390,6 +464,10 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { return createObject("java", "io.redisearch.client.Client").init(variables.name, variables.connectionPool.getJedisPool()); } + struct function getInfo() { + return getClient().getInfo(); + } + any function getRow() { local.document = getClient().getDocument(getRowKey(argumentCollection = arguments)); @@ -406,6 +484,10 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { } } + lib.sql.InsertStatement function insert(required struct fields) { + throw(type = "lib.redis.UnsupportedOperationException", message = "Insert is not supported in this implementation"); + } + boolean function isRedisDate(required string field) { switch(getQueryable().getFieldSQLType(arguments.field)) { case "date": @@ -446,15 +528,29 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { // https://oss.redislabs.com/redisearch/Escaping.html arguments.input = listReduce( - arguments.input, - function(result, item) { - return listAppend(arguments.result, arguments.item.REReplace("\W+", " ", "all").trim(), chr(31)); + variables.normalizer.normalize(javaCast("string", arguments.input), variables.normalizerForm).replaceAll("\p{InCombiningDiacriticalMarks}+", ""), + function(outerResult, outerItem) { + arguments.outerItem = listReduce( + trim(REReplace(arguments.outerItem, "\W+", " ", "all")), + function(innerResult, innerItem) { + // these values have all been lCase'd at this point + if(!listFind(variables.stopWords, arguments.innerItem, " ")) { + arguments.innerResult = listAppend(arguments.innerResult, arguments.innerItem, " "); + } + + return arguments.innerResult; + }, + "", + " " + ); + + return listAppend(arguments.outerResult, arguments.outerItem, chr(31)); }, "", chr(31) ); - return variables.normalizer.normalize(javaCast("string", arguments.input), variables.normalizerForm).replaceAll("\p{InCombiningDiacriticalMarks}+", ""); + return trim(arguments.input); } void function putRow(required struct row) { @@ -462,7 +558,7 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { toRediSearchDocument(arguments.row), createObject("java", "io.redisearch.client.AddOptions") .setLanguage(getLanguage()) - .setReplacementPolicy(createObject("java", "io.redisearch.client.AddOptions$ReplacementPolicy").PARTIAL) + .setReplacementPolicy(createObject("java", "io.redisearch.client.AddOptions$ReplacementPolicy").FULL) ); } @@ -470,14 +566,14 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { getClient().deleteDocument(javaCast("string", getRowKey(argumentCollection = arguments)), true); } - void function seedFromQueryable(boolean overwrite = false) { + void function seedFromQueryable(boolean overwrite = false, string where = "") { local.documents = []; local.replacementPolicy = createObject("java", "io.redisearch.client.AddOptions$ReplacementPolicy"); local.addOptions = createObject("java", "io.redisearch.client.AddOptions") .setLanguage(getLanguage()) .setReplacementPolicy(arguments.overwrite ? local.replacementPolicy.FULL : local.replacementPolicy.NONE); - for(local.row in getQueryable().select().execute()) { + for(local.row in getQueryable().select().where(arguments.where).execute()) { arrayAppend(local.documents, toRediSearchDocument(local.row)); if(arrayLen(local.documents) == getImportBatchSize()) { @@ -512,6 +608,25 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { return this; } + lib.sql.QueryableCache function setRowKeyMask(required string rowKeyMask) { + super.setRowKeyMask(argumentCollection = arguments); + + local.fl = getFieldList(); + + // parse the rowKeyMask to determine what properties we need + variables.aggregateApply = arrayReduce( + getKeyFields(), + function(result, field) { + return listAppend(replace(arguments.result, "{#lCase(arguments.field)#}", "%s"), (isRedisNumeric(arguments.field) ? "@" : "@_NFD_") & listGetAt(fl, listFindNoCase(fl, arguments.field))); + }, + "'#getRowKeyMask()#'" + ); + + variables.aggregateApply = "format(#variables.aggregateApply#)"; + + return this; + } + lib.sql.QueryableCache function setStopWords(required string stopWords) { if(structKeyExists(variables, "queryable")) { throw(type = "lib.redis.QueryableDefinedException", message = "an IQueryable has been furnished already"); @@ -549,7 +664,7 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { } else { local.document.set(local.field, javaCast("string", arguments.row[local.field])); - if(getQueryable().fieldIsFilterable(local.field)) { + if(getQueryable().fieldIsFilterable(local.field) || local.field == getIdentifierField()) { local.document.set("_NFD_" & local.field, javaCast("string", normalize(arguments.row[local.field]))); } } @@ -558,4 +673,12 @@ component accessors = "true" extends = "lib.sql.QueryableCache" { return local.document; } + lib.sql.UpdateStatement function update(required struct fields) { + throw(type = "lib.redis.UnsupportedOperationException", message = "Update is not supported in this implementation"); + } + + lib.sql.UpsertStatement function upsert(required struct fields) { + throw(type = "lib.redis.UnsupportedOperationException", message = "Upsert is not supported in this implementation"); + } + } \ No newline at end of file diff --git a/tests/TestQueryableCache.cfc b/tests/TestQueryableCache.cfc index 9a16063..06c0d1f 100644 --- a/tests/TestQueryableCache.cfc +++ b/tests/TestQueryableCache.cfc @@ -13,8 +13,8 @@ component extends = "mxunit.framework.TestCase" { variables.cache = new lib.redis.QueryableCache(connectionPool = variables.connectionPool, name = "mxunit:indexed"); variables.query = queryNew( - "id, createdTimestamp, createdDate, createdTime, foo, bar, letter", - "integer, timestamp, date, time, varchar, bit, varchar" + "id, createdTimestamp, createdDate, createdTime, foo, bar, letter, floatie", + "integer, timestamp, date, time, varchar, bit, varchar, double" ); queryAddRow( @@ -27,7 +27,8 @@ component extends = "mxunit.framework.TestCase" { "createdTimestamp": parseDateTime("2019-12-12T00:00:00.123Z"), "createdDate": createDate(2019, 12, 12), "createdTime": createTime(0, 0, 0), - "letter": "A" + "letter": "A", + "floatie": 10000000.00 }, { "id": 5002, @@ -36,7 +37,8 @@ component extends = "mxunit.framework.TestCase" { "createdTimestamp": parseDateTime("2019-12-25T00:00:00.001Z"), "createdDate": createDate(2019, 12, 25), "createdTime": createTime(0, 0, 0), - "letter": "B" + "letter": "B", + "floatie": 1.00 }, { "id": 5003, @@ -45,7 +47,8 @@ component extends = "mxunit.framework.TestCase" { "createdTimestamp": parseDateTime("2019-12-31T00:00:00.000Z"), "createdDate": createDate(2019, 12, 31), "createdTime": createTime(0, 0, 0), - "letter": "C" + "letter": "C", + "floatie": -1.300001 } ] ); @@ -57,11 +60,12 @@ component extends = "mxunit.framework.TestCase" { { "id": 5000 + local.i, "bar": (!randRange(1, 3) % 2 ? local.i % 2 : javaCast("null", "")), - "foo": (local.i == 500 ? "Šťŕĭńġ" : local.i == 501 ? "the rain in spain" : createUUID()), + "foo": (local.i == 500 ? "Šťŕĭńġ" : local.i == 501 ? "the rain in spain" : local.i == 502 ? "6k" : createUUID()), "createdTimestamp": (!randRange(1, 3) % 2 ? dateAdd("s", -(local.i), variables.now) : javaCast("null", "")), "createdDate": (!randRange(1, 3) % 2 ? variables.now : javaCast("null", "")), "createdTime": (!randRange(1, 3) % 2 ? variables.now : javaCast("null", "")), - "letter": chr(64 + randRange(1, 26) + (local.i % 2 ? 32 : 0)) + "letter": chr(64 + randRange(1, 26) + (local.i % 2 ? 32 : 0)), + "floatie": 0.01 } ); } @@ -76,8 +80,6 @@ component extends = "mxunit.framework.TestCase" { function test_0_createIndex() { // debug(variables.exception); return; - // indexes are prefixed w/ idx: - local.queryableCache = new lib.redis.Cache(variables.connectionPool, "idx"); try{ // create the index @@ -86,7 +88,9 @@ component extends = "mxunit.framework.TestCase" { // index exists already, do nothing } - assertTrue(local.queryableCache.containsKey("mxunit:indexed")); + local.info = variables.cache.getInfo(); +// debug(local.info); + assertEquals("mxunit:indexed", local.info["index_name"]); } function test_0_seedFromQueryable() { @@ -98,6 +102,22 @@ component extends = "mxunit.framework.TestCase" { assertFalse(variables.cache.containsRow(id = "boo-boo-butt")); } + function test_delete() { + assertTrue(variables.cache.select().where("id > 5970 AND id < 5980").execute().recordCount > 0); + variables.cache.delete().where("id > 5970 AND id < 5980").execute(); + local.result = variables.cache.select().where("id > 5970 AND id < 5980").execute(); + assertEquals(0, local.result.recordCount); + } + + function test_delete_all() { + assertNotEquals(0, variables.cache.select().execute().recordCount); + variables.cache.delete().execute(); + assertEquals(0, variables.cache.select().execute().recordCount); + + // re-add all the things + variables.cache.seedFromQueryable(); + } + function test_fieldExists() { assertTrue(variables.cache.fieldExists("id")); assertFalse(variables.cache.fieldExists("asdfasfasdf")); @@ -127,6 +147,16 @@ component extends = "mxunit.framework.TestCase" { assertEquals("mxunit:indexed:id:5001", variables.cache.getRowKey(id = 5001)); } + function test_insert() { + try { + variables.cache.insert({ id: 10001, foo: "f00" }).execute(); + } catch(lib.redis.UnsupportedOperationException e) { + return; + } + + fail("exception not thrown"); + } + function test_putRow_getRow() { local.compareDate = createDateTime(1948, 12, 10, 10, 0, 0); local.queryRow = queryGetRow(variables.query, 1); @@ -146,6 +176,15 @@ component extends = "mxunit.framework.TestCase" { assertEquals("", local.compare.foo); } + function test_putRow_getRow_DDMAINT_27043() { + local.queryRow = queryGetRow(variables.query, 1); + local.queryRow.createdTimestamp = ""; + variables.cache.putRow(local.queryRow); + local.compare = variables.cache.getRow(argumentCollection = local.queryRow); + + assertEquals("", local.compare.createdTimestamp); + } + function test_removeRow() { local.queryRow = queryGetRow(variables.query, 1); local.queryRow.foo = createUUID(); @@ -156,7 +195,7 @@ component extends = "mxunit.framework.TestCase" { assertFalse(variables.cache.containsRow(id = local.queryRow.id)); } - function test_seedFromQueryable_overwrite() { + function test_seedFromQueryable_overwrite_where() { variables.cache.seedFromQueryable(); local.row = queryGetRow(variables.query, 2); @@ -166,7 +205,7 @@ component extends = "mxunit.framework.TestCase" { // debug(local.row); - variables.cache.seedFromQueryable(overwrite = true); + variables.cache.seedFromQueryable(overwrite = true, where = "id = #local.row.id#"); local.overwriteElement = variables.cache.getRow(id = local.row.id); @@ -299,6 +338,42 @@ component extends = "mxunit.framework.TestCase" { debug(local.result); } + + function test_select_where_DDMAINT_27088_KEEP_FIELD_FLAGS() { + local.where = "letter = 'Šťŕĭńġ'"; + local.result = variables.cache.select().where(local.where).execute(); + + assertEquals(0, local.result.recordCount); + + debug(local.where); + debug(local.result); + } + + function test_select_where_DDMAINT_27088_KEEP_FIELD_FLAGS_2() { + local.where = "foo IN (6k)"; + local.result = variables.cache.select().where(local.where).execute(); + + assertEquals(1, local.result.recordCount); + + debug(local.where); + debug(local.result); + } + + function test_select_where_DDMAINT_27088_USE_TERM_OFFSETS() { + local.where = "foo = 'the rain in spain'"; + local.result = variables.cache.select().where(local.where).execute(); + + assertEquals(1, local.result.recordCount); + + debug(local.where); + debug(local.result); + + local.where = "foo = 'the spain in rain'"; + local.result = variables.cache.select().where(local.where).execute(); + + assertEquals(0, local.result.recordCount); + } + function test_select_where_in() { local.result = variables.cache.select("id, foo").where("foo IN ('#variables.query.foo[1]#', '#variables.query.foo[2]#', '#variables.query.foo[3]#') OR id IN (5005, 5010, 5015)").execute(); @@ -366,4 +441,24 @@ component extends = "mxunit.framework.TestCase" { assertEquals(local.row, local.rowFromDocument); } + function test_update() { + try { + variables.cache.update({ id: 10001, foo: "f00" }).execute(); + } catch(lib.redis.UnsupportedOperationException e) { + return; + } + + fail("exception not thrown"); + } + + function test_upsert() { + try { + variables.cache.upsert({ id: 10001, foo: "f00" }).execute(); + } catch(lib.redis.UnsupportedOperationException e) { + return; + } + + fail("exception not thrown"); + } + } \ No newline at end of file