1 | #include "duckdb/common/box_renderer.hpp" |
2 | |
3 | #include "duckdb/common/printer.hpp" |
4 | #include "duckdb/common/types/column/column_data_collection.hpp" |
5 | #include "duckdb/common/vector_operations/vector_operations.hpp" |
6 | #include "utf8proc_wrapper.hpp" |
7 | |
8 | #include <sstream> |
9 | |
10 | namespace duckdb { |
11 | |
12 | const idx_t BoxRenderer::SPLIT_COLUMN = idx_t(-1); |
13 | |
14 | BoxRenderer::BoxRenderer(BoxRendererConfig config_p) : config(std::move(config_p)) { |
15 | } |
16 | |
17 | string BoxRenderer::ToString(ClientContext &context, const vector<string> &names, const ColumnDataCollection &result) { |
18 | std::stringstream ss; |
19 | Render(context, names, op: result, ss); |
20 | return ss.str(); |
21 | } |
22 | |
23 | void BoxRenderer::Print(ClientContext &context, const vector<string> &names, const ColumnDataCollection &result) { |
24 | Printer::Print(str: ToString(context, names, result)); |
25 | } |
26 | |
27 | void BoxRenderer::RenderValue(std::ostream &ss, const string &value, idx_t column_width, |
28 | ValueRenderAlignment alignment) { |
29 | auto render_width = Utf8Proc::RenderWidth(str: value); |
30 | |
31 | const string *render_value = &value; |
32 | string small_value; |
33 | if (render_width > column_width) { |
34 | // the string is too large to fit in this column! |
35 | // the size of this column must have been reduced |
36 | // figure out how much of this value we can render |
37 | idx_t pos = 0; |
38 | idx_t current_render_width = config.DOTDOTDOT_LENGTH; |
39 | while (pos < value.size()) { |
40 | // check if this character fits... |
41 | auto char_size = Utf8Proc::RenderWidth(s: value.c_str(), len: value.size(), pos); |
42 | if (current_render_width + char_size >= column_width) { |
43 | // it doesn't! stop |
44 | break; |
45 | } |
46 | // it does! move to the next character |
47 | current_render_width += char_size; |
48 | pos = Utf8Proc::NextGraphemeCluster(s: value.c_str(), len: value.size(), pos); |
49 | } |
50 | small_value = value.substr(pos: 0, n: pos) + config.DOTDOTDOT; |
51 | render_value = &small_value; |
52 | render_width = current_render_width; |
53 | } |
54 | auto padding_count = (column_width - render_width) + 2; |
55 | idx_t lpadding; |
56 | idx_t rpadding; |
57 | switch (alignment) { |
58 | case ValueRenderAlignment::LEFT: |
59 | lpadding = 1; |
60 | rpadding = padding_count - 1; |
61 | break; |
62 | case ValueRenderAlignment::MIDDLE: |
63 | lpadding = padding_count / 2; |
64 | rpadding = padding_count - lpadding; |
65 | break; |
66 | case ValueRenderAlignment::RIGHT: |
67 | lpadding = padding_count - 1; |
68 | rpadding = 1; |
69 | break; |
70 | default: |
71 | throw InternalException("Unrecognized value renderer alignment" ); |
72 | } |
73 | ss << config.VERTICAL; |
74 | ss << string(lpadding, ' '); |
75 | ss << *render_value; |
76 | ss << string(rpadding, ' '); |
77 | } |
78 | |
79 | string BoxRenderer::RenderType(const LogicalType &type) { |
80 | switch (type.id()) { |
81 | case LogicalTypeId::TINYINT: |
82 | return "int8" ; |
83 | case LogicalTypeId::SMALLINT: |
84 | return "int16" ; |
85 | case LogicalTypeId::INTEGER: |
86 | return "int32" ; |
87 | case LogicalTypeId::BIGINT: |
88 | return "int64" ; |
89 | case LogicalTypeId::HUGEINT: |
90 | return "int128" ; |
91 | case LogicalTypeId::UTINYINT: |
92 | return "uint8" ; |
93 | case LogicalTypeId::USMALLINT: |
94 | return "uint16" ; |
95 | case LogicalTypeId::UINTEGER: |
96 | return "uint32" ; |
97 | case LogicalTypeId::UBIGINT: |
98 | return "uint64" ; |
99 | case LogicalTypeId::LIST: { |
100 | auto child = RenderType(type: ListType::GetChildType(type)); |
101 | return child + "[]" ; |
102 | } |
103 | default: |
104 | return StringUtil::Lower(str: type.ToString()); |
105 | } |
106 | } |
107 | |
108 | ValueRenderAlignment BoxRenderer::TypeAlignment(const LogicalType &type) { |
109 | switch (type.id()) { |
110 | case LogicalTypeId::TINYINT: |
111 | case LogicalTypeId::SMALLINT: |
112 | case LogicalTypeId::INTEGER: |
113 | case LogicalTypeId::BIGINT: |
114 | case LogicalTypeId::HUGEINT: |
115 | case LogicalTypeId::UTINYINT: |
116 | case LogicalTypeId::USMALLINT: |
117 | case LogicalTypeId::UINTEGER: |
118 | case LogicalTypeId::UBIGINT: |
119 | case LogicalTypeId::DECIMAL: |
120 | case LogicalTypeId::FLOAT: |
121 | case LogicalTypeId::DOUBLE: |
122 | return ValueRenderAlignment::RIGHT; |
123 | default: |
124 | return ValueRenderAlignment::LEFT; |
125 | } |
126 | } |
127 | |
128 | list<ColumnDataCollection> BoxRenderer::FetchRenderCollections(ClientContext &context, |
129 | const ColumnDataCollection &result, idx_t top_rows, |
130 | idx_t bottom_rows) { |
131 | auto column_count = result.ColumnCount(); |
132 | vector<LogicalType> varchar_types; |
133 | for (idx_t c = 0; c < column_count; c++) { |
134 | varchar_types.emplace_back(args: LogicalType::VARCHAR); |
135 | } |
136 | std::list<ColumnDataCollection> collections; |
137 | collections.emplace_back(args&: context, args&: varchar_types); |
138 | collections.emplace_back(args&: context, args&: varchar_types); |
139 | |
140 | auto &top_collection = collections.front(); |
141 | auto &bottom_collection = collections.back(); |
142 | |
143 | DataChunk fetch_result; |
144 | fetch_result.Initialize(context, types: result.Types()); |
145 | |
146 | DataChunk insert_result; |
147 | insert_result.Initialize(context, types: varchar_types); |
148 | |
149 | // fetch the top rows from the ColumnDataCollection |
150 | idx_t chunk_idx = 0; |
151 | idx_t row_idx = 0; |
152 | while (row_idx < top_rows) { |
153 | fetch_result.Reset(); |
154 | insert_result.Reset(); |
155 | // fetch the next chunk |
156 | result.FetchChunk(chunk_idx, result&: fetch_result); |
157 | idx_t insert_count = MinValue<idx_t>(a: fetch_result.size(), b: top_rows - row_idx); |
158 | |
159 | // cast all columns to varchar |
160 | for (idx_t c = 0; c < column_count; c++) { |
161 | VectorOperations::Cast(context, source&: fetch_result.data[c], result&: insert_result.data[c], count: insert_count); |
162 | } |
163 | insert_result.SetCardinality(insert_count); |
164 | |
165 | // construct the render collection |
166 | top_collection.Append(new_chunk&: insert_result); |
167 | |
168 | chunk_idx++; |
169 | row_idx += fetch_result.size(); |
170 | } |
171 | |
172 | // fetch the bottom rows from the ColumnDataCollection |
173 | row_idx = 0; |
174 | chunk_idx = result.ChunkCount() - 1; |
175 | while (row_idx < bottom_rows) { |
176 | fetch_result.Reset(); |
177 | insert_result.Reset(); |
178 | // fetch the next chunk |
179 | result.FetchChunk(chunk_idx, result&: fetch_result); |
180 | idx_t insert_count = MinValue<idx_t>(a: fetch_result.size(), b: bottom_rows - row_idx); |
181 | |
182 | // invert the rows |
183 | SelectionVector inverted_sel(insert_count); |
184 | for (idx_t r = 0; r < insert_count; r++) { |
185 | inverted_sel.set_index(idx: r, loc: fetch_result.size() - r - 1); |
186 | } |
187 | |
188 | for (idx_t c = 0; c < column_count; c++) { |
189 | Vector slice(fetch_result.data[c], inverted_sel, insert_count); |
190 | VectorOperations::Cast(context, source&: slice, result&: insert_result.data[c], count: insert_count); |
191 | } |
192 | insert_result.SetCardinality(insert_count); |
193 | // construct the render collection |
194 | bottom_collection.Append(new_chunk&: insert_result); |
195 | |
196 | chunk_idx--; |
197 | row_idx += fetch_result.size(); |
198 | } |
199 | return collections; |
200 | } |
201 | |
202 | list<ColumnDataCollection> BoxRenderer::PivotCollections(ClientContext &context, list<ColumnDataCollection> input, |
203 | vector<string> &column_names, |
204 | vector<LogicalType> &result_types, idx_t row_count) { |
205 | auto &top = input.front(); |
206 | auto &bottom = input.back(); |
207 | |
208 | vector<LogicalType> varchar_types; |
209 | vector<string> new_names; |
210 | new_names.emplace_back(args: "Column" ); |
211 | new_names.emplace_back(args: "Type" ); |
212 | varchar_types.emplace_back(args: LogicalType::VARCHAR); |
213 | varchar_types.emplace_back(args: LogicalType::VARCHAR); |
214 | for (idx_t r = 0; r < top.Count(); r++) { |
215 | new_names.emplace_back(args: "Row " + to_string(val: r + 1)); |
216 | varchar_types.emplace_back(args: LogicalType::VARCHAR); |
217 | } |
218 | for (idx_t r = 0; r < bottom.Count(); r++) { |
219 | auto row_index = row_count - bottom.Count() + r + 1; |
220 | new_names.emplace_back(args: "Row " + to_string(val: row_index)); |
221 | varchar_types.emplace_back(args: LogicalType::VARCHAR); |
222 | } |
223 | // |
224 | DataChunk row_chunk; |
225 | row_chunk.Initialize(allocator&: Allocator::DefaultAllocator(), types: varchar_types); |
226 | std::list<ColumnDataCollection> result; |
227 | result.emplace_back(args&: context, args&: varchar_types); |
228 | result.emplace_back(args&: context, args&: varchar_types); |
229 | auto &res_coll = result.front(); |
230 | ColumnDataAppendState append_state; |
231 | res_coll.InitializeAppend(state&: append_state); |
232 | for (idx_t c = 0; c < top.ColumnCount(); c++) { |
233 | vector<column_t> column_ids {c}; |
234 | auto row_index = row_chunk.size(); |
235 | idx_t current_index = 0; |
236 | row_chunk.SetValue(col_idx: current_index++, index: row_index, val: column_names[c]); |
237 | row_chunk.SetValue(col_idx: current_index++, index: row_index, val: RenderType(type: result_types[c])); |
238 | for (auto &collection : input) { |
239 | for (auto &chunk : collection.Chunks(column_ids)) { |
240 | for (idx_t r = 0; r < chunk.size(); r++) { |
241 | row_chunk.SetValue(col_idx: current_index++, index: row_index, val: chunk.GetValue(col_idx: 0, index: r)); |
242 | } |
243 | } |
244 | } |
245 | row_chunk.SetCardinality(row_chunk.size() + 1); |
246 | if (row_chunk.size() == STANDARD_VECTOR_SIZE || c + 1 == top.ColumnCount()) { |
247 | res_coll.Append(state&: append_state, new_chunk&: row_chunk); |
248 | row_chunk.Reset(); |
249 | } |
250 | } |
251 | column_names = std::move(new_names); |
252 | result_types = std::move(varchar_types); |
253 | return result; |
254 | } |
255 | |
256 | string ConvertRenderValue(const string &input) { |
257 | return StringUtil::Replace(source: StringUtil::Replace(source: input, from: "\n" , to: "\\n" ), from: string("\0" , 1), to: "\\0" ); |
258 | } |
259 | |
260 | string BoxRenderer::GetRenderValue(ColumnDataRowCollection &rows, idx_t c, idx_t r) { |
261 | try { |
262 | auto row = rows.GetValue(column: c, index: r); |
263 | if (row.IsNull()) { |
264 | return config.null_value; |
265 | } |
266 | return ConvertRenderValue(input: StringValue::Get(value: row)); |
267 | } catch (std::exception &ex) { |
268 | return "????INVALID VALUE - " + string(ex.what()) + "?????" ; |
269 | } |
270 | } |
271 | |
272 | vector<idx_t> BoxRenderer::ComputeRenderWidths(const vector<string> &names, const vector<LogicalType> &result_types, |
273 | list<ColumnDataCollection> &collections, idx_t min_width, |
274 | idx_t max_width, vector<idx_t> &column_map, idx_t &total_length) { |
275 | auto column_count = result_types.size(); |
276 | |
277 | vector<idx_t> widths; |
278 | widths.reserve(n: column_count); |
279 | for (idx_t c = 0; c < column_count; c++) { |
280 | auto name_width = Utf8Proc::RenderWidth(str: ConvertRenderValue(input: names[c])); |
281 | auto type_width = Utf8Proc::RenderWidth(str: RenderType(type: result_types[c])); |
282 | widths.push_back(x: MaxValue<idx_t>(a: name_width, b: type_width)); |
283 | } |
284 | |
285 | // now iterate over the data in the render collection and find out the true max width |
286 | for (auto &collection : collections) { |
287 | for (auto &chunk : collection.Chunks()) { |
288 | for (idx_t c = 0; c < column_count; c++) { |
289 | auto string_data = FlatVector::GetData<string_t>(vector&: chunk.data[c]); |
290 | for (idx_t r = 0; r < chunk.size(); r++) { |
291 | string render_value; |
292 | if (FlatVector::IsNull(vector: chunk.data[c], idx: r)) { |
293 | render_value = config.null_value; |
294 | } else { |
295 | render_value = ConvertRenderValue(input: string_data[r].GetString()); |
296 | } |
297 | auto render_width = Utf8Proc::RenderWidth(str: render_value); |
298 | widths[c] = MaxValue<idx_t>(a: render_width, b: widths[c]); |
299 | } |
300 | } |
301 | } |
302 | } |
303 | |
304 | // figure out the total length |
305 | // we start off with a pipe (|) |
306 | total_length = 1; |
307 | for (idx_t c = 0; c < widths.size(); c++) { |
308 | // each column has a space at the beginning, and a space plus a pipe (|) at the end |
309 | // hence + 3 |
310 | total_length += widths[c] + 3; |
311 | } |
312 | if (total_length < min_width) { |
313 | // if there are hidden rows we should always display that |
314 | // stretch up the first column until we have space to show the row count |
315 | widths[0] += min_width - total_length; |
316 | total_length = min_width; |
317 | } |
318 | // now we need to constrain the length |
319 | unordered_set<idx_t> pruned_columns; |
320 | if (total_length > max_width) { |
321 | // before we remove columns, check if we can just reduce the size of columns |
322 | for (auto &w : widths) { |
323 | if (w > config.max_col_width) { |
324 | auto max_diff = w - config.max_col_width; |
325 | if (total_length - max_diff <= max_width) { |
326 | // if we reduce the size of this column we fit within the limits! |
327 | // reduce the width exactly enough so that the box fits |
328 | w -= total_length - max_width; |
329 | total_length = max_width; |
330 | break; |
331 | } else { |
332 | // reducing the width of this column does not make the result fit |
333 | // reduce the column width by the maximum amount anyway |
334 | w = config.max_col_width; |
335 | total_length -= max_diff; |
336 | } |
337 | } |
338 | } |
339 | |
340 | if (total_length > max_width) { |
341 | // the total length is still too large |
342 | // we need to remove columns! |
343 | // first, we add 6 characters to the total length |
344 | // this is what we need to add the "..." in the middle |
345 | total_length += 3 + config.DOTDOTDOT_LENGTH; |
346 | // now select columns to prune |
347 | // we select columns in zig-zag order starting from the middle |
348 | // e.g. if we have 10 columns, we remove #5, then #4, then #6, then #3, then #7, etc |
349 | int64_t offset = 0; |
350 | while (total_length > max_width) { |
351 | idx_t c = column_count / 2 + offset; |
352 | total_length -= widths[c] + 3; |
353 | pruned_columns.insert(x: c); |
354 | if (offset >= 0) { |
355 | offset = -offset - 1; |
356 | } else { |
357 | offset = -offset; |
358 | } |
359 | } |
360 | } |
361 | } |
362 | |
363 | bool added_split_column = false; |
364 | vector<idx_t> new_widths; |
365 | for (idx_t c = 0; c < column_count; c++) { |
366 | if (pruned_columns.find(x: c) == pruned_columns.end()) { |
367 | column_map.push_back(x: c); |
368 | new_widths.push_back(x: widths[c]); |
369 | } else { |
370 | if (!added_split_column) { |
371 | // "..." |
372 | column_map.push_back(x: SPLIT_COLUMN); |
373 | new_widths.push_back(x: config.DOTDOTDOT_LENGTH); |
374 | added_split_column = true; |
375 | } |
376 | } |
377 | } |
378 | return new_widths; |
379 | } |
380 | |
381 | void BoxRenderer::(const vector<string> &names, const vector<LogicalType> &result_types, |
382 | const vector<idx_t> &column_map, const vector<idx_t> &widths, |
383 | const vector<idx_t> &boundaries, idx_t total_length, bool has_results, |
384 | std::ostream &ss) { |
385 | auto column_count = column_map.size(); |
386 | // render the top line |
387 | ss << config.LTCORNER; |
388 | idx_t column_index = 0; |
389 | for (idx_t k = 0; k < total_length - 2; k++) { |
390 | if (column_index + 1 < column_count && k == boundaries[column_index]) { |
391 | ss << config.TMIDDLE; |
392 | column_index++; |
393 | } else { |
394 | ss << config.HORIZONTAL; |
395 | } |
396 | } |
397 | ss << config.RTCORNER; |
398 | ss << std::endl; |
399 | |
400 | // render the header names |
401 | for (idx_t c = 0; c < column_count; c++) { |
402 | auto column_idx = column_map[c]; |
403 | string name; |
404 | if (column_idx == SPLIT_COLUMN) { |
405 | name = config.DOTDOTDOT; |
406 | } else { |
407 | name = ConvertRenderValue(input: names[column_idx]); |
408 | } |
409 | RenderValue(ss, value: name, column_width: widths[c]); |
410 | } |
411 | ss << config.VERTICAL; |
412 | ss << std::endl; |
413 | |
414 | // render the types |
415 | if (config.render_mode == RenderMode::ROWS) { |
416 | for (idx_t c = 0; c < column_count; c++) { |
417 | auto column_idx = column_map[c]; |
418 | auto type = column_idx == SPLIT_COLUMN ? "" : RenderType(type: result_types[column_idx]); |
419 | RenderValue(ss, value: type, column_width: widths[c]); |
420 | } |
421 | ss << config.VERTICAL; |
422 | ss << std::endl; |
423 | } |
424 | |
425 | // render the line under the header |
426 | ss << config.LMIDDLE; |
427 | column_index = 0; |
428 | for (idx_t k = 0; k < total_length - 2; k++) { |
429 | if (has_results && column_index + 1 < column_count && k == boundaries[column_index]) { |
430 | ss << config.MIDDLE; |
431 | column_index++; |
432 | } else { |
433 | ss << config.HORIZONTAL; |
434 | } |
435 | } |
436 | ss << config.RMIDDLE; |
437 | ss << std::endl; |
438 | } |
439 | |
440 | void BoxRenderer::RenderValues(const list<ColumnDataCollection> &collections, const vector<idx_t> &column_map, |
441 | const vector<idx_t> &widths, const vector<LogicalType> &result_types, std::ostream &ss) { |
442 | auto &top_collection = collections.front(); |
443 | auto &bottom_collection = collections.back(); |
444 | // render the top rows |
445 | auto top_rows = top_collection.Count(); |
446 | auto bottom_rows = bottom_collection.Count(); |
447 | auto column_count = column_map.size(); |
448 | |
449 | vector<ValueRenderAlignment> alignments; |
450 | if (config.render_mode == RenderMode::ROWS) { |
451 | for (idx_t c = 0; c < column_count; c++) { |
452 | auto column_idx = column_map[c]; |
453 | if (column_idx == SPLIT_COLUMN) { |
454 | alignments.push_back(x: ValueRenderAlignment::MIDDLE); |
455 | } else { |
456 | alignments.push_back(x: TypeAlignment(type: result_types[column_idx])); |
457 | } |
458 | } |
459 | } |
460 | |
461 | auto rows = top_collection.GetRows(); |
462 | for (idx_t r = 0; r < top_rows; r++) { |
463 | for (idx_t c = 0; c < column_count; c++) { |
464 | auto column_idx = column_map[c]; |
465 | string str; |
466 | if (column_idx == SPLIT_COLUMN) { |
467 | str = config.DOTDOTDOT; |
468 | } else { |
469 | str = GetRenderValue(rows, c: column_idx, r); |
470 | } |
471 | ValueRenderAlignment alignment; |
472 | if (config.render_mode == RenderMode::ROWS) { |
473 | alignment = alignments[c]; |
474 | } else { |
475 | if (c < 2) { |
476 | alignment = ValueRenderAlignment::LEFT; |
477 | } else if (c == SPLIT_COLUMN) { |
478 | alignment = ValueRenderAlignment::MIDDLE; |
479 | } else { |
480 | alignment = ValueRenderAlignment::RIGHT; |
481 | } |
482 | } |
483 | RenderValue(ss, value: str, column_width: widths[c], alignment); |
484 | } |
485 | ss << config.VERTICAL; |
486 | ss << std::endl; |
487 | } |
488 | |
489 | if (bottom_rows > 0) { |
490 | if (config.render_mode == RenderMode::COLUMNS) { |
491 | throw InternalException("Columns render mode does not support bottom rows" ); |
492 | } |
493 | // render the bottom rows |
494 | // first render the divider |
495 | auto brows = bottom_collection.GetRows(); |
496 | for (idx_t k = 0; k < 3; k++) { |
497 | for (idx_t c = 0; c < column_count; c++) { |
498 | auto column_idx = column_map[c]; |
499 | string str; |
500 | auto alignment = alignments[c]; |
501 | if (alignment == ValueRenderAlignment::MIDDLE || column_idx == SPLIT_COLUMN) { |
502 | str = config.DOT; |
503 | } else { |
504 | // align the dots in the center of the column |
505 | auto top_value = GetRenderValue(rows, c: column_idx, r: top_rows - 1); |
506 | auto bottom_value = GetRenderValue(rows&: brows, c: column_idx, r: bottom_rows - 1); |
507 | auto top_length = MinValue<idx_t>(a: widths[c], b: Utf8Proc::RenderWidth(str: top_value)); |
508 | auto bottom_length = MinValue<idx_t>(a: widths[c], b: Utf8Proc::RenderWidth(str: bottom_value)); |
509 | auto dot_length = MinValue<idx_t>(a: top_length, b: bottom_length); |
510 | if (top_length == 0) { |
511 | dot_length = bottom_length; |
512 | } else if (bottom_length == 0) { |
513 | dot_length = top_length; |
514 | } |
515 | if (dot_length > 1) { |
516 | auto padding = dot_length - 1; |
517 | idx_t left_padding, right_padding; |
518 | switch (alignment) { |
519 | case ValueRenderAlignment::LEFT: |
520 | left_padding = padding / 2; |
521 | right_padding = padding - left_padding; |
522 | break; |
523 | case ValueRenderAlignment::RIGHT: |
524 | right_padding = padding / 2; |
525 | left_padding = padding - right_padding; |
526 | break; |
527 | default: |
528 | throw InternalException("Unrecognized value renderer alignment" ); |
529 | } |
530 | str = string(left_padding, ' ') + config.DOT + string(right_padding, ' '); |
531 | } else { |
532 | if (dot_length == 0) { |
533 | // everything is empty |
534 | alignment = ValueRenderAlignment::MIDDLE; |
535 | } |
536 | str = config.DOT; |
537 | } |
538 | } |
539 | RenderValue(ss, value: str, column_width: widths[c], alignment); |
540 | } |
541 | ss << config.VERTICAL; |
542 | ss << std::endl; |
543 | } |
544 | // note that the bottom rows are in reverse order |
545 | for (idx_t r = 0; r < bottom_rows; r++) { |
546 | for (idx_t c = 0; c < column_count; c++) { |
547 | auto column_idx = column_map[c]; |
548 | string str; |
549 | if (column_idx == SPLIT_COLUMN) { |
550 | str = config.DOTDOTDOT; |
551 | } else { |
552 | str = GetRenderValue(rows&: brows, c: column_idx, r: bottom_rows - r - 1); |
553 | } |
554 | RenderValue(ss, value: str, column_width: widths[c], alignment: alignments[c]); |
555 | } |
556 | ss << config.VERTICAL; |
557 | ss << std::endl; |
558 | } |
559 | } |
560 | } |
561 | |
562 | void BoxRenderer::RenderRowCount(string row_count_str, string shown_str, const string &column_count_str, |
563 | const vector<idx_t> &boundaries, bool has_hidden_rows, bool has_hidden_columns, |
564 | idx_t total_length, idx_t row_count, idx_t column_count, idx_t minimum_row_length, |
565 | std::ostream &ss) { |
566 | // check if we can merge the row_count_str and the shown_str |
567 | bool display_shown_separately = has_hidden_rows; |
568 | if (has_hidden_rows && total_length >= row_count_str.size() + shown_str.size() + 5) { |
569 | // we can! |
570 | row_count_str += " " + shown_str; |
571 | shown_str = string(); |
572 | display_shown_separately = false; |
573 | minimum_row_length = row_count_str.size() + 4; |
574 | } |
575 | auto minimum_length = row_count_str.size() + column_count_str.size() + 6; |
576 | bool render_rows_and_columns = total_length >= minimum_length && |
577 | ((has_hidden_columns && row_count > 0) || (row_count >= 10 && column_count > 1)); |
578 | bool render_rows = total_length >= minimum_row_length && (row_count == 0 || row_count >= 10); |
579 | bool render_anything = true; |
580 | if (!render_rows && !render_rows_and_columns) { |
581 | render_anything = false; |
582 | } |
583 | // render the bottom of the result values, if there are any |
584 | if (row_count > 0) { |
585 | ss << (render_anything ? config.LMIDDLE : config.LDCORNER); |
586 | idx_t column_index = 0; |
587 | for (idx_t k = 0; k < total_length - 2; k++) { |
588 | if (column_index + 1 < boundaries.size() && k == boundaries[column_index]) { |
589 | ss << config.DMIDDLE; |
590 | column_index++; |
591 | } else { |
592 | ss << config.HORIZONTAL; |
593 | } |
594 | } |
595 | ss << (render_anything ? config.RMIDDLE : config.RDCORNER); |
596 | ss << std::endl; |
597 | } |
598 | if (!render_anything) { |
599 | return; |
600 | } |
601 | |
602 | if (render_rows_and_columns) { |
603 | ss << config.VERTICAL; |
604 | ss << " " ; |
605 | ss << row_count_str; |
606 | ss << string(total_length - row_count_str.size() - column_count_str.size() - 4, ' '); |
607 | ss << column_count_str; |
608 | ss << " " ; |
609 | ss << config.VERTICAL; |
610 | ss << std::endl; |
611 | } else if (render_rows) { |
612 | RenderValue(ss, value: row_count_str, column_width: total_length - 4); |
613 | ss << config.VERTICAL; |
614 | ss << std::endl; |
615 | |
616 | if (display_shown_separately) { |
617 | RenderValue(ss, value: shown_str, column_width: total_length - 4); |
618 | ss << config.VERTICAL; |
619 | ss << std::endl; |
620 | } |
621 | } |
622 | // render the bottom line |
623 | ss << config.LDCORNER; |
624 | for (idx_t k = 0; k < total_length - 2; k++) { |
625 | ss << config.HORIZONTAL; |
626 | } |
627 | ss << config.RDCORNER; |
628 | ss << std::endl; |
629 | } |
630 | |
631 | void BoxRenderer::Render(ClientContext &context, const vector<string> &names, const ColumnDataCollection &result, |
632 | std::ostream &ss) { |
633 | if (result.ColumnCount() != names.size()) { |
634 | throw InternalException("Error in BoxRenderer::Render - unaligned columns and names" ); |
635 | } |
636 | auto max_width = config.max_width; |
637 | if (max_width == 0) { |
638 | if (Printer::IsTerminal(stream: OutputStream::STREAM_STDOUT)) { |
639 | max_width = Printer::TerminalWidth(); |
640 | } else { |
641 | max_width = 120; |
642 | } |
643 | } |
644 | // we do not support max widths under 80 |
645 | max_width = MaxValue<idx_t>(a: 80, b: max_width); |
646 | |
647 | // figure out how many/which rows to render |
648 | idx_t row_count = result.Count(); |
649 | idx_t rows_to_render = MinValue<idx_t>(a: row_count, b: config.max_rows); |
650 | if (row_count <= config.max_rows + 3) { |
651 | // hiding rows adds 3 extra rows |
652 | // so hiding rows makes no sense if we are only slightly over the limit |
653 | // if we are 1 row over the limit hiding rows will actually increase the number of lines we display! |
654 | // in this case render all the rows |
655 | rows_to_render = row_count; |
656 | } |
657 | idx_t top_rows; |
658 | idx_t bottom_rows; |
659 | if (rows_to_render == row_count) { |
660 | top_rows = row_count; |
661 | bottom_rows = 0; |
662 | } else { |
663 | top_rows = rows_to_render / 2 + (rows_to_render % 2 != 0 ? 1 : 0); |
664 | bottom_rows = rows_to_render - top_rows; |
665 | } |
666 | auto row_count_str = to_string(val: row_count) + " rows" ; |
667 | bool has_limited_rows = config.limit > 0 && row_count == config.limit; |
668 | if (has_limited_rows) { |
669 | row_count_str = "? rows" ; |
670 | } |
671 | string shown_str; |
672 | bool has_hidden_rows = top_rows < row_count; |
673 | if (has_hidden_rows) { |
674 | shown_str = "(" ; |
675 | if (has_limited_rows) { |
676 | shown_str += ">" + to_string(val: config.limit - 1) + " rows, " ; |
677 | } |
678 | shown_str += to_string(val: top_rows + bottom_rows) + " shown)" ; |
679 | } |
680 | auto minimum_row_length = MaxValue<idx_t>(a: row_count_str.size(), b: shown_str.size()) + 4; |
681 | |
682 | // fetch the top and bottom render collections from the result |
683 | auto collections = FetchRenderCollections(context, result, top_rows, bottom_rows); |
684 | auto column_names = names; |
685 | auto result_types = result.Types(); |
686 | if (config.render_mode == RenderMode::COLUMNS) { |
687 | collections = PivotCollections(context, input: std::move(collections), column_names, result_types, row_count); |
688 | } |
689 | |
690 | // for each column, figure out the width |
691 | // start off by figuring out the name of the header by looking at the column name and column type |
692 | idx_t min_width = has_hidden_rows || row_count == 0 ? minimum_row_length : 0; |
693 | vector<idx_t> column_map; |
694 | idx_t total_length; |
695 | auto widths = |
696 | ComputeRenderWidths(names: column_names, result_types, collections, min_width, max_width, column_map, total_length); |
697 | |
698 | // render boundaries for the individual columns |
699 | vector<idx_t> boundaries; |
700 | for (idx_t c = 0; c < widths.size(); c++) { |
701 | idx_t render_boundary; |
702 | if (c == 0) { |
703 | render_boundary = widths[c] + 2; |
704 | } else { |
705 | render_boundary = boundaries[c - 1] + widths[c] + 3; |
706 | } |
707 | boundaries.push_back(x: render_boundary); |
708 | } |
709 | |
710 | // now begin rendering |
711 | // first render the header |
712 | RenderHeader(names: column_names, result_types, column_map, widths, boundaries, total_length, has_results: row_count > 0, ss); |
713 | |
714 | // render the values, if there are any |
715 | RenderValues(collections, column_map, widths, result_types, ss); |
716 | |
717 | // render the row count and column count |
718 | auto column_count_str = to_string(val: result.ColumnCount()) + " column" ; |
719 | if (result.ColumnCount() > 1) { |
720 | column_count_str += "s" ; |
721 | } |
722 | bool has_hidden_columns = false; |
723 | for (auto entry : column_map) { |
724 | if (entry == SPLIT_COLUMN) { |
725 | has_hidden_columns = true; |
726 | break; |
727 | } |
728 | } |
729 | idx_t column_count = column_map.size(); |
730 | if (config.render_mode == RenderMode::COLUMNS) { |
731 | if (has_hidden_columns) { |
732 | has_hidden_rows = true; |
733 | shown_str = " (" + to_string(val: column_count - 3) + " shown)" ; |
734 | } else { |
735 | shown_str = string(); |
736 | } |
737 | } else { |
738 | if (has_hidden_columns) { |
739 | column_count--; |
740 | column_count_str += " (" + to_string(val: column_count) + " shown)" ; |
741 | } |
742 | } |
743 | |
744 | RenderRowCount(row_count_str: std::move(row_count_str), shown_str: std::move(shown_str), column_count_str, boundaries, has_hidden_rows, |
745 | has_hidden_columns, total_length, row_count, column_count, minimum_row_length, ss); |
746 | } |
747 | |
748 | } // namespace duckdb |
749 | |