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