diff --git a/C/tests/c4ArrayIndexTest.cc b/C/tests/c4ArrayIndexTest.cc index 60079b143..c525d783e 100644 --- a/C/tests/c4ArrayIndexTest.cc +++ b/C/tests/c4ArrayIndexTest.cc @@ -358,26 +358,30 @@ TEST_CASE_METHOD(ArrayIndexTest, "CRUD Array Index Shared Path", "[C][ArrayIndex bool deleted = c4coll_deleteIndex(coll, "phones"_sl, ERROR_INFO()); REQUIRE(deleted); - // We must re-create the Query objects because we deleted the index - which means the unnest tables may be missing. - cityQuery = c4query_new2( - db, kC4N1QLQuery, - R"(SELECT p.pid, c.address.city, c.address.state FROM profiles AS p UNNEST p.contacts AS c WHERE c.address.state = "CA")"_sl, - nullptr, ERROR_INFO()); - REQUIRE(cityQuery); + // cityQuery is not affected by the deletion of index "phones" + queryenum = REQUIRED(c4query_run(cityQuery, nullslice, nullptr)); + validateQuery(queryenum, { + R"(["p-0001", "San Pedro", "CA"])", + R"(["p-0001", "San Pedro", "CA"])", + }); + + // phoneQuery is affected by the deletion of index "phones" + // Following error will be logged, + // 2024-10-29T21:14:28.226339 DB ERROR SQLite error (code 1): no such table: 152b9815998e188eb99eb1612aafbb3ee6031535 in "SELECT fl_result(fl_value(prof.body, 'pid')), fl_result(fl_unnested_value(c.body, 'address.city')), fl_result(fl_unnested_value(c.body, 'address.state')), fl_result(fl_unnested_value(p.body, 'type')), fl_result(fl_unnested_value(p.body, 'numbers')) FROM "kv_.profiles" AS prof JOIN bc89db8a20fe759bf161b84adf2294d9bfe0c88d AS c ON c.docid=prof.rowid JOIN "152b9815998e188eb99eb1612aafbb3ee6031535" AS p ON p.docid=c.rowid WHERE fl_unnested_value(p.body, 'type') = 'mobile'". This table is referenced by an array index, which may have been deleted. + C4Error error; + queryenum = c4query_run(phoneQuery, nullslice, &error); + CHECK(!queryenum); // This query relies on the index that has been deleted. + CHECK((error.domain == SQLiteDomain && error.code == 1)); + // Recompile the query phoneQuery = c4query_new2( db, kC4N1QLQuery, R"(SELECT prof.pid, c.address.city, c.address.state, p.type, p.numbers FROM profiles AS prof UNNEST prof.contacts AS c UNNEST c.phones AS p WHERE p.type = "mobile")"_sl, nullptr, ERROR_INFO()); REQUIRE(phoneQuery); + queryenum = c4query_run(phoneQuery, nullslice, &error); + CHECK(queryenum); - - queryenum = REQUIRED(c4query_run(cityQuery, nullslice, nullptr)); - validateQuery(queryenum, { - R"(["p-0001", "San Pedro", "CA"])", - R"(["p-0001", "San Pedro", "CA"])", - }); - queryenum = REQUIRED(c4query_run(phoneQuery, nullslice, nullptr)); validateQuery(queryenum, { R"(["p-0001", "San Pedro", "CA", "mobile", ["310-9601308"]])", R"(["p-0001", "San Pedro", "CA", "mobile", ["310-4833623"]])", @@ -654,6 +658,9 @@ TEST_CASE_METHOD(ArrayIndexTest, "Unnest Without Alias", "[C][Unnest]") { // 7. TestUnnestArrayLiteralNotSupport TEST_CASE_METHOD(ArrayIndexTest, "Unnest Array Literal Not Supported", "[C][Unnest]") { + C4Collection* coll = createCollection(db, {"profiles"_sl, "_default"_sl}); + importTestData(coll); + C4Error err{}; c4::ref query = c4query_new2( db, kC4N1QLQuery, diff --git a/C/tests/c4QueryTest.cc b/C/tests/c4QueryTest.cc index ebe3ee4e7..bf4ebb547 100644 --- a/C/tests/c4QueryTest.cc +++ b/C/tests/c4QueryTest.cc @@ -1487,6 +1487,15 @@ TEST_CASE_METHOD(CollectionTest, "C4Query FTS Multiple collections", "[Query][C] CHECK(run().size() == 50); CHECK(runFTS().size() == 50); + + auto deleted = c4coll_deleteIndex(names, C4STR("byStreet"), nullptr); + CHECK(deleted); + // The query won't run after the index is deleted. We should see following error in the log, + // 2024-10-29T20:57:00.896439 DB ERROR SQLite error (code 1): no such table: kv_.namedscope.names::by\Street in "SELECT "namedscope.names".rowid, offsets(fts1."kv_.namedscope.names::by\Street"), offsets(fts2."kv_.wiki::by\Text"), fl_result("namedscope.names".key), fl_result(wiki.key) FROM "kv_.namedscope.names" AS "namedscope.names" INNER JOIN "kv_.wiki" AS wiki ON (fl_value("namedscope.names".body, 'birthday') != fl_value(wiki.body, 'title')) JOIN "kv_.namedscope.names::by\Street" AS fts1 ON fts1.docid = "namedscope.names".rowid JOIN "kv_.wiki::by\Text" AS fts2 ON fts2.docid = wiki.rowid WHERE fts1."kv_.namedscope.names::by\Street" MATCH 'Hwy' AND fts2. This table is referenced by an FTS index, which may have been deleted. + C4Error error; + auto qenum = c4query_run(query, c4str(nullptr), &error); + CHECK(!qenum); + CHECK((error.domain == SQLiteDomain && error.code == 1)); } #pragma mark - OBSERVERS: diff --git a/LiteCore/Storage/SQLiteDataFile.cc b/LiteCore/Storage/SQLiteDataFile.cc index e8fd49a7b..f68417544 100644 --- a/LiteCore/Storage/SQLiteDataFile.cc +++ b/LiteCore/Storage/SQLiteDataFile.cc @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -114,6 +115,28 @@ namespace litecore { void LogStatement(const SQLite::Statement& st) { LogTo(SQL, "... %s", st.getQuery().c_str()); } + static std::pair enhanceSQLiteErrorLog(int errCode, const char* msg) { + std::regex noTableRegx{"no such table: (\\S+) in "}; + std::cmatch match; + const char* extra = nullptr; + if ( std::regex_search(msg, match, noTableRegx) ) { + if ( std::regex_search(match.suffix().str(), std::regex{"fl_unnested_value"}) + && std::regex_match(match[1].str(), std::regex{"[0-9a-z]{40}"}) ) { + extra = "This table is referenced by an array index, which may have been deleted."; + } else if ( std::regex_match(match[1].str(), std::regex{"kv_\\..+::.+"}) ) { + // example target: match[1].str() = "kv_.namedscope.names::by\\Street" + // where "::" = KeyStore::kIndexSeparator + extra = "This table is referenced by an FTS index, which may have been deleted."; + } + } + std::string enhanced; + if ( extra ) { + enhanced = msg; + enhanced += ". "s + extra; + } + return std::make_pair(extra, enhanced); + } + static void sqlite3_log_callback(C4UNUSED void* pArg, int errCode, const char* msg) { switch ( errCode & 0xFF ) { case SQLITE_OK: @@ -132,7 +155,11 @@ namespace litecore { LogWarn(DBLog, "SQLite warning: %s", msg); break; default: - LogError(DBLog, "SQLite error (code %d): %s", errCode, msg); + { + auto [enhanced, enhancedMsg] = enhanceSQLiteErrorLog(errCode, msg); + if ( enhanced ) msg = enhancedMsg.c_str(); + LogError(DBLog, "SQLite error (code %d): %s", errCode, msg); + } break; } }