LCOV - code coverage report
Current view: top level - sql/backends/monet5/vaults/csv - csv.c (source / functions) Hit Total Coverage
Test: coverage.info Lines: 221 286 77.3 %
Date: 2024-04-25 23:25:41 Functions: 17 17 100.0 %

          Line data    Source code
       1             : /*
       2             :  * SPDX-License-Identifier: MPL-2.0
       3             :  *
       4             :  * This Source Code Form is subject to the terms of the Mozilla Public
       5             :  * License, v. 2.0.  If a copy of the MPL was not distributed with this
       6             :  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
       7             :  *
       8             :  * Copyright 2024 MonetDB Foundation;
       9             :  * Copyright August 2008 - 2023 MonetDB B.V.;
      10             :  * Copyright 1997 - July 2008 CWI.
      11             :  */
      12             : 
      13             : #include "monetdb_config.h"
      14             : #include "rel_file_loader.h"
      15             : #include "rel_exp.h"
      16             : 
      17             : #include "mal_instruction.h"
      18             : #include "mal_interpreter.h"
      19             : #include "mal_parser.h"
      20             : #include "mal_builder.h"
      21             : #include "mal_namespace.h"
      22             : #include "mal_exception.h"
      23             : #include "mal_linker.h"
      24             : #include "mal_backend.h"
      25             : #include "sql_types.h"
      26             : #include "rel_bin.h"
      27             : 
      28             : #include <unistd.h>
      29             : 
      30             : static stream *
      31          22 : csv_open_file(char* filename)
      32             : {
      33          22 :         return open_rastream(filename);
      34             : }
      35             : 
      36             : /* todo handle escapes */
      37             : static const char *
      38         112 : next_delim(const char *s, const char *e, char delim, char quote)
      39             : {
      40         712 :         bool inquote = false;
      41        1784 :         for(;  s < e; s++) {
      42        1816 :                 if (*s == quote)
      43         304 :                         inquote = !inquote;
      44        1512 :                 else if (!inquote && *s == delim)
      45             :                         return s;
      46             :         }
      47             :         if (s < e)
      48             :                 return s;
      49             :         return NULL;
      50             : }
      51             : 
      52             : /* todo detect escapes */
      53             : static char
      54           8 : detect_quote(const char *buf)
      55             : {
      56           8 :         const char *cur = buf;
      57           8 :         const char *l = NULL;
      58             :         /* "'(none) */
      59           8 :         bool has_double_quote = true, has_single_quote = true;
      60          40 :         while ((has_double_quote || has_single_quote) && (l = strchr(cur, '\n')) != NULL) {
      61          32 :                 const char *s = cur, *t;
      62          32 :                 if (has_double_quote && ((t = strchr(s, '"')) == NULL || t > l))  /* no quote not used */
      63           0 :                         has_double_quote = false;
      64          32 :                 if (has_single_quote && ((t = strchr(s, '\'')) == NULL || t > l))  /* no quote not used */
      65          32 :                         has_single_quote = false;
      66          32 :                 cur = l+1;
      67             :         }
      68           8 :         if (has_double_quote && !has_single_quote)
      69             :                 return '"';
      70           0 :         if (has_single_quote && !has_double_quote)
      71           0 :                 return '\'';
      72             :         /* no quote */
      73             :         return '\0';
      74             : }
      75             : 
      76             : #define DLEN 4
      77             : static char
      78           8 : detect_delimiter(const char *buf, char q, int *nr_fields)
      79             : {
      80           8 :         const char *delimiter = ",|;\t";
      81           8 :         int cnts[DLEN][2] = { 0 }, l = 0;
      82             : 
      83           8 :         const char *cur = buf;
      84             : 
      85          24 :         for (l = 0; l < 2; l++) { /* start with 2 lines only */
      86          16 :                 const char *e = strchr(cur, '\n');
      87          16 :                 if (!e)
      88             :                         break;
      89             :                 int i = 0;
      90             :                 const char *dp = delimiter;
      91          80 :                 for (char d = *dp; d; d=*(++dp), i++) {
      92          64 :                         const char *s = cur;
      93             :                         /* all lines should have some numbers */
      94          64 :                         if (l && cnts[i][l])
      95           0 :                                 if (cnts[i][0] != cnts[i][1])
      96             :                                         break;
      97             :                         int nr = 1;
      98         224 :                         while( (s = next_delim(s, e, d, q)) != NULL && s<e ) {
      99          48 :                                 if (s+1 <= e)
     100          48 :                                         nr++;
     101             :                                 s++;
     102             :                         }
     103          64 :                         cnts[i][l] = nr;
     104             :                 }
     105          16 :                 cur = e+1;
     106             :         }
     107           8 :         if (l) {
     108             :                 int maxpos = -1, maxcnt = 0;
     109          40 :                 for (int i = 0; i<DLEN; i++) {
     110          32 :                         if (cnts[i][0] == cnts[i][1] && maxcnt < cnts[i][0]) {
     111          32 :                                 maxcnt = cnts[i][0];
     112          32 :                                 maxpos = i;
     113             :                         }
     114             :                 }
     115           8 :                 if (maxpos>=0) {
     116           8 :                         *nr_fields = maxcnt;
     117           8 :                         return delimiter[maxpos];
     118             :                 }
     119             :         }
     120             :         /* nothing detected */
     121             :         return ' ';
     122             : }
     123             : 
     124             : typedef enum csv {
     125             :         CSV_NULL = 0,
     126             :         CSV_BOOLEAN,
     127             :         CSV_BIGINT,
     128             :         CSV_DECIMAL,
     129             :         CSV_DOUBLE,
     130             :         CSV_TIME,
     131             :         CSV_DATE,
     132             :         CSV_TIMESTAMP,
     133             :         CSV_STRING,
     134             :         //later: UUID, INET, JSON, URL etc
     135             : } csv_types_t;
     136             : 
     137             : typedef struct csv_type {
     138             :         csv_types_t type;
     139             :         int scale;
     140             : } csv_type;
     141             : 
     142             : static char*
     143          32 : csv_type_map(csv_type ct)
     144             : {
     145          32 :         switch(ct.type) {
     146             :         case CSV_NULL:
     147             :                 return  "null";
     148           0 :         case CSV_BOOLEAN:
     149           0 :                 return  "boolean";
     150          16 :         case CSV_BIGINT:
     151          16 :                 return  "bigint";
     152           0 :         case CSV_DECIMAL:
     153           0 :                 return  "decimal";
     154           0 :         case CSV_DOUBLE:
     155           0 :                 return  "double";
     156           0 :         case CSV_TIME:
     157           0 :                 return  "time";
     158           0 :         case CSV_DATE:
     159           0 :                 return  "date";
     160           0 :         case CSV_TIMESTAMP:
     161           0 :                 return  "timestamp";
     162             :         case CSV_STRING:
     163             :                 return  "varchar";
     164             :         }
     165             :         return  "varchar";
     166             : }
     167             : 
     168             : static bool
     169         128 : detect_null(const char *s, const char *e)
     170             : {
     171         128 :         if (e == s)
     172          16 :                 return true;
     173             :         /* TODO parse NULL value(s) */
     174             :         return false;
     175             : }
     176             : 
     177             : static bool
     178         112 : detect_bool(const char *s, const char *e)
     179             : {
     180         112 :         if ((e - s) == 1 && (*s == 'T' || *s == 't' || *s == 'F' || *s == 'f'))
     181             :                 return true;
     182         112 :         if (strcmp(s,"TRUE") == 0 || strcmp(s,"true") == 0 || strcmp(s,"FALSE") == 0 || strcmp(s,"false") == 0)
     183             :                 return true;
     184         112 :         if (strcmp(s,"NULL") == 0)
     185           0 :                 return true;
     186             :         return false;
     187             : }
     188             : 
     189             : static bool
     190         112 : detect_bigint(const char *s, const char *e)
     191             : {
     192         112 :         if (s[0] == '-' || s[0] == '+')
     193           0 :                 s++;
     194         336 :         while(s < e) {
     195         280 :                 if (!isdigit(*s))
     196             :                         break;
     197         224 :                 s++;
     198             :         }
     199         112 :         if (s==e)
     200          56 :                 return true;
     201             :         return false;
     202             : }
     203             : 
     204             : static bool
     205          56 : detect_decimal(const char *s, const char *e, int *scale)
     206             : {
     207          56 :         int dotseen = 0;
     208             : 
     209          56 :         if (s[0] == '-' || s[0] == '+')
     210           0 :                 s++;
     211          56 :         while(s < e) {
     212          56 :                 if (!dotseen && *s == '.')
     213           0 :                         dotseen = (int)(e-(s+1));
     214          56 :                 else if (!isdigit(*s))
     215             :                         break;
     216           0 :                 s++;
     217             :         }
     218          56 :         if (s==e && dotseen) {
     219           0 :                 *scale = dotseen;
     220           0 :                 return true;
     221             :         }
     222             :         return false;
     223             : }
     224             : 
     225             : static bool
     226          56 : detect_time(const char *s, const char *e)
     227             : {
     228             :         /* TODO detect time with timezone */
     229          56 :         if ((e-s) != 5)
     230             :                 return false;
     231             :         /* 00:00 - 23:59 */
     232           0 :         if (s[2] != ':')
     233             :                 return false;
     234           0 :         if ((((s[0] == '0' || s[0] == '1') &&
     235           0 :               (s[1] >= '0' && s[1] <= '9'))  ||
     236           0 :               (s[0] == '2' && (s[1] >= '0' && s[1] <= '3'))) &&
     237           0 :                 (s[3] >= '0' && s[3] <= '5' && s[4] >= '0' && s[4] <= '9'))
     238           0 :                 return true;
     239             :         return false;
     240             : }
     241             : 
     242             : static bool
     243          56 : detect_date(const char *s, const char *e)
     244             : {
     245          56 :         if ((e-s) != 10)
     246             :                 return false;
     247             :         /* YYYY-MM-DD */
     248           0 :         if ( s[4] == '-' && s[7] == '-' &&
     249           0 :            ((s[5] == '0' && s[6] >= '0' && s[6] <= '9') ||
     250           0 :             (s[5] == '1' && s[6] >= '0' && s[6] <= '2')) &&
     251           0 :             (s[8] >= '0' && s[8] <= '3' && s[9] >= '0' && s[9] <= '9'))
     252           0 :                 return true;
     253             :         return false;
     254             : }
     255             : 
     256             : static bool
     257          56 : detect_timestamp(const char *s, const char *e)
     258             : {
     259          56 :         if ((e-s) != 16)
     260             :                 return false;
     261             :         /* DATE TIME */
     262           0 :         if (detect_date(s, s+5) && detect_time(s+6, e))
     263             :                 return true;
     264             :         return false;
     265             : }
     266             : 
     267             : /* per row */
     268             : static  csv_type *
     269          32 : detect_types_row(const char *s, const char *e, char delim, char quote, int nr_fields)
     270             : {
     271          32 :         csv_type *types = (csv_type*)GDKmalloc(sizeof(csv_type)*nr_fields);
     272          32 :         if (!types)
     273             :                 return NULL;
     274         160 :         for(int i = 0; i< nr_fields; i++) {
     275         632 :                 const char *n = (i<nr_fields-1)?next_delim(s, e, delim, quote):e;
     276         128 :                 int scale = 0;
     277             : 
     278         128 :                 types[i].type = CSV_STRING;
     279         128 :                 if (n) {
     280         128 :                         if (detect_null(s,n))
     281          16 :                                 types[i].type = CSV_NULL;
     282         112 :                         else if (detect_bool(s,n))
     283           0 :                                 types[i].type = CSV_BOOLEAN;
     284         112 :                         else if (detect_bigint(s, n))
     285          56 :                                 types[i].type = CSV_BIGINT;
     286          56 :                         else if (detect_decimal(s, n, &scale))
     287           0 :                                 types[i].type = CSV_DECIMAL;
     288          56 :                         else if (detect_time(s, n))
     289           0 :                                 types[i].type = CSV_TIME;
     290          56 :                         else if (detect_date(s, n))
     291           0 :                                 types[i].type = CSV_DATE;
     292          56 :                         else if (detect_timestamp(s, n))
     293           0 :                                 types[i].type = CSV_TIMESTAMP;
     294         128 :                         types[i].scale = scale;
     295             :                 }
     296         128 :                 s = n+1;
     297             :         }
     298             :         return types;
     299             : }
     300             : 
     301             : static csv_type *
     302           8 : detect_types(const char *buf, char delim, char quote, int nr_fields, bool *has_header)
     303             : {
     304           8 :         const char *cur = buf;
     305           8 :         csv_type *types = NULL;
     306           8 :         int nr_lines = 0;
     307             : 
     308          72 :         while ( true ) {
     309          40 :                 const char *e = strchr(cur, '\n');
     310             : 
     311          40 :                 if (!e)
     312             :                         break;
     313          32 :                 csv_type *ntypes = detect_types_row( cur, e, delim, quote, nr_fields);
     314          32 :                 if (!ntypes)
     315             :                         return NULL;
     316          32 :                 cur = e+1;
     317          32 :                 int i = 0;
     318          32 :                 if (!types) {
     319           8 :                         for(i = 0; i<nr_fields && ntypes[i].type == CSV_STRING; i++)  ;
     320             : 
     321           8 :                         if (i == nr_fields)
     322           0 :                                 *has_header = true;
     323             :                 } else { /* check if all are string, then no header */
     324          24 :                         for(i = 0; i<nr_fields && ntypes[i].type == CSV_STRING; i++)  ;
     325             : 
     326          24 :                         if (i == nr_fields)
     327           0 :                                 *has_header = false;
     328             :                 }
     329          32 :                 if (nr_lines == 1)
     330          40 :                         for(i = 0; i<nr_fields; i++)
     331          32 :                                 types[i] = ntypes[i];
     332          32 :                 if (nr_lines > 1) {
     333          80 :                         for(i = 0; i<nr_fields; i++) {
     334          64 :                                 if (types[i].type == ntypes[i].type && types[i].type == CSV_DECIMAL && types[i].scale != ntypes[i].scale) {
     335           0 :                                         types[i].type = CSV_DOUBLE;
     336           0 :                                         types[i].scale = 0;
     337          64 :                                 } else if (types[i].type < ntypes[i].type)
     338          16 :                                         types[i] = ntypes[i];
     339             :                         }
     340             :                 }
     341          32 :                 if (types)
     342          24 :                         GDKfree(ntypes);
     343             :                 else
     344             :                         types = ntypes;
     345          32 :                 nr_lines++;
     346             :         }
     347             :         return types;
     348             : }
     349             : 
     350             : static const char *
     351          32 : get_name(allocator *sa, const char *s, const char *es, const char **E, char delim, char quote, bool has_header, int col)
     352             : {
     353          32 :         if (!has_header) {
     354          32 :                 char buff[25];
     355          32 :                 snprintf(buff, 25, "name_%i", col);
     356          32 :                 return SA_STRDUP(sa, buff);
     357             :         } else {
     358           0 :                 const char *e = next_delim(s, es, delim, quote);
     359           0 :                 if (e) {
     360           0 :                         char *end = (char*)e;
     361           0 :                         if (s[0] == quote) {
     362           0 :                                 s++;
     363           0 :                                 end--;
     364             :                         }
     365           0 :                         end[0] = 0;
     366           0 :                         *E = e+1;
     367           0 :                         return SA_STRDUP(sa, s);
     368             :                 }
     369             :         }
     370             :         return NULL;
     371             : }
     372             : 
     373             : typedef struct csv_t {
     374             :         char sname[1];
     375             :         char quote;
     376             :         char delim;
     377             :         bool has_header;
     378             :         bool extra_tsep;
     379             : } csv_t;
     380             : 
     381             : /*
     382             :  * returns an error string (static or via tmp sa_allocator allocated), NULL on success
     383             :  *
     384             :  * Extend the subfunc f with result columns, ie.
     385             :         f->res = typelist;
     386             :         f->coltypes = typelist;
     387             :         f->colnames = nameslist; use tname if passed, for the relation name
     388             :  * Fill the list res_exps, with one result expressions per resulting column.
     389             :  */
     390             : static str
     391          22 : csv_relation(mvc *sql, sql_subfunc *f, char *filename, list *res_exps, char *tname)
     392             : {
     393          22 :         stream *file = csv_open_file(filename);
     394          22 :         char buf[8196+1];
     395             : 
     396          22 :         if(file == NULL)
     397             :                 return RUNTIME_FILE_NOT_FOUND;
     398             : 
     399             :         /*
     400             :          * detect delimiter ;|,\t  using quote \" or \' or none TODO escape \"\'\\ or none
     401             :          * detect types
     402             :          * detect header
     403             :          */
     404           8 :         ssize_t l = mnstr_read(file, buf, 1, 8196);
     405           8 :         mnstr_close(file);
     406           8 :         mnstr_destroy(file);
     407           8 :         if (l<0)
     408             :                 return RUNTIME_LOAD_ERROR;
     409           8 :         buf[l] = 0;
     410           8 :         bool has_header = false, extra_tsep = false;
     411           8 :         int nr_fields = 0;
     412           8 :         char q = detect_quote(buf);
     413           8 :         char d = detect_delimiter(buf, q, &nr_fields);
     414           8 :         csv_type *types = detect_types(buf, d, q, nr_fields, &has_header);
     415             : 
     416           8 :         if (!tname)
     417           0 :                 tname = "csv";
     418             : 
     419           8 :         f->tname = tname;
     420             : 
     421           8 :         const char *p = buf, *ep = strchr(p, '\n');;
     422           8 :         list *typelist = sa_list(sql->sa);
     423           8 :         list *nameslist = sa_list(sql->sa);
     424          40 :         for(int col = 0; col < nr_fields; col++) {
     425          32 :                 const char *name = get_name(sql->sa, p, ep, &p, d, q, has_header, col);
     426          32 :                 append(nameslist, (char*)name);
     427          32 :                 char* st = csv_type_map(types[col]);
     428             : 
     429          32 :                 if(st) {
     430          64 :                         sql_subtype *t = (types[col].type == CSV_DECIMAL)?
     431          32 :                                         sql_bind_subtype(sql->sa, st, 18, types[col].scale):
     432          32 :                                         sql_bind_subtype(sql->sa, st,  0, types[col].scale);
     433          32 :                         if (!t && (col+1) == nr_fields && types[col].type == CSV_NULL) {
     434           0 :                                 nr_fields--;
     435           0 :                                 extra_tsep = true;
     436           0 :                         } else if (t) {
     437          32 :                                 list_append(typelist, t);
     438          32 :                                 list_append(res_exps, exp_column(sql->sa, NULL, name, t, CARD_MULTI, 1, 0, 0));
     439             :                         } else {
     440           0 :                                 GDKfree(types);
     441           0 :                                 throw(SQL, SQLSTATE(42000), "csv" "type %s not found\n", st);
     442             :                         }
     443             :                 } else {
     444             :                         /* shouldn't be possible, we fallback to strings */
     445           0 :                         GDKfree(types);
     446           0 :                         throw(SQL, SQLSTATE(42000), "csv" "type unknown\n");
     447             :                 }
     448             :         }
     449           8 :         GDKfree(types);
     450           8 :         f->res = typelist;
     451           8 :         f->coltypes = typelist;
     452           8 :         f->colnames = nameslist;
     453             : 
     454           8 :         csv_t *r = (csv_t *)sa_alloc(sql->sa, sizeof(csv_t));
     455           8 :         r->sname[0] = 0;
     456           8 :         r->quote = q;
     457           8 :         r->delim = d;
     458           8 :         r->extra_tsep = extra_tsep;
     459           8 :         r->has_header = has_header;
     460           8 :         f->sname = (char*)r; /* pass schema++ */
     461           8 :         return MAL_SUCCEED;
     462             : }
     463             : 
     464             : static void *
     465           6 : csv_load(void *BE, sql_subfunc *f, char *filename, sql_exp *topn)
     466             : {
     467           6 :         backend *be = (backend*)BE;
     468           6 :         mvc *sql = be->mvc;
     469           6 :         csv_t *r = (csv_t *)f->sname;
     470           6 :         sql_table *t = NULL;
     471             : 
     472           6 :         if (mvc_create_table( &t, be->mvc, be->mvc->session->tr->tmp/* misuse tmp schema */, f->tname /*gettable name*/, tt_table, false, SQL_DECLARED_TABLE, 0, 0, false) != LOG_OK)
     473             :                 //throw(SQL, SQLSTATE(42000), "csv" RUNTIME_FILE_NOT_FOUND);
     474             :                 /* alloc error */
     475             :                 return NULL;
     476             : 
     477           6 :         node *n, *nn = f->colnames->h, *tn = f->coltypes->h;
     478          30 :         for (n = f->res->h; n; n = n->next, nn = nn->next, tn = tn->next) {
     479          24 :                 const char *name = nn->data;
     480          24 :                 sql_subtype *tp = tn->data;
     481          24 :                 sql_column *c = NULL;
     482             : 
     483          24 :                 if (!tp || mvc_create_column(&c, be->mvc, t, name, tp) != LOG_OK) {
     484             :                         //throw(SQL, SQLSTATE(42000), "csv" RUNTIME_LOAD_ERROR);
     485           0 :                         return NULL;
     486             :                 }
     487             :         }
     488             :         /* (res bats) := import(table T, 'delimit', '\n', 'quote', str:nil, fname, lng:nil, 0/1, 0, str:nil, int:nil, * int:nil ); */
     489             : 
     490             :         /* lookup copy_from */
     491           6 :         sql_subfunc *cf = sql_find_func(sql, "sys", "copyfrom", 14, F_UNION, true, NULL);
     492           6 :         cf->res = f->res;
     493             : 
     494           6 :         sql_subtype tpe;
     495           6 :         sql_find_subtype(&tpe, "varchar", 0, 0);
     496           6 :         char tsep[2], rsep[3], ssep[2];
     497           6 :         tsep[0] = r->delim;
     498           6 :         tsep[1] = 0;
     499           6 :         ssep[0] = r->quote;
     500           6 :         ssep[1] = 0;
     501           6 :         if (r->extra_tsep) {
     502           0 :                 rsep[0] = r->delim;
     503           0 :                 rsep[1] = '\n';
     504           0 :                 rsep[2] = 0;
     505             :         } else {
     506           6 :                 rsep[0] = '\n';
     507           6 :                 rsep[1] = 0;
     508             :         }
     509           6 :         list *args = new_exp_list(sql->sa);
     510             : 
     511           6 :         append(args, exp_atom_ptr(sql->sa, t));
     512           6 :         append(args, exp_atom_str(sql->sa, tsep, &tpe));
     513           6 :         append(args, exp_atom_str(sql->sa, rsep, &tpe));
     514           6 :         append(args, exp_atom_str(sql->sa, ssep, &tpe));
     515             : 
     516           6 :         append(args, exp_atom_str(sql->sa, "", &tpe));
     517           6 :         append(args, exp_atom_str(sql->sa, filename, &tpe));
     518           6 :         append(args, topn ? topn: exp_atom_lng(sql->sa, -1));
     519          12 :         append(args, exp_atom_lng(sql->sa, r->has_header?2:1));
     520             : 
     521           6 :         append(args, exp_atom_int(sql->sa, 0));
     522           6 :         append(args, exp_atom_str(sql->sa, NULL, &tpe));
     523           6 :         append(args, exp_atom_int(sql->sa, 0));
     524           6 :         append(args, exp_atom_int(sql->sa, 0));
     525             : 
     526           6 :         append(args, exp_atom_str(sql->sa, ".", &tpe));
     527           6 :         append(args, exp_atom_str(sql->sa, NULL, &tpe));
     528             : 
     529           6 :         sql_exp *import = exp_op(sql->sa, args, cf);
     530             : 
     531           6 :         return exp_bin(be, import, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0, 0);
     532             : }
     533             : 
     534             : static str
     535         329 : CSVprelude(Client cntxt, MalBlkPtr mb, MalStkPtr stk, InstrPtr pci)
     536             : {
     537         329 :         (void)cntxt; (void)mb; (void)stk; (void)pci;
     538             : 
     539         329 :         fl_register("csv", &csv_relation, &csv_load);
     540         329 :         fl_register("tsv", &csv_relation, &csv_load);
     541         329 :         fl_register("psv", &csv_relation, &csv_load);
     542         329 :         return MAL_SUCCEED;
     543             : }
     544             : 
     545             : static str
     546         328 : CSVepilogue(void *ret)
     547             : {
     548         328 :         fl_unregister("csv");
     549         328 :         fl_unregister("tsv");
     550         328 :         fl_unregister("psv");
     551         328 :         (void)ret;
     552         328 :         return MAL_SUCCEED;
     553             : }
     554             : 
     555             : #include "sql_scenario.h"
     556             : #include "mel.h"
     557             : 
     558             : static mel_func csv_init_funcs[] = {
     559             :         pattern("csv", "prelude", CSVprelude, false, "", noargs),
     560             :         command("csv", "epilogue", CSVepilogue, false, "", noargs),
     561             : { .imp=NULL }
     562             : };
     563             : 
     564             : #include "mal_import.h"
     565             : #ifdef _MSC_VER
     566             : #undef read
     567             : #pragma section(".CRT$XCU",read)
     568             : #endif
     569         329 : LIB_STARTUP_FUNC(init_csv_mal)
     570         329 : { mal_module("csv", NULL, csv_init_funcs); }
     571             : 

Generated by: LCOV version 1.14