Skip to content

Commit

Permalink
CBL-6156: Support Inner Unnest Query in JSON
Browse files Browse the repository at this point in the history
Added tests for multiple indexes, multi-leveled index, index with N1QL expression.
  • Loading branch information
jianminzhao committed Sep 5, 2024
1 parent 163b832 commit cff1e8a
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 11 deletions.
56 changes: 56 additions & 0 deletions C/tests/c4QueryTest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,22 @@ N_WAY_TEST_CASE_METHOD(C4QueryTest, "C4Query UNNEST", "[Query][C]") {
auto defaultColl = getCollection(db, kC4DefaultCollectionSpec);
REQUIRE(c4coll_createIndex(defaultColl, C4STR("likes"), C4STR("[[\".likes\"]]"), kC4JSONQuery,
kC4ArrayIndex, nullptr, nullptr));
REQUIRE(c4coll_createIndex(defaultColl, C4STR("phone"), C4STR("contact.phone"), kC4N1QLQuery, kC4ArrayIndex,
nullptr, nullptr));
}

// Two UNNESTs for two array properties.
compileSelect(json5("{WHAT: ['.person._id', '.phone'],\
FROM: [{as: 'person'}, \
{as: 'like', unnest: ['.person.likes']},\
{as: 'phone', unnest: ['.person.contact.phone']}],\
WHERE: ['=', ['.like'], 'climbing'],\
ORDER_BY: [['.person.name.first']]}"));
checkExplanation(withIndex);
CHECK(run2()
== (vector<string>{"0000021, 802-4827967", "0000017, 315-7142142", "0000017, 315-0405535",
"0000045, 501-7977106", "0000045, 501-7138486"}));

compileSelect(json5("{WHAT: ['.person._id'],\
FROM: [{as: 'person'}, \
{as: 'like', unnest: ['.person.likes']}],\
Expand Down Expand Up @@ -819,6 +834,8 @@ N_WAY_TEST_CASE_METHOD(NestedQueryTest, "C4Query UNNEST objects", "[Query][C]")
C4Log("-------- Repeating with index --------");
REQUIRE(c4db_createIndex(db, C4STR("shapes"), C4STR("[[\".shapes\"], [\".color\"]]"), kC4ArrayIndex,
nullptr, nullptr));
REQUIRE(c4db_createIndex2(db, C4STR("shapes2"), C4STR("shapes, concat(color, to_string(size))"),
kC4N1QLQuery, kC4ArrayIndex, nullptr, nullptr));
}
compileSelect(json5("{WHAT: ['.shape.color'],\
DISTINCT: true,\
Expand All @@ -840,9 +857,48 @@ N_WAY_TEST_CASE_METHOD(NestedQueryTest, "C4Query UNNEST objects", "[Query][C]")
WHERE: ['=', ['.shape.color'], 'red']}"));
checkExplanation(withIndex);
CHECK(run() == (vector<string>{"11"}));

compileSelect(json5("{WHAT: [['sum()', ['.shape.size']]],\
FROM: [{as: 'doc'}, \
{as: 'shape', unnest: ['.doc.shapes']}],\
WHERE: ['=', ['concat()', ['.shape.color'], ['to_string()',['.shape.size']]], 'red3']}"));
checkExplanation(withIndex);
CHECK(run() == (vector<string>{"3"}));
}
}

N_WAY_TEST_CASE_METHOD(NestedQueryTest, "C4Query Nested UNNEST", "[Query][C]") {
deleteDatabase();
db = c4db_openNamed(kDatabaseName, &dbConfig(), ERROR_INFO());
importJSONLines(sFixturesDir + "students.json");

compileSelect(json5("{WHAT: [['AS', ['.doc.name'], 'college'], ['.student.id'], ['.student.class'], ['.interest']],"
" FROM: [{as: 'doc'},"
" {as: 'student', unnest: ['.doc.students']},"
" {as: 'interest', unnest: ['.student.interests']}]"
"}"));
vector<string> results{
"Univ of Michigan, student_112, 3, violin", "Univ of Michigan, student_112, 3, baseball",
"Univ of Michigan, student_189, 2, violin", "Univ of Michigan, student_189, 2, tennis",
"Univ of Michigan, student_1209, 3, art", "Univ of Michigan, student_1209, 3, writing",
"Univ of Pennsylvania, student_112, 3, piano", "Univ of Pennsylvania, student_112, 3, swimming",
"Univ of Pennsylvania, student_189, 2, violin", "Univ of Pennsylvania, student_189, 2, movies"};

CHECK(run2(nullptr, 4) == results);

deleteDatabase();
db = c4db_openNamed(kDatabaseName, &dbConfig(), ERROR_INFO());
// The only difference from "students.json" is that there is an extra property from student to interests.
importJSONLines(sFixturesDir + "students2.json");

compileSelect(json5("{WHAT: [['AS', ['.doc.name'], 'college'], ['.student.id'], ['.student.class'], ['.interest']],"
" FROM: [{as: 'doc'},"
" {as: 'student', unnest: ['.doc.students']},"
" {as: 'interest', unnest: ['.student.extra.interests']}]"
"}"));
CHECK(run2(nullptr, 4) == results);
}

N_WAY_TEST_CASE_METHOD(C4QueryTest, "C4Query Seek", "[Query][C]") {
compile(json5("['=', ['.', 'contact', 'address', 'state'], 'CA']"));
C4Error error;
Expand Down
22 changes: 14 additions & 8 deletions C/tests/c4QueryTest.hh
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,21 @@ class C4QueryTest : public C4Test {
});
}

// Runs query, returning vector of doc IDs
std::vector<std::string> run2(const char* bindings = nullptr) {
// Runs query, returning vector of rows. Columns are comma separated.
std::vector<std::string> run2(const char* bindings = nullptr, unsigned colnCount = 2) {
REQUIRE(colnCount >= 2);
return runCollecting<std::string>(bindings, [&](C4QueryEnumerator* e) {
REQUIRE(FLArrayIterator_GetCount(&e->columns) >= 2);
fleece::alloc_slice c1 = FLValue_ToString(FLArrayIterator_GetValueAt(&e->columns, 0));
fleece::alloc_slice c2 = FLValue_ToString(FLArrayIterator_GetValueAt(&e->columns, 1));
if ( e->missingColumns & 1 ) c1 = "MISSING"_sl;
if ( e->missingColumns & 2 ) c2 = "MISSING"_sl;
return c1.asString() + ", " + c2.asString();
REQUIRE(FLArrayIterator_GetCount(&e->columns) >= colnCount);
std::string res;
for ( unsigned c = 0; c < colnCount; ++c ) {
if ( c > 0 ) res = res + ", ";
if ( e->missingColumns & (1 << c) ) res += "MISSING";
else {
fleece::alloc_slice c1 = FLValue_ToString(FLArrayIterator_GetValueAt(&e->columns, c));
res += c1.asString();
}
}
return res;
});
}

Expand Down
2 changes: 2 additions & 0 deletions C/tests/data/students.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type":"university","name":"Univ of Michigan","students":[{"id":"student_112","class":"3","order":"1","interests":["violin","baseball"]},{"id":"student_189","class":"2","order":"5","interests":["violin","tennis"]},{"id":"student_1209","class":"3","order":"15","interests":["art","writing"]}]}
{"type":"university","name":"Univ of Pennsylvania","students":[{"id":"student_112","class":"3","order":"1","interests":["piano","swimming"]},{"id":"student_189","class":"2","order":"5","interests":["violin","movies"]}]}
2 changes: 2 additions & 0 deletions C/tests/data/students2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type":"university","name":"Univ of Michigan","students":[{"id":"student_112","class":"3","order":"1","extra":{"interests":["violin","baseball"]}},{"id":"student_189","class":"2","order":"5","extra":{"interests":["violin","tennis"]}},{"id":"student_1209","class":"3","order":"15","extra":{"interests":["art","writing"]}}]}
{"type":"university","name":"Univ of Pennsylvania","students":[{"id":"student_112","class":"3","order":"1","extra":{"interests":["piano","swimming"]}},{"id":"student_189","class":"2","order":"5","extra":{"interests":["violin","movies"]}}]}
13 changes: 10 additions & 3 deletions LiteCore/Query/QueryParser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1767,8 +1767,15 @@ namespace litecore {
if ( !isInColumnList ) { fail("%s", verifyDbAliasError.c_str()); }
}

if ( verifyDbAliasError.empty() && type >= kUnnestVirtualTableAlias ) {
// The alias is to an UNNEST. This needs to be written specially:
bool isNestedUnnest = fn == kEachFnName && type == kUnnestVirtualTableAlias;
// ex. of nested unnest: "FROM: [{as: 'doc'}, {as: 'student', unnest: ['.doc.students']},
// {as: 'interest', unnest: ['.student.interests']}]"
// Target SQL: "FROM kv_default AS doc JOIN fl_each(doc.body, 'students') AS student
// JOIN fl_each(student.value, 'interests') AS interest

if ( !isNestedUnnest && verifyDbAliasError.empty() && type >= kUnnestVirtualTableAlias ) {
// The alias is to an UNNEST, but not inside fl_each. This needs to be written specially:
// The following function will reject any fn's but kValueFnName
writeUnnestPropertyGetter(fn, property, alias, type);
return;
}
Expand Down Expand Up @@ -1844,7 +1851,7 @@ namespace litecore {
if ( property.empty() && fn == kValueFnName ) fn = kRootFnName;

// Write the function call:
_sql << fn << "(" << tablePrefix << _bodyColumnName;
_sql << fn << "(" << tablePrefix << (isNestedUnnest ? "value" : _bodyColumnName);
if ( !property.empty() ) { _sql << ", " << sqlString(string(property)); }
if ( param ) {
_sql << ", ";
Expand Down

0 comments on commit cff1e8a

Please sign in to comment.