@@ -6,11 +6,16 @@ static const char NEWLINE = '\n';
66static const char DQUOTE = '"' ;
77static const char CR = '\r' ;
88
9+ typedef struct {
10+ char delim ;
11+ } CsvOptions ;
12+
913typedef struct {
1014 StringInfoData accum_buf ;
1115 bool header_done ;
1216 bool first_row ;
1317 TupleDesc tupdesc ;
18+ CsvOptions * options ;
1419} CsvAggState ;
1520
1621static inline bool is_reserved (char c ) {
@@ -48,40 +53,65 @@ static char *datum_to_cstring(Datum datum, Oid typeoid) {
4853 return OidOutputFunctionCall (out_func , datum );
4954}
5055
56+ static void parse_csv_options (HeapTupleHeader opts_hdr , CsvOptions * csv_opts ) {
57+ // defaults
58+ csv_opts -> delim = ',' ;
59+
60+ if (opts_hdr == NULL ) return ;
61+
62+ TupleDesc desc = lookup_rowtype_tupdesc (HeapTupleHeaderGetTypeId (opts_hdr ),
63+ HeapTupleHeaderGetTypMod (opts_hdr ));
64+
65+ Datum values [1 ];
66+ bool nulls [1 ];
67+
68+ heap_deform_tuple (
69+ & (HeapTupleData ){.t_len = HeapTupleHeaderGetDatumLength (opts_hdr ), .t_data = opts_hdr }, desc ,
70+ values , nulls );
71+
72+ if (!nulls [0 ]) {
73+ csv_opts -> delim = DatumGetChar (values [0 ]);
74+ if (is_reserved (csv_opts -> delim ))
75+ ereport (ERROR , (errcode (ERRCODE_INVALID_PARAMETER_VALUE ),
76+ errmsg ("delimiter cannot be newline, carriage return or "
77+ "double quote" )));
78+ }
79+
80+ ReleaseTupleDesc (desc );
81+ }
82+
5183PG_FUNCTION_INFO_V1 (csv_agg_transfn );
5284Datum csv_agg_transfn (PG_FUNCTION_ARGS ) {
53- CsvAggState * state ;
85+ CsvAggState * state = !PG_ARGISNULL (0 ) ? (CsvAggState * )PG_GETARG_POINTER (0 ) : NULL ;
86+ HeapTupleHeader next = !PG_ARGISNULL (1 ) ? PG_GETARG_HEAPTUPLEHEADER (1 ) : NULL ;
5487
5588 // first call when the accumulator is NULL
5689 // pretty standard stuff, for example see the jsonb_agg transition function
5790 // https://github.com/postgres/postgres/blob/3c4e26a62c31ebe296e3aedb13ac51a7a35103bd/src/backend/utils/adt/jsonb.c#L1521
58- if (PG_ARGISNULL ( 0 ) ) {
91+ if (state == NULL ) {
5992 MemoryContext aggctx , oldctx ;
6093
6194 if (!AggCheckCallContext (fcinfo , & aggctx ))
6295 elog (ERROR , "csv_agg_transfn called in non‑aggregate context" );
6396
6497 oldctx = MemoryContextSwitchTo (aggctx );
6598
66- state = ( CsvAggState * ) palloc (sizeof (CsvAggState ));
99+ state = palloc (sizeof (CsvAggState ));
67100 initStringInfo (& state -> accum_buf );
68101 state -> header_done = false;
69102 state -> first_row = true;
70103 state -> tupdesc = NULL ;
104+ state -> options = palloc (sizeof (CsvOptions ));
71105
72- MemoryContextSwitchTo (oldctx );
73- } else
74- state = (CsvAggState * )PG_GETARG_POINTER (0 );
75-
76- if (PG_ARGISNULL (1 )) PG_RETURN_POINTER (state ); // skip NULL rows
77-
78- HeapTupleHeader next = PG_GETARG_HEAPTUPLEHEADER (1 );
106+ // we'll parse the csv options only once
107+ HeapTupleHeader opts_hdr =
108+ PG_NARGS () >= 3 && !PG_ARGISNULL (2 ) ? PG_GETARG_HEAPTUPLEHEADER (2 ) : NULL ;
109+ parse_csv_options (opts_hdr , state -> options );
79110
80- char delim = PG_NARGS () >= 3 && !PG_ARGISNULL (2 ) ? PG_GETARG_CHAR (2 ) : ',' ;
111+ MemoryContextSwitchTo (oldctx );
112+ }
81113
82- if (is_reserved (delim ))
83- ereport (ERROR , (errcode (ERRCODE_INVALID_PARAMETER_VALUE ),
84- errmsg ("delimiter cannot be newline, carriage return or double quote" )));
114+ if (next == NULL ) PG_RETURN_POINTER (state ); // skip NULL rows
85115
86116 // build header and cache tupdesc once
87117 if (!state -> header_done ) {
@@ -95,10 +125,10 @@ Datum csv_agg_transfn(PG_FUNCTION_ARGS) {
95125 continue ;
96126
97127 if (i > 0 ) // only append delimiter after the first value
98- appendStringInfoChar (& state -> accum_buf , delim );
128+ appendStringInfoChar (& state -> accum_buf , state -> options -> delim );
99129
100130 char * cstr = NameStr (att -> attname );
101- csv_append_field (& state -> accum_buf , cstr , strlen (cstr ), delim );
131+ csv_append_field (& state -> accum_buf , cstr , strlen (cstr ), state -> options -> delim );
102132 }
103133
104134 appendStringInfoChar (& state -> accum_buf , NEWLINE );
@@ -131,12 +161,12 @@ Datum csv_agg_transfn(PG_FUNCTION_ARGS) {
131161 if (att -> attisdropped ) // pg always keeps dropped columns, guard against this
132162 continue ;
133163
134- if (i > 0 ) appendStringInfoChar (& state -> accum_buf , delim );
164+ if (i > 0 ) appendStringInfoChar (& state -> accum_buf , state -> options -> delim );
135165
136166 if (nulls [i ]) continue ; // empty field for NULL
137167
138168 char * cstr = datum_to_cstring (datums [i ], att -> atttypid );
139- csv_append_field (& state -> accum_buf , cstr , strlen (cstr ), delim );
169+ csv_append_field (& state -> accum_buf , cstr , strlen (cstr ), state -> options -> delim );
140170 }
141171
142172 PG_RETURN_POINTER (state );
0 commit comments