LCOV - code coverage report
Current view: top level - backend/storage/duckdb - duckdb_storage_read_test.cc (source / functions) Coverage Total Hit
Test: _coverage_report.dat Lines: 100.0 % 377 377
Test Date: 2026-07-02 21:01:18 Functions: 100.0 % 18 18

            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
        

Generated by: LCOV version 2.0-1