source: trunk/FACT++/src/fits2sql.cc@ 19126

Last change on this file since 19126 was 19126, checked in by tbretz, 6 years ago
Implemented the possibility to print basic connection informations.
File size: 24.5 KB
Line 
1#include <boost/algorithm/string/join.hpp>
2#include <boost/regex.hpp>
3
4#include "Database.h"
5
6#include "Time.h"
7#include "Configuration.h"
8
9#include "zfits.h"
10
11using namespace std;
12
13// ------------------------------------------------------------------------
14
15struct Map : pair<string, string>
16{
17 Map() { }
18};
19
20std::istream &operator>>(std::istream &in, Map &m)
21{
22 const istreambuf_iterator<char> eos;
23 string txt(istreambuf_iterator<char>(in), eos);
24
25 const boost::regex expr("((.*)[^\\\\])/(.*)");
26 boost::smatch match;
27 if (!boost::regex_match(txt, match, expr))
28 throw runtime_error("Could not evaluate map argument: "+txt);
29
30 m.first = match[1].str();
31 m.second = match[3].str();
32
33 return in;
34}
35
36
37void SetupConfiguration(Configuration &conf)
38{
39 po::options_description control("Root to SQL");
40 control.add_options()
41 ("uri,u", var<string>()
42#if BOOST_VERSION >= 104200
43 ->required()
44#endif
45 , "Database link as in\n\tuser:password@server[:port]/database[/comp].")
46 ("file", var<string>("")
47#if BOOST_VERSION >= 104200
48 ->required()
49#endif
50 , "The root file to read from")
51 ("force", po_switch(), "Force processing even if there is no database connection")
52 ("create", po_switch(), "Create the database if not existing")
53 ("drop", po_switch(), "Drop the table (implies create)")
54 ("extension,e", var<string>(""), "Name of the fits extension (table) to convert")
55 ("table", var<string>(""), "Name of the table to use (default is the tree name)")
56 ("map", vars<Map>(), "A regular expression which is applied to the leaf name befoee it is used as SQL column name)")
57 ("sql-type", vars<Map>(), "Allows to overwrite the calculated SQL type for a given column e.g. 'sql-column-name/UNSIGNED IN'")
58 ("ignore", vars<string>(), "Ignore the given leaf, if the given regular expression matches")
59 ("unsigned", vars<string>(), "In fits files per default columns are signed. This interpretss the column as unsigned value. Use the FITS column name.")
60 ("primary", vars<string>(), "List of columns to be used as primary keys during table creation (in connection with --create)")
61 ("first", var<int64_t>(int64_t(0)), "First event to start with (default: 0), mainly for test purpose")
62 ("max", var<int64_t>(int64_t(0)), "Maximum number of events to process (0: all), mainly for test purpose")
63 ("engine", var<string>(""), "Database engine to be used when a new table is created")
64 ("row-format", var<string>(""), "Defines the ROW_FORMAT keyword for table creation")
65 ("duplicate", vars<string>(""), "Specifies an assignment_list for an 'ON DUPLICATE KEY UPDATE' expression")
66 ("ignore-errors", po_switch(), "Adds the IGNORE keyword to the INSERT query (turns errors into warnings, ignores rows with errors)")
67 ;
68
69 po::options_description debug("Debug options");
70 debug.add_options()
71 ("no-insert", po_switch(), "Does not insert any data into the table")
72 ("dry-run", po_switch(), "Skip any query which changes the databse (might result in consecutive failures)")
73 ("print-extensions", po_switch(), "Print extensions (tables) from fits file")
74 ("print-connection", po_switch(), "Print database connection information")
75 ("print-columns", po_switch(), "Print columns in fits table")
76 ("print-insert", po_switch(), "Print the INSERT query (note that it contains all data)")
77 ("print-create", po_switch(), "Print the CREATE query")
78 ("verbose,v", var<uint16_t>(1), "Verbosity (0: quiet, 1: default, 2: more, 3, ...)")
79 ;
80
81 po::positional_options_description p;
82 p.add("file", 1); // The 1st positional options (n=1)
83
84 conf.AddOptions(control);
85 conf.AddOptions(debug);
86 conf.SetArgumentPositions(p);
87}
88
89void PrintUsage()
90{
91 cout <<
92 "fits2sql - Fills the data from a fits file into a database\n"
93 "\n"
94 "For convenience, this documentation uses the extended version of the options, "
95 "refer to the output below to get the abbreviations.\n"
96 "\n"
97 "This is a general purpose tool to fill the contents of a fite file into a database.\n"
98 "\n"
99 "Each fits file contians several table, so called extenbsions. Each tables has "
100 "a number of columns compilsed from basic data types. The default extension to "
101 "read from is the first one on the file but the name can be overwritten using "
102 "--extension. The default table name to fill the data into is identical to "
103 "the extension name. It can be overwritten using --table.\n"
104 "\n"
105 "The name of each column to which data is filled is obtained from "
106 "the fits column names. The column names can be checked using --print-columns. "
107 "Sometimes these names might not be convenient. To allow to simplify or replace "
108 "column names, regular expressions (using boost's regex) can be defined to change "
109 "the names. Note that these regular expressions are applied one by one on each "
110 "columns's name. A valid expression could "
111 "be:\n"
112 " --map=MHillas\\.f/\n"
113 "which would remove all occurances of 'MHillas.f'. This option can be used more than "
114 "once. They are applied in sequence. A single match does not stop the sequence.\n"
115 "\n"
116 "Sometimes it might also be convenient to skip a column. This can be done with "
117 "the --ignore resource. If the given regular expresion yields a match, the "
118 "column will be ignored. Note that the regular expression works on the raw-name "
119 "of the column not the readily mapped SQL column names. Example:\n"
120 " --ignore=ThetaSq\\..*\n"
121 "will skip all leaved which start with 'ThetaSq.'. This option can be used"
122 "more than once.\n"
123 "\n"
124 "The data type of each column is kept as close as possible to the columns' data "
125 "types. If for some reason this is not wanted, the data type of the SQL column "
126 "can be overwritten with --sql-type sql-column/sql-ytpe, for example:\n"
127 " --sql-type=FileId/UNSIGNED INT\n"
128 "while the first argument of the name of the SQL column to which the data type "
129 "should be applied. The second column is the basic SQL data type. The option can "
130 "be given more than once.\n"
131 "\n"
132 "Database interaction:\n"
133 "\n"
134 "To drop an existing table, --drop can be used.\n"
135 "\n"
136 "To create a table according to theSQL column names and data types, --create "
137 "can be used. The query used can be printed with --print-create even --create "
138 "has not been specified.\n"
139 "\n"
140 "To choose the columns which should become primary keys, use --primary, "
141 "for exmaple:\n"
142 " --primary=col1\n"
143 "To define more than one column as primary key, the option can be given more than "
144 "once. Note that the combination of these columns must be unique.\n"
145 "\n"
146 "All columns are created as NOT NULL as default. To force a database engine "
147 "and/or a storage format, use --engine and --rot-format.\n"
148 "\n"
149 "Usually, the INSERT query would fail if the PRIMARY key exists already. "
150 "This can be avoided using the 'ON DUPLICATE KEY UPDATE' directive. With the "
151 "--duplicate, you can specify what should be updated in case of a duplicate key. "
152 "To keep the row untouched, you can just update the primary key "
153 "with the identical primary key, e.g. --duplicate='MyPrimary=VALUES(MyPrimary)'. "
154 "The --duplicate resource can be specified more than once to add more expressions "
155 "to the assignment_list. For more details, see the MySQL manual.\n"
156 "\n"
157 "Another possibility is to add the IGNORE keyword to the INSERT query by "
158 "--ignore-errors, which essentially ignores all errors and turns them into "
159 "warnings which are printed after the query succeeded.\n"
160 "\n"
161 "For debugging purpose, or to just create or drop a table, the final insert "
162 "query can be skipped using --no-insert. Note that for performance reason, "
163 "all data is collected in memory and a single INSERT query is issued at the "
164 "end.\n"
165 "\n"
166 "Using a higher verbosity level (-v), an overview of the written columns or all "
167 "processed leaves is printed depending on the verbosity level. The output looks "
168 "like the following\n"
169 " Leaf name [root data type] (SQL name)\n"
170 "for example\n"
171 " MTime.fTime.fMilliSec [Long64_t] (MilliSec)\n"
172 "which means that the leaf MTime.fTime.fMilliSec is detected to be a Long64_t "
173 "which is filled into a column called MilliSec. Leaves with non basic data types "
174 "are ignored automatically and are marked as (-n/a-). User ignored columnd "
175 "are marked as (-ignored-).\n"
176 "\n"
177 "If a query failed, the query is printed to stderr together with the error message. "
178 "For the main INSERT query, this is only true if the verbosity level is at least 2 "
179 "or the query has less than 80*25 bytes.\n"
180 "\n"
181 "In case of succes, 0 is returned, a value>0 otherwise.\n"
182 "\n"
183 "Usage: fits2sql [options] --uri URI fitsfile.fits[.gz]\n"
184 "\n"
185 ;
186 cout << endl;
187}
188
189enum BasicType_t
190{
191 kNone = 0,
192 kVarchar,
193 kBool,
194 kFloat,
195 kDouble,
196 kInt8,
197 kUInt8,
198 kInt16,
199 kUInt16,
200 kInt32,
201 kUInt32,
202 kInt64,
203 kUInt64,
204// kMJD,
205};
206
207static const map<char, pair<BasicType_t, string>> ConvFits =
208{
209 { 'A', { kVarchar, "VARCHAR" } },
210 { 'a', { kVarchar, "VARCHAR" } },
211 { 'L', { kBool, "BOOLEAN" } },
212 { 'l', { kBool, "BOOLEAN" } },
213 { 'B', { kInt8, "TINYINT" } },
214 { 'b', { kUInt8, "TINYINT UNSIGNED" } },
215 { 'I', { kInt16, "SMALLINT" } },
216 { 'i', { kUInt16, "SMALLINT UNSIGNED" } },
217 { 'J', { kInt32, "INT" } },
218 { 'j', { kUInt32, "INT UNSIGNED" } },
219 { 'K', { kInt64, "BIGINT" } },
220 { 'k', { kUInt64, "BIGINT UNSIGNED" } },
221 { 'E', { kFloat, "FLOAT" } },
222 { 'e', { kFloat, "FLOAT" } },
223 { 'D', { kDouble, "DOUBLE" } },
224 { 'd', { kDouble, "DOUBLE" } },
225};
226
227struct Container
228{
229 string branch; // fits column name
230 string column; // sql column name
231 BasicType_t type;
232 size_t num;
233// double offset;
234 void *ptr;
235
236 Container(const string &b, const string &c, const BasicType_t &t, const size_t &n/*, const double &offset=0*/) : branch(b), column(c), type(t), num(n), ptr(0)
237 {
238 }
239 ~Container()
240 {
241 }
242
243 string fmt(const size_t &index) const
244 {
245 ostringstream str;
246
247 switch (type)
248 {
249 //case kVarchar: str << string(reinterpret_cast<char*>(ptr), num); break;
250 case kVarchar: str << string(reinterpret_cast<char*>(ptr), num).c_str(); break;
251 case kFloat: str << setprecision(8) << reinterpret_cast<float*>(ptr)[index]; break;
252// case kMJD: str << setprecision(16) << reinterpret_cast<double*>(ptr)[0]+offset; break;
253 case kDouble: str << setprecision(16) << reinterpret_cast<double*>(ptr)[index]; break;
254 case kBool:
255 case kInt8: str << int32_t(reinterpret_cast<int8_t*>(ptr)[index]); break;
256 case kUInt8: str << uint32_t(reinterpret_cast<uint8_t*>(ptr)[index]); break;
257 case kInt16: str << reinterpret_cast<int16_t*>(ptr)[index]; break;
258 case kUInt16: str << reinterpret_cast<uint16_t*>(ptr)[index]; break;
259 case kInt32: str << reinterpret_cast<int32_t*>(ptr)[index]; break;
260 case kUInt32: str << reinterpret_cast<uint32_t*>(ptr)[index]; break;
261 case kInt64: str << reinterpret_cast<int64_t*>(ptr)[index]; break;
262 case kUInt64: str << reinterpret_cast<uint64_t*>(ptr)[index]; break;
263 case kNone:
264 break;
265 }
266
267 return str.str();
268 }
269};
270
271int main(int argc, const char* argv[])
272{
273 Time start;
274
275 Configuration conf(argv[0]);
276 conf.SetPrintUsage(PrintUsage);
277 SetupConfiguration(conf);
278
279 if (!conf.DoParse(argc, argv))
280 return 127;
281
282 // ----------------------------- Evaluate options --------------------------
283 const string uri = conf.Get<string>("uri");
284 const string file = conf.Get<string>("file");
285 const string extension = conf.Get<string>("extension");
286 string table = conf.Get<string>("table");
287
288 const uint16_t verbose = conf.Get<uint16_t>("verbose");
289 const int64_t first = conf.Get<int64_t>("first");
290 const int64_t max = conf.Get<int64_t>("max");
291
292 const bool force = conf.Get<bool>("force");
293 const bool drop = conf.Get<bool>("drop");
294 const bool create = conf.Get<bool>("create") || drop;
295 const bool noinsert = conf.Get<bool>("no-insert");
296 const bool dry_run = conf.Get<bool>("dry-run");
297
298 const string engine = conf.Get<string>("engine");
299 const string row_format = conf.Get<string>("row-format");
300
301 const bool ignore_errors = conf.Get<bool>("ignore-errors");
302
303 const bool print_connection = conf.Get<bool>("print-connection");
304 const bool print_extensions = conf.Get<bool>("print-extensions");
305 const bool print_columns = conf.Get<bool>("print-columns");
306 const bool print_create = conf.Get<bool>("print-create");
307 const bool print_insert = conf.Get<bool>("print-insert");
308
309 const vector<Map> mymap = conf.Vec<Map>("map");
310 const vector<Map> sqltypes = conf.Vec<Map>("sql-type");
311 const vector<string> _ignore = conf.Vec<string>("ignore");
312 const vector<string> primary = conf.Vec<string>("primary");
313
314 const vactor<string> duplicate = conf.Vec<string>("duplicate");
315 const vector<string> notsigned = conf.Vec<string>("unsigned");
316
317 // -------------------------------------------------------------------------
318
319 if (verbose>0)
320 cout << "\n-------------------------- Evaluating file -------------------------" << endl;
321
322 zfits f(file.c_str(), extension.c_str());
323 if (!f)
324 {
325 cerr << "Could not open file " << file << ": " << strerror(errno) << endl;
326 return 1;
327 }
328
329 if (verbose>0)
330 cout << "File: " << file << endl;
331
332 if (!extension.empty() && extension!=f.Get<string>("EXTNAME"))
333 {
334 cerr << "Extension " << extension << " not found in file." << endl;
335 return 2;
336 }
337
338 if (print_extensions)
339 {
340 cout << "\nTables:\n - " << boost::join(f.GetTables(), "\n - ") << '\n' << endl;
341 return 3;
342 }
343
344 if (verbose>0)
345 cout << "FITS extension [table]: " << f.Get<string>("EXTNAME") << endl;
346
347 if (table.empty())
348 table = f.Get<string>("EXTNAME");
349
350 if (verbose>0)
351 cout << "SQL table: " << table << endl;
352
353// const double mjdref = f.Get("MJDREF", double(0));
354
355 if (print_columns)
356 {
357 cout << '\n';
358 f.PrintColumns();
359 cout << '\n';
360 }
361
362 const auto cols = f.GetColumns();
363
364 if (verbose>0)
365 {
366 cout << f.GetNumRows() << " events found." << endl;
367 cout << cols.size() << " columns found." << endl;
368 }
369
370 string query =
371 "CREATE TABLE IF NOT EXISTS `"+table+"`\n"
372 "(\n";
373
374 vector<Container> vec;
375
376 for (const auto &ic : cols)
377 {
378 const auto &col = ic.second;
379
380 if (verbose>2)
381 cout << '\n' << col.type << " " << ic.first << "[" << col.num << "]";
382
383 string name = ic.first;
384
385 bool found = false;
386 for (auto b=_ignore.cbegin(); b!=_ignore.cend(); b++)
387 {
388 if (boost::regex_match(name, boost::regex(*b)))
389 {
390 found = true;
391 if (verbose>2)
392 cout << " (-ignored-)";
393 break;
394 }
395 }
396 if (found)
397 continue;
398
399 const char tn = find(notsigned.cbegin(), notsigned.cend(), ic.first)!=notsigned.cend() ?
400 tolower(col.type) : toupper(col.type);
401
402 auto it = ConvFits.find(tn);
403 if (it==ConvFits.end())
404 {
405 if (verbose>2)
406 cout << " (-n/a-)";
407 continue;
408 }
409
410 if (verbose==2)
411 cout << '\n' << name << " [" << tn << "]";
412
413 for (auto m=mymap.cbegin(); m!=mymap.cend(); m++)
414 name = boost::regex_replace(name, boost::regex(m->first), m->second);
415
416 if (verbose>1)
417 cout << " (" << name << ")";
418
419 string sqltype = it->second.second;
420
421 for (auto m=sqltypes.cbegin(); m!=sqltypes.cend(); m++)
422 if (m->first==name)
423 sqltype = m->second;
424
425 if (!vec.empty())
426 query += ",\n";
427
428 const size_t N = col.type=='A' ? 1 : col.num;
429 for (int i=0; i<N; i++)
430 {
431 query += " `"+name;
432 if (N>1)
433 query += "["+to_string(i)+"]";
434 query += "` "+sqltype;
435 if (col.type=='A')
436 query += '('+to_string(col.num)+')';
437 query += " NOT NULL COMMENT '"+ic.first;
438 if (!col.unit.empty())
439 query += "["+col.unit+"]";
440 if (!col.comment.empty())
441 query += ": "+col.comment;
442 query += +"'";
443 if (N>1 && i!=N-1)
444 query += ",\n";
445 }
446
447 const BasicType_t bt =
448 /*ic.first=="Time" && col.num==1 && col.unit=="MJD" && it->second.first==kDouble ?
449 kMJD :*/ it->second.first;
450
451 vec.emplace_back(ic.first, name, bt, col.num/*, mjdref*/);
452 vec.back().ptr = f.SetPtrAddress(ic.first);
453 }
454
455 if (verbose>1)
456 cout << "\n\n";
457 if (verbose>0)
458 cout << vec.size() << " columns setup for reading." << endl;
459
460 // -------------------------------------------------------------------------
461 // Checking for database connection
462
463 Database connection(uri);
464
465 if (print_connection)
466 {
467 try
468 {
469 const auto &res1 = connection.query("SHOW STATUS LIKE 'Compression'").store();
470 cout << "Compression of databse connection is " << string(res1[0][1]) << endl;
471
472 const auto &res2 = connection.query("SHOW STATUS LIKE 'Ssl_cipher'").store();
473 cout << "Connection to databases is " << (string(res2[0][1]).empty()?"UNENCRYPTED":"ENCRYPTED ("+string(res2[0][1])+")") << endl;
474 }
475 catch (const exception &e)
476 {
477 cerr << "\nSHOW STATUS LIKE COMPRESSION\n\n";
478 cerr << "SQL query failed:\n" << e.what() << endl;
479 return 6;
480 }
481 }
482
483 try
484 {
485 if (!force)
486 connection.connected();
487 }
488 catch (const exception &e)
489 {
490 cerr << "SQL connection failed: " << e.what() << '\n' << endl;
491 return 4;
492 }
493
494 // -------------------------------------------------------------------------
495
496 if (verbose>0)
497 cout << "\n--------------------------- Database Table -------------------------" << endl;
498
499 if (!primary.empty())
500 query += ",\n PRIMARY KEY USING BTREE (`"+boost::algorithm::join(primary, "`, `")+"`)\n";
501
502 query +=
503 ")\n"
504 "DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci\n";
505 if (!engine.empty())
506 query += "ENGINE="+engine+"\n";
507 if (!row_format.empty())
508 query += "ROW_FORMAT="+row_format+"\n";
509 query += "COMMENT='created by "+conf.GetName()+"'\n";
510
511
512 // FIXME: Can we omit the catching to be able to print the
513 // query 'autmatically'?
514 try
515 {
516 if (drop)
517 {
518 // => Simple result
519 if (!dry_run)
520 connection.query("DROP TABLE `"+table+"`").execute();
521 if (verbose>0)
522 cout << "Table `" << table << "` dropped." << endl;
523 }
524 }
525 catch (const exception &e)
526 {
527 cerr << "DROP TABLE `" << table << "`\n\n";
528 cerr << "SQL query failed:\n" << e.what() << '\n' << endl;
529 return 5;
530 }
531
532 try
533 {
534 if (create && !dry_run)
535 connection.query(query).execute();
536 }
537 catch (const exception &e)
538 {
539 cerr << query << "\n\n";
540 cerr << "SQL query failed:\n" << e.what() << '\n' << endl;
541 return 6;
542 }
543
544 if (print_create)
545 cout << query << endl;
546
547 if (create && verbose>0)
548 cout << "Table `" << table << "` created." << endl;
549
550 // -------------------------------------------------------------------------
551
552 if (verbose>0)
553 cout << "\n---------------------------- Reading file --------------------------" << endl;
554
555 //query = update ? "UPDATE" : "INSERT";
556 query = "INSERT ";
557 if (ignore_errors)
558 query += "IGNORE ";
559 query += "`"+table+"`\n"
560 "(\n";
561
562 for (auto c=vec.cbegin(); c!=vec.cend(); c++)
563 {
564 const size_t N = c->type==kVarchar ? 1 : c->num;
565 for (int i=0; i<N; i++)
566 {
567 if (c!=vec.cbegin())
568 query += ",\n";
569
570 if (N==1)
571 query += " `"+c->column+"`";
572 else
573 query += " `"+c->column+"["+to_string(i)+"]`";
574
575 if (N>1 && i!=N-1)
576 query += ",\n";
577 }
578 }
579
580 query +=
581 "\n)\n"
582 "VALUES\n";
583
584 size_t count = 0;
585
586 const size_t num = max>0 && (max-first)<f.GetNumRows() ? (max-first) : f.GetNumRows();
587 for (size_t j=first; j<num; j++)
588 {
589 f.GetRow(j);
590
591 if (count>0)
592 query += ",\n";
593
594 query += "(\n";
595
596 for (auto c=vec.cbegin(); c!=vec.cend(); c++)
597 {
598 const size_t N = c->type==kVarchar ? 1 : c->num;
599 for (int i=0; i<N; i++)
600 {
601 if (c!=vec.cbegin())
602 query += ",\n";
603
604 if (c->type==kVarchar)
605 query += " '"+c->fmt(i)+"'";
606 else
607 query += " "+c->fmt(i);
608
609 if (print_insert && i==0)
610 query += " /* "+c->column+" -> "+c->branch+" */";
611
612 if (N>1 && i!=N-1)
613 query += ",\n";
614 }
615 }
616 query += "\n)";
617
618 count ++;
619 }
620
621 if (!duplicate.empty())
622 query += "\nON DUPLICATE KEY UPDATE\n " + boost::join(duplicate, ",\n ");
623
624 if (verbose>0)
625 cout << count << " out of " << num << " row(s) read from file [N=" << first << ".." << num-1 << "]." << endl;
626
627 if (count==0)
628 {
629 if (verbose>0)
630 cout << "Total execution time: " << Time().UnixTime()-start.UnixTime() << "s\n" << endl;
631 return 0;
632 }
633
634 // -------------------------------------------------------------------------
635
636 if (verbose>0)
637 {
638 cout << "\n--------------------------- Inserting data -------------------------" << endl;
639 cout << "Sending INSERT query (" << query.length() << " bytes)" << endl;
640 }
641
642 try
643 {
644 if (!noinsert && !dry_run)
645 {
646 auto q = connection.query(query);
647 q.execute();
648 cout << q.info() << '\n' << endl;
649 }
650 else
651 cout << "Insert query skipped!" << endl;
652
653 if (print_insert)
654 cout << query << endl;
655 }
656 catch (const exception &e)
657 {
658 if (verbose>1 || query.length()<80*25)
659 cerr << query << "\n\n";
660 cerr << "SQL query failed (" << query.length() << " bytes):\n" << e.what() << '\n' << endl;
661 return 7;
662 }
663
664 if (verbose>0)
665 {
666 const auto sec = Time().UnixTime()-start.UnixTime();
667 cout << "Total execution time: " << sec << "s ";
668 cout << "(" << Tools::Fractional(sec/count) << "s/row)\n";
669
670 try
671 {
672 const auto resw =
673 connection.query("SHOW WARNINGS").store();
674
675 for (size_t i=0; i<resw.num_rows(); i++)
676 {
677 const mysqlpp::Row &roww = resw[i];
678
679 cout << roww["Level"] << '[' << roww["Code"] << "]: ";
680 cout << roww["Message"] << '\n';
681 }
682 cout << endl;
683
684 }
685 catch (const exception &e)
686 {
687 cerr << "\nSHOW WARNINGS\n\n";
688 cerr << "SQL query failed:\n" << e.what() << '\n' <<endl;
689 return 8;
690 }
691 }
692
693 return 0;
694}
Note: See TracBrowser for help on using the repository browser.