Skip to content

Commit db3224f

Browse files
committed
sheet: Add pivot builtin command
The user can either pivot on the column under the cursor with 'v' or enter an SQL group-by expression with V
1 parent 8cd97b6 commit db3224f

File tree

7 files changed

+319
-1
lines changed

7 files changed

+319
-1
lines changed

app/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ ${CLI} ${STANDALONE_PFX}2json${EXE}: MORE_OBJECTS+= ${BUILD_DIR}/objs/utils/db.o
471471
# pretty uses termcap
472472
${CLI} ${STANDALONE_PFX}pretty${EXE}: MORE_LIBS+=${LDFLAGS_TERMCAP}
473473

474+
${CLI} ${STANDALONE_PFX}sheet${EXE} ${STANDALONE_PFX}sql${EXE}: ${SQL_INTERNAL_OBJECT}
474475
${CLI} ${STANDALONE_PFX}sheet${EXE} ${STANDALONE_PFX}sql${EXE}: MORE_OBJECTS+=${SQL_INTERNAL_OBJECT}
475476

476477
${STANDALONE_PFX}%${EXE}: %.c ${OBJECTS} ${MORE_OBJECTS} ${LIBZSV_INSTALL} ${UTF8PROC_OBJECT}

app/sheet.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ static zsvsheet_status zsvsheet_help_handler(struct zsvsheet_proc_context *ctx)
538538
return stat;
539539
}
540540

541+
#include "sheet/pivot.c"
541542
#include "sheet/newline_handler.c"
542543

543544
/* We do most procedures in one handler. More complex procedures can be
@@ -624,7 +625,9 @@ struct builtin_proc_desc {
624625
{ zsvsheet_builtin_proc_filter, "filter", "Hide rows that do not contain the specified text", zsvsheet_filter_handler },
625626
{ zsvsheet_builtin_proc_subcommand, "subcommand", "Editor subcommand", zsvsheet_subcommand_handler },
626627
{ zsvsheet_builtin_proc_help, "help", "Display a list of actions and key-bindings", zsvsheet_help_handler },
627-
{ zsvsheet_builtin_proc_newline, "<Enter>","Follow hyperlink (if any)", zsvsheet_newline_handler },
628+
{ zsvsheet_builtin_proc_newline, "<Enter>","Follow hyperlink (if any) or drill down", zsvsheet_newline_handler },
629+
{ zsvsheet_builtin_proc_pivot_cur_col, "pivotcur","Group rows by the column under the cursor", zsvsheet_pivot_handler },
630+
{ zsvsheet_builtin_proc_pivot_expr, "pivotexpr","Group rows with group-by SQL expression", zsvsheet_pivot_handler },
628631
{ -1, NULL, NULL, NULL }
629632
};
630633
/* clang-format on */

app/sheet/file.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ int zsvsheet_ui_buffer_open_file(const char *filename, const struct zsv_opts *zs
77
struct zsvsheet_ui_buffer **ui_buffer_stack_top);
88

99
zsvsheet_status zsvsheet_open_file_opts(struct zsvsheet_proc_context *ctx, struct zsvsheet_ui_buffer_opts *opts);
10+
zsvsheet_status zsvsheet_open_file(struct zsvsheet_proc_context *ctx, const char *filepath, struct zsv_opts *zopts);
1011

1112
#endif

app/sheet/key-bindings.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ struct zsvsheet_key_binding zsvsheet_vim_key_bindings[] = {
174174
{ .ch = '?', .proc_id = zsvsheet_builtin_proc_help, },
175175
{ .ch = '\n', .proc_id = zsvsheet_builtin_proc_newline, },
176176
{ .ch = '\r', .proc_id = zsvsheet_builtin_proc_newline, },
177+
{ .ch = 'v', .proc_id = zsvsheet_builtin_proc_pivot_cur_col, },
178+
{ .ch = 'V', .proc_id = zsvsheet_builtin_proc_pivot_expr, },
177179

178180
{ .ch = -1 }
179181
};

app/sheet/pivot.c

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
/*
2+
* Copyright (C) 2021 Liquidaty and zsv contributors. All rights reserved.
3+
* This file is part of zsv/lib, distributed under the license defined at
4+
* https://opensource.org/licenses/MIT
5+
*/
6+
7+
#include <assert.h>
8+
#include <errno.h>
9+
#include "../external/sqlite3/sqlite3.h"
10+
#include <zsv/ext/implementation.h>
11+
#include <zsv/ext/sheet.h>
12+
#include <zsv/utils/writer.h>
13+
#include <zsv/utils/file.h>
14+
#include <zsv/utils/prop.h>
15+
#include "file.h"
16+
#include "handlers_internal.h"
17+
#include "../curses.h"
18+
#include "../sql_internal.h"
19+
20+
struct pivot_row {
21+
char *value; // to do: this will be the drill-down criteria
22+
};
23+
24+
struct pivot_data {
25+
char *value_sql; // the sql expression entered by the user e.g. City
26+
char *data_filename;
27+
struct {
28+
struct pivot_row *data; // for each row, the value of the sql expression e.g. New York
29+
size_t capacity;
30+
size_t used;
31+
} rows;
32+
};
33+
34+
static void pivot_data_delete(void *h) {
35+
struct pivot_data *pd = h;
36+
if (pd) {
37+
for (size_t i = 0; i < pd->rows.used; i++)
38+
free(pd->rows.data[i].value);
39+
free(pd->rows.data);
40+
free(pd->value_sql);
41+
free(pd->data_filename);
42+
free(pd);
43+
}
44+
}
45+
46+
static struct pivot_data *pivot_data_new(const char *data_filename, const char *value_sql) {
47+
struct pivot_data *pd = calloc(1, sizeof(*pd));
48+
if (pd && (pd->value_sql = strdup(value_sql)) && (pd->data_filename = strdup(data_filename)))
49+
return pd;
50+
pivot_data_delete(pd);
51+
return NULL;
52+
}
53+
54+
#define ZSV_MYSHEET_PIVOT_DATA_ROWS_INITIAL 32
55+
static int pivot_data_grow(struct pivot_data *pd) {
56+
if (pd->rows.used == pd->rows.capacity) {
57+
size_t new_capacity = pd->rows.capacity == 0 ? ZSV_MYSHEET_PIVOT_DATA_ROWS_INITIAL : pd->rows.capacity * 2;
58+
struct pivot_row *new_data = realloc(pd->rows.data, new_capacity * sizeof(*pd->rows.data));
59+
if (!new_data)
60+
return ENOMEM;
61+
pd->rows.data = new_data;
62+
pd->rows.capacity = new_capacity;
63+
}
64+
return 0;
65+
}
66+
67+
static int add_pivot_row(struct pivot_data *pd, const char *value, size_t len) {
68+
int err = pivot_data_grow(pd);
69+
char *value_dup = NULL;
70+
if (!err && value && len) {
71+
value_dup = malloc(len + 1);
72+
if (value_dup) {
73+
memcpy(value_dup, value, len);
74+
value_dup[len] = '\0';
75+
}
76+
}
77+
pd->rows.data[pd->rows.used++].value = value_dup;
78+
return err;
79+
}
80+
81+
static struct pivot_row *get_pivot_row_data(struct pivot_data *pd, size_t row_ix) {
82+
if (pd && row_ix < pd->rows.used)
83+
return &pd->rows.data[row_ix];
84+
return NULL;
85+
}
86+
87+
// TO DO: return zsvsheet_status
88+
static enum zsv_ext_status get_cell_attrs(void *pdh, int *attrs, size_t start_row, size_t row_count, size_t cols) {
89+
struct pivot_data *pd = pdh;
90+
size_t end_row = start_row + row_count;
91+
int attr = 0;
92+
93+
#ifdef A_BOLD
94+
attr |= A_BOLD;
95+
#endif
96+
// Absent on Mac OSX 13
97+
#ifdef A_ITALIC
98+
attr |= A_ITALIC;
99+
#endif
100+
101+
if (end_row > pd->rows.used)
102+
end_row = pd->rows.used;
103+
for (size_t i = start_row; i < end_row; i++)
104+
attrs[i * cols] = attr;
105+
return zsv_ext_status_ok;
106+
}
107+
108+
static void pivot_on_header_cell(void *ctx, size_t col_ix, const char *colname) {
109+
(void)colname;
110+
if (col_ix == 0)
111+
add_pivot_row(ctx, NULL, 0);
112+
}
113+
114+
static void pivot_on_data_cell(void *ctx, size_t col_ix, const char *text, size_t len) {
115+
if (col_ix == 0)
116+
add_pivot_row(ctx, text, len);
117+
}
118+
119+
static zsvsheet_status zsv_sqlite3_to_csv(zsvsheet_proc_context_t pctx, struct zsv_sqlite3_db *zdb, const char *sql,
120+
void *ctx, void (*on_header_cell)(void *, size_t, const char *),
121+
void (*on_data_cell)(void *, size_t, const char *, size_t len)) {
122+
const char *err_msg = NULL;
123+
zsvsheet_status zst = zsvsheet_status_error;
124+
sqlite3_stmt *stmt = NULL;
125+
126+
if ((zdb->rc = sqlite3_prepare_v2(zdb->db, sql, -1, &stmt, NULL)) == SQLITE_OK) {
127+
char *tmp_fn = zsv_get_temp_filename("zsv_mysheet_ext_XXXXXXXX");
128+
struct zsv_csv_writer_options writer_opts = zsv_writer_get_default_opts();
129+
zsv_csv_writer cw = NULL;
130+
if (!tmp_fn)
131+
zst = zsvsheet_status_memory;
132+
else if (!(writer_opts.stream = fopen(tmp_fn, "wb"))) {
133+
zst = zsvsheet_status_error;
134+
err_msg = strerror(errno);
135+
} else if (!(cw = zsv_writer_new(&writer_opts)))
136+
zst = zsvsheet_status_memory;
137+
else {
138+
zst = zsvsheet_status_ok;
139+
unsigned char cw_buff[1024];
140+
zsv_writer_set_temp_buff(cw, cw_buff, sizeof(cw_buff));
141+
142+
int col_count = sqlite3_column_count(stmt);
143+
// write header row
144+
for (int i = 0; i < col_count; i++) {
145+
const char *colname = sqlite3_column_name(stmt, i);
146+
zsv_writer_cell(cw, !i, (const unsigned char *)colname, colname ? strlen(colname) : 0, 1);
147+
if (on_header_cell)
148+
on_header_cell(ctx, i, colname);
149+
}
150+
151+
// write sql results
152+
while (sqlite3_step(stmt) == SQLITE_ROW) {
153+
for (int i = 0; i < col_count; i++) {
154+
const unsigned char *text = sqlite3_column_text(stmt, i);
155+
int len = text ? sqlite3_column_bytes(stmt, i) : 0;
156+
zsv_writer_cell(cw, !i, text, len, 1);
157+
if (on_data_cell)
158+
on_data_cell(ctx, i, (const char *)text, len);
159+
}
160+
}
161+
}
162+
if (cw)
163+
zsv_writer_delete(cw);
164+
if (writer_opts.stream)
165+
fclose(writer_opts.stream);
166+
167+
if (tmp_fn && zsv_file_exists(tmp_fn)) {
168+
struct zsvsheet_ui_buffer_opts uibopts = {0};
169+
uibopts.data_filename = tmp_fn;
170+
zst = zsvsheet_open_file_opts(pctx, &uibopts);
171+
} else {
172+
if (zst == zsvsheet_status_ok) {
173+
zst = zsvsheet_status_error; // to do: make this more specific
174+
if (!err_msg && zdb && zdb->rc != SQLITE_OK)
175+
err_msg = sqlite3_errmsg(zdb->db);
176+
}
177+
}
178+
if (zst != zsvsheet_status_ok)
179+
free(tmp_fn);
180+
}
181+
if (stmt)
182+
sqlite3_finalize(stmt);
183+
if (err_msg)
184+
zsvsheet_set_status(ctx, "Error: %s", err_msg);
185+
return zst;
186+
}
187+
188+
zsvsheet_status pivot_drill_down(zsvsheet_proc_context_t ctx) {
189+
enum zsvsheet_status zst = zsvsheet_status_ok;
190+
zsvsheet_buffer_t buff = zsvsheet_buffer_current(ctx);
191+
struct pivot_data *pd;
192+
struct zsvsheet_rowcol rc;
193+
if (zsvsheet_buffer_get_ctx(buff, (void **)&pd) != zsv_ext_status_ok ||
194+
zsvsheet_buffer_get_selected_cell(buff, &rc) != zsvsheet_status_ok) {
195+
return zsvsheet_status_error;
196+
}
197+
struct pivot_row *pr = get_pivot_row_data(pd, rc.row);
198+
if (pd && pd->data_filename && pd->value_sql && pr) {
199+
struct zsv_sqlite3_dbopts dbopts = {0};
200+
sqlite3_str *sql_str = NULL;
201+
struct zsv_sqlite3_db *zdb = zsv_sqlite3_db_new(&dbopts);
202+
203+
if (!zdb || !(sql_str = sqlite3_str_new(zdb->db)))
204+
zst = zsvsheet_status_memory;
205+
else if (zdb->rc == SQLITE_OK && zsv_sqlite3_add_csv(zdb, pd->data_filename, NULL, NULL) == SQLITE_OK) {
206+
if (zsvsheet_buffer_info(buff).has_row_num)
207+
sqlite3_str_appendf(sql_str, "select *");
208+
else
209+
sqlite3_str_appendf(sql_str, "select rowid as [Row #], *");
210+
sqlite3_str_appendf(sql_str, " from data where %s = %Q", pd->value_sql, pr->value);
211+
fprintf(stderr, "SQL: %s\n", sqlite3_str_value(sql_str));
212+
zst = zsv_sqlite3_to_csv(ctx, zdb, sqlite3_str_value(sql_str), NULL, NULL, NULL);
213+
}
214+
215+
if (sql_str)
216+
sqlite3_free(sqlite3_str_finish(sql_str));
217+
if (zdb) {
218+
if (zst != zsvsheet_status_ok) {
219+
// to do: consolidate this with same code in sql.c
220+
if (zdb->err_msg)
221+
fprintf(stderr, "Error: %s\n", zdb->err_msg);
222+
else if (!zdb->db)
223+
fprintf(stderr, "Error (unable to open db, code %i): %s\n", zdb->rc, sqlite3_errstr(zdb->rc));
224+
else if (zdb->rc != SQLITE_OK)
225+
fprintf(stderr, "Error (code %i): %s\n", zdb->rc, sqlite3_errstr(zdb->rc));
226+
}
227+
zsv_sqlite3_db_delete(zdb);
228+
}
229+
}
230+
return zst;
231+
}
232+
233+
/**
234+
* Here we define a custom command for the zsv `sheet` feature
235+
*/
236+
static enum zsvsheet_status zsvsheet_pivot_handler(struct zsvsheet_proc_context *ctx, char *prompt_expr) {
237+
char result_buffer[256] = {0};
238+
const char *expr;
239+
struct zsvsheet_rowcol rc;
240+
int ch = zsvsheet_ext_keypress(ctx);
241+
if (ch < 0)
242+
return zsvsheet_status_error;
243+
244+
zsvsheet_buffer_t buff = zsvsheet_buffer_current(ctx);
245+
const char *data_filename = NULL;
246+
if (buff)
247+
data_filename = zsvsheet_buffer_data_filename(buff);
248+
249+
if (!data_filename) { // TO DO: check that the underlying data is a tabular file and we know how to parse
250+
zsvsheet_set_status(ctx, "Pivot table only available for tabular data buffers");
251+
return zsvsheet_status_ok;
252+
}
253+
254+
switch (ctx->proc_id) {
255+
case zsvsheet_builtin_proc_pivot_expr:
256+
zsvsheet_ext_prompt(ctx, result_buffer, sizeof(result_buffer), "Pivot table: Enter group-by SQL expr");
257+
if (*result_buffer == '\0')
258+
return zsvsheet_status_ok;
259+
expr = result_buffer;
260+
break;
261+
case zsvsheet_builtin_proc_pivot_cur_col:
262+
if (zsvsheet_buffer_get_selected_cell(buff, &rc) != zsvsheet_status_ok)
263+
return zsvsheet_status_error;
264+
expr = zsvsheet_ui_buffer_get_header(buff, rc.col);
265+
assert(expr);
266+
break;
267+
default:
268+
assert(0);
269+
return zsvsheet_status_error;
270+
}
271+
272+
enum zsvsheet_status zst = zsvsheet_status_ok;
273+
struct zsv_sqlite3_dbopts dbopts = {0};
274+
struct zsv_sqlite3_db *zdb = zsv_sqlite3_db_new(&dbopts);
275+
sqlite3_str *sql_str = NULL;
276+
struct pivot_data *pd = NULL;
277+
if (!zdb || !(sql_str = sqlite3_str_new(zdb->db)))
278+
zst = zsvsheet_status_memory;
279+
else if (zdb->rc == SQLITE_OK && zsv_sqlite3_add_csv(zdb, data_filename, NULL, NULL) == SQLITE_OK) {
280+
sqlite3_str_appendf(sql_str, "select %s as value, count(1) as Count from data group by %s", expr, expr);
281+
if (!(pd = pivot_data_new(data_filename, expr)))
282+
zst = zsvsheet_status_memory;
283+
else {
284+
zst = zsv_sqlite3_to_csv(ctx, zdb, sqlite3_str_value(sql_str), pd, pivot_on_header_cell, pivot_on_data_cell);
285+
if (zst == zsvsheet_status_ok) {
286+
buff = zsvsheet_buffer_current(ctx);
287+
zsvsheet_buffer_set_ctx(buff, pd, pivot_data_delete);
288+
zsvsheet_buffer_set_cell_attrs(buff, get_cell_attrs);
289+
zsvsheet_buffer_on_newline(buff, pivot_drill_down);
290+
pd = NULL; // so that it isn't cleaned up below
291+
}
292+
}
293+
// TO DO: add param to ext_sheet_open_file to set filename vs data_filename, and set buffer type or proc owner
294+
// TO DO: add way to attach custom context, and custom context destructor, to the new buffer
295+
// TO DO: add cell highlighting
296+
}
297+
298+
zsv_sqlite3_db_delete(zdb);
299+
if (sql_str)
300+
sqlite3_free(sqlite3_str_finish(sql_str));
301+
pivot_data_delete(pd);
302+
return zst;
303+
}

app/sheet/procedure.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ enum {
3434
zsvsheet_builtin_proc_help,
3535
zsvsheet_builtin_proc_vim_g_key_binding_dmux,
3636
zsvsheet_builtin_proc_newline,
37+
zsvsheet_builtin_proc_pivot_expr,
38+
zsvsheet_builtin_proc_pivot_cur_col,
3739
};
3840

3941
#define ZSVSHEET_PROC_INVALID 0

app/sheet/ui_buffer.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,9 @@ int zsvsheet_ui_buffer_pop(struct zsvsheet_ui_buffer **base, struct zsvsheet_ui_
193193
}
194194
return 0;
195195
}
196+
197+
static const char *zsvsheet_ui_buffer_get_header(struct zsvsheet_ui_buffer *uib, size_t col) {
198+
struct zsvsheet_screen_buffer *sb = uib->buffer;
199+
200+
return (char *)zsvsheet_screen_buffer_cell_display(sb, 0, col);
201+
}

0 commit comments

Comments
 (0)