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 :
|