/Users/deen/code/yugabyte-db/src/yb/yql/pggate/pg_dml.cc
Line | Count | Source (jump to first uncovered line) |
1 | | //-------------------------------------------------------------------------------------------------- |
2 | | // Copyright (c) YugaByte, Inc. |
3 | | // |
4 | | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
5 | | // in compliance with the License. You may obtain a copy of the License at |
6 | | // |
7 | | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software distributed under the License |
10 | | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
11 | | // or implied. See the License for the specific language governing permissions and limitations |
12 | | // under the License. |
13 | | // |
14 | | //-------------------------------------------------------------------------------------------------- |
15 | | |
16 | | #include "yb/yql/pggate/pg_dml.h" |
17 | | |
18 | | #include "yb/client/yb_op.h" |
19 | | |
20 | | #include "yb/common/pg_system_attr.h" |
21 | | |
22 | | #include "yb/util/atomic.h" |
23 | | #include "yb/util/status_format.h" |
24 | | |
25 | | #include "yb/yql/pggate/pg_select_index.h" |
26 | | #include "yb/yql/pggate/pggate_flags.h" |
27 | | #include "yb/yql/pggate/util/pg_doc_data.h" |
28 | | #include "yb/yql/pggate/ybc_pggate.h" |
29 | | |
30 | | namespace yb { |
31 | | namespace pggate { |
32 | | |
33 | | using namespace std::literals; // NOLINT |
34 | | using std::list; |
35 | | |
36 | | // TODO(neil) This should be derived from a GFLAGS. |
37 | | static MonoDelta kSessionTimeout = 60s; |
38 | | |
39 | | //-------------------------------------------------------------------------------------------------- |
40 | | // PgDml |
41 | | //-------------------------------------------------------------------------------------------------- |
42 | | |
43 | | PgDml::PgDml(PgSession::ScopedRefPtr pg_session, const PgObjectId& table_id) |
44 | 2.84M | : PgStatement(std::move(pg_session)), table_id_(table_id) { |
45 | 2.84M | } |
46 | | |
47 | | PgDml::PgDml(PgSession::ScopedRefPtr pg_session, |
48 | | const PgObjectId& table_id, |
49 | | const PgObjectId& index_id, |
50 | | const PgPrepareParameters *prepare_params) |
51 | 713k | : PgDml(pg_session, table_id) { |
52 | | |
53 | 713k | if (prepare_params) { |
54 | 678k | prepare_params_ = *prepare_params; |
55 | | // Primary index does not have its own data table. |
56 | 678k | if (prepare_params_.use_secondary_index) { |
57 | 347k | index_id_ = index_id; |
58 | 347k | } |
59 | 678k | } |
60 | 713k | } |
61 | | |
62 | 2.84M | PgDml::~PgDml() { |
63 | 2.84M | } |
64 | | |
65 | | //-------------------------------------------------------------------------------------------------- |
66 | | |
67 | 7.54M | Status PgDml::AppendTarget(PgExpr *target) { |
68 | | // Except for base_ctid, all targets should be appended to this DML. |
69 | 7.54M | if (target_ && (prepare_params_.index_only_scan || !target->is_ybbasetid())) { |
70 | 7.39M | RETURN_NOT_OK(AppendTargetPB(target)); |
71 | 151k | } else { |
72 | | // Append base_ctid to the index_query. |
73 | 151k | RETURN_NOT_OK(secondary_index_query_->AppendTargetPB(target)); |
74 | 151k | } |
75 | | |
76 | 7.54M | return Status::OK(); |
77 | 7.54M | } |
78 | | |
79 | 7.54M | Status PgDml::AppendTargetPB(PgExpr *target) { |
80 | | // Append to targets_. |
81 | 7.54M | targets_.push_back(target); |
82 | | |
83 | | // Allocate associated protobuf. |
84 | 7.54M | PgsqlExpressionPB *expr_pb = AllocTargetPB(); |
85 | | |
86 | | // Prepare expression. Except for constants and place_holders, all other expressions can be |
87 | | // evaluate just one time during prepare. |
88 | 7.54M | RETURN_NOT_OK(target->PrepareForRead(this, expr_pb)); |
89 | | |
90 | | // Link the given expression "attr_value" with the allocated protobuf. Note that except for |
91 | | // constants and place_holders, all other expressions can be setup just one time during prepare. |
92 | | // Example: |
93 | | // - Bind values for a target of SELECT |
94 | | // SELECT AVG(col + ?) FROM a_table; |
95 | 7.54M | expr_binds_[expr_pb] = target; |
96 | 7.54M | return Status::OK(); |
97 | 7.54M | } |
98 | | |
99 | 22 | Status PgDml::AppendQual(PgExpr *qual) { |
100 | | // Append to quals_. |
101 | 22 | quals_.push_back(qual); |
102 | | |
103 | | // Allocate associated protobuf. |
104 | 22 | PgsqlExpressionPB *expr_pb = AllocQualPB(); |
105 | | |
106 | | // Populate the expr_pb with data from the qual expression. |
107 | | // Side effect of PrepareForRead is to call PrepareColumnForRead on "this" being passed in |
108 | | // for any column reference found in the expression. However, the serialized Postgres expressions, |
109 | | // the only kind of Postgres expressions supported as quals, can not be searched. |
110 | | // Their column references should be explicitly appended with AppendColumnRef() |
111 | 22 | return qual->PrepareForRead(this, expr_pb); |
112 | 22 | } |
113 | | |
114 | 3.94k | Status PgDml::AppendColumnRef(PgExpr *colref) { |
115 | 0 | DCHECK(colref->is_colref()) << "Colref is expected"; |
116 | | // Postgres attribute number, this is column id to refer the column from Postgres code |
117 | 3.94k | int attr_num = static_cast<PgColumnRef *>(colref)->attr_num(); |
118 | | // Retrieve column metadata from the target relation metadata |
119 | 3.94k | PgColumn& col = VERIFY_RESULT(target_.ColumnForAttr(attr_num)); |
120 | 3.94k | if (!col.is_virtual_column()) { |
121 | | // Do not overwrite Postgres |
122 | 3.94k | if (!col.has_pg_type_info()) { |
123 | | // Postgres type information is required to get column value to evaluate serialized Postgres |
124 | | // expressions. For other purposes it is OK to use InvalidOids (zeroes). That would not make |
125 | | // the column to appear like it has Postgres type information. |
126 | | // Note, that for expression kinds other than serialized Postgres expressions column |
127 | | // references are set automatically: when the expressions are being appended they call either |
128 | | // PrepareColumnForRead or PrepareColumnForWrite for each column reference expression they |
129 | | // contain. |
130 | 3.93k | col.set_pg_type_info(colref->get_pg_typid(), |
131 | 3.93k | colref->get_pg_typmod(), |
132 | 3.93k | colref->get_pg_collid()); |
133 | 3.93k | } |
134 | | // Flag column as used, so it is added to the request |
135 | 3.94k | col.set_read_requested(true); |
136 | 3.94k | } |
137 | 3.94k | return Status::OK(); |
138 | 3.94k | } |
139 | | |
140 | 7.54M | Result<const PgColumn&> PgDml::PrepareColumnForRead(int attr_num, PgsqlExpressionPB *target_pb) { |
141 | | // Find column from targeted table. |
142 | 7.54M | PgColumn& col = VERIFY_RESULT(target_.ColumnForAttr(attr_num)); |
143 | | |
144 | | // Prepare protobuf to send to DocDB. |
145 | 7.54M | if (target_pb) { |
146 | 7.54M | target_pb->set_column_id(col.id()); |
147 | 7.54M | } |
148 | | |
149 | | // Mark non-virtual column reference for DocDB. |
150 | 7.54M | if (!col.is_virtual_column()) { |
151 | 7.05M | col.set_read_requested(true); |
152 | 7.05M | } |
153 | | |
154 | 7.54M | return const_cast<const PgColumn&>(col); |
155 | 7.54M | } |
156 | | |
157 | 259k | Status PgDml::PrepareColumnForWrite(PgColumn *pg_col, PgsqlExpressionPB *assign_pb) { |
158 | | // Prepare protobuf to send to DocDB. |
159 | 259k | assign_pb->set_column_id(pg_col->id()); |
160 | | |
161 | | // Mark non-virtual column reference for DocDB. |
162 | 259k | if (!pg_col->is_virtual_column()) { |
163 | 259k | pg_col->set_write_requested(true); |
164 | 259k | } |
165 | | |
166 | 259k | return Status::OK(); |
167 | 259k | } |
168 | | |
169 | 2.84M | void PgDml::ColumnRefsToPB(PgsqlColumnRefsPB *column_refs) { |
170 | 2.84M | column_refs->Clear(); |
171 | 19.8M | for (const PgColumn& col : target_.columns()) { |
172 | 19.8M | if (col.read_requested() || col.write_requested()) { |
173 | 7.31M | column_refs->add_ids(col.id()); |
174 | 7.31M | } |
175 | 19.8M | } |
176 | 2.84M | } |
177 | | |
178 | 2.84M | void PgDml::ColRefsToPB() { |
179 | | // Remove previously set column references in case if the statement is being reexecuted |
180 | 2.84M | ClearColRefPBs(); |
181 | 19.8M | for (const PgColumn& col : target_.columns()) { |
182 | | // Only used columns are added to the request |
183 | 19.8M | if (col.read_requested() || col.write_requested()) { |
184 | | // Allocate a protobuf entry |
185 | 7.31M | PgsqlColRefPB *col_ref = AllocColRefPB(); |
186 | | // Add DocDB identifier |
187 | 7.31M | col_ref->set_column_id(col.id()); |
188 | | // Add Postgres identifier |
189 | 7.31M | col_ref->set_attno(col.attr_num()); |
190 | | // Add Postgres type information, if defined |
191 | 7.31M | if (col.has_pg_type_info()) { |
192 | 3.93k | col_ref->set_typid(col.pg_typid()); |
193 | 3.93k | col_ref->set_typmod(col.pg_typmod()); |
194 | 3.93k | col_ref->set_collid(col.pg_collid()); |
195 | 3.93k | } |
196 | 7.31M | } |
197 | 19.8M | } |
198 | 2.84M | } |
199 | | |
200 | | //-------------------------------------------------------------------------------------------------- |
201 | | |
202 | 10.0M | Status PgDml::BindColumn(int attr_num, PgExpr *attr_value) { |
203 | 10.0M | if (secondary_index_query_) { |
204 | | // Bind by secondary key. |
205 | 313k | return secondary_index_query_->BindColumn(attr_num, attr_value); |
206 | 313k | } |
207 | | |
208 | | // Find column to bind. |
209 | 9.75M | PgColumn& column = VERIFY_RESULT(bind_.ColumnForAttr(attr_num)); |
210 | | |
211 | | // Check datatype. |
212 | 9.75M | if (attr_value->internal_type() != InternalType::kGinNullValue) { |
213 | 9.75M | SCHECK_EQ(column.internal_type(), attr_value->internal_type(), Corruption, |
214 | 9.75M | "Attribute value type does not match column type"); |
215 | 9.75M | } |
216 | | |
217 | | // Alloc the protobuf. |
218 | 9.75M | PgsqlExpressionPB *bind_pb = column.bind_pb(); |
219 | 9.75M | if (bind_pb == nullptr) { |
220 | 6.25M | bind_pb = AllocColumnBindPB(&column); |
221 | 3.49M | } else { |
222 | 3.49M | if (expr_binds_.find(bind_pb) != expr_binds_.end()) { |
223 | 0 | LOG(WARNING) << strings::Substitute("Column $0 is already bound to another value.", attr_num); |
224 | 0 | } |
225 | 3.49M | } |
226 | | |
227 | | // Link the given expression "attr_value" with the allocated protobuf. Note that except for |
228 | | // constants and place_holders, all other expressions can be setup just one time during prepare. |
229 | | // Examples: |
230 | | // - Bind values for primary columns in where clause. |
231 | | // WHERE hash = ? |
232 | | // - Bind values for a column in INSERT statement. |
233 | | // INSERT INTO a_table(hash, key, col) VALUES(?, ?, ?) |
234 | 9.75M | expr_binds_[bind_pb] = attr_value; |
235 | 9.75M | if (attr_num == static_cast<int>(PgSystemAttrNum::kYBTupleId)) { |
236 | 18.4E | CHECK(attr_value->is_constant()) << "Column ybctid must be bound to constant"; |
237 | 1.78M | ybctid_bind_ = true; |
238 | 1.78M | } |
239 | 9.75M | return Status::OK(); |
240 | 9.75M | } |
241 | | |
242 | 2.84M | Status PgDml::UpdateBindPBs() { |
243 | 18.6M | for (const auto &entry : expr_binds_) { |
244 | 18.6M | PgsqlExpressionPB *expr_pb = entry.first; |
245 | 18.6M | PgExpr *attr_value = entry.second; |
246 | 18.6M | RETURN_NOT_OK(attr_value->Eval(expr_pb)); |
247 | 18.6M | } |
248 | | |
249 | 2.84M | return Status::OK(); |
250 | 2.84M | } |
251 | | |
252 | | //-------------------------------------------------------------------------------------------------- |
253 | | |
254 | 19 | Status PgDml::BindTable() { |
255 | 19 | bind_table_ = true; |
256 | 19 | return Status::OK(); |
257 | 19 | } |
258 | | |
259 | | //-------------------------------------------------------------------------------------------------- |
260 | | |
261 | 259k | Status PgDml::AssignColumn(int attr_num, PgExpr *attr_value) { |
262 | | // Find column from targeted table. |
263 | 259k | PgColumn& column = VERIFY_RESULT(target_.ColumnForAttr(attr_num)); |
264 | | |
265 | | // Check datatype. |
266 | 259k | SCHECK_EQ(column.internal_type(), attr_value->internal_type(), Corruption, |
267 | 259k | "Attribute value type does not match column type"); |
268 | | |
269 | | // Alloc the protobuf. |
270 | 259k | PgsqlExpressionPB *assign_pb = column.assign_pb(); |
271 | 259k | if (assign_pb == nullptr) { |
272 | 259k | assign_pb = AllocColumnAssignPB(&column); |
273 | 0 | } else { |
274 | 0 | if (expr_assigns_.find(assign_pb) != expr_assigns_.end()) { |
275 | 0 | return STATUS_SUBSTITUTE(InvalidArgument, |
276 | 0 | "Column $0 is already assigned to another value", attr_num); |
277 | 0 | } |
278 | 259k | } |
279 | | |
280 | | // Link the expression and protobuf. During execution, expr will write result to the pb. |
281 | | // - Prepare the left hand side for write. |
282 | | // - Prepare the right hand side for read. Currently, the right hand side is always constant. |
283 | 259k | RETURN_NOT_OK(PrepareColumnForWrite(&column, assign_pb)); |
284 | 259k | RETURN_NOT_OK(attr_value->PrepareForRead(this, assign_pb)); |
285 | | |
286 | | // Link the given expression "attr_value" with the allocated protobuf. Note that except for |
287 | | // constants and place_holders, all other expressions can be setup just one time during prepare. |
288 | | // Examples: |
289 | | // - Setup rhs values for SET column = assign_pb in UPDATE statement. |
290 | | // UPDATE a_table SET col = assign_expr; |
291 | 259k | expr_assigns_[assign_pb] = attr_value; |
292 | | |
293 | 259k | return Status::OK(); |
294 | 259k | } |
295 | | |
296 | 2.12M | Status PgDml::UpdateAssignPBs() { |
297 | | // Process the column binds for two cases. |
298 | | // For performance reasons, we might evaluate these expressions together with bind values in YB. |
299 | 259k | for (const auto &entry : expr_assigns_) { |
300 | 259k | PgsqlExpressionPB *expr_pb = entry.first; |
301 | 259k | PgExpr *attr_value = entry.second; |
302 | 259k | RETURN_NOT_OK(attr_value->Eval(expr_pb)); |
303 | 259k | } |
304 | | |
305 | 2.12M | return Status::OK(); |
306 | 2.12M | } |
307 | | |
308 | | //-------------------------------------------------------------------------------------------------- |
309 | | |
310 | 963k | Result<bool> PgDml::ProcessSecondaryIndexRequest(const PgExecParameters *exec_params) { |
311 | 963k | if (!secondary_index_query_) { |
312 | | // Secondary INDEX is not used in this request. |
313 | 684k | return false; |
314 | 684k | } |
315 | | |
316 | | // Execute query in PgGate. |
317 | | // If index query is not yet executed, run it. |
318 | 278k | if (!secondary_index_query_->is_executed()) { |
319 | 151k | secondary_index_query_->set_is_executed(true); |
320 | 151k | RETURN_NOT_OK(secondary_index_query_->Exec(exec_params)); |
321 | 151k | } |
322 | | |
323 | | // Not processing index request if it does not require its own doc operator. |
324 | | // |
325 | | // When INDEX is used for system catalog (colocated table), the index subquery does not have its |
326 | | // own operator. The request is combined with 'this' outer SELECT using 'index_request' attribute. |
327 | | // (PgDocOp)doc_op_->(YBPgsqlReadOp)read_op_->(PgsqlReadRequestPB)read_request_::index_request |
328 | 278k | if (!secondary_index_query_->has_doc_op()) { |
329 | 276k | return false; |
330 | 276k | } |
331 | | |
332 | | // When INDEX has its own doc_op, execute it to fetch next batch of ybctids which is then used |
333 | | // to read data from the main table. |
334 | 1.66k | const vector<Slice> *ybctids; |
335 | 1.66k | if (!VERIFY_RESULT(secondary_index_query_->FetchYbctidBatch(&ybctids))) { |
336 | | // No more rows of ybctids. |
337 | 473 | return false; |
338 | 473 | } |
339 | | |
340 | | // Update request with the new batch of ybctids to fetch the next batch of rows. |
341 | 1.19k | RETURN_NOT_OK(doc_op_->PopulateDmlByYbctidOps(*ybctids)); |
342 | 1.19k | AtomicFlagSleepMs(&FLAGS_TEST_inject_delay_between_prepare_ybctid_execute_batch_ybctid_ms); |
343 | 1.19k | return true; |
344 | 1.19k | } |
345 | | |
346 | | Status PgDml::Fetch(int32_t natts, |
347 | | uint64_t *values, |
348 | | bool *isnulls, |
349 | | PgSysColumns *syscols, |
350 | 20.5M | bool *has_data) { |
351 | | // Each isnulls and values correspond (in order) to columns from the table schema. |
352 | | // Initialize to nulls for any columns not present in result. |
353 | 20.5M | if (isnulls) { |
354 | 20.5M | memset(isnulls, true, natts * sizeof(bool)); |
355 | 20.5M | } |
356 | 20.5M | if (syscols) { |
357 | 20.5M | memset(syscols, 0, sizeof(PgSysColumns)); |
358 | 20.5M | } |
359 | | |
360 | | // Keep reading until we either reach the end or get some rows. |
361 | 20.5M | *has_data = true; |
362 | 20.5M | PgTuple pg_tuple(values, isnulls, syscols); |
363 | 21.2M | while (!VERIFY_RESULT(GetNextRow(&pg_tuple))) { |
364 | 861k | if (!VERIFY_RESULT(FetchDataFromServer())) { |
365 | | // Stop processing as server returns no more rows. |
366 | 249k | *has_data = false; |
367 | 249k | return Status::OK(); |
368 | 249k | } |
369 | 861k | } |
370 | | |
371 | 20.3M | return Status::OK(); |
372 | 20.5M | } |
373 | | |
374 | 863k | Result<bool> PgDml::FetchDataFromServer() { |
375 | | // Get the rowsets from doc-operator. |
376 | 863k | RETURN_NOT_OK(doc_op_->GetResult(&rowsets_)); |
377 | | |
378 | | // Check if EOF is reached. |
379 | 839k | if (rowsets_.empty()) { |
380 | | // Process the secondary index to find the next WHERE condition. |
381 | | // DML(Table) WHERE ybctid IN (SELECT base_ybctid FROM IndexTable), |
382 | | // The nested query would return many rows each of which yields different result-set. |
383 | 250k | if (!VERIFY_RESULT(ProcessSecondaryIndexRequest(nullptr))) { |
384 | | // Return EOF as the nested subquery does not have any more data. |
385 | 249k | return false; |
386 | 249k | } |
387 | | |
388 | | // Execute doc_op_ again for the new set of WHERE condition from the nested query. |
389 | 750 | SCHECK_EQ(VERIFY_RESULT(doc_op_->Execute()), RequestSent::kTrue, IllegalState, |
390 | 750 | "YSQL read operation was not sent"); |
391 | | |
392 | | // Get the rowsets from doc-operator. |
393 | 750 | RETURN_NOT_OK(doc_op_->GetResult(&rowsets_)); |
394 | 750 | } |
395 | | |
396 | | // Return the output parameter back to Postgres if server wants. |
397 | 590k | if (doc_op_->has_out_param_backfill_spec() && pg_exec_params_) { |
398 | 257 | PgExecOutParamValue value; |
399 | 257 | value.bfoutput = doc_op_->out_param_backfill_spec(); |
400 | 257 | YBCGetPgCallbacks()->WriteExecOutParam(pg_exec_params_->out_param, &value); |
401 | 257 | } |
402 | | |
403 | 590k | return true; |
404 | 839k | } |
405 | | |
406 | 21.1M | Result<bool> PgDml::GetNextRow(PgTuple *pg_tuple) { |
407 | 21.1M | for (;;) { |
408 | 22.2M | for (auto rowset_iter = rowsets_.begin(); rowset_iter != rowsets_.end();) { |
409 | | // Check if the rowset has any data. |
410 | 21.4M | auto& rowset = *rowset_iter; |
411 | 21.4M | if (rowset.is_eof()) { |
412 | 1.06M | rowset_iter = rowsets_.erase(rowset_iter); |
413 | 1.06M | continue; |
414 | 1.06M | } |
415 | | |
416 | | // If this rowset has the next row of the index order, load it. Otherwise, continue looking |
417 | | // for the next row in the order. |
418 | | // |
419 | | // NOTE: |
420 | | // DML <Table> WHERE ybctid IN (SELECT base_ybctid FROM <Index> ORDER BY <Index Range>) |
421 | | // The nested subquery should return rows in indexing order, but the ybctids are then grouped |
422 | | // by hash-code for BATCH-DML-REQUEST, so the response here are out-of-order. |
423 | 20.3M | if (rowset.NextRowOrder() <= current_row_order_) { |
424 | | // Write row to postgres tuple. |
425 | 20.3M | int64_t row_order = -1; |
426 | 20.3M | RETURN_NOT_OK(rowset.WritePgTuple(targets_, pg_tuple, &row_order)); |
427 | 20.3M | SCHECK(row_order == -1 || row_order == current_row_order_, InternalError, |
428 | 20.3M | "The resulting row are not arranged in indexing order"); |
429 | | |
430 | | // Found the current row. Move cursor to next row. |
431 | 20.3M | current_row_order_++; |
432 | 20.3M | return true; |
433 | 39.2k | } |
434 | | |
435 | 39.2k | rowset_iter++; |
436 | 39.2k | } |
437 | | |
438 | 862k | if (!rowsets_.empty() && doc_op_->end_of_data()) { |
439 | | // If the current desired row is missing, skip it and continue to look for the next |
440 | | // desired row in order. A row is deemed missing if it is not found and the doc op |
441 | | // has no more rows to return. |
442 | 0 | current_row_order_++; |
443 | 862k | } else { |
444 | 862k | break; |
445 | 862k | } |
446 | 862k | } |
447 | | |
448 | 861k | return false; |
449 | 21.1M | } |
450 | | |
451 | 864k | bool PgDml::has_aggregate_targets() { |
452 | 864k | size_t num_aggregate_targets = 0; |
453 | 9.54M | for (const auto& target : targets_) { |
454 | 9.54M | if (target->is_aggregate()) { |
455 | 221 | num_aggregate_targets++; |
456 | 221 | } |
457 | 9.54M | } |
458 | | |
459 | 90 | CHECK(num_aggregate_targets == 0 || num_aggregate_targets == targets_.size()) |
460 | 90 | << "Some, but not all, targets are aggregate expressions."; |
461 | | |
462 | 864k | return num_aggregate_targets > 0; |
463 | 864k | } |
464 | | |
465 | 1.16M | Result<YBCPgColumnInfo> PgDml::GetColumnInfo(int attr_num) const { |
466 | 1.16M | if (secondary_index_query_) { |
467 | 30 | return secondary_index_query_->GetColumnInfo(attr_num); |
468 | 30 | } |
469 | 1.16M | return bind_->GetColumnInfo(attr_num); |
470 | 1.16M | } |
471 | | |
472 | | } // namespace pggate |
473 | | } // namespace yb |