Line data Source code
1 : #include <cstddef>
2 : #include <cstdint>
3 : #include <memory>
4 : #include <string>
5 : #include <utility>
6 : #include <vector>
7 :
8 : #include "absl/status/status.h"
9 : #include "absl/strings/str_cat.h"
10 : #include "absl/types/span.h"
11 : #include "backend/schema/schema.h"
12 : #include "backend/storage/duckdb/duckdb_storage.h"
13 : #include "backend/storage/duckdb/duckdb_storage_test_fixture.h"
14 : #include "backend/storage/row_restriction.h"
15 : #include "backend/storage/storage.h"
16 : #include "gtest/gtest.h"
17 :
18 : namespace bigquery_emulator {
19 : namespace backend {
20 : namespace storage {
21 : namespace duckdb {
22 : namespace {
23 :
24 : namespace {
25 :
26 : // Drains the iterator into a vector. Reused across CreateReadStream
27 : // tests; the DuckDB backend pre-materializes the rows under the lock,
28 : // so this loop is just a thin wrapper around Next() for symmetry with
29 : // the memory store's test fixture.
30 11 : std::vector<Row> Drain(std::unique_ptr<RowIterator> iter) {
31 11 : std::vector<Row> out;
32 11 : Row r;
33 33 : while (true) {
34 33 : auto has = iter->Next(&r);
35 33 : EXPECT_TRUE(has.ok());
36 33 : if (!has.ok() || !*has) break;
37 22 : out.push_back(r);
38 22 : }
39 11 : return out;
40 11 : }
41 :
42 : } // namespace
43 :
44 1 : TEST_F(DuckDBStorageTest, CreateReadStreamReturnsAllRowsByDefault) {
45 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
46 2 : ASSERT_TRUE(store_or.ok()) << store_or.status();
47 1 : auto& store = **store_or;
48 :
49 1 : const DatasetId ds{"proj-1", "ds_1"};
50 1 : const TableId table{"proj-1", "ds_1", "people"};
51 1 : ASSERT_TRUE(store.CreateDataset(ds, "US").ok());
52 1 : ASSERT_TRUE(store.CreateTable(table, PeopleSchema()).ok());
53 :
54 1 : std::vector<Row> rows;
55 6 : for (int64_t i = 0; i < 5; ++i) {
56 5 : rows.push_back(MakePerson(i, absl::StrCat("person-", i)));
57 5 : }
58 1 : ASSERT_TRUE(store.AppendRows(table, absl::MakeConstSpan(rows)).ok());
59 :
60 1 : auto iter_or = store.CreateReadStream(table, ReadFilter{});
61 2 : ASSERT_TRUE(iter_or.ok()) << iter_or.status();
62 1 : std::vector<Row> scanned = Drain(std::move(*iter_or));
63 1 : ASSERT_EQ(scanned.size(), 5u);
64 : // CreateReadStream pins the order to the parquet file_row_number,
65 : // which mirrors INSERT order; rows[i] == person-i.
66 6 : for (size_t i = 0; i < scanned.size(); ++i) {
67 5 : EXPECT_EQ(scanned[i].cells[0].int64_value(), static_cast<int64_t>(i));
68 5 : EXPECT_EQ(scanned[i].cells[1].string_value(), absl::StrCat("person-", i));
69 5 : }
70 1 : }
71 :
72 1 : TEST_F(DuckDBStorageTest, CreateReadStreamHonorsRowLimit) {
73 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
74 1 : ASSERT_TRUE(store_or.ok());
75 1 : auto& store = **store_or;
76 :
77 1 : const DatasetId ds{"proj-1", "ds_1"};
78 1 : const TableId table{"proj-1", "ds_1", "people"};
79 1 : ASSERT_TRUE(store.CreateDataset(ds, "US").ok());
80 1 : ASSERT_TRUE(store.CreateTable(table, PeopleSchema()).ok());
81 :
82 1 : std::vector<Row> rows;
83 11 : for (int64_t i = 0; i < 10; ++i) {
84 10 : rows.push_back(MakePerson(i, absl::StrCat("person-", i)));
85 10 : }
86 1 : ASSERT_TRUE(store.AppendRows(table, absl::MakeConstSpan(rows)).ok());
87 :
88 1 : ReadFilter filter;
89 1 : filter.row_limit = 3;
90 1 : auto iter_or = store.CreateReadStream(table, filter);
91 2 : ASSERT_TRUE(iter_or.ok()) << iter_or.status();
92 1 : std::vector<Row> scanned = Drain(std::move(*iter_or));
93 1 : ASSERT_EQ(scanned.size(), 3u);
94 1 : EXPECT_EQ(scanned[0].cells[0].int64_value(), 0);
95 1 : EXPECT_EQ(scanned[1].cells[0].int64_value(), 1);
96 1 : EXPECT_EQ(scanned[2].cells[0].int64_value(), 2);
97 1 : }
98 :
99 1 : TEST_F(DuckDBStorageTest, CreateReadStreamHonorsOffsetAndLimit) {
100 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
101 1 : ASSERT_TRUE(store_or.ok());
102 1 : auto& store = **store_or;
103 :
104 1 : const DatasetId ds{"proj-1", "ds_1"};
105 1 : const TableId table{"proj-1", "ds_1", "people"};
106 1 : ASSERT_TRUE(store.CreateDataset(ds, "US").ok());
107 1 : ASSERT_TRUE(store.CreateTable(table, PeopleSchema()).ok());
108 :
109 1 : std::vector<Row> rows;
110 11 : for (int64_t i = 0; i < 10; ++i) {
111 10 : rows.push_back(MakePerson(i, absl::StrCat("person-", i)));
112 10 : }
113 1 : ASSERT_TRUE(store.AppendRows(table, absl::MakeConstSpan(rows)).ok());
114 :
115 1 : ReadFilter filter;
116 1 : filter.offset = 4;
117 1 : filter.row_limit = 3;
118 1 : auto iter_or = store.CreateReadStream(table, filter);
119 2 : ASSERT_TRUE(iter_or.ok()) << iter_or.status();
120 1 : std::vector<Row> scanned = Drain(std::move(*iter_or));
121 1 : ASSERT_EQ(scanned.size(), 3u);
122 1 : EXPECT_EQ(scanned[0].cells[0].int64_value(), 4);
123 1 : EXPECT_EQ(scanned[1].cells[0].int64_value(), 5);
124 1 : EXPECT_EQ(scanned[2].cells[0].int64_value(), 6);
125 1 : }
126 :
127 1 : TEST_F(DuckDBStorageTest, CreateReadStreamOffsetOnlyReturnsTail) {
128 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
129 1 : ASSERT_TRUE(store_or.ok());
130 1 : auto& store = **store_or;
131 :
132 1 : const DatasetId ds{"proj-1", "ds_1"};
133 1 : const TableId table{"proj-1", "ds_1", "people"};
134 1 : ASSERT_TRUE(store.CreateDataset(ds, "US").ok());
135 1 : ASSERT_TRUE(store.CreateTable(table, PeopleSchema()).ok());
136 :
137 1 : std::vector<Row> rows;
138 5 : for (int64_t i = 0; i < 4; ++i) {
139 4 : rows.push_back(MakePerson(i, absl::StrCat("person-", i)));
140 4 : }
141 1 : ASSERT_TRUE(store.AppendRows(table, absl::MakeConstSpan(rows)).ok());
142 :
143 1 : ReadFilter filter;
144 1 : filter.offset = 2;
145 : // No row_limit -- DuckDB receives LIMIT ALL OFFSET 2 and yields the tail.
146 1 : auto iter_or = store.CreateReadStream(table, filter);
147 2 : ASSERT_TRUE(iter_or.ok()) << iter_or.status();
148 1 : std::vector<Row> scanned = Drain(std::move(*iter_or));
149 1 : ASSERT_EQ(scanned.size(), 2u);
150 1 : EXPECT_EQ(scanned[0].cells[0].int64_value(), 2);
151 1 : EXPECT_EQ(scanned[1].cells[0].int64_value(), 3);
152 1 : }
153 :
154 1 : TEST_F(DuckDBStorageTest, CreateReadStreamOnMissingTableIsNotFound) {
155 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
156 1 : ASSERT_TRUE(store_or.ok());
157 1 : auto& store = **store_or;
158 :
159 1 : const TableId table{"proj-1", "ds_1", "ghost"};
160 1 : auto iter_or = store.CreateReadStream(table, ReadFilter{});
161 1 : ASSERT_FALSE(iter_or.ok());
162 1 : EXPECT_EQ(iter_or.status().code(), absl::StatusCode::kNotFound);
163 1 : }
164 :
165 : // ---------------------------------------------------------------------------
166 : // row_restriction predicate pushdown
167 : //
168 : // The handler parses `<column> = <literal>` into a typed
169 : // `EqualityPredicate` and hands it to `CreateReadStream`. The DuckDB
170 : // backend renders the predicate as a `WHERE` clause and lets DuckDB
171 : // push it into the parquet scan. The literal is rendered using the
172 : // same escaping the rest of the .cc uses for INSERT, so the parser's
173 : // quoted-string form (`'O''Reilly'`) round-trips through a literal
174 : // rendering of the parsed unescaped value (`O'Reilly`).
175 : // ---------------------------------------------------------------------------
176 :
177 1 : TEST_F(DuckDBStorageTest, CreateReadStreamFiltersInt64Predicate) {
178 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
179 1 : ASSERT_TRUE(store_or.ok());
180 1 : auto& store = **store_or;
181 :
182 1 : const DatasetId ds{"proj-1", "ds_1"};
183 1 : const TableId table{"proj-1", "ds_1", "people"};
184 1 : ASSERT_TRUE(store.CreateDataset(ds, "US").ok());
185 1 : ASSERT_TRUE(store.CreateTable(table, PeopleSchema()).ok());
186 :
187 1 : std::vector<Row> rows;
188 6 : for (int64_t i = 0; i < 5; ++i) {
189 5 : rows.push_back(MakePerson(i, absl::StrCat("person-", i)));
190 5 : }
191 1 : ASSERT_TRUE(store.AppendRows(table, absl::MakeConstSpan(rows)).ok());
192 :
193 1 : EqualityPredicate pred;
194 1 : pred.column = "id";
195 1 : pred.column_index = 0;
196 1 : pred.kind = EqualityPredicate::Kind::kInt64;
197 1 : pred.int64_value = 2;
198 1 : ReadFilter filter;
199 1 : filter.equality_predicate = pred;
200 1 : auto iter_or = store.CreateReadStream(table, filter);
201 2 : ASSERT_TRUE(iter_or.ok()) << iter_or.status();
202 1 : std::vector<Row> scanned = Drain(std::move(*iter_or));
203 1 : ASSERT_EQ(scanned.size(), 1u);
204 1 : EXPECT_EQ(scanned[0].cells[0].int64_value(), 2);
205 1 : EXPECT_EQ(scanned[0].cells[1].string_value(), "person-2");
206 1 : }
207 :
208 1 : TEST_F(DuckDBStorageTest, CreateReadStreamFiltersStringPredicate) {
209 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
210 1 : ASSERT_TRUE(store_or.ok());
211 1 : auto& store = **store_or;
212 :
213 1 : const DatasetId ds{"proj-1", "ds_1"};
214 1 : const TableId table{"proj-1", "ds_1", "people"};
215 1 : ASSERT_TRUE(store.CreateDataset(ds, "US").ok());
216 1 : ASSERT_TRUE(store.CreateTable(table, PeopleSchema()).ok());
217 :
218 1 : std::vector<Row> rows = {
219 1 : MakePerson(1, "ada"),
220 : // Apostrophe in the cell exercises the literal-escape path on
221 : // the SQL-side WHERE renderer.
222 1 : MakePerson(2, "O'Reilly"),
223 1 : MakePerson(3, "grace"),
224 1 : };
225 1 : ASSERT_TRUE(store.AppendRows(table, absl::MakeConstSpan(rows)).ok());
226 :
227 1 : EqualityPredicate pred;
228 1 : pred.column = "name";
229 1 : pred.column_index = 1;
230 1 : pred.kind = EqualityPredicate::Kind::kString;
231 1 : pred.string_value = "O'Reilly";
232 1 : ReadFilter filter;
233 1 : filter.equality_predicate = pred;
234 1 : auto iter_or = store.CreateReadStream(table, filter);
235 2 : ASSERT_TRUE(iter_or.ok()) << iter_or.status();
236 1 : std::vector<Row> scanned = Drain(std::move(*iter_or));
237 1 : ASSERT_EQ(scanned.size(), 1u);
238 1 : EXPECT_EQ(scanned[0].cells[0].int64_value(), 2);
239 1 : EXPECT_EQ(scanned[0].cells[1].string_value(), "O'Reilly");
240 1 : }
241 :
242 1 : TEST_F(DuckDBStorageTest, CreateReadStreamPredicateNoMatchYieldsEmpty) {
243 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
244 1 : ASSERT_TRUE(store_or.ok());
245 1 : auto& store = **store_or;
246 :
247 1 : const DatasetId ds{"proj-1", "ds_1"};
248 1 : const TableId table{"proj-1", "ds_1", "people"};
249 1 : ASSERT_TRUE(store.CreateDataset(ds, "US").ok());
250 1 : ASSERT_TRUE(store.CreateTable(table, PeopleSchema()).ok());
251 :
252 1 : std::vector<Row> rows = {
253 1 : MakePerson(1, "ada"),
254 1 : MakePerson(2, "linus"),
255 1 : };
256 1 : ASSERT_TRUE(store.AppendRows(table, absl::MakeConstSpan(rows)).ok());
257 :
258 1 : EqualityPredicate pred;
259 1 : pred.column = "id";
260 1 : pred.column_index = 0;
261 1 : pred.kind = EqualityPredicate::Kind::kInt64;
262 1 : pred.int64_value = 999;
263 1 : ReadFilter filter;
264 1 : filter.equality_predicate = pred;
265 1 : auto iter_or = store.CreateReadStream(table, filter);
266 2 : ASSERT_TRUE(iter_or.ok()) << iter_or.status();
267 1 : std::vector<Row> scanned = Drain(std::move(*iter_or));
268 1 : EXPECT_TRUE(scanned.empty());
269 1 : }
270 :
271 : // selected_fields projection pushdown.
272 : // The DuckDB backend filters the SELECT projection list down to the
273 : // caller-supplied subset and the row decoder reads cells back in the
274 : // projected order. Verifies both the cell count and the projected
275 : // order: passing `[name, id]` returns rows where cells[0] is name
276 : // and cells[1] is id, even though the table declared `[id, name]`.
277 1 : TEST_F(DuckDBStorageTest, CreateReadStreamProjectsSelectedFields) {
278 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
279 1 : ASSERT_TRUE(store_or.ok());
280 1 : auto& store = **store_or;
281 :
282 1 : const DatasetId ds{"proj-1", "ds_1"};
283 1 : const TableId table{"proj-1", "ds_1", "people"};
284 1 : ASSERT_TRUE(store.CreateDataset(ds, "US").ok());
285 1 : ASSERT_TRUE(store.CreateTable(table, PeopleSchema()).ok());
286 :
287 1 : std::vector<Row> rows;
288 4 : for (int64_t i = 0; i < 3; ++i) {
289 3 : rows.push_back(MakePerson(i, absl::StrCat("person-", i)));
290 3 : }
291 1 : ASSERT_TRUE(store.AppendRows(table, absl::MakeConstSpan(rows)).ok());
292 :
293 1 : ReadFilter filter;
294 1 : filter.selected_fields = {"name", "id"};
295 1 : auto iter_or = store.CreateReadStream(table, filter);
296 2 : ASSERT_TRUE(iter_or.ok()) << iter_or.status();
297 1 : std::vector<Row> scanned = Drain(std::move(*iter_or));
298 1 : ASSERT_EQ(scanned.size(), 3u);
299 4 : for (size_t i = 0; i < scanned.size(); ++i) {
300 3 : ASSERT_EQ(scanned[i].cells.size(), 2u);
301 3 : EXPECT_EQ(scanned[i].cells[0].string_value(), absl::StrCat("person-", i));
302 3 : EXPECT_EQ(scanned[i].cells[1].int64_value(), static_cast<int64_t>(i));
303 3 : }
304 1 : }
305 :
306 1 : TEST_F(DuckDBStorageTest, CreateReadStreamRejectsUnknownSelectedField) {
307 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
308 1 : ASSERT_TRUE(store_or.ok());
309 1 : auto& store = **store_or;
310 :
311 1 : const DatasetId ds{"proj-1", "ds_1"};
312 1 : const TableId table{"proj-1", "ds_1", "people"};
313 1 : ASSERT_TRUE(store.CreateDataset(ds, "US").ok());
314 1 : ASSERT_TRUE(store.CreateTable(table, PeopleSchema()).ok());
315 1 : ASSERT_TRUE(
316 1 : store.AppendRows(table, absl::MakeConstSpan({MakePerson(1, "ada")}))
317 1 : .ok());
318 :
319 1 : ReadFilter filter;
320 1 : filter.selected_fields = {"phone"}; // not on the people schema
321 1 : auto iter_or = store.CreateReadStream(table, filter);
322 1 : ASSERT_FALSE(iter_or.ok());
323 1 : EXPECT_EQ(iter_or.status().code(), absl::StatusCode::kInvalidArgument);
324 1 : }
325 :
326 1 : TEST_F(DuckDBStorageTest, CreateReadStreamPredicateBeforeOffsetLimit) {
327 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
328 1 : ASSERT_TRUE(store_or.ok());
329 1 : auto& store = **store_or;
330 :
331 1 : const DatasetId ds{"proj-1", "ds_1"};
332 1 : const TableId table{"proj-1", "ds_1", "people"};
333 1 : ASSERT_TRUE(store.CreateDataset(ds, "US").ok());
334 1 : ASSERT_TRUE(store.CreateTable(table, PeopleSchema()).ok());
335 :
336 1 : std::vector<Row> rows;
337 : // Two pools of names; predicate keeps only the "odd" name pool.
338 11 : for (int64_t i = 0; i < 10; ++i) {
339 10 : rows.push_back(MakePerson(i, (i % 2 == 0) ? "even" : "odd"));
340 10 : }
341 1 : ASSERT_TRUE(store.AppendRows(table, absl::MakeConstSpan(rows)).ok());
342 :
343 1 : EqualityPredicate pred;
344 1 : pred.column = "name";
345 1 : pred.column_index = 1;
346 1 : pred.kind = EqualityPredicate::Kind::kString;
347 1 : pred.string_value = "odd";
348 1 : ReadFilter filter;
349 1 : filter.equality_predicate = pred;
350 1 : filter.offset = 1;
351 1 : filter.row_limit = 2;
352 1 : auto iter_or = store.CreateReadStream(table, filter);
353 2 : ASSERT_TRUE(iter_or.ok()) << iter_or.status();
354 1 : std::vector<Row> scanned = Drain(std::move(*iter_or));
355 : // Filtered ids: 1, 3, 5, 7, 9. offset=1, limit=2 → 3, 5.
356 1 : ASSERT_EQ(scanned.size(), 2u);
357 1 : EXPECT_EQ(scanned[0].cells[0].int64_value(), 3);
358 1 : EXPECT_EQ(scanned[1].cells[0].int64_value(), 5);
359 1 : }
360 :
361 1 : TEST(SchemaToDuckDBStorageType, MapsBignumericToVarchar) {
362 1 : EXPECT_EQ(schema::ToDuckDBStorageType(schema::ColumnType::kBignumeric),
363 1 : "VARCHAR");
364 1 : schema::ColumnSchema col;
365 1 : col.name = "amount";
366 1 : col.type = schema::ColumnType::kBignumeric;
367 1 : EXPECT_EQ(schema::ColumnSchemaToDuckDBStorageType(col), "VARCHAR");
368 1 : }
369 :
370 1 : TEST_F(DuckDBStorageTest, AppendRowsRoundTripsExtremeBignumericLiteral) {
371 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
372 2 : ASSERT_TRUE(store_or.ok()) << store_or.status();
373 1 : auto& store = **store_or;
374 :
375 1 : const DatasetId ds{"proj-1", "ds_1"};
376 1 : const TableId table{"proj-1", "ds_1", "amounts"};
377 1 : ASSERT_TRUE(store.CreateDataset(ds, "US").ok());
378 :
379 1 : schema::TableSchema schema;
380 1 : schema::ColumnSchema amount;
381 1 : amount.name = "amount";
382 1 : amount.type = schema::ColumnType::kBignumeric;
383 1 : schema.columns.push_back(amount);
384 1 : ASSERT_TRUE(store.CreateTable(table, schema).ok());
385 :
386 1 : const char* kManagedWriterLiteral =
387 1 : "578960446186580977117854925043439539266."
388 1 : "34992332820282019728792003956564819967";
389 1 : Row row;
390 1 : row.cells.push_back(Value::String(kManagedWriterLiteral));
391 1 : ASSERT_TRUE(store.AppendRows(table, absl::MakeConstSpan({row})).ok());
392 :
393 1 : auto iter_or = store.CreateReadStream(table, ReadFilter{});
394 2 : ASSERT_TRUE(iter_or.ok()) << iter_or.status();
395 1 : std::vector<Row> scanned = Drain(std::move(*iter_or));
396 1 : ASSERT_EQ(scanned.size(), 1u);
397 1 : ASSERT_EQ(scanned[0].cells[0].string_value(), kManagedWriterLiteral);
398 1 : }
399 :
400 1 : TEST(SchemaToDuckDBType, RoundTripsAllPlanCoveredTypes) {
401 1 : struct Case {
402 1 : schema::ColumnType bq = schema::ColumnType::kUnknown;
403 1 : absl::string_view duckdb;
404 1 : };
405 1 : const Case cases[] = {
406 1 : {schema::ColumnType::kInt64, "BIGINT"},
407 1 : {schema::ColumnType::kFloat64, "DOUBLE"},
408 1 : {schema::ColumnType::kBool, "BOOLEAN"},
409 1 : {schema::ColumnType::kString, "VARCHAR"},
410 1 : {schema::ColumnType::kBytes, "BLOB"},
411 1 : {schema::ColumnType::kDate, "DATE"},
412 1 : {schema::ColumnType::kTime, "TIME"},
413 1 : {schema::ColumnType::kDatetime, "TIMESTAMP"},
414 1 : {schema::ColumnType::kTimestamp, "TIMESTAMP WITH TIME ZONE"},
415 1 : {schema::ColumnType::kNumeric, "DECIMAL(38, 9)"},
416 1 : {schema::ColumnType::kBignumeric, "DECIMAL(38, 38)"},
417 1 : {schema::ColumnType::kJson, "JSON"},
418 1 : };
419 12 : for (const auto& c : cases) {
420 24 : EXPECT_EQ(schema::ToDuckDBType(c.bq), c.duckdb)
421 24 : << "kind=" << static_cast<int>(c.bq);
422 : // FromDuckDBType only needs to accept the bare head; the
423 : // TIMESTAMP WITH TIME ZONE alias falls through the suffix
424 : // check inside the function and round-trips back to
425 : // kTimestamp.
426 12 : if (c.bq != schema::ColumnType::kBignumeric) {
427 22 : EXPECT_EQ(schema::FromDuckDBType(c.duckdb), c.bq)
428 22 : << "duckdb=" << c.duckdb;
429 11 : }
430 12 : }
431 1 : }
432 :
433 1 : TEST(SchemaToDuckDBType, RendersRepeatedAsList) {
434 1 : schema::ColumnSchema col;
435 1 : col.name = "tags";
436 1 : col.type = schema::ColumnType::kString;
437 1 : col.mode = schema::ColumnMode::kRepeated;
438 1 : EXPECT_EQ(schema::ColumnSchemaToDuckDBType(col), "VARCHAR[]");
439 1 : }
440 :
441 1 : TEST_F(DuckDBStorageTest, CreateReadStreamRoundTripsRepeatedStringColumn) {
442 1 : auto store_or = DuckDBStorage::Open(data_dir_.string());
443 2 : ASSERT_TRUE(store_or.ok()) << store_or.status();
444 1 : auto& store = **store_or;
445 :
446 1 : const DatasetId ds{"proj-1", "ds_1"};
447 1 : const TableId table{"proj-1", "ds_1", "people_tags"};
448 1 : ASSERT_TRUE(store.CreateDataset(ds, "US").ok());
449 :
450 1 : schema::TableSchema schema;
451 1 : schema::ColumnSchema id;
452 1 : id.name = "id";
453 1 : id.type = schema::ColumnType::kInt64;
454 1 : schema.columns.push_back(id);
455 1 : schema::ColumnSchema tags;
456 1 : tags.name = "tags";
457 1 : tags.type = schema::ColumnType::kString;
458 1 : tags.mode = schema::ColumnMode::kRepeated;
459 1 : schema.columns.push_back(tags);
460 1 : ASSERT_TRUE(store.CreateTable(table, schema).ok());
461 :
462 1 : Row row;
463 1 : row.cells.push_back(Value::Int64(0));
464 1 : std::vector<Value> tag_values;
465 1 : tag_values.push_back(Value::String("t0"));
466 1 : row.cells.push_back(Value::Array(std::move(tag_values)));
467 1 : ASSERT_TRUE(store.AppendRows(table, absl::MakeConstSpan({row})).ok());
468 :
469 1 : auto iter_or = store.CreateReadStream(table, ReadFilter{});
470 2 : ASSERT_TRUE(iter_or.ok()) << iter_or.status();
471 1 : std::vector<Row> scanned = Drain(std::move(*iter_or));
472 1 : ASSERT_EQ(scanned.size(), 1u);
473 1 : ASSERT_EQ(scanned[0].cells[1].kind(), Value::Kind::kArray);
474 1 : ASSERT_EQ(scanned[0].cells[1].array_value().size(), 1u);
475 1 : EXPECT_EQ(scanned[0].cells[1].array_value()[0].string_value(), "t0");
476 1 : }
477 :
478 1 : TEST(SchemaToDuckDBType, RendersStructWithNestedFields) {
479 1 : schema::ColumnSchema col;
480 1 : col.name = "person";
481 1 : col.type = schema::ColumnType::kStruct;
482 1 : schema::ColumnSchema id;
483 1 : id.name = "id";
484 1 : id.type = schema::ColumnType::kInt64;
485 1 : schema::ColumnSchema labels;
486 1 : labels.name = "labels";
487 1 : labels.type = schema::ColumnType::kString;
488 1 : labels.mode = schema::ColumnMode::kRepeated;
489 1 : col.fields = {id, labels};
490 1 : EXPECT_EQ(schema::ColumnSchemaToDuckDBType(col),
491 1 : "STRUCT(\"id\" BIGINT, \"labels\" VARCHAR[])");
492 1 : }
493 :
494 : } // namespace
495 : } // namespace duckdb
496 : } // namespace storage
497 : } // namespace backend
498 : } // namespace bigquery_emulator
|