v2 / cmd / tools / vsqlite / format.v
425 lines · 381 sloc · 8.58 KB · 3d60410b605d001e54f280070d5f952da9de1112
Raw
1module main
2
3import db.sqlite
4import term
5
6pub enum OutputMode {
7 table
8 csv
9 line
10 box
11 markdown
12 json
13 html
14 insert
15 quote
16}
17
18// FormatOptions controls how query results are rendered.
19pub struct FormatOptions {
20pub:
21 mode OutputMode = .table
22 headers bool = true
23 nullvalue string = 'NULL'
24 separator string = ','
25 col_widths map[int]int // key = column index (0-based); value = fixed width; 0 = auto
26 table_name string = 'tbl' // used by insert mode
27}
28
29// format renders rows using mode and headers with default options.
30pub fn format(rows []sqlite.Row, mode OutputMode, headers bool) string {
31 return format_opts(rows, FormatOptions{
32 mode: mode
33 headers: headers
34 })
35}
36
37// format_opts renders rows using the full FormatOptions.
38pub fn format_opts(rows []sqlite.Row, opts FormatOptions) string {
39 return match opts.mode {
40 .table { format_table_ex(rows, opts) }
41 .csv { format_csv_ex(rows, opts) }
42 .line { format_line_ex(rows, opts) }
43 .box { format_box_ex(rows, opts) }
44 .markdown { format_markdown_ex(rows, opts) }
45 .json { format_json_ex(rows, opts) }
46 .html { format_html_ex(rows, opts) }
47 .insert { format_insert_ex(rows, opts) }
48 .quote { format_quote_ex(rows, opts) }
49 }
50}
51
52// ---------- internal helpers ----------
53
54fn eff_width(opts FormatOptions, i int, auto_w int) int {
55 w := opts.col_widths[i]
56 if w > 0 {
57 return w
58 }
59 return auto_w
60}
61
62fn null_disp(s string, nullvalue string) string {
63 return if s == '' { nullvalue } else { s }
64}
65
66fn resolve_names(names []string, ncols int) []string {
67 if names.len == ncols {
68 return names
69 }
70 mut generated := []string{cap: ncols}
71 for i in 0 .. ncols {
72 generated << 'col${i}'
73 }
74 return generated
75}
76
77fn str_pad(s string, w int) string {
78 if s.len >= w {
79 return s
80 }
81 return s + ' '.repeat(w - s.len)
82}
83
84fn str_pad_left(s string, w int) string {
85 if s.len >= w {
86 return s
87 }
88 return ' '.repeat(w - s.len) + s
89}
90
91fn compute_widths(rows []sqlite.Row, names []string, opts FormatOptions) []int {
92 ncols := names.len
93 mut auto_w := []int{len: ncols, init: names[index].len}
94 for row in rows {
95 for i, val in row.vals {
96 if i < ncols {
97 v := null_disp(val, opts.nullvalue)
98 if v.len > auto_w[i] {
99 auto_w[i] = v.len
100 }
101 }
102 }
103 }
104 mut widths := []int{len: ncols}
105 for i in 0 .. ncols {
106 widths[i] = eff_width(opts, i, auto_w[i])
107 }
108 return widths
109}
110
111// ---------- table ----------
112
113fn format_table_ex(rows []sqlite.Row, opts FormatOptions) string {
114 if rows.len == 0 {
115 return ''
116 }
117 ncols := rows[0].vals.len
118 names := resolve_names(rows[0].names, ncols)
119 widths := compute_widths(rows, names, opts)
120
121 mut sep := '+'
122 for w in widths {
123 sep += '-'.repeat(w + 2) + '+'
124 }
125
126 mut out := ''
127 if opts.headers {
128 out += sep + '\n'
129 mut header := '|'
130 for i, col in names {
131 header += ' ' + term.bold(str_pad(col.limit(widths[i]), widths[i])) + ' |'
132 }
133 out += header + '\n'
134 }
135 out += sep + '\n'
136 for row in rows {
137 mut line := '|'
138 for i, val in row.vals {
139 if i >= ncols {
140 break
141 }
142 v := null_disp(val, opts.nullvalue)
143 line += ' ' + str_pad(v.limit(widths[i]), widths[i]) + ' |'
144 }
145 out += line + '\n'
146 }
147 out += sep
148 return out
149}
150
151// ---------- csv ----------
152
153fn format_csv_ex(rows []sqlite.Row, opts FormatOptions) string {
154 if rows.len == 0 {
155 return ''
156 }
157 ncols := rows[0].vals.len
158 names := resolve_names(rows[0].names, ncols)
159 sep := opts.separator
160
161 mut lines := []string{}
162 if opts.headers {
163 lines << names.map(csv_escape_sep(it, sep)).join(sep)
164 }
165 for row in rows {
166 lines << row.vals.map(csv_escape_sep(null_disp(it, opts.nullvalue), sep)).join(sep)
167 }
168 return lines.join('\n')
169}
170
171// ---------- line ----------
172
173fn format_line_ex(rows []sqlite.Row, opts FormatOptions) string {
174 if rows.len == 0 {
175 return ''
176 }
177 ncols := rows[0].vals.len
178 names := resolve_names(rows[0].names, ncols)
179
180 mut max_len := 0
181 for col in names {
182 if col.len > max_len {
183 max_len = col.len
184 }
185 }
186
187 mut blocks := []string{}
188 for row in rows {
189 mut lines := []string{}
190 for j, val in row.vals {
191 if j >= ncols {
192 break
193 }
194 lines << '${str_pad_left(names[j], max_len)}: ${null_disp(val, opts.nullvalue)}'
195 }
196 blocks << lines.join('\n')
197 }
198 return blocks.join('\n\n')
199}
200
201// ---------- box (Unicode box-drawing) ----------
202
203fn format_box_ex(rows []sqlite.Row, opts FormatOptions) string {
204 if rows.len == 0 {
205 return ''
206 }
207 ncols := rows[0].vals.len
208 names := resolve_names(rows[0].names, ncols)
209 widths := compute_widths(rows, names, opts)
210
211 mut top := '┌'
212 mut mid := '├'
213 mut bot := '└'
214 for j, w in widths {
215 top += '─'.repeat(w + 2)
216 mid += '─'.repeat(w + 2)
217 bot += '─'.repeat(w + 2)
218 if j < ncols - 1 {
219 top += '┬'
220 mid += '┼'
221 bot += '┴'
222 }
223 }
224 top += '┐'
225 mid += '┤'
226 bot += '┘'
227
228 mut out := top + '\n'
229 if opts.headers {
230 mut hdr := '│'
231 for i, col in names {
232 hdr += ' ' + term.bold(str_pad(col.limit(widths[i]), widths[i])) + ' │'
233 }
234 out += hdr + '\n' + mid + '\n'
235 }
236 for row in rows {
237 mut line := '│'
238 for i, val in row.vals {
239 if i >= ncols {
240 break
241 }
242 v := null_disp(val, opts.nullvalue)
243 line += ' ' + str_pad(v.limit(widths[i]), widths[i]) + ' │'
244 }
245 out += line + '\n'
246 }
247 out += bot
248 return out
249}
250
251// ---------- markdown ----------
252
253fn format_markdown_ex(rows []sqlite.Row, opts FormatOptions) string {
254 if rows.len == 0 {
255 return ''
256 }
257 ncols := rows[0].vals.len
258 names := resolve_names(rows[0].names, ncols)
259 raw_w := compute_widths(rows, names, opts)
260 mut widths := []int{len: ncols}
261 for i, w in raw_w {
262 widths[i] = if w < 3 { 3 } else { w }
263 }
264
265 mut lines := []string{}
266 if opts.headers {
267 mut hdr := '|'
268 mut sep_row := '|'
269 for i, col in names {
270 hdr += ' ' + str_pad(col.limit(widths[i]), widths[i]) + ' |'
271 sep_row += ' ' + '-'.repeat(widths[i]) + ' |'
272 }
273 lines << hdr
274 lines << sep_row
275 }
276 for row in rows {
277 mut line := '|'
278 for i, val in row.vals {
279 if i >= ncols {
280 break
281 }
282 v := null_disp(val, opts.nullvalue)
283 line += ' ' + str_pad(v.limit(widths[i]), widths[i]) + ' |'
284 }
285 lines << line
286 }
287 return lines.join('\n')
288}
289
290// ---------- json ----------
291
292fn format_json_ex(rows []sqlite.Row, _opts FormatOptions) string {
293 if rows.len == 0 {
294 return '[]'
295 }
296 ncols := rows[0].vals.len
297 names := resolve_names(rows[0].names, ncols)
298
299 mut items := []string{}
300 for row in rows {
301 mut fields := []string{}
302 for i, val in row.vals {
303 if i >= ncols {
304 break
305 }
306 key := '"${json_escape(names[i])}"'
307 if val == '' {
308 fields << '${key}:null'
309 } else {
310 fields << '${key}:"${json_escape(val)}"'
311 }
312 }
313 items << '{${fields.join(',')}}'
314 }
315 return '[${items.join(',\n ')}]'
316}
317
318fn json_escape(s string) string {
319 return s
320 .replace('\\', '\\\\')
321 .replace('"', '\\"')
322 .replace('\n', '\\n')
323 .replace('\r', '\\r')
324 .replace('\t', '\\t')
325}
326
327// ---------- html ----------
328
329fn format_html_ex(rows []sqlite.Row, opts FormatOptions) string {
330 if rows.len == 0 {
331 return ''
332 }
333 ncols := rows[0].vals.len
334 names := resolve_names(rows[0].names, ncols)
335
336 mut out := '<table>\n'
337 if opts.headers {
338 out += '<tr>'
339 for col in names {
340 out += '<th>${html_escape(col)}</th>'
341 }
342 out += '</tr>\n'
343 }
344 for row in rows {
345 out += '<tr>'
346 for i, val in row.vals {
347 if i >= ncols {
348 break
349 }
350 v := null_disp(val, opts.nullvalue)
351 out += '<td>${html_escape(v)}</td>'
352 }
353 out += '</tr>\n'
354 }
355 out += '</table>'
356 return out
357}
358
359fn html_escape(s string) string {
360 return s
361 .replace('&', '&')
362 .replace('<', '<')
363 .replace('>', '>')
364 .replace('"', '"')
365}
366
367// ---------- insert ----------
368
369fn format_insert_ex(rows []sqlite.Row, opts FormatOptions) string {
370 if rows.len == 0 {
371 return ''
372 }
373 ncols := rows[0].vals.len
374 names := resolve_names(rows[0].names, ncols)
375 tbl := if opts.table_name != '' { opts.table_name } else { 'tbl' }
376 col_list := names.join(',')
377
378 mut lines := []string{}
379 for row in rows {
380 mut vals := []string{}
381 for i, val in row.vals {
382 if i >= ncols {
383 break
384 }
385 if val == '' {
386 vals << 'NULL'
387 } else {
388 vals << "'" + val.replace("'", "''") + "'"
389 }
390 }
391 lines << 'INSERT INTO ${tbl}(${col_list}) VALUES(${vals.join(',')});'
392 }
393 return lines.join('\n')
394}
395
396// ---------- quote ----------
397
398fn format_quote_ex(rows []sqlite.Row, opts FormatOptions) string {
399 if rows.len == 0 {
400 return ''
401 }
402 ncols := rows[0].vals.len
403 names := resolve_names(rows[0].names, ncols)
404 sep := opts.separator
405
406 mut lines := []string{}
407 if opts.headers {
408 lines << names.join(sep)
409 }
410 for row in rows {
411 mut vals := []string{}
412 for i, val in row.vals {
413 if i >= ncols {
414 break
415 }
416 if val == '' {
417 vals << 'NULL'
418 } else {
419 vals << "'" + val.replace("'", "''") + "'"
420 }
421 }
422 lines << vals.join(sep)
423 }
424 return lines.join('\n')
425}
426