| 160 | } |
| 161 | |
| 162 | Result<FtsCondInfo::Ptr> SQLEngineImpl::parse_fts_query( |
| 163 | CollectionSchema::Ptr collection, const std::string &field_name, |
| 164 | const FtsClause &fts, const QueryParams::Ptr &query_params) { |
| 165 | // Exactly one of query_string_ or match_string_ must be provided. |
| 166 | bool has_query = !fts.query_string_.empty(); |
| 167 | bool has_match_string = !fts.match_string_.empty(); |
| 168 | if (has_query == has_match_string) { |
| 169 | return tl::make_unexpected(Status::InvalidArgument( |
| 170 | "Exactly one of query_string or match_string must be provided")); |
| 171 | } |
| 172 | |
| 173 | auto *fts_query_param = dynamic_cast<FtsQueryParams *>(query_params.get()); |
| 174 | if (query_params && !fts_query_param) { |
| 175 | return tl::make_unexpected(Status::InvalidArgument( |
| 176 | "FTS query only accepts FtsQueryParam, got incompatible query param " |
| 177 | "type")); |
| 178 | } |
| 179 | |
| 180 | // Determine default operator once, shared by both query_string and |
| 181 | // match_string paths. Accept "and"/"or" case-insensitively, empty means OR; |
| 182 | // any other value is a user error and must be reported, not silently |
| 183 | // downgraded to OR. strcasecmp is mapped to _stricmp on MSVC by platform.h. |
| 184 | fts::FtsDefaultOperator default_op = fts::FtsDefaultOperator::OR; |
| 185 | if (fts_query_param) { |
| 186 | const auto &op_str = fts_query_param->default_operator(); |
| 187 | if (op_str.empty() || strcasecmp(op_str.c_str(), "or") == 0) { |
| 188 | default_op = fts::FtsDefaultOperator::OR; |
| 189 | } else if (strcasecmp(op_str.c_str(), "and") == 0) { |
| 190 | default_op = fts::FtsDefaultOperator::AND; |
| 191 | } else { |
| 192 | return tl::make_unexpected(Status::InvalidArgument( |
| 193 | "FTS default_operator must be empty, 'and' or 'or' (case-insensitive)" |
| 194 | ", got: ", |
| 195 | op_str)); |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | // Tokenizer pipeline is required by both branches: query_string needs it to |
| 200 | // tokenize phrase contents and bare terms, match_string needs it to split |
| 201 | // the natural-language input. Resolve once and share. |
| 202 | auto *field_schema = collection->get_field(field_name); |
| 203 | if (!field_schema) { |
| 204 | return tl::make_unexpected( |
| 205 | Status::InvalidArgument("FTS field not found: ", field_name)); |
| 206 | } |
| 207 | auto fts_idx_param = |
| 208 | std::dynamic_pointer_cast<FtsIndexParams>(field_schema->index_params()); |
| 209 | if (!fts_idx_param) { |
| 210 | return tl::make_unexpected(Status::InvalidArgument( |
| 211 | "FTS field has no FtsIndexParams: ", field_name)); |
| 212 | } |
| 213 | auto pipeline_result = detail::AcquireFtsPipeline(*fts_idx_param); |
| 214 | if (!pipeline_result.has_value()) { |
| 215 | return tl::make_unexpected(Status::InternalError( |
| 216 | "Failed to create tokenizer pipeline for field: ", field_name, " ", |
| 217 | pipeline_result.error().message())); |
| 218 | } |
| 219 | auto &pipeline = pipeline_result.value(); |
nothing calls this directly
no test coverage detected