couchbase 3.4.1 → 3.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/ext/couchbase/CMakeLists.txt +2 -0
  4. data/ext/couchbase/cmake/ThirdPartyDependencies.cmake +4 -0
  5. data/ext/couchbase/core/cluster_options.hxx +0 -1
  6. data/ext/couchbase/core/config_profile.cxx +23 -1
  7. data/ext/couchbase/core/config_profile.hxx +2 -12
  8. data/ext/couchbase/core/impl/analytics.cxx +236 -0
  9. data/ext/couchbase/core/impl/cluster.cxx +0 -1
  10. data/ext/couchbase/core/impl/dns_srv_tracker.cxx +5 -3
  11. data/ext/couchbase/core/impl/query.cxx +5 -5
  12. data/ext/couchbase/core/io/dns_client.cxx +225 -0
  13. data/ext/couchbase/core/io/dns_client.hxx +19 -188
  14. data/ext/couchbase/core/transactions/active_transaction_record.hxx +2 -2
  15. data/ext/couchbase/core/transactions/attempt_context_impl.cxx +3 -0
  16. data/ext/couchbase/core/transactions/attempt_context_impl.hxx +1 -1
  17. data/ext/couchbase/core/transactions/internal/transaction_context.hxx +12 -12
  18. data/ext/couchbase/core/transactions/internal/transactions_cleanup.hxx +7 -1
  19. data/ext/couchbase/core/transactions/transaction_context.cxx +1 -0
  20. data/ext/couchbase/core/transactions/transactions_cleanup.cxx +144 -155
  21. data/ext/couchbase/core/utils/connection_string.cxx +10 -3
  22. data/ext/couchbase/core/utils/connection_string.hxx +3 -3
  23. data/ext/couchbase/couchbase/analytics_error_context.hxx +143 -0
  24. data/ext/couchbase/couchbase/analytics_meta_data.hxx +155 -0
  25. data/ext/couchbase/couchbase/analytics_metrics.hxx +163 -0
  26. data/ext/couchbase/couchbase/analytics_options.hxx +359 -0
  27. data/ext/couchbase/couchbase/analytics_result.hxx +102 -0
  28. data/ext/couchbase/couchbase/analytics_scan_consistency.hxx +46 -0
  29. data/ext/couchbase/couchbase/analytics_status.hxx +41 -0
  30. data/ext/couchbase/couchbase/analytics_warning.hxx +85 -0
  31. data/ext/couchbase/couchbase/cluster.hxx +33 -0
  32. data/ext/couchbase/couchbase/fmt/analytics_status.hxx +76 -0
  33. data/ext/couchbase/couchbase/query_options.hxx +0 -1
  34. data/ext/couchbase/couchbase/scope.hxx +33 -0
  35. data/ext/couchbase/couchbase/transactions/attempt_context.hxx +1 -1
  36. data/ext/couchbase/test/CMakeLists.txt +1 -2
  37. data/ext/couchbase/test/test_helper.hxx +1 -1
  38. data/ext/couchbase/test/test_integration_analytics.cxx +289 -13
  39. data/ext/couchbase/test/test_integration_crud.cxx +8 -1
  40. data/ext/couchbase/test/test_integration_examples.cxx +41 -0
  41. data/ext/couchbase/test/test_integration_management.cxx +15 -3
  42. data/ext/couchbase/test/test_integration_search.cxx +601 -0
  43. data/ext/couchbase/test/test_transaction_transaction_simple.cxx +73 -0
  44. data/ext/couchbase/test/test_unit_config_profiles.cxx +12 -12
  45. data/ext/couchbase/test/test_unit_connection_string.cxx +35 -0
  46. data/ext/couchbase/third_party/snappy/CMakeLists.txt +150 -27
  47. data/ext/couchbase/third_party/snappy/cmake/config.h.in +28 -24
  48. data/ext/couchbase/third_party/snappy/snappy-internal.h +189 -25
  49. data/ext/couchbase/third_party/snappy/snappy-sinksource.cc +26 -9
  50. data/ext/couchbase/third_party/snappy/snappy-sinksource.h +11 -11
  51. data/ext/couchbase/third_party/snappy/snappy-stubs-internal.cc +1 -1
  52. data/ext/couchbase/third_party/snappy/snappy-stubs-internal.h +227 -308
  53. data/ext/couchbase/third_party/snappy/snappy-stubs-public.h.in +0 -11
  54. data/ext/couchbase/third_party/snappy/snappy.cc +1176 -410
  55. data/ext/couchbase/third_party/snappy/snappy.h +19 -4
  56. data/ext/couchbase.cxx +27 -6
  57. data/ext/revisions.rb +3 -3
  58. data/lib/couchbase/cluster.rb +13 -9
  59. data/lib/couchbase/cluster_registry.rb +7 -2
  60. data/lib/couchbase/configuration.rb +3 -4
  61. data/lib/couchbase/options.rb +85 -2
  62. data/lib/couchbase/search_options.rb +158 -240
  63. data/lib/couchbase/version.rb +1 -1
  64. metadata +17 -6
  65. data/ext/couchbase/core/CMakeLists.txt +0 -0
@@ -0,0 +1,601 @@
1
+ /* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2
+ /*
3
+ * Copyright 2020-2021 Couchbase, Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ #include "test_helper_integration.hxx"
19
+
20
+ #include "core/operations/management/collection_create.hxx"
21
+ #include "core/operations/management/search_index_drop.hxx"
22
+ #include "core/operations/management/search_index_upsert.hxx"
23
+
24
+ using Catch::Matchers::StartsWith;
25
+
26
+ TEST_CASE("integration: search query")
27
+ {
28
+ test::utils::integration_test_guard integration;
29
+
30
+ test::utils::open_bucket(integration.cluster, integration.ctx.bucket);
31
+
32
+ {
33
+ auto sample_data =
34
+ couchbase::core::utils::json::parse(couchbase::core::json_string(test::utils::read_test_data("search_beers_dataset.json")));
35
+ auto const& o = sample_data.get_object();
36
+ for (const auto& [key, value] : o) {
37
+ couchbase::core::document_id id(integration.ctx.bucket, "_default", "_default", key);
38
+ couchbase::core::operations::upsert_request req{ id, couchbase::core::utils::json::generate_binary(value) };
39
+ auto resp = test::utils::execute(integration.cluster, req);
40
+ REQUIRE_SUCCESS(resp.ctx.ec());
41
+ }
42
+ }
43
+
44
+ std::string index_name = test::utils::uniq_id("beer-search-index");
45
+
46
+ {
47
+ auto params = test::utils::read_test_data("search_beers_index_params.json");
48
+
49
+ couchbase::core::management::search::index index{};
50
+ index.name = index_name;
51
+ index.params_json = params;
52
+ index.type = "fulltext-index";
53
+ index.source_name = integration.ctx.bucket;
54
+ index.source_type = "couchbase";
55
+ if (integration.cluster_version().requires_search_replicas()) {
56
+ index.plan_params_json = couchbase::core::utils::json::generate({
57
+ { "indexPartitions", 1 },
58
+ { "numReplicas", 1 },
59
+ });
60
+ }
61
+ couchbase::core::operations::management::search_index_upsert_request req{};
62
+ req.index = index;
63
+
64
+ auto resp = test::utils::execute(integration.cluster, req);
65
+ REQUIRE((!resp.ctx.ec || resp.ctx.ec == couchbase::errc::common::index_exists));
66
+ if (index_name != resp.name) {
67
+ CB_LOG_INFO("update index name \"{}\" -> \"{}\"", index_name, resp.name);
68
+ }
69
+ index_name = resp.name;
70
+ }
71
+
72
+ couchbase::core::json_string simple_query(R"({"query": "description:belgian"})");
73
+
74
+ std::uint64_t beer_sample_doc_count = 5;
75
+ // Wait until expected documents are indexed
76
+ {
77
+ REQUIRE(test::utils::wait_until_indexed(integration.cluster, index_name, beer_sample_doc_count));
78
+ auto ok = test::utils::wait_until(
79
+ [&]() {
80
+ couchbase::core::operations::search_request req{};
81
+ req.index_name = index_name;
82
+ req.query = simple_query;
83
+ auto resp = test::utils::execute(integration.cluster, req);
84
+ REQUIRE_SUCCESS(resp.ctx.ec);
85
+ return resp.rows.size() == beer_sample_doc_count;
86
+ },
87
+ std::chrono::minutes(5));
88
+ REQUIRE(ok);
89
+ }
90
+
91
+ SECTION("simple query")
92
+ {
93
+ couchbase::core::operations::search_request req{};
94
+ req.index_name = index_name;
95
+ req.query = simple_query;
96
+ req.sort_specs.emplace_back(couchbase::core::utils::json::generate("_id"));
97
+ auto resp = test::utils::execute(integration.cluster, req);
98
+ REQUIRE_SUCCESS(resp.ctx.ec);
99
+ REQUIRE(resp.rows.size() == 5);
100
+ REQUIRE(resp.rows[0].id == "avery_brewing_company-reverend_the");
101
+ REQUIRE(resp.rows[0].score > 0);
102
+ REQUIRE_THAT(resp.rows[0].index, StartsWith(index_name));
103
+ REQUIRE(resp.rows[0].locations.empty());
104
+ REQUIRE(resp.rows[0].explanation.empty());
105
+ REQUIRE(resp.rows[0].fields.empty());
106
+ REQUIRE(resp.rows[0].fragments.empty());
107
+ REQUIRE(resp.meta.metrics.max_score > 0);
108
+ REQUIRE(resp.meta.metrics.total_rows == 5);
109
+ REQUIRE(resp.meta.metrics.took > std::chrono::nanoseconds(0));
110
+ }
111
+
112
+ SECTION("limit")
113
+ {
114
+ couchbase::core::operations::search_request req{};
115
+ req.index_name = index_name;
116
+ req.query = simple_query;
117
+ req.limit = 1;
118
+ auto resp = test::utils::execute(integration.cluster, req);
119
+ REQUIRE_SUCCESS(resp.ctx.ec);
120
+ REQUIRE(resp.rows.size() == 1);
121
+ }
122
+
123
+ SECTION("skip")
124
+ {
125
+ couchbase::core::operations::search_request req{};
126
+ req.index_name = index_name;
127
+ req.query = simple_query;
128
+ req.skip = 1;
129
+ req.sort_specs.emplace_back(couchbase::core::utils::json::generate("_id"));
130
+ auto resp = test::utils::execute(integration.cluster, req);
131
+ REQUIRE_SUCCESS(resp.ctx.ec);
132
+ REQUIRE(resp.rows.size() == beer_sample_doc_count - 1);
133
+ REQUIRE(resp.rows[0].id == "bear_republic_brewery-red_rocket_ale");
134
+ }
135
+
136
+ SECTION("explain")
137
+ {
138
+ couchbase::core::operations::search_request req{};
139
+ req.index_name = index_name;
140
+ req.query = simple_query;
141
+ req.explain = true;
142
+ auto resp = test::utils::execute(integration.cluster, req);
143
+ REQUIRE_SUCCESS(resp.ctx.ec);
144
+ REQUIRE_FALSE(resp.rows[0].explanation.empty());
145
+ }
146
+
147
+ if (integration.cluster_version().supports_search_disable_scoring()) {
148
+ SECTION("disable scoring")
149
+ {
150
+ couchbase::core::operations::search_request req{};
151
+ req.index_name = index_name;
152
+ req.query = simple_query;
153
+ req.disable_scoring = true;
154
+ auto resp = test::utils::execute(integration.cluster, req);
155
+ REQUIRE_SUCCESS(resp.ctx.ec);
156
+ REQUIRE(resp.rows[0].score == 0);
157
+ REQUIRE(resp.meta.metrics.max_score == 0);
158
+ }
159
+ }
160
+
161
+ SECTION("include locations")
162
+ {
163
+ couchbase::core::operations::search_request req{};
164
+ req.index_name = index_name;
165
+ req.query = simple_query;
166
+ req.sort_specs.emplace_back(couchbase::core::utils::json::generate("_id"));
167
+ req.include_locations = true;
168
+ auto resp = test::utils::execute(integration.cluster, req);
169
+ REQUIRE_SUCCESS(resp.ctx.ec);
170
+ REQUIRE(resp.rows[0].locations.size() == 1);
171
+ REQUIRE(resp.rows[0].locations[0].field == "description");
172
+ REQUIRE(resp.rows[0].locations[0].term == "belgian");
173
+ REQUIRE(resp.rows[0].locations[0].position == 1);
174
+ REQUIRE(resp.rows[0].locations[0].start_offset == 0);
175
+ REQUIRE(resp.rows[0].locations[0].end_offset == 7);
176
+ }
177
+
178
+ SECTION("highlight fields default highlight style")
179
+ {
180
+ couchbase::core::operations::search_request req{};
181
+ req.index_name = index_name;
182
+ req.query = simple_query;
183
+ req.sort_specs.emplace_back(couchbase::core::utils::json::generate("_id"));
184
+ req.highlight_fields = { "description" };
185
+ auto resp = test::utils::execute(integration.cluster, req);
186
+ REQUIRE_SUCCESS(resp.ctx.ec);
187
+ REQUIRE(resp.rows[0].fragments["description"][0] == "<mark>Belgian</mark>-Style Quadrupel Ale");
188
+ }
189
+
190
+ SECTION("highlight style")
191
+ {
192
+ {
193
+ couchbase::core::operations::search_request req{};
194
+ req.index_name = index_name;
195
+ req.query = simple_query;
196
+ req.sort_specs.emplace_back(couchbase::core::utils::json::generate("_id"));
197
+ req.highlight_fields = { "description" };
198
+ req.highlight_style = couchbase::core::search_highlight_style::html;
199
+ auto resp = test::utils::execute(integration.cluster, req);
200
+ REQUIRE_SUCCESS(resp.ctx.ec);
201
+ REQUIRE(resp.rows[0].fragments["description"][0] == "<mark>Belgian</mark>-Style Quadrupel Ale");
202
+ }
203
+
204
+ {
205
+ couchbase::core::operations::search_request req{};
206
+ req.index_name = index_name;
207
+ req.query = simple_query;
208
+ req.sort_specs.emplace_back(couchbase::core::utils::json::generate("_id"));
209
+ req.highlight_fields = { "description" };
210
+ req.highlight_style = couchbase::core::search_highlight_style::ansi;
211
+ auto resp = test::utils::execute(integration.cluster, req);
212
+ REQUIRE_SUCCESS(resp.ctx.ec);
213
+ // TODO: is there a better way to compare ansi strings?
214
+ std::string snippet = resp.rows[0].fragments["description"][0];
215
+ std::string open = "\x1b[43m";
216
+ std::string close = "\x1b[0m";
217
+ snippet.replace(snippet.find(open), open.size(), "<mark>");
218
+ snippet.replace(snippet.find(close), close.size(), "</mark>");
219
+ REQUIRE(snippet == "<mark>Belgian</mark>-Style Quadrupel Ale");
220
+ }
221
+ }
222
+
223
+ SECTION("fields")
224
+ {
225
+ couchbase::core::operations::search_request req{};
226
+ req.index_name = index_name;
227
+ req.query = simple_query;
228
+ req.sort_specs.emplace_back(couchbase::core::utils::json::generate("_id"));
229
+ req.fields.emplace_back("description");
230
+ auto resp = test::utils::execute(integration.cluster, req);
231
+ REQUIRE_SUCCESS(resp.ctx.ec);
232
+ auto fields = couchbase::core::utils::json::parse(resp.rows[0].fields).get_object();
233
+ REQUIRE(fields.at("description").get_string() == "Belgian-Style Quadrupel Ale");
234
+ }
235
+
236
+ SECTION("sort")
237
+ {
238
+ couchbase::core::operations::search_request req{};
239
+ req.index_name = index_name;
240
+ req.query = simple_query;
241
+ req.sort_specs.emplace_back(couchbase::core::utils::json::generate("_score"));
242
+ req.timeout = std::chrono::seconds(1);
243
+ auto resp = test::utils::execute(integration.cluster, req);
244
+ REQUIRE_SUCCESS(resp.ctx.ec);
245
+ REQUIRE(resp.rows[0].id == "bear_republic_brewery-red_rocket_ale");
246
+ }
247
+
248
+ SECTION("term facet")
249
+ {
250
+ couchbase::core::operations::search_request req{};
251
+ req.index_name = index_name;
252
+ req.query = simple_query;
253
+ req.facets.insert(std::make_pair("type", R"({"field": "type", "size": 1})"));
254
+ auto resp = test::utils::execute(integration.cluster, req);
255
+ REQUIRE_SUCCESS(resp.ctx.ec);
256
+ REQUIRE(resp.facets.size() == 1);
257
+ REQUIRE(resp.facets[0].name == "type");
258
+ REQUIRE(resp.facets[0].field == "type");
259
+ REQUIRE(resp.facets[0].total == 5);
260
+ REQUIRE(resp.facets[0].missing == 0);
261
+ REQUIRE(resp.facets[0].other == 0);
262
+ REQUIRE(resp.facets[0].terms.size() == 1);
263
+ REQUIRE(resp.facets[0].terms[0].term == "beer");
264
+ REQUIRE(resp.facets[0].terms[0].count == 5);
265
+ }
266
+
267
+ SECTION("date range facet")
268
+ {
269
+ couchbase::core::operations::search_request req{};
270
+ req.index_name = index_name;
271
+ req.query = simple_query;
272
+ req.facets.insert(std::make_pair(
273
+ "updated",
274
+ R"({"field": "updated", "size": 2, "date_ranges": [{"name": "old", "end": "2010-08-01"},{"name": "new", "start": "2010-08-01"}]})"));
275
+ auto resp = test::utils::execute(integration.cluster, req);
276
+ REQUIRE_SUCCESS(resp.ctx.ec);
277
+ REQUIRE(resp.facets.size() == 1);
278
+ REQUIRE(resp.facets[0].name == "updated");
279
+ REQUIRE(resp.facets[0].field == "updated");
280
+ REQUIRE(resp.facets[0].total == 5);
281
+ REQUIRE(resp.facets[0].missing == 0);
282
+ REQUIRE(resp.facets[0].other == 0);
283
+ REQUIRE(resp.facets[0].date_ranges.size() == 2);
284
+ REQUIRE(resp.facets[0].date_ranges[0].name == "old");
285
+ REQUIRE(resp.facets[0].date_ranges[0].count == 4);
286
+ REQUIRE_FALSE(resp.facets[0].date_ranges[0].start.has_value());
287
+ REQUIRE(resp.facets[0].date_ranges[0].end == "2010-08-01T00:00:00Z");
288
+ REQUIRE(resp.facets[0].date_ranges[1].name == "new");
289
+ REQUIRE(resp.facets[0].date_ranges[1].count == 1);
290
+ REQUIRE(resp.facets[0].date_ranges[1].start == "2010-08-01T00:00:00Z");
291
+ REQUIRE_FALSE(resp.facets[0].date_ranges[1].end.has_value());
292
+ }
293
+
294
+ SECTION("numeric range facet")
295
+ {
296
+ couchbase::core::operations::search_request req{};
297
+ req.index_name = index_name;
298
+ req.query = simple_query;
299
+ req.facets.insert(std::make_pair(
300
+ "abv", R"({"field": "abv", "size": 2, "numeric_ranges": [{"name": "high", "min": 7},{"name": "low", "max": 7}]})"));
301
+ auto resp = test::utils::execute(integration.cluster, req);
302
+ REQUIRE_SUCCESS(resp.ctx.ec);
303
+ REQUIRE(resp.facets.size() == 1);
304
+ REQUIRE(resp.facets[0].name == "abv");
305
+ REQUIRE(resp.facets[0].field == "abv");
306
+ REQUIRE(resp.facets[0].total == 5);
307
+ REQUIRE(resp.facets[0].missing == 0);
308
+ REQUIRE(resp.facets[0].other == 0);
309
+ REQUIRE(resp.facets[0].numeric_ranges.size() == 2);
310
+ auto high_range = std::find_if(resp.facets[0].numeric_ranges.begin(), resp.facets[0].numeric_ranges.end(), [](const auto& range) {
311
+ return range.name == "high";
312
+ });
313
+ REQUIRE(high_range != resp.facets[0].numeric_ranges.end());
314
+ REQUIRE(high_range->count == 2);
315
+ REQUIRE(std::get<std::uint64_t>(high_range->min) == 7);
316
+ REQUIRE(std::holds_alternative<std::monostate>(high_range->max));
317
+ auto low_range = std::find_if(resp.facets[0].numeric_ranges.begin(), resp.facets[0].numeric_ranges.end(), [](const auto& range) {
318
+ return range.name == "low";
319
+ });
320
+ REQUIRE(low_range != resp.facets[0].numeric_ranges.end());
321
+ REQUIRE(low_range->count == 3);
322
+ REQUIRE(std::holds_alternative<std::monostate>(low_range->min));
323
+ REQUIRE(std::get<std::uint64_t>(low_range->max) == 7);
324
+ }
325
+
326
+ SECTION("raw")
327
+ {
328
+ couchbase::core::operations::search_request req{};
329
+ req.index_name = index_name;
330
+ req.query = simple_query;
331
+ std::map<std::string, couchbase::core::json_string> raw{};
332
+ raw.insert(std::make_pair("size", couchbase::core::json_string("1")));
333
+ req.raw = raw;
334
+ auto resp = test::utils::execute(integration.cluster, req);
335
+ REQUIRE_SUCCESS(resp.ctx.ec);
336
+ REQUIRE(resp.rows.size() == 1);
337
+ }
338
+
339
+ {
340
+ couchbase::core::operations::management::search_index_drop_request req{};
341
+ req.index_name = index_name;
342
+ auto resp = test::utils::execute(integration.cluster, req);
343
+ REQUIRE_SUCCESS(resp.ctx.ec);
344
+ }
345
+ }
346
+
347
+ TEST_CASE("integration: search query consistency", "[integration]")
348
+ {
349
+ test::utils::integration_test_guard integration;
350
+
351
+ test::utils::open_bucket(integration.cluster, integration.ctx.bucket);
352
+
353
+ const std::string params =
354
+ R"(
355
+ {
356
+ "mapping": {
357
+ "default_mapping": {
358
+ "enabled": true,
359
+ "dynamic": true
360
+ },
361
+ "default_type": "_default",
362
+ "default_analyzer": "standard",
363
+ "default_field": "_all"
364
+ },
365
+ "doc_config": {
366
+ "mode": "type_field",
367
+ "type_field": "_type"
368
+ }
369
+ }
370
+ )";
371
+
372
+ auto index_name = test::utils::uniq_id("search_index");
373
+
374
+ {
375
+ couchbase::core::management::search::index index{};
376
+ index.name = index_name;
377
+ index.params_json = params;
378
+ index.type = "fulltext-index";
379
+ index.source_name = integration.ctx.bucket;
380
+ index.source_type = "couchbase";
381
+ if (integration.cluster_version().requires_search_replicas()) {
382
+ index.plan_params_json = couchbase::core::utils::json::generate({
383
+ { "indexPartitions", 1 },
384
+ { "numReplicas", 1 },
385
+ });
386
+ }
387
+ couchbase::core::operations::management::search_index_upsert_request req{};
388
+ req.index = index;
389
+ auto resp = test::utils::execute(integration.cluster, req);
390
+ REQUIRE_SUCCESS(resp.ctx.ec);
391
+ if (index_name != resp.name) {
392
+ CB_LOG_INFO("update index name \"{}\" -> \"{}\"", index_name, resp.name);
393
+ }
394
+ index_name = resp.name;
395
+ }
396
+
397
+ REQUIRE(test::utils::wait_for_search_pindexes_ready(integration.cluster, integration.ctx.bucket, index_name));
398
+
399
+ auto value = test::utils::uniq_id("value");
400
+ auto id = couchbase::core::document_id(integration.ctx.bucket, "_default", "_default", test::utils::uniq_id("key"));
401
+
402
+ // FIXME: MB-55920, consistency checks is broken in all known versions of the servers at the moment,
403
+ // it might return empty results without waiting for the mutation. We cannot workaround it in any way.
404
+ // We know that doing a mutation, and then query with consistency checks in the loop is not proper test,
405
+ // but at least it will check the payload format for now, and later when the issue is fixed, the loop
406
+ // has to be removed.
407
+ couchbase::mutation_token token;
408
+ {
409
+ // now update the document and use its mutation token in query later
410
+ auto resp = test::utils::execute(integration.cluster,
411
+ couchbase::core::operations::upsert_request{
412
+ id,
413
+ couchbase::core::utils::json::generate_binary(tao::json::value{
414
+ { "_type", "test_doc" },
415
+ { "value", value },
416
+ }),
417
+ });
418
+ REQUIRE_SUCCESS(resp.ctx.ec());
419
+ token = resp.token;
420
+ }
421
+
422
+ /*
423
+ * Retry query with consistency check until it will succeed or reach 20 attempts.
424
+ *
425
+ * FTS might return empty results. See MB-55920.
426
+ */
427
+ int attempt = 0;
428
+ bool done = false;
429
+ while (!done) {
430
+ const tao::json::value query{ { "query", fmt::format("value:{}", value) } };
431
+ auto query_json = couchbase::core::json_string(couchbase::core::utils::json::generate(query));
432
+
433
+ {
434
+ couchbase::core::operations::search_request req{};
435
+ req.index_name = index_name;
436
+ req.query = query_json;
437
+ req.mutation_state.emplace_back(token);
438
+ auto resp = test::utils::execute(integration.cluster, req);
439
+ if (resp.ctx.ec == couchbase::errc::search::consistency_mismatch) {
440
+ // FIXME(MB-55920): ignore "err: bleve: pindex_consistency mismatched partition"
441
+ CB_LOG_INFO("ignore consistency_mismatch: {}", resp.ctx.http_body);
442
+ continue;
443
+ }
444
+ INFO(resp.ctx.http_body)
445
+ REQUIRE_SUCCESS(resp.ctx.ec);
446
+ switch (resp.rows.size()) {
447
+ case 1:
448
+ done = true;
449
+ break;
450
+
451
+ case 0:
452
+ if (attempt > 20) {
453
+ FAIL("Unable to use search query with consistency. Giving up.");
454
+ }
455
+ ++attempt;
456
+ break;
457
+
458
+ default:
459
+ REQUIRE(resp.rows.size() == 1);
460
+ break;
461
+ }
462
+ }
463
+ }
464
+
465
+ {
466
+ couchbase::core::operations::management::search_index_drop_request req{};
467
+ req.index_name = index_name;
468
+ auto resp = test::utils::execute(integration.cluster, req);
469
+ REQUIRE_SUCCESS(resp.ctx.ec);
470
+ }
471
+ }
472
+
473
+ TEST_CASE("integration: search query collections")
474
+ {
475
+ test::utils::integration_test_guard integration;
476
+
477
+ if (!integration.cluster_version().supports_collections()) {
478
+ return;
479
+ }
480
+
481
+ test::utils::open_bucket(integration.cluster, integration.ctx.bucket);
482
+
483
+ auto index_name = test::utils::uniq_id("search_index");
484
+ auto collection1_name = test::utils::uniq_id("collection");
485
+ auto collection2_name = test::utils::uniq_id("collection");
486
+ std::string doc = R"({"name": "test"})";
487
+
488
+ for (const auto& collection : { collection1_name, collection2_name }) {
489
+ {
490
+ couchbase::core::operations::management::collection_create_request req{ integration.ctx.bucket, "_default", collection };
491
+ auto resp = test::utils::execute(integration.cluster, req);
492
+ REQUIRE_SUCCESS(resp.ctx.ec);
493
+ auto created = test::utils::wait_until_collection_manifest_propagated(integration.cluster, integration.ctx.bucket, resp.uid);
494
+ REQUIRE(created);
495
+ }
496
+
497
+ {
498
+ auto key = test::utils::uniq_id("key");
499
+ auto id = couchbase::core::document_id(integration.ctx.bucket, "_default", collection, key);
500
+ couchbase::core::operations::upsert_request req{ id, couchbase::core::utils::to_binary(doc) };
501
+ auto resp = test::utils::execute(integration.cluster, req);
502
+ REQUIRE_SUCCESS(resp.ctx.ec());
503
+ }
504
+ }
505
+
506
+ {
507
+ // clang-format off
508
+ std::string params =
509
+ R"(
510
+ {
511
+ "mapping": {
512
+ "types": {
513
+ "_default.)" + collection1_name + R"(": {
514
+ "enabled": true,
515
+ "dynamic": true
516
+ },
517
+ "_default.)" + collection2_name + R"(": {
518
+ "enabled": true,
519
+ "dynamic": true
520
+ }
521
+ },
522
+ "default_mapping": {
523
+ "enabled": false
524
+ },
525
+ "default_type": "_default",
526
+ "default_analyzer": "standard",
527
+ "default_field": "_all"
528
+ },
529
+ "doc_config": {
530
+ "mode": "scope.collection.type_field"
531
+ }
532
+ }
533
+ )";
534
+ // clang-format on
535
+
536
+ couchbase::core::management::search::index index{};
537
+ index.name = index_name;
538
+ index.params_json = params;
539
+ index.type = "fulltext-index";
540
+ index.source_name = integration.ctx.bucket;
541
+ index.source_type = "couchbase";
542
+ if (integration.cluster_version().requires_search_replicas()) {
543
+ index.plan_params_json = couchbase::core::utils::json::generate({
544
+ { "indexPartitions", 1 },
545
+ { "numReplicas", 1 },
546
+ });
547
+ }
548
+ couchbase::core::operations::management::search_index_upsert_request req{};
549
+ req.index = index;
550
+ auto resp = test::utils::execute(integration.cluster, req);
551
+ REQUIRE_SUCCESS(resp.ctx.ec);
552
+ if (index_name != resp.name) {
553
+ CB_LOG_INFO("update index name \"{}\" -> \"{}\"", index_name, resp.name);
554
+ }
555
+ index_name = resp.name;
556
+ }
557
+
558
+ REQUIRE(test::utils::wait_until_indexed(integration.cluster, index_name, 2));
559
+
560
+ couchbase::core::json_string simple_query(R"({"query": "name:test"})");
561
+
562
+ // no collection parameter - both docs returned
563
+ {
564
+ couchbase::core::operations::search_request req{};
565
+ req.index_name = index_name;
566
+ req.query = simple_query;
567
+ auto resp = test::utils::execute(integration.cluster, req);
568
+ REQUIRE_SUCCESS(resp.ctx.ec);
569
+ REQUIRE(resp.rows.size() == 2);
570
+ }
571
+
572
+ // one collection - only docs from that collection returned
573
+ {
574
+ couchbase::core::operations::search_request req{};
575
+ req.index_name = index_name;
576
+ req.query = simple_query;
577
+ req.collections.emplace_back(collection1_name);
578
+ auto resp = test::utils::execute(integration.cluster, req);
579
+ REQUIRE_SUCCESS(resp.ctx.ec);
580
+ REQUIRE(resp.rows.size() == 1);
581
+ }
582
+
583
+ // two collections - both docs returned
584
+ {
585
+ couchbase::core::operations::search_request req{};
586
+ req.index_name = index_name;
587
+ req.query = simple_query;
588
+ req.collections.emplace_back(collection1_name);
589
+ req.collections.emplace_back(collection2_name);
590
+ auto resp = test::utils::execute(integration.cluster, req);
591
+ REQUIRE_SUCCESS(resp.ctx.ec);
592
+ REQUIRE(resp.rows.size() == 2);
593
+ }
594
+
595
+ {
596
+ couchbase::core::operations::management::search_index_drop_request req{};
597
+ req.index_name = index_name;
598
+ auto resp = test::utils::execute(integration.cluster, req);
599
+ REQUIRE_SUCCESS(resp.ctx.ec);
600
+ }
601
+ }
@@ -896,4 +896,77 @@ TEST_CASE("transactions: atr and client_record are binary documents", "[transact
896
896
  REQUIRE_SUCCESS(resp.ctx.ec());
897
897
  REQUIRE(resp.value == binary_null);
898
898
  }
899
+ }
900
+ TEST_CASE("transactions: get non-existent doc fails txn", "[transactions]")
901
+ {
902
+ test::utils::integration_test_guard integration;
903
+ auto cluster = integration.cluster;
904
+ transactions txn(cluster, get_conf());
905
+ couchbase::core::document_id id{ integration.ctx.bucket, "_default", "_default", test::utils::uniq_id("txn") };
906
+ REQUIRE_THROWS_AS(txn.run([id](attempt_context& ctx) { ctx.get(id); }), transaction_exception);
907
+ }
908
+
909
+ TEST_CASE("transactions: get_optional on non-existent doc doesn't fail txn", "[transactions]")
910
+ {
911
+ test::utils::integration_test_guard integration;
912
+ auto cluster = integration.cluster;
913
+ transactions txn(cluster, get_conf());
914
+ couchbase::core::document_id id{ integration.ctx.bucket, "_default", "_default", test::utils::uniq_id("txn") };
915
+ REQUIRE_NOTHROW(txn.run([id](attempt_context& ctx) { ctx.get_optional(id); }));
916
+ }
917
+ TEST_CASE("transactions: get after query behaves same as before a query", "[transactions]")
918
+ {
919
+ test::utils::integration_test_guard integration;
920
+ auto cluster = integration.cluster;
921
+ transactions txn(cluster, get_conf());
922
+ couchbase::core::document_id id{ integration.ctx.bucket, "_default", "_default", test::utils::uniq_id("txn") };
923
+ REQUIRE_THROWS_AS(txn.run([id](attempt_context& ctx) {
924
+ ctx.query("select * from `default` limit 1");
925
+ ctx.get(id);
926
+ }),
927
+ transaction_exception);
928
+ }
929
+
930
+ TEST_CASE("transactions: get_optional after query behaves same as before a query", "[transactions]")
931
+ {
932
+ test::utils::integration_test_guard integration;
933
+ auto cluster = integration.cluster;
934
+ transactions txn(cluster, get_conf());
935
+ couchbase::core::document_id id{ integration.ctx.bucket, "_default", "_default", test::utils::uniq_id("txn") };
936
+ REQUIRE_NOTHROW(txn.run([id](attempt_context& ctx) {
937
+ ctx.query("select * from `default` limit 1");
938
+ ctx.get_optional(id);
939
+ }));
940
+ }
941
+ TEST_CASE("transactions: sergey example", "[transactions]")
942
+ {
943
+ test::utils::integration_test_guard integration;
944
+ auto cluster = integration.cluster;
945
+ transactions txn(cluster, get_conf());
946
+ couchbase::core::document_id id_to_remove{ integration.ctx.bucket, "_default", "_default", test::utils::uniq_id("txn") };
947
+ couchbase::core::document_id id_to_replace{ integration.ctx.bucket, "_default", "_default", test::utils::uniq_id("txn") };
948
+ couchbase::core::document_id id_to_insert{ integration.ctx.bucket, "_default", "_default", test::utils::uniq_id("txn") };
949
+ {
950
+ couchbase::core::operations::upsert_request req{ id_to_remove, content_json };
951
+ auto resp = test::utils::execute(integration.cluster, req);
952
+ REQUIRE_SUCCESS(resp.ctx.ec());
953
+ }
954
+ {
955
+ couchbase::core::operations::upsert_request req{ id_to_replace, content_json };
956
+ auto resp = test::utils::execute(integration.cluster, req);
957
+ REQUIRE_SUCCESS(resp.ctx.ec());
958
+ }
959
+
960
+ REQUIRE_NOTHROW(txn.run([&](attempt_context& ctx) {
961
+ ctx.query(fmt::format(
962
+ "INSERT INTO `default` (KEY, VALUE) VALUES ('{}', {})", id_to_insert.key(), couchbase::core::utils::json::generate(content)));
963
+ ctx.query(fmt::format("UPDATE `default` USE KEYS '{}' SET `some_number` = 10 ", id_to_replace.key()));
964
+ ctx.query(fmt::format("DELETE FROM `default` WHERE META().id = '{}'", id_to_remove.key()));
965
+ auto insert_res = ctx.get(id_to_insert);
966
+ CHECK(insert_res.content<tao::json::value>() == content);
967
+ auto replace_res = ctx.get(id_to_replace);
968
+ CHECK(replace_res.content<tao::json::value>()["some_number"] == 10);
969
+ auto remove_res = ctx.get_optional(id_to_remove);
970
+ CHECK_FALSE(remove_res.has_value());
971
+ }));
899
972
  }