From 06822ca95559bf76f2d54f8fe9aac1845eec58d5 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 9 Oct 2026 14:05:47 -0700 Subject: [PATCH 1/2] fix(fts): make index lifecycle tolerant to interrupted CREATE/DROP (fixes #77) CREATE_FTS_INDEX / DROP_FTS_INDEX run as sequential statements outside a single transaction, so an interruption left internal tables behind with no in-place recovery (next CREATE failed 'already exists'; a torn DROP could leave states no command accepts; a torn index failed the whole DB open at LOAD). - CREATE self-heals: when the index entry is absent, internal-only residue tables (appears_in/info, docs, terms, per-index stopwords) are dropped first via DROP TABLE IF EXISTS, so a failed CREATE is simply retryable. Genuine user tables with colliding names still raise; the shared default stopwords table is never dropped. The tokenize macro is dropped + recreated so a retry always matches the current config. - DROP is guarded: all drops use IF EXISTS in referential order and now also cover appears_info, the per-index stopwords copy, and the tokenize macro, making an interrupted DROP retryable. - LOAD is tolerant: an index whose internal tables are incomplete is skipped instead of throwing, so the database still opens; repair with DROP_FTS_INDEX + CREATE_FTS_INDEX (queries fail cleanly with 'not loaded yet' until repaired). --- fts/src/function/create_fts_index.cpp | 78 ++++++++++++++++----------- fts/src/function/drop_fts_index.cpp | 24 +++++++-- fts/src/main/fts_extension.cpp | 22 ++++++++ 3 files changed, 90 insertions(+), 34 deletions(-) diff --git a/fts/src/function/create_fts_index.cpp b/fts/src/function/create_fts_index.cpp index e3a1cc91..6bae4cd9 100644 --- a/fts/src/function/create_fts_index.cpp +++ b/fts/src/function/create_fts_index.cpp @@ -63,26 +63,38 @@ static std::vector bindProperties(const catalog::NodeTableCatalog return result; } -static void validateInternalTableNotExist(const std::string& tableName, - const catalog::Catalog& catalog, const transaction::Transaction* transaction) { - if (catalog.containsTable(transaction, tableName)) { - throw BinderException{ - std::format("Table: {} already exists. Please drop or rename the table before " - "creating a full text search index.", - tableName)}; +// Drops internal-only residue left behind by an interrupted CREATE/DROP so a failed +// CREATE can simply be retried in place. The catalog index entry is written last +// during CREATE, so any internal table/macro present without an index entry is +// residue by definition (bind already validated the index itself doesn't exist). +// Genuine user tables with colliding names are never dropped; they still raise. +static std::string dropResidueStatements(ClientContext& context, table_id_t tableID, + const std::string& indexName, const StopWordsTableInfo& stopWordsInfo) { + auto catalog = catalog::Catalog::Get(context); + auto transaction = transaction::Transaction::Get(context); + std::string query; + auto maybeDropTable = [&](const std::string& tableName) { + if (catalog->containsTable(transaction, tableName, false /* useInternal */)) { + throw BinderException{ + std::format("Table: {} already exists. Please drop or rename the table before " + "creating a full text search index.", + tableName)}; + } + if (catalog->containsTable(transaction, tableName)) { + query += std::format("DROP TABLE IF EXISTS `{}`;", tableName); + } + }; + // Drop the rel table before the node tables it references. + maybeDropTable(FTSUtils::getAppearsInTableName(tableID, indexName)); + maybeDropTable(FTSUtils::getAppearsInfoTableName(tableID, indexName)); + maybeDropTable(FTSUtils::getDocsTableName(tableID, indexName)); + maybeDropTable(FTSUtils::getTermsTableName(tableID, indexName)); + // The shared default stopwords table is idempotent (IF NOT EXISTS + MERGE) and + // must never be dropped; only per-index copies are residue candidates. + if (stopWordsInfo.source != StopWordsSource::DEFAULT) { + maybeDropTable(stopWordsInfo.tableName); } -} - -static void validateInternalTablesNotExist(table_id_t tableID, const std::string& indexName, - const catalog::Catalog& catalog, const transaction::Transaction* transaction) { - validateInternalTableNotExist(FTSUtils::getDocsTableName(tableID, indexName), catalog, - transaction); - validateInternalTableNotExist(FTSUtils::getAppearsInTableName(tableID, indexName), catalog, - transaction); - validateInternalTableNotExist(FTSUtils::getAppearsInfoTableName(tableID, indexName), catalog, - transaction); - validateInternalTableNotExist(FTSUtils::getTermsTableName(tableID, indexName), catalog, - transaction); + return query; } static std::unique_ptr bindFunc(ClientContext* context, @@ -148,23 +160,27 @@ std::string createFTSIndexQuery(ClientContext& context, const TableFuncBindData& auto ftsBindData = bindData.constPtrCast(); auto tableID = ftsBindData->tableID; auto indexName = ftsBindData->indexName; - validateInternalTablesNotExist(ftsBindData->tableID, ftsBindData->indexName, - *catalog::Catalog::Get(context), transaction::Transaction::Get(context)); context.setUseInternalCatalogEntry(true /* useInternalCatalogEntry */); // TODO(Ziyi): Copy statement can't be wrapped in manual transaction, so we can't wrap all // statements in a single transaction there. - // Create the tokenize macro. std::string query = ""; - if (!catalog::Catalog::Get(context)->containsMacro(transaction::Transaction::Get(context), - FTSUtils::getTokenizeMacroName(tableID, indexName))) { - // TOKENIZE(text, tokenizer, extra_param) - query += std::format(R"(CREATE MACRO `{}`(query) AS - TOKENIZE(lower(regexp_replace(CAST(query as STRING), '{}', ' ', 'g')), '{}', '{}');)", - FTSUtils::getTokenizeMacroName(tableID, indexName), - formatStrInCypher(ftsBindData->createFTSConfig.ignorePattern), - ftsBindData->createFTSConfig.tokenizerInfo.tokenizer, - ftsBindData->createFTSConfig.tokenizerInfo.jiebaDictDir); + // Self-heal internal-only residue from an interrupted CREATE/DROP before rebuilding. + query += dropResidueStatements(context, tableID, indexName, + ftsBindData->createFTSConfig.stopWordsTableInfo); + // Create the tokenize macro (recreated unconditionally: any residue macro was just + // dropped above, and DROP MACRO IF EXISTS is a no-op otherwise). + auto tokenizeMacroName = FTSUtils::getTokenizeMacroName(tableID, indexName); + if (catalog::Catalog::Get(context)->containsMacro(transaction::Transaction::Get(context), + tokenizeMacroName)) { + query += std::format("DROP MACRO IF EXISTS `{}`;", tokenizeMacroName); } + // TOKENIZE(text, tokenizer, extra_param) + query += std::format(R"(CREATE MACRO `{}`(query) AS + TOKENIZE(lower(regexp_replace(CAST(query as STRING), '{}', ' ', 'g')), '{}', '{}');)", + FTSUtils::getTokenizeMacroName(tableID, indexName), + formatStrInCypher(ftsBindData->createFTSConfig.ignorePattern), + ftsBindData->createFTSConfig.tokenizerInfo.tokenizer, + ftsBindData->createFTSConfig.tokenizerInfo.jiebaDictDir); // Create the stop words table if not exists, or the user is not using the default english // one. diff --git a/fts/src/function/drop_fts_index.cpp b/fts/src/function/drop_fts_index.cpp index fadf414f..56dd1004 100644 --- a/fts/src/function/drop_fts_index.cpp +++ b/fts/src/function/drop_fts_index.cpp @@ -1,6 +1,7 @@ #include "function/drop_fts_index.h" #include "catalog/catalog.h" +#include "catalog/fts_index_catalog_entry.h" #include "function/fts_bind_data.h" #include "function/fts_index_utils.h" #include "function/table/bind_data.h" @@ -32,14 +33,31 @@ static std::unique_ptr bindFunc(ClientContext* context, std::string dropFTSIndexQuery(ClientContext& context, const TableFuncBindData& bindData) { context.setUseInternalCatalogEntry(true /* useInternalCatalogEntry */); auto ftsBindData = bindData.constPtrCast(); + auto catalog = catalog::Catalog::Get(context); + auto transaction = transaction::Transaction::Get(context); auto query = std::format("CALL _DROP_FTS_INDEX('{}', '{}');", ftsBindData->tableName, ftsBindData->indexName); - query += std::format("DROP TABLE `{}`;", + // Drop the rel table before the node tables it references. IF EXISTS keeps a + // previously-interrupted DROP retryable: _DROP_FTS_INDEX above removes the catalog + // index entry first, so a retry can no longer rely on the entry being present. + query += std::format("DROP TABLE IF EXISTS `{}`;", FTSUtils::getAppearsInTableName(ftsBindData->tableID, ftsBindData->indexName)); - query += std::format("DROP TABLE `{}`;", + // Transient build table; residue if CREATE was interrupted before its final DROP. + query += std::format("DROP TABLE IF EXISTS `{}`;", + FTSUtils::getAppearsInfoTableName(ftsBindData->tableID, ftsBindData->indexName)); + query += std::format("DROP TABLE IF EXISTS `{}`;", FTSUtils::getDocsTableName(ftsBindData->tableID, ftsBindData->indexName)); - query += std::format("DROP TABLE `{}`;", + query += std::format("DROP TABLE IF EXISTS `{}`;", FTSUtils::getTermsTableName(ftsBindData->tableID, ftsBindData->indexName)); + // Per-index stopwords copy (the shared default table is never dropped). + auto indexEntry = catalog->getIndex(transaction, ftsBindData->tableID, ftsBindData->indexName); + auto stopWordsTableName = + indexEntry->getAuxInfo().cast().config.stopWordsTableName; + if (stopWordsTableName != FTSUtils::getDefaultStopWordsTableName()) { + query += std::format("DROP TABLE IF EXISTS `{}`;", stopWordsTableName); + } + query += std::format("DROP MACRO IF EXISTS `{}`;", + FTSUtils::getTokenizeMacroName(ftsBindData->tableID, ftsBindData->indexName)); return query; } diff --git a/fts/src/main/fts_extension.cpp b/fts/src/main/fts_extension.cpp index 2d3ce598..ffd389eb 100644 --- a/fts/src/main/fts_extension.cpp +++ b/fts/src/main/fts_extension.cpp @@ -10,18 +10,40 @@ #include "index/fts_index.h" #include "main/client_context.h" #include "storage/storage_manager.h" +#include "utils/fts_utils.h" namespace lbug { namespace fts_extension { using namespace extension; +// An interrupted CREATE/DROP can leave the catalog index entry present while some of +// its internal tables are missing. Loading such an index throws and would fail the +// whole database open, so detect the torn state up front and skip the index instead. +// The database still opens; repair with DROP_FTS_INDEX + CREATE_FTS_INDEX (a plain +// re-CREATE also works: CREATE self-heals internal-only residue). +static bool ftsInternalTablesComplete(main::ClientContext* context, common::table_id_t tableID, + const std::string& indexName, const std::string& stopWordsTableName) { + auto catalog = catalog::Catalog::Get(*context); + auto transaction = transaction::Transaction::Get(*context); + return catalog->containsTable(transaction, stopWordsTableName) && + catalog->containsTable(transaction, FTSUtils::getDocsTableName(tableID, indexName)) && + catalog->containsTable(transaction, FTSUtils::getTermsTableName(tableID, indexName)) && + catalog->containsTable(transaction, + FTSUtils::getAppearsInTableName(tableID, indexName)); +} + static void initFTSEntries(main::ClientContext* context, catalog::Catalog& catalog) { auto storageManager = storage::StorageManager::Get(*context); for (auto& indexEntry : catalog.getIndexEntries(transaction::Transaction::Get(*context))) { if (indexEntry->getIndexType() == FTSIndexCatalogEntry::TYPE_NAME && !indexEntry->isLoaded()) { indexEntry->setAuxInfo(FTSIndexAuxInfo::deserialize(indexEntry->getAuxBufferReader())); + auto& auxConfig = indexEntry->getAuxInfo().cast().config; + if (!ftsInternalTablesComplete(context, indexEntry->getTableID(), + indexEntry->getIndexName(), auxConfig.stopWordsTableName)) { + continue; + } auto& nodeTable = storageManager->getTable(indexEntry->getTableID())->cast(); auto optionalIndex = nodeTable.getIndexHolder(indexEntry->getIndexName()); From 6bf1588863cdb7d8b8df1d71811367c8068358ec Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 9 Oct 2026 15:35:52 -0700 Subject: [PATCH 2/2] fix(fts): schema-confirm residue + preserve user tables on failed CREATE (fixes #77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up hardening from CI feedback (3 failures): - InternalTableNamingConflict: the user/internal catalog split alone cannot tell residue from user tables, because useInternalCatalogEntry leaks as true when a statement throws (ClientContext resets it only on the success path), misfiling later user DDL into the internal set. Two safeguards: (a) save/restore the flag around residue cleanup so a failed CREATE no longer misfiles subsequent user tables; (b) drop internal-set candidates only if their schema exactly matches the FTS internal schema (docs/terms/appears_info/stopwords node shapes incl. PK, appears_in rel shape incl. endpoints when resolvable) —Fail safe: anything else still raises 'already exists'. - MultiFTSIndex / DropFTSCommitRecovery: stop emitting DROP MACRO. Macro-drop WAL replay is broken engine-side (getScalarMacroCatalogEntry looks in the functions set instead of macros), so any un-checkpointed DROP MACRO breaks the next open, and DROP_FTS_INDEX removing the macro broke explicit DROP MACRO test steps. Macros are left behind as before (CREATE reuses via skip-if-exists); revisit once the engine bug is fixed. --- fts/src/function/create_fts_index.cpp | 151 +++++++++++++++++++++----- fts/src/function/drop_fts_index.cpp | 6 +- fts/src/main/fts_extension.cpp | 3 +- 3 files changed, 129 insertions(+), 31 deletions(-) diff --git a/fts/src/function/create_fts_index.cpp b/fts/src/function/create_fts_index.cpp index 6bae4cd9..b4e9f976 100644 --- a/fts/src/function/create_fts_index.cpp +++ b/fts/src/function/create_fts_index.cpp @@ -1,8 +1,13 @@ #include "function/create_fts_index.h" +#include + #include "binder/expression/literal_expression.h" +#include "catalog/catalog_entry/node_table_catalog_entry.h" +#include "catalog/catalog_entry/rel_group_catalog_entry.h" #include "catalog/fts_index_catalog_entry.h" #include "common/exception/binder.h" +#include "common/string_utils.h" #include "common/types/value/nested.h" #include "function/fts_bind_data.h" #include "function/fts_config.h" @@ -65,34 +70,119 @@ static std::vector bindProperties(const catalog::NodeTableCatalog // Drops internal-only residue left behind by an interrupted CREATE/DROP so a failed // CREATE can simply be retried in place. The catalog index entry is written last -// during CREATE, so any internal table/macro present without an index entry is -// residue by definition (bind already validated the index itself doesn't exist). -// Genuine user tables with colliding names are never dropped; they still raise. +// during CREATE, so an internal table present without an index entry is residue +// (bind already validated the index itself doesn't exist) — with two safeguards: +// (1) tables visible in the regular catalog are genuine user tables and still raise; +// (2) remaining candidates are dropped only if their schema exactly matches the FTS +// internal schema, so a misfiled user table can never be dropped silently. (A +// useInternalCatalogEntry leak on failed statements — reset at the end of +// ClientContext::query but not on its exception path — can misfile later user DDL +// into the internal set; see the save/restore in createFTSIndexQuery.) +static bool hasExactProperties(const catalog::TableCatalogEntry& entry, + const std::vector>& expected) { + if (entry.getNumProperties() != expected.size()) { + return false; + } + for (auto& prop : entry.getProperties()) { + auto it = std::find_if(expected.begin(), expected.end(), [&](auto& e) { + return common::StringUtils::caseInsensitiveEquals(e.first, prop.getName()); + }); + if (it == expected.end() || prop.getType().getLogicalTypeID() != it->second) { + return false; + } + } + return true; +} + +static bool isResidueNodeTable(catalog::TableCatalogEntry* entry, const std::string& pkName, + const std::vector>& expectedProps) { + if (entry->getTableType() != common::TableType::NODE) { + return false; + } + auto& nodeEntry = entry->constCast(); + return common::StringUtils::caseInsensitiveEquals(nodeEntry.getPrimaryKeyName(), pkName) && + hasExactProperties(*entry, expectedProps); +} + +static bool isResidueAppearsInTable(catalog::Catalog* catalog, + const transaction::Transaction* transaction, catalog::TableCatalogEntry* entry, + const std::string& termsTableName, const std::string& docsTableName) { + if (entry->getTableType() != common::TableType::REL) { + return false; + } + auto& relGroup = entry->constCast(); + auto& infos = relGroup.getRelEntryInfos(); + if (infos.size() != 1) { + return false; + } + // When both endpoint tables exist, require exact endpoint match; when an endpoint + // is missing the rel table is orphaned and dropping it is still the safe repair. + if (catalog->containsTable(transaction, termsTableName) && + catalog->containsTable(transaction, docsTableName)) { + auto termsID = catalog->getTableCatalogEntry(transaction, termsTableName)->getTableID(); + auto docsID = catalog->getTableCatalogEntry(transaction, docsTableName)->getTableID(); + if (infos[0].nodePair.srcTableID != termsID || infos[0].nodePair.dstTableID != docsID) { + return false; + } + } + return hasExactProperties(*entry, {{"tf", common::LogicalTypeID::UINT64}}); +} + static std::string dropResidueStatements(ClientContext& context, table_id_t tableID, const std::string& indexName, const StopWordsTableInfo& stopWordsInfo) { auto catalog = catalog::Catalog::Get(context); auto transaction = transaction::Transaction::Get(context); + auto docsTableName = FTSUtils::getDocsTableName(tableID, indexName); + auto termsTableName = FTSUtils::getTermsTableName(tableID, indexName); + auto appearsInfoTableName = FTSUtils::getAppearsInfoTableName(tableID, indexName); + auto appearsInTableName = FTSUtils::getAppearsInTableName(tableID, indexName); std::string query; - auto maybeDropTable = [&](const std::string& tableName) { + // Throws if tableName is a genuine user table; emits a guarded DROP if it is + // schema-confirmed residue; emits nothing if the name is free. isResidue must + // exactly mirror the CREATE statements below. + auto maybeDropTable = [&](const std::string& tableName, + const std::function& isResidue) { if (catalog->containsTable(transaction, tableName, false /* useInternal */)) { throw BinderException{ std::format("Table: {} already exists. Please drop or rename the table before " "creating a full text search index.", tableName)}; } - if (catalog->containsTable(transaction, tableName)) { - query += std::format("DROP TABLE IF EXISTS `{}`;", tableName); + if (!catalog->containsTable(transaction, tableName)) { + return; } + auto entry = catalog->getTableCatalogEntry(transaction, tableName); + if (!isResidue(entry)) { + throw BinderException{ + std::format("Table: {} already exists. Please drop or rename the table before " + "creating a full text search index.", + tableName)}; + } + query += std::format("DROP TABLE IF EXISTS `{}`;", tableName); }; // Drop the rel table before the node tables it references. - maybeDropTable(FTSUtils::getAppearsInTableName(tableID, indexName)); - maybeDropTable(FTSUtils::getAppearsInfoTableName(tableID, indexName)); - maybeDropTable(FTSUtils::getDocsTableName(tableID, indexName)); - maybeDropTable(FTSUtils::getTermsTableName(tableID, indexName)); + maybeDropTable(appearsInTableName, [&](catalog::TableCatalogEntry* entry) { + return isResidueAppearsInTable(catalog, transaction, entry, termsTableName, docsTableName); + }); + maybeDropTable(appearsInfoTableName, [](catalog::TableCatalogEntry* entry) { + return isResidueNodeTable(entry, "ID", + {{"ID", common::LogicalTypeID::SERIAL}, {"term", common::LogicalTypeID::STRING}, + {"docID", common::LogicalTypeID::INT64}}); + }); + maybeDropTable(docsTableName, [](catalog::TableCatalogEntry* entry) { + return isResidueNodeTable(entry, "docID", + {{"docID", common::LogicalTypeID::INT64}, {"len", common::LogicalTypeID::UINT64}}); + }); + maybeDropTable(termsTableName, [](catalog::TableCatalogEntry* entry) { + return isResidueNodeTable(entry, "term", + {{"term", common::LogicalTypeID::STRING}, {"df", common::LogicalTypeID::UINT64}}); + }); // The shared default stopwords table is idempotent (IF NOT EXISTS + MERGE) and // must never be dropped; only per-index copies are residue candidates. if (stopWordsInfo.source != StopWordsSource::DEFAULT) { - maybeDropTable(stopWordsInfo.tableName); + maybeDropTable(stopWordsInfo.tableName, [](catalog::TableCatalogEntry* entry) { + return isResidueNodeTable(entry, "sw", {{"sw", common::LogicalTypeID::STRING}}); + }); } return query; } @@ -164,23 +254,30 @@ std::string createFTSIndexQuery(ClientContext& context, const TableFuncBindData& // TODO(Ziyi): Copy statement can't be wrapped in manual transaction, so we can't wrap all // statements in a single transaction there. std::string query = ""; - // Self-heal internal-only residue from an interrupted CREATE/DROP before rebuilding. - query += dropResidueStatements(context, tableID, indexName, - ftsBindData->createFTSConfig.stopWordsTableInfo); - // Create the tokenize macro (recreated unconditionally: any residue macro was just - // dropped above, and DROP MACRO IF EXISTS is a no-op otherwise). - auto tokenizeMacroName = FTSUtils::getTokenizeMacroName(tableID, indexName); - if (catalog::Catalog::Get(context)->containsMacro(transaction::Transaction::Get(context), - tokenizeMacroName)) { - query += std::format("DROP MACRO IF EXISTS `{}`;", tokenizeMacroName); + // Self-heal schema-confirmed residue from an interrupted CREATE/DROP before rebuilding. + // Save/restore the internal-catalog flag: dropResidueStatements throws on user-table + // collisions, and ClientContext only resets the flag on the success path, so + // without this a failed CREATE would misfile later user DDL into the internal set. + auto prevUseInternalCatalogEntry = context.useInternalCatalogEntry(); + try { + query += dropResidueStatements(context, tableID, indexName, + ftsBindData->createFTSConfig.stopWordsTableInfo); + } catch (...) { + context.setUseInternalCatalogEntry(prevUseInternalCatalogEntry); + throw; + } + // Create the tokenize macro unless one already exists (a residue macro is reused; + // dropping it is avoided: macro-drop WAL replay is currently broken engine-side). + if (!catalog::Catalog::Get(context)->containsMacro(transaction::Transaction::Get(context), + FTSUtils::getTokenizeMacroName(tableID, indexName))) { + // TOKENIZE(text, tokenizer, extra_param) + query += std::format(R"(CREATE MACRO `{}`(query) AS + TOKENIZE(lower(regexp_replace(CAST(query as STRING), '{}', ' ', 'g')), '{}', '{}');)", + FTSUtils::getTokenizeMacroName(tableID, indexName), + formatStrInCypher(ftsBindData->createFTSConfig.ignorePattern), + ftsBindData->createFTSConfig.tokenizerInfo.tokenizer, + ftsBindData->createFTSConfig.tokenizerInfo.jiebaDictDir); } - // TOKENIZE(text, tokenizer, extra_param) - query += std::format(R"(CREATE MACRO `{}`(query) AS - TOKENIZE(lower(regexp_replace(CAST(query as STRING), '{}', ' ', 'g')), '{}', '{}');)", - FTSUtils::getTokenizeMacroName(tableID, indexName), - formatStrInCypher(ftsBindData->createFTSConfig.ignorePattern), - ftsBindData->createFTSConfig.tokenizerInfo.tokenizer, - ftsBindData->createFTSConfig.tokenizerInfo.jiebaDictDir); // Create the stop words table if not exists, or the user is not using the default english // one. diff --git a/fts/src/function/drop_fts_index.cpp b/fts/src/function/drop_fts_index.cpp index 56dd1004..d3733b63 100644 --- a/fts/src/function/drop_fts_index.cpp +++ b/fts/src/function/drop_fts_index.cpp @@ -56,8 +56,10 @@ std::string dropFTSIndexQuery(ClientContext& context, const TableFuncBindData& b if (stopWordsTableName != FTSUtils::getDefaultStopWordsTableName()) { query += std::format("DROP TABLE IF EXISTS `{}`;", stopWordsTableName); } - query += std::format("DROP MACRO IF EXISTS `{}`;", - FTSUtils::getTokenizeMacroName(ftsBindData->tableID, ftsBindData->indexName)); + // NOTE: the per-index tokenize macro is intentionally left behind. Dropping it + // here (or recreating it in CREATE) would emit macro-drop WAL records whose + // replay is currently broken engine-side (getScalarMacroCatalogEntry looks in the + // wrong catalog set), breaking un-checkpointed reopen. Revisit once fixed. return query; } diff --git a/fts/src/main/fts_extension.cpp b/fts/src/main/fts_extension.cpp index ffd389eb..fe5f4f2c 100644 --- a/fts/src/main/fts_extension.cpp +++ b/fts/src/main/fts_extension.cpp @@ -29,8 +29,7 @@ static bool ftsInternalTablesComplete(main::ClientContext* context, common::tabl return catalog->containsTable(transaction, stopWordsTableName) && catalog->containsTable(transaction, FTSUtils::getDocsTableName(tableID, indexName)) && catalog->containsTable(transaction, FTSUtils::getTermsTableName(tableID, indexName)) && - catalog->containsTable(transaction, - FTSUtils::getAppearsInTableName(tableID, indexName)); + catalog->containsTable(transaction, FTSUtils::getAppearsInTableName(tableID, indexName)); } static void initFTSEntries(main::ClientContext* context, catalog::Catalog& catalog) {