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
10namespace duckdb {
11
12const idx_t BoxRenderer::SPLIT_COLUMN = idx_t(-1);
13
14BoxRenderer::BoxRenderer(BoxRendererConfig config_p) : config(std::move(config_p)) {
15}
16
17string 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
23void BoxRenderer::Print(ClientContext &context, const vector<string> &names, const ColumnDataCollection &result) {
24 Printer::Print(str: ToString(context, names, result));
25}
26
27void 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
79string 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
108ValueRenderAlignment 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
128list<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
202list<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
256string 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
260string 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
272vector<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
381void BoxRenderer::RenderHeader(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
440void 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
562void 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
631void 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