Line data Source code
1 : // Direct (no gRPC socket) tests for `StorageReadService::CreateReadSession`.
2 :
3 : #include "frontend/handlers/storage_read_internal.h"
4 : #include "frontend/handlers/storage_read_test_fixture.h"
5 : #include "grpcpp/grpcpp.h"
6 :
7 : namespace bigquery_emulator {
8 : namespace frontend {
9 : namespace {
10 1 : TEST_F(StorageReadServiceTest, CreateReadSessionReturnsSessionStreamAndSchema) {
11 1 : CreatePeopleTable();
12 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
13 1 : v1::ReadSession resp;
14 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
15 2 : ASSERT_TRUE(status.ok()) << status.error_message();
16 :
17 : // Session name uses the canonical
18 : // `projects/{p}/locations/-/sessions/s{N}` shape so the gateway can
19 : // synthesize matching BigQuery REST IDs.
20 1 : EXPECT_EQ(resp.name(), "projects/proj-test/locations/-/sessions/s1");
21 :
22 : // Table is echoed verbatim so the caller does not need to re-derive it.
23 1 : EXPECT_EQ(resp.table(), "projects/proj-test/datasets/ds/tables/t");
24 :
25 : // Schema is attached so a follow-up ReadRows does not have to
26 : // round-trip back through DescribeTable. The handler sends every
27 : // column in declaration order.
28 1 : ASSERT_EQ(resp.schema().fields_size(), 3);
29 1 : EXPECT_EQ(resp.schema().fields(0).name(), "id");
30 1 : EXPECT_EQ(resp.schema().fields(0).type(), "INT64");
31 1 : EXPECT_EQ(resp.schema().fields(0).mode(), "REQUIRED");
32 1 : EXPECT_EQ(resp.schema().fields(1).name(), "name");
33 1 : EXPECT_EQ(resp.schema().fields(1).type(), "STRING");
34 1 : EXPECT_EQ(resp.schema().fields(2).name(), "tags");
35 1 : EXPECT_EQ(resp.schema().fields(2).type(), "STRING");
36 1 : EXPECT_EQ(resp.schema().fields(2).mode(), "REPEATED");
37 :
38 : // Exactly one stream today; `max_stream_count`-driven parallel
39 : // streams are not implemented. The stream id nests under the
40 : // session name.
41 1 : ASSERT_EQ(resp.streams_size(), 1);
42 1 : EXPECT_EQ(resp.streams(0).name(),
43 1 : "projects/proj-test/locations/-/sessions/s1/streams/0");
44 :
45 : // SessionsForTesting reflects the mint we just did (and only that
46 : // one). This is the test hook the ReadRows tests use to assert
47 : // the lookup actually consults the session map.
48 1 : EXPECT_EQ(service_->SessionsForTesting(), 1u);
49 1 : }
50 :
51 1 : TEST_F(StorageReadServiceTest, CreateReadSessionEchoesReadOptions) {
52 1 : CreatePeopleTable();
53 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
54 1 : auto* options = req.mutable_read_session()->mutable_read_options();
55 1 : options->add_selected_fields("id");
56 1 : options->add_selected_fields("name");
57 : // The handler parses the row_restriction at session-mint
58 : // time. `id = 0` is the canonical happy-path shape (single
59 : // `<column> = <literal>` equality); range / connective forms now
60 : // surface INVALID_ARGUMENT from CreateReadSession, which has its
61 : // own dedicated test below.
62 1 : options->set_row_restriction("id = 0");
63 1 : v1::ReadSession resp;
64 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
65 2 : ASSERT_TRUE(status.ok()) << status.error_message();
66 :
67 : // The proto contract says read_options round-trips on the reply so
68 : // the gateway can show the caller what shape the session was minted
69 : // with. The handler honors selected_fields, so the
70 : // response *schema* reflects the projection (id, name only)
71 : // while the read_options round-trip still echoes the request.
72 1 : ASSERT_EQ(resp.read_options().selected_fields_size(), 2);
73 1 : EXPECT_EQ(resp.read_options().selected_fields(0), "id");
74 1 : EXPECT_EQ(resp.read_options().selected_fields(1), "name");
75 1 : EXPECT_EQ(resp.read_options().row_restriction(), "id = 0");
76 :
77 : // The response schema is projected to the caller's
78 : // selected_fields list, in caller-supplied order. The full table
79 : // has three columns; pinning "id" + "name" must drop "tags".
80 1 : ASSERT_EQ(resp.schema().fields_size(), 2);
81 1 : EXPECT_EQ(resp.schema().fields(0).name(), "id");
82 1 : EXPECT_EQ(resp.schema().fields(1).name(), "name");
83 1 : }
84 :
85 1 : TEST_F(StorageReadServiceTest, CreateReadSessionRejectsUnknownSelectedField) {
86 1 : CreatePeopleTable();
87 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
88 : // `phone` is not a column on the people table; the handler rejects
89 : // this at session-mint time so the streaming RPC never starts
90 : // against an invalid projection.
91 1 : req.mutable_read_session()->mutable_read_options()->add_selected_fields(
92 1 : "phone");
93 1 : v1::ReadSession resp;
94 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
95 2 : EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
96 2 : << status.error_message();
97 1 : EXPECT_EQ(service_->SessionsForTesting(), 0u);
98 1 : }
99 :
100 1 : TEST_F(StorageReadServiceTest, CreateReadSessionRejectsEmptySelectedField) {
101 1 : CreatePeopleTable();
102 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
103 : // An empty entry is a wire-level oddity but the handler refuses
104 : // it explicitly so a buggy client cannot smuggle a no-match
105 : // projection that would silently approximate "all columns".
106 1 : req.mutable_read_session()->mutable_read_options()->add_selected_fields("");
107 1 : v1::ReadSession resp;
108 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
109 2 : EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
110 2 : << status.error_message();
111 1 : }
112 :
113 1 : TEST_F(StorageReadServiceTest, CreateReadSessionAcceptsRangeRestriction) {
114 1 : CreatePeopleTable();
115 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
116 1 : req.mutable_read_session()->mutable_read_options()->set_row_restriction(
117 1 : "id > 0");
118 1 : v1::ReadSession resp;
119 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
120 2 : ASSERT_TRUE(status.ok()) << status.error_message();
121 1 : EXPECT_EQ(resp.read_options().row_restriction(), "id > 0");
122 1 : }
123 :
124 : TEST_F(StorageReadServiceTest,
125 1 : CreateReadSessionRejectsUnknownColumnRestriction) {
126 1 : CreatePeopleTable();
127 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
128 1 : req.mutable_read_session()->mutable_read_options()->set_row_restriction(
129 1 : "missing = 1");
130 1 : v1::ReadSession resp;
131 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
132 2 : EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
133 2 : << status.error_message();
134 1 : }
135 :
136 1 : TEST_F(StorageReadServiceTest, CreateReadSessionMintsUniqueSessionIds) {
137 1 : CreatePeopleTable();
138 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
139 :
140 1 : v1::ReadSession first;
141 1 : v1::ReadSession second;
142 1 : ASSERT_TRUE(service_->CreateReadSession(nullptr, &req, &first).ok());
143 1 : ASSERT_TRUE(service_->CreateReadSession(nullptr, &req, &second).ok());
144 1 : EXPECT_NE(first.name(), second.name());
145 1 : EXPECT_EQ(service_->SessionsForTesting(), 2u);
146 1 : }
147 :
148 : // ---------------------------------------------------------------------------
149 : // CreateReadSession: validation errors
150 : // ---------------------------------------------------------------------------
151 :
152 1 : TEST_F(StorageReadServiceTest, CreateReadSessionEmptyParentIsInvalidArgument) {
153 1 : CreatePeopleTable();
154 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
155 1 : req.set_parent("");
156 1 : v1::ReadSession resp;
157 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
158 2 : EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
159 2 : << status.error_message();
160 1 : EXPECT_EQ(service_->SessionsForTesting(), 0u);
161 1 : }
162 :
163 : TEST_F(StorageReadServiceTest,
164 1 : CreateReadSessionMalformedParentIsInvalidArgument) {
165 1 : CreatePeopleTable();
166 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
167 : // Missing the `projects/` prefix; matches the public BigQuery
168 : // surface, which rejects the same shape.
169 1 : req.set_parent("proj-test");
170 1 : v1::ReadSession resp;
171 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
172 2 : EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
173 2 : << status.error_message();
174 1 : }
175 :
176 1 : TEST_F(StorageReadServiceTest, CreateReadSessionEmptyTableIsInvalidArgument) {
177 1 : CreatePeopleTable();
178 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
179 1 : req.mutable_read_session()->set_table("");
180 1 : v1::ReadSession resp;
181 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
182 2 : EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
183 2 : << status.error_message();
184 1 : }
185 :
186 : TEST_F(StorageReadServiceTest,
187 1 : CreateReadSessionMalformedTableIsInvalidArgument) {
188 1 : CreatePeopleTable();
189 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
190 : // Drop the `tables/` segment so the path no longer has 6 parts.
191 1 : req.mutable_read_session()->set_table("projects/proj-test/datasets/ds/t");
192 1 : v1::ReadSession resp;
193 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
194 2 : EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
195 2 : << status.error_message();
196 1 : }
197 :
198 : TEST_F(StorageReadServiceTest,
199 1 : CreateReadSessionParentTableProjectMismatchIsInvalidArgument) {
200 1 : CreatePeopleTable();
201 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
202 1 : req.set_parent("projects/other-proj");
203 1 : v1::ReadSession resp;
204 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
205 2 : EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
206 2 : << status.error_message();
207 1 : EXPECT_EQ(service_->SessionsForTesting(), 0u);
208 1 : }
209 :
210 : TEST_F(StorageReadServiceTest,
211 1 : CreateReadSessionAllowsPublicDataTableWithCallerParent) {
212 1 : backend::schema::TableSchema schema;
213 1 : backend::schema::ColumnSchema name;
214 1 : name.name = "name";
215 1 : name.type = backend::schema::ColumnType::kString;
216 1 : schema.columns.push_back(name);
217 1 : ASSERT_TRUE(
218 1 : storage_->CreateDataset({internal::kPublicDataProject, "usa_names"}, "US")
219 1 : .ok());
220 1 : ASSERT_TRUE(
221 1 : storage_
222 1 : ->CreateTable({internal::kPublicDataProject, "usa_names", "names"},
223 1 : schema)
224 1 : .ok());
225 :
226 1 : v1::CreateReadSessionRequest req;
227 1 : req.set_parent("projects/dev");
228 1 : req.mutable_read_session()->set_table(
229 1 : "projects/bigquery-public-data/datasets/usa_names/tables/names");
230 1 : v1::ReadSession resp;
231 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
232 2 : ASSERT_TRUE(status.ok()) << status.error_message();
233 1 : EXPECT_EQ(service_->SessionsForTesting(), 1u);
234 1 : }
235 :
236 1 : TEST_F(StorageReadServiceTest, CreateReadSessionMissingTableIsNotFound) {
237 : // Dataset + project exist but the table does not. Storage::GetSchema
238 : // returns NotFound and the handler maps that onto gRPC NOT_FOUND.
239 1 : ASSERT_TRUE(storage_->CreateDataset({"proj-test", "ds"}, "US").ok());
240 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
241 1 : v1::ReadSession resp;
242 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
243 2 : EXPECT_EQ(status.error_code(), ::grpc::StatusCode::NOT_FOUND)
244 2 : << status.error_message();
245 1 : }
246 :
247 1 : TEST_F(StorageReadServiceTest, CreateReadSessionMissingDatasetIsNotFound) {
248 : // Neither dataset nor table exist; same NOT_FOUND mapping but
249 : // through a different storage path.
250 1 : v1::CreateReadSessionRequest req = MakePeopleRequest();
251 1 : v1::ReadSession resp;
252 1 : ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
253 2 : EXPECT_EQ(status.error_code(), ::grpc::StatusCode::NOT_FOUND)
254 2 : << status.error_message();
255 1 : }
256 :
257 : // ---------------------------------------------------------------------------
258 : // ReadRows: validation-only tests that do not require a real
259 : // ServerWriter. Happy-path tests live in the StorageReadGrpcTest
260 : // fixture below; they need an in-process server because
261 : // ::grpc::ServerWriter is concrete and not designed to be mocked.
262 : // ---------------------------------------------------------------------------
263 :
264 1 : TEST_F(StorageReadServiceTest, ReadRowsRejectsMalformedReadStream) {
265 1 : v1::ReadRowsRequest req;
266 : // Missing the `/streams/0` suffix entirely. The handler only mints
267 : // streams/0 and refuses every other shape.
268 1 : req.set_read_stream("projects/proj-test/locations/-/sessions/s1");
269 1 : ::grpc::Status status = service_->ReadRows(nullptr, &req, nullptr);
270 2 : EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
271 2 : << status.error_message();
272 1 : }
273 :
274 1 : TEST_F(StorageReadServiceTest, ReadRowsRejectsEmptyReadStream) {
275 1 : v1::ReadRowsRequest req;
276 1 : ::grpc::Status status = service_->ReadRows(nullptr, &req, nullptr);
277 2 : EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
278 2 : << status.error_message();
279 1 : }
280 : } // namespace
281 : } // namespace frontend
282 : } // namespace bigquery_emulator
|