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