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 28 : csv_open_file(char* filename)
32 : {
33 28 : return open_rastream(filename);
34 : }
35 :
36 : /* todo handle escapes */
37 : static const char *
38 286 : next_delim(const char *s, const char *e, char delim, char quote)
39 : {
40 286 : bool inquote = false;
41 2228 : for(; s < e; s++) {
42 2116 : if (*s == quote)
43 304 : inquote = !inquote;
44 1812 : else if (!inquote && *s == delim)
45 174 : return s;
46 : }
47 112 : if (s <= e)
48 112 : return s;
49 : return NULL;
50 : }
51 :
52 : /* todo detect escapes */
53 : static char
54 14 : detect_quote(const char *buf)
55 : {
56 14 : const char *cur = buf;
57 14 : const char *l = NULL;
58 : /* "'(none) */
59 14 : bool has_double_quote = true, has_single_quote = true;
60 52 : while ((has_double_quote || has_single_quote) && (l = strchr(cur, '\n')) != NULL) {
61 38 : const char *s = cur, *t;
62 38 : if (has_double_quote && ((t = strchr(s, '"')) == NULL || t > l)) /* no quote not used */
63 6 : has_double_quote = false;
64 38 : if (has_single_quote && ((t = strchr(s, '\'')) == NULL || t > l)) /* no quote not used */
65 38 : has_single_quote = false;
66 38 : cur = l+1;
67 : }
68 14 : if (has_double_quote && !has_single_quote)
69 : return '"';
70 6 : 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 14 : detect_delimiter(const char *buf, char q, int *nr_fields)
79 : {
80 14 : const char delimiter[] = ",|;\t";
81 14 : int cnts[DLEN][2] = { 0 }, l = 0;
82 :
83 14 : const char *cur = buf;
84 :
85 42 : for (l = 0; l < 2; l++) { /* start with 2 lines only */
86 28 : const char *e = strchr(cur, '\n');
87 28 : if (!e)
88 : break;
89 : int i = 0;
90 : const char *dp = delimiter;
91 140 : for (char d = *dp; d; d=*(++dp), i++) {
92 112 : const char *s = cur;
93 : /* all lines should have some numbers */
94 112 : if (l && cnts[i][l])
95 0 : if (cnts[i][0] != cnts[i][1])
96 : break;
97 : int nr = 1;
98 172 : while( (s = next_delim(s, e, d, q)) != NULL && s<e ) {
99 60 : if (s+1 <= e)
100 60 : nr++;
101 : s++;
102 : }
103 112 : cnts[i][l] = nr;
104 : }
105 28 : cur = e+1;
106 : }
107 14 : if (l) {
108 : int maxpos = -1, maxcnt = 0;
109 70 : for (int i = 0; i<DLEN; i++) {
110 56 : if (cnts[i][0] == cnts[i][1] && maxcnt < cnts[i][0]) {
111 56 : maxcnt = cnts[i][0];
112 56 : maxpos = i;
113 : }
114 : }
115 14 : if (maxpos>=0) {
116 14 : *nr_fields = maxcnt;
117 14 : 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 44 : csv_type_map(csv_type ct)
144 : {
145 44 : switch(ct.type) {
146 : case CSV_NULL:
147 : return "null";
148 0 : case CSV_BOOLEAN:
149 0 : return "boolean";
150 22 : case CSV_BIGINT:
151 22 : 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 164 : detect_null(const char *s, const char *e)
170 : {
171 164 : if (e == s)
172 16 : return true;
173 : /* TODO parse NULL value(s) */
174 : return false;
175 : }
176 :
177 : static bool
178 148 : detect_bool(const char *s, const char *e)
179 : {
180 148 : if ((e - s) == 1 && (*s == 'T' || *s == 't' || *s == 'F' || *s == 'f'))
181 : return true;
182 148 : if (strcmp(s,"TRUE") == 0 || strcmp(s,"true") == 0 || strcmp(s,"FALSE") == 0 || strcmp(s,"false") == 0)
183 : return true;
184 148 : if (strcmp(s,"NULL") == 0)
185 0 : return true;
186 : return false;
187 : }
188 :
189 : static bool
190 148 : detect_bigint(const char *s, const char *e)
191 : {
192 148 : if (s[0] == '-' || s[0] == '+')
193 0 : s++;
194 390 : while(s < e) {
195 316 : if (!isdigit(*s))
196 : break;
197 242 : s++;
198 : }
199 148 : if (s==e)
200 74 : return true;
201 : return false;
202 : }
203 :
204 : static bool
205 74 : detect_decimal(const char *s, const char *e, int *scale)
206 : {
207 74 : int dotseen = 0;
208 :
209 74 : if (s[0] == '-' || s[0] == '+')
210 0 : s++;
211 74 : while(s < e) {
212 74 : if (!dotseen && *s == '.')
213 0 : dotseen = (int)(e-(s+1));
214 74 : else if (!isdigit(*s))
215 : break;
216 0 : s++;
217 : }
218 74 : if (s==e && dotseen) {
219 0 : *scale = dotseen;
220 0 : return true;
221 : }
222 : return false;
223 : }
224 :
225 : static bool
226 74 : detect_time(const char *s, const char *e)
227 : {
228 : /* TODO detect time with timezone */
229 74 : 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 74 : detect_date(const char *s, const char *e)
244 : {
245 74 : 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 74 : detect_timestamp(const char *s, const char *e)
258 : {
259 74 : 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 50 : detect_types_row(const char *s, const char *e, char delim, char quote, int nr_fields)
270 : {
271 50 : csv_type *types = (csv_type*)GDKmalloc(sizeof(csv_type)*nr_fields);
272 50 : if (!types)
273 : return NULL;
274 214 : for(int i = 0; i< nr_fields; i++) {
275 164 : const char *n = (i<nr_fields-1)?next_delim(s, e, delim, quote):e;
276 164 : int scale = 0;
277 :
278 164 : types[i].type = CSV_STRING;
279 164 : if (n) {
280 164 : if (detect_null(s,n))
281 16 : types[i].type = CSV_NULL;
282 148 : else if (detect_bool(s,n))
283 0 : types[i].type = CSV_BOOLEAN;
284 148 : else if (detect_bigint(s, n))
285 74 : types[i].type = CSV_BIGINT;
286 74 : else if (detect_decimal(s, n, &scale))
287 0 : types[i].type = CSV_DECIMAL;
288 74 : else if (detect_time(s, n))
289 0 : types[i].type = CSV_TIME;
290 74 : else if (detect_date(s, n))
291 0 : types[i].type = CSV_DATE;
292 74 : else if (detect_timestamp(s, n))
293 0 : types[i].type = CSV_TIMESTAMP;
294 164 : types[i].scale = scale;
295 : }
296 164 : s = n+1;
297 : }
298 : return types;
299 : }
300 :
301 : static csv_type *
302 14 : detect_types(const char *buf, char delim, char quote, int nr_fields, bool *has_header)
303 : {
304 14 : const char *cur = buf;
305 14 : csv_type *types = NULL;
306 14 : int nr_lines = 0;
307 :
308 114 : while ( true ) {
309 64 : const char *e = strchr(cur, '\n');
310 :
311 64 : if (!e)
312 : break;
313 50 : csv_type *ntypes = detect_types_row( cur, e, delim, quote, nr_fields);
314 50 : if (!ntypes)
315 : return NULL;
316 50 : cur = e+1;
317 50 : int i = 0;
318 50 : if (!types) {
319 14 : for(i = 0; i<nr_fields && ntypes[i].type == CSV_STRING; i++) ;
320 :
321 14 : if (i == nr_fields)
322 0 : *has_header = true;
323 : } else { /* check if all are string, then no header */
324 36 : for(i = 0; i<nr_fields && ntypes[i].type == CSV_STRING; i++) ;
325 :
326 36 : if (i == nr_fields)
327 0 : *has_header = false;
328 : }
329 50 : if (nr_lines == 1)
330 58 : for(i = 0; i<nr_fields; i++)
331 44 : types[i] = ntypes[i];
332 50 : if (nr_lines > 1) {
333 98 : for(i = 0; i<nr_fields; i++) {
334 76 : 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 76 : } else if (types[i].type < ntypes[i].type)
338 16 : types[i] = ntypes[i];
339 : }
340 : }
341 50 : if (types)
342 36 : GDKfree(ntypes);
343 : else
344 : types = ntypes;
345 50 : nr_lines++;
346 : }
347 14 : if (types) { /* NULL -> STRING */
348 58 : for(int i = 0; i<nr_fields; i++) {
349 44 : if (types[i].type == CSV_NULL)
350 0 : types[i].type = CSV_STRING;
351 : }
352 : }
353 : return types;
354 : }
355 :
356 : static const char *
357 44 : get_name(allocator *sa, const char *s, const char *es, const char **E, char delim, char quote, bool has_header, int col)
358 : {
359 44 : if (!has_header) {
360 44 : char buff[25];
361 44 : snprintf(buff, 25, "name_%i", col);
362 44 : return SA_STRDUP(sa, buff);
363 : } else {
364 0 : const char *e = next_delim(s, es, delim, quote);
365 0 : if (e) {
366 0 : char *end = (char*)e;
367 0 : if (s[0] == quote) {
368 0 : s++;
369 0 : end--;
370 : }
371 0 : end[0] = 0;
372 0 : *E = e+1;
373 0 : return SA_STRDUP(sa, s);
374 : }
375 : }
376 : return NULL;
377 : }
378 :
379 : typedef struct csv_t {
380 : char sname[1];
381 : char quote;
382 : char delim;
383 : bool has_header;
384 : bool extra_tsep;
385 : } csv_t;
386 :
387 : /*
388 : * returns an error string (static or via tmp sa_allocator allocated), NULL on success
389 : *
390 : * Extend the subfunc f with result columns, ie.
391 : f->res = typelist;
392 : f->coltypes = typelist;
393 : f->colnames = nameslist; use tname if passed, for the relation name
394 : * Fill the list res_exps, with one result expressions per resulting column.
395 : */
396 : static str
397 28 : csv_relation(mvc *sql, sql_subfunc *f, char *filename, list *res_exps, char *tname)
398 : {
399 28 : stream *file = csv_open_file(filename);
400 28 : char buf[8196+1];
401 :
402 28 : if(file == NULL)
403 : return RUNTIME_FILE_NOT_FOUND;
404 :
405 : /*
406 : * detect delimiter ;|,\t using quote \" or \' or none TODO escape \"\'\\ or none
407 : * detect types
408 : * detect header
409 : */
410 14 : ssize_t l = mnstr_read(file, buf, 1, 8196);
411 14 : mnstr_close(file);
412 14 : mnstr_destroy(file);
413 14 : if (l<0)
414 : return RUNTIME_LOAD_ERROR;
415 14 : buf[l] = 0;
416 14 : bool has_header = false, extra_tsep = false;
417 14 : int nr_fields = 0;
418 14 : char q = detect_quote(buf);
419 14 : char d = detect_delimiter(buf, q, &nr_fields);
420 14 : csv_type *types = detect_types(buf, d, q, nr_fields, &has_header);
421 :
422 14 : if (!tname)
423 0 : tname = "csv";
424 :
425 14 : f->tname = tname;
426 :
427 14 : const char *p = buf, *ep = strchr(p, '\n');;
428 14 : list *typelist = sa_list(sql->sa);
429 14 : list *nameslist = sa_list(sql->sa);
430 58 : for(int col = 0; col < nr_fields; col++) {
431 44 : const char *name = get_name(sql->sa, p, ep, &p, d, q, has_header, col);
432 44 : append(nameslist, (char*)name);
433 44 : char* st = csv_type_map(types[col]);
434 :
435 44 : if(st) {
436 88 : sql_subtype *t = (types[col].type == CSV_DECIMAL)?
437 44 : sql_bind_subtype(sql->sa, st, 18, types[col].scale):
438 44 : sql_bind_subtype(sql->sa, st, 0, types[col].scale);
439 44 : if (!t && (col+1) == nr_fields && types[col].type == CSV_NULL) {
440 0 : nr_fields--;
441 0 : extra_tsep = true;
442 0 : } else if (t) {
443 44 : list_append(typelist, t);
444 44 : sql_exp *ne = exp_column(sql->sa, tname, name, t, CARD_MULTI, 1, 0, 0);
445 44 : set_basecol(ne);
446 44 : ne->alias.label = -(sql->nid++);
447 44 : list_append(res_exps, ne);
448 : } else {
449 0 : GDKfree(types);
450 0 : throw(SQL, SQLSTATE(42000), "csv" "type %s not found\n", st);
451 : }
452 : } else {
453 : /* shouldn't be possible, we fallback to strings */
454 0 : GDKfree(types);
455 0 : throw(SQL, SQLSTATE(42000), "csv" "type unknown\n");
456 : }
457 : }
458 14 : GDKfree(types);
459 14 : f->res = typelist;
460 14 : f->coltypes = typelist;
461 14 : f->colnames = nameslist;
462 :
463 14 : csv_t *r = (csv_t *)sa_alloc(sql->sa, sizeof(csv_t));
464 14 : r->sname[0] = 0;
465 14 : r->quote = q;
466 14 : r->delim = d;
467 14 : r->extra_tsep = extra_tsep;
468 14 : r->has_header = has_header;
469 14 : f->sname = (char*)r; /* pass schema++ */
470 14 : return MAL_SUCCEED;
471 : }
472 :
473 : static void *
474 12 : csv_load(void *BE, sql_subfunc *f, char *filename, sql_exp *topn)
475 : {
476 12 : backend *be = (backend*)BE;
477 12 : mvc *sql = be->mvc;
478 12 : csv_t *r = (csv_t *)f->sname;
479 12 : sql_table *t = NULL;
480 :
481 12 : 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)
482 : //throw(SQL, SQLSTATE(42000), "csv" RUNTIME_FILE_NOT_FOUND);
483 : /* alloc error */
484 : return NULL;
485 :
486 12 : node *n, *nn = f->colnames->h, *tn = f->coltypes->h;
487 48 : for (n = f->res->h; n; n = n->next, nn = nn->next, tn = tn->next) {
488 36 : const char *name = nn->data;
489 36 : sql_subtype *tp = tn->data;
490 36 : sql_column *c = NULL;
491 :
492 36 : if (!tp || mvc_create_column(&c, be->mvc, t, name, tp) != LOG_OK) {
493 : //throw(SQL, SQLSTATE(42000), "csv" RUNTIME_LOAD_ERROR);
494 0 : return NULL;
495 : }
496 : }
497 : /* (res bats) := import(table T, 'delimit', '\n', 'quote', str:nil, fname, lng:nil, 0/1, 0, str:nil, int:nil, * int:nil ); */
498 :
499 : /* lookup copy_from */
500 12 : sql_subfunc *cf = sql_find_func(sql, "sys", "copyfrom", 14, F_UNION, true, NULL);
501 12 : cf->res = f->res;
502 :
503 12 : sql_subtype tpe;
504 12 : sql_find_subtype(&tpe, "varchar", 0, 0);
505 12 : char tsep[2], rsep[3], ssep[2];
506 12 : tsep[0] = r->delim;
507 12 : tsep[1] = 0;
508 12 : ssep[0] = r->quote;
509 12 : ssep[1] = 0;
510 12 : if (r->extra_tsep) {
511 0 : rsep[0] = r->delim;
512 0 : rsep[1] = '\n';
513 0 : rsep[2] = 0;
514 : } else {
515 12 : rsep[0] = '\n';
516 12 : rsep[1] = 0;
517 : }
518 12 : list *args = new_exp_list(sql->sa);
519 :
520 12 : append(args, exp_atom_ptr(sql->sa, t));
521 12 : append(args, exp_atom_str(sql->sa, tsep, &tpe));
522 12 : append(args, exp_atom_str(sql->sa, rsep, &tpe));
523 12 : append(args, exp_atom_str(sql->sa, ssep, &tpe));
524 :
525 12 : append(args, exp_atom_str(sql->sa, "", &tpe));
526 12 : append(args, exp_atom_str(sql->sa, filename, &tpe));
527 12 : append(args, topn ? topn: exp_atom_lng(sql->sa, -1));
528 24 : append(args, exp_atom_lng(sql->sa, r->has_header?2:1));
529 :
530 12 : append(args, exp_atom_int(sql->sa, 0));
531 12 : append(args, exp_atom_str(sql->sa, NULL, &tpe));
532 12 : append(args, exp_atom_int(sql->sa, 0));
533 12 : append(args, exp_atom_int(sql->sa, 0));
534 :
535 12 : append(args, exp_atom_str(sql->sa, ".", &tpe));
536 12 : append(args, exp_atom_str(sql->sa, NULL, &tpe));
537 :
538 12 : sql_exp *import = exp_op(sql->sa, args, cf);
539 :
540 12 : return exp_bin(be, import, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0, 0);
541 : }
542 :
543 : static str
544 319 : CSVprelude(Client cntxt, MalBlkPtr mb, MalStkPtr stk, InstrPtr pci)
545 : {
546 319 : (void)cntxt; (void)mb; (void)stk; (void)pci;
547 :
548 319 : fl_register("csv", &csv_relation, &csv_load);
549 319 : fl_register("tsv", &csv_relation, &csv_load);
550 319 : fl_register("psv", &csv_relation, &csv_load);
551 319 : return MAL_SUCCEED;
552 : }
553 :
554 : static str
555 318 : CSVepilogue(void *ret)
556 : {
557 318 : fl_unregister("csv");
558 318 : fl_unregister("tsv");
559 318 : fl_unregister("psv");
560 318 : (void)ret;
561 318 : return MAL_SUCCEED;
562 : }
563 :
564 : #include "sql_scenario.h"
565 : #include "mel.h"
566 :
567 : static mel_func csv_init_funcs[] = {
568 : pattern("csv", "prelude", CSVprelude, false, "", noargs),
569 : command("csv", "epilogue", CSVepilogue, false, "", noargs),
570 : { .imp=NULL }
571 : };
572 :
573 : #include "mal_import.h"
574 : #ifdef _MSC_VER
575 : #undef read
576 : #pragma section(".CRT$XCU",read)
577 : #endif
578 319 : LIB_STARTUP_FUNC(init_csv_mal)
579 319 : { mal_module("csv", NULL, csv_init_funcs); }
|