Skip to content

Commit

Permalink
CBL-6651: createIndex doesn't support creating partial index with N1Q…
Browse files Browse the repository at this point in the history
…L language

Extended IndexSpec to allow the where clause when the query langugage is N1QL. We put it in IndexSpec::options. In this commit, we only do it for ValueIndex and FTSIndex.

With JSON query language, the where clause can be either in the new option or in IndexSpec::expression together with the what clause. With N1QL query language, the where clause has to be in the new option, and IndexSpec::expression contains only the what clause.

This commit is to get ready for C4IndexOptions
  • Loading branch information
jianminzhao committed Jan 22, 2025
1 parent c5ec167 commit 099d822
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 16 deletions.
35 changes: 30 additions & 5 deletions LiteCore/Query/IndexSpec.cc
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ namespace litecore {
switch ( queryLanguage ) {
case QueryLanguage::kJSON:
try {
_doc = Doc::fromJSON(expression);
if ( const char* optWhere = optionWhere() ) {
std::stringstream ss;
ss << R"({"WHAT": )" << expression.asString() << R"(, "WHERE": )" << optWhere << "}";
_doc = Doc::fromJSON(ss.str());
} else {
_doc = Doc::fromJSON(expression);
}
} catch ( const FleeceException& ) {
error::_throw(error::InvalidQuery, "Invalid JSON in index expression");
}
Expand All @@ -63,13 +69,23 @@ namespace litecore {
int errPos;
alloc_slice json;
if ( !expression.empty() ) {
FLMutableDict result = n1ql::parse(string(expression), &errPos);
MutableDict* result = nullptr;
bool hasWhere = false;
if ( const char* optWhere = optionWhere() ) {
hasWhere = true;
std::stringstream ss;
ss << "SELECT " << expression.asString() << " FROM _ WHERE " << optWhere;
result = (MutableDict*)n1ql::parse(ss.str(), &errPos);
} else {
result = (MutableDict*)n1ql::parse(expression.asString(), &errPos);
}
if ( !result ) { throw Query::parseError("N1QL syntax error in index expression", errPos); }
json = ((MutableDict*)result)->toJSON(true);
FLMutableDict_Release(result);
if ( hasWhere ) result->remove("FROM"_sl);
json = result->toJSON(true);
FLMutableDict_Release((FLMutableDict)result);
} else {
// n1ql parser won't compile empty string to empty array. Do it manually.
json = "[]";
json = "[]"; // empty WHAT cannot followed by WHERE clause.
}
_doc = Doc::fromJSON(json);
} catch ( const std::runtime_error& ) {
Expand Down Expand Up @@ -155,4 +171,13 @@ namespace litecore {
return _unnestDoc;
}

const char* IndexSpec::optionWhere() const {
if ( const FTSOptions* optFTS = ftsOptions() ) {
return optFTS->where;
} else if ( const ValueOptions* optValue = valueOptions() ) {
return optValue->where;
} else
return nullptr;
}

} // namespace litecore
11 changes: 10 additions & 1 deletion LiteCore/Query/IndexSpec.hh
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ namespace litecore {
bool ignoreDiacritics{}; ///< True to strip diacritical marks/accents from letters
bool disableStemming{}; ///< Disables stemming
const char* stopWords{}; ///< NULL for default, or comma-delimited string, or empty
const char* where;
};

struct ValueOptions {
const char* where;
};

/// Options for an ArrayIndex
Expand All @@ -60,7 +65,7 @@ namespace litecore {
static constexpr vectorsearch::SQEncoding DefaultEncoding{8};

/// Index options. If not empty (the first state), must match the index type.
using Options = std::variant<std::monostate, FTSOptions, VectorOptions, ArrayOptions>;
using Options = std::variant<std::monostate, FTSOptions, VectorOptions, ArrayOptions, ValueOptions>;

/// Constructs an index spec.
/// @param name_ Name of the index (must be unique in its collection.)
Expand Down Expand Up @@ -89,6 +94,8 @@ namespace litecore {

const ArrayOptions* arrayOptions() const { return std::get_if<ArrayOptions>(&options); }

const ValueOptions* valueOptions() const { return std::get_if<ValueOptions>(&options); }

/** The required WHAT clause: the list of expressions to index */
const fleece::impl::Array* NONNULL what() const;

Expand All @@ -108,6 +115,8 @@ namespace litecore {
fleece::impl::Doc* doc() const;
fleece::impl::Doc* unnestDoc() const;

const char* optionWhere() const;

mutable Retained<fleece::impl::Doc> _doc;
mutable Retained<fleece::impl::Doc> _unnestDoc;
};
Expand Down
23 changes: 21 additions & 2 deletions LiteCore/tests/FTSTest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,27 @@ TEST_CASE_METHOD(FTSTest, "Query Full-Text Stop-words In Target", "[Query][FTS]"
TEST_CASE_METHOD(FTSTest, "Query Full-Text Partial Index", "[Query][FTS]") {
// the WHERE clause prevents row 4 from being indexed/searched.
IndexSpec::FTSOptions options{"english", true};
store->createIndex("sentence", R"-({"WHAT": [[".sentence"]], "WHERE": [">", ["length()", [".sentence"]], 70]})-",
IndexSpec::kFullText, options);
switch ( GENERATE(0, 1, 2) ) {
case 0:
logSection("JSON Index Spec with combinged \"what\" and \"where\"");
REQUIRE(store->createIndex(
"sentence", R"-({"WHAT": [[".sentence"]], "WHERE": [">", ["length()", [".sentence"]], 70]})-",
IndexSpec::kFullText, options));
break;
case 1:
logSection("JSON Index Spec with option \"where\"");
options.where = R"-([">", ["length()", [".sentence"]], 70])-";
REQUIRE(store->createIndex("sentence", R"-([[".sentence"]])-", IndexSpec::kFullText, options));
break;
case 2:
logSection("N1QL Index Spec");
options.where = "length(sentence) > 70";
REQUIRE(store->createIndex("sentence", "sentence", QueryLanguage::kN1QL, IndexSpec::kFullText, options));
break;
default:
break;
}

testQuery("['SELECT', {'WHERE': ['MATCH()', 'sentence', 'search'],\
ORDER_BY: [['DESC', ['rank()', 'sentence']]],\
WHAT: [['.sentence']]}]",
Expand Down
7 changes: 7 additions & 0 deletions LiteCore/tests/LiteCoreTest.hh
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,11 @@ class DataFileTestFixture
alloc_slice blobAccessor(const fleece::impl::Dict*) const override;

string _databaseName{"db"};

protected:
static void logSection(const string& name, int level = 0) {
size_t numSpaces = 8 + level * 4;
std::string spaces(numSpaces, ' ');
fprintf(stderr, "%s--- %s\n", spaces.c_str(), name.c_str());
}
};
21 changes: 19 additions & 2 deletions LiteCore/tests/QueryTest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -562,14 +562,31 @@ TEST_CASE_METHOD(QueryTest, "Create Partial Index", "[Query]") {
addNumberedDocs(1, 100);
addArrayDocs(101, 100);

store->createIndex("nums"_sl, R"({"WHAT":[[".num"]], "WHERE":["=",[".type"],"number"]})"_sl);
switch ( GENERATE(0, 1, 2) ) {
case 0:
logSection("JSON Index Spec with combinged \"what\" and \"where\"");
REQUIRE(store->createIndex("nums"_sl, R"({"WHAT":[[".num"]], "WHERE":["=",[".type"],"number"]})"_sl));
break;
case 1:
logSection("JSON Index Spec with option \"where\"");
REQUIRE(store->createIndex("nums"_sl, R"([[".num"]])", QueryLanguage::kJSON, IndexSpec::kValue,
IndexSpec::ValueOptions{R"(["=",[".type"],"number"])"}));
break;
case 2:
logSection("N1QL Index Spec");
REQUIRE(store->createIndex("nums"_sl, "num", QueryLanguage::kN1QL, IndexSpec::kValue,
IndexSpec::ValueOptions{"type = 'number'"}));
break;
default:
return;
}

auto [queryJson, expectOptimized] =
GENERATE(pair<const char*, bool>{"['AND', ['=', ['.type'], 'number'], "
"['>=', ['.', 'num'], 30], ['<=', ['.', 'num'], 40]]",
true},
pair<const char*, bool>{"['AND', ['>=', ['.', 'num'], 30], ['<=', ['.', 'num'], 40]]", false});
logSection(string("Query: ") + queryJson);
logSection(string("Query: ") + queryJson + (expectOptimized ? ", " : ", not ") + "optimized");
Retained<Query> query = store->compileQuery(json5(queryJson));
checkOptimized(query, expectOptimized);

Expand Down
6 changes: 0 additions & 6 deletions LiteCore/tests/QueryTest.hh
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,6 @@ class QueryTest : public DataFileTestFixture {
}
}

static void logSection(const string& name, int level = 0) {
size_t numSpaces = 8 + level * 4;
std::string spaces(numSpaces, ' ');
fprintf(stderr, "%s--- %s\n", spaces.c_str(), name.c_str());
}

static string numberString(int n) {
static const char* kDigit[10] = {"zero", "one", "two", "three", "four",
"five", "six", "seven", "eight", "nine"};
Expand Down

0 comments on commit 099d822

Please sign in to comment.