diff --git a/Makefile b/Makefile index fa72836..cdea62f 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ else endif EXTENSION = pg_csv -EXTVERSION = 0.3 +EXTVERSION = 0.4 DATA = $(wildcard sql/*--*.sql) diff --git a/README.md b/README.md index 06bf374..5f429cb 100644 --- a/README.md +++ b/README.md @@ -71,3 +71,19 @@ select csv_agg(x, csv_options(bom := true)) from projects x; 5,Orphan, (1 row) ``` + +### Header + +You can omit or include the CSV header. + +```psql +select csv_agg(x, csv_options(header := false)) from projects x; + csv_agg +------------------- + 1,Windows 7,1 + + 2,Windows 10,1 + + 3,IOS,2 + + 4,OSX,2 + + 5,Orphan, +(1 row) +``` diff --git a/sql/pg_csv--0.3--0.4.sql b/sql/pg_csv--0.3--0.4.sql new file mode 100644 index 0000000..472d56d --- /dev/null +++ b/sql/pg_csv--0.3--0.4.sql @@ -0,0 +1,9 @@ +alter type csv_options add attribute header bool; + +create or replace function csv_options( + delimiter "char" default NULL, + bom bool default NULL, + header bool default NULL +) returns csv_options as $$ + select row(delimiter, bom, header)::csv_options; +$$ language sql; diff --git a/sql/pg_csv.sql b/sql/pg_csv.sql index 9932321..8fed440 100644 --- a/sql/pg_csv.sql +++ b/sql/pg_csv.sql @@ -1,10 +1,15 @@ create type csv_options as ( delimiter "char" , bom bool +, header bool ); -create or replace function csv_options(delimiter "char" default NULL, bom bool default NULL) returns csv_options as $$ - select row(delimiter, bom)::csv_options; +create or replace function csv_options( + delimiter "char" default NULL, + bom bool default NULL, + header bool default NULL +) returns csv_options as $$ + select row(delimiter, bom, header)::csv_options; $$ language sql; create function csv_agg_transfn(internal, anyelement) diff --git a/src/pg_csv.c b/src/pg_csv.c index 21535c2..9570a3f 100644 --- a/src/pg_csv.c +++ b/src/pg_csv.c @@ -10,6 +10,7 @@ static const char BOM[3] = "\xEF\xBB\xBF"; typedef struct { char delim; bool with_bom; + bool header; } CsvOptions; typedef struct { @@ -59,14 +60,15 @@ static void parse_csv_options(HeapTupleHeader opts_hdr, CsvOptions *csv_opts) { // defaults csv_opts->delim = ','; csv_opts->with_bom = false; + csv_opts->header = true; if (opts_hdr == NULL) return; TupleDesc desc = lookup_rowtype_tupdesc(HeapTupleHeaderGetTypeId(opts_hdr), HeapTupleHeaderGetTypMod(opts_hdr)); - Datum values[2]; - bool nulls[2]; + Datum values[3]; + bool nulls[3]; heap_deform_tuple( &(HeapTupleData){.t_len = HeapTupleHeaderGetDatumLength(opts_hdr), .t_data = opts_hdr}, desc, @@ -84,6 +86,10 @@ static void parse_csv_options(HeapTupleHeader opts_hdr, CsvOptions *csv_opts) { csv_opts->with_bom = DatumGetBool(values[1]); } + if (!nulls[2]) { + csv_opts->header = DatumGetBool(values[2]); + } + ReleaseTupleDesc(desc); } @@ -128,19 +134,21 @@ Datum csv_agg_transfn(PG_FUNCTION_ARGS) { if (state->options->with_bom) appendBinaryStringInfo(&state->accum_buf, BOM, sizeof(BOM)); // build header row - for (int i = 0; i < tdesc->natts; i++) { - Form_pg_attribute att = TupleDescAttr(tdesc, i); - if (att->attisdropped) // pg always keeps dropped columns, guard against this - continue; + if (state->options->header) { + for (int i = 0; i < tdesc->natts; i++) { + Form_pg_attribute att = TupleDescAttr(tdesc, i); + if (att->attisdropped) // pg always keeps dropped columns, guard against this + continue; - if (i > 0) // only append delimiter after the first value - appendStringInfoChar(&state->accum_buf, state->options->delim); + if (i > 0) // only append delimiter after the first value + appendStringInfoChar(&state->accum_buf, state->options->delim); - char *cstr = NameStr(att->attname); - csv_append_field(&state->accum_buf, cstr, strlen(cstr), state->options->delim); - } + char *cstr = NameStr(att->attname); + csv_append_field(&state->accum_buf, cstr, strlen(cstr), state->options->delim); + } - appendStringInfoChar(&state->accum_buf, NEWLINE); + appendStringInfoChar(&state->accum_buf, NEWLINE); + } state->tupdesc = tdesc; state->header_done = true; diff --git a/test/expected/bom.out b/test/expected/bom.out index f339569..0b2ea3d 100644 --- a/test/expected/bom.out +++ b/test/expected/bom.out @@ -36,3 +36,7 @@ FROM projects x; 7;"has CR";8 8;"has CRLF""";8 +\echo + +\pset format aligned +\pset tuples_only off diff --git a/test/expected/header.out b/test/expected/header.out new file mode 100644 index 0000000..6ca8e79 --- /dev/null +++ b/test/expected/header.out @@ -0,0 +1,78 @@ +-- header +SELECT csv_agg(x, csv_options(header:=true)) AS body +FROM projects x; + body +------------------------------- + id,name,client_id + + 1,Windows 7,1 + + 2,"has,comma",1 + + ,, + + 4,OSX,2 + + ,"has""quote", + + 5,"has,comma and ""quote""",7+ + 6,"has + + LF",7 + + 7,"has \r CR",8 + + 8,"has \r + + CRLF""",8 +(1 row) + +-- no header +SELECT csv_agg(x, csv_options(header:=false)) AS body +FROM projects x; + body +------------------------------- + 1,Windows 7,1 + + 2,"has,comma",1 + + ,, + + 4,OSX,2 + + ,"has""quote", + + 5,"has,comma and ""quote""",7+ + 6,"has + + LF",7 + + 7,"has \r CR",8 + + 8,"has \r + + CRLF""",8 +(1 row) + +-- no header with delimiter +SELECT csv_agg(x, csv_options(delimiter:='|', header:=false)) AS body +FROM projects x; + body +------------------------------- + 1|Windows 7|1 + + 2|has,comma|1 + + || + + 4|OSX|2 + + |"has""quote"| + + 5|"has,comma and ""quote"""|7+ + 6|"has + + LF"|7 + + 7|"has \r CR"|8 + + 8|"has \r + + CRLF"""|8 +(1 row) + +-- see bom.sql for an explanation of these settings +\pset format unaligned +\pset tuples_only on +\echo + +-- no header with delimiter and BOM +SELECT csv_agg(x, csv_options(delimiter:='|', header:=false, bom := true)) AS body +FROM projects x; +1|Windows 7|1 +2|has,comma|1 +|| +4|OSX|2 +|"has""quote"| +5|"has,comma and ""quote"""|7 +6|"has + LF"|7 +7|"has CR"|8 +8|"has + CRLF"""|8 +\echo + +\pset format aligned +\pset tuples_only off diff --git a/test/sql/bom.sql b/test/sql/bom.sql index 60f086e..a9f98d6 100644 --- a/test/sql/bom.sql +++ b/test/sql/bom.sql @@ -12,3 +12,7 @@ FROM projects x; -- include BOM with custom delimiter SELECT csv_agg(x, csv_options(delimiter := ';', bom := true)) AS body FROM projects x; +\echo + +\pset format aligned +\pset tuples_only off diff --git a/test/sql/header.sql b/test/sql/header.sql new file mode 100644 index 0000000..3f73573 --- /dev/null +++ b/test/sql/header.sql @@ -0,0 +1,24 @@ +-- header +SELECT csv_agg(x, csv_options(header:=true)) AS body +FROM projects x; + +-- no header +SELECT csv_agg(x, csv_options(header:=false)) AS body +FROM projects x; + +-- no header with delimiter +SELECT csv_agg(x, csv_options(delimiter:='|', header:=false)) AS body +FROM projects x; + +-- see bom.sql for an explanation of these settings +\pset format unaligned +\pset tuples_only on +\echo + +-- no header with delimiter and BOM +SELECT csv_agg(x, csv_options(delimiter:='|', header:=false, bom := true)) AS body +FROM projects x; +\echo + +\pset format aligned +\pset tuples_only off