mdbtools/src/util/mdb-sql.c
Evan Miller a44a8ed8ae
Postgres-style ILIKE operator (with Unicode support) (#244)
Access's `LIKE` is actually case-insensitive, but to prevent breaking existing
programs that rely on mdbtools' case-sensitive behavior, introduce a new
`ILIKE` operator to perform a case-insensitive match. Use GLib's `g_utf8_casefold`
to make the comparison UTF-8 aware. A "poor man's" version is implemented
in fakeglib, which relies on `towlower`, and won't work with multi-grapheme
case transformations (e.g. German Eszett).

Fixes #233
2021-08-04 14:45:31 -04:00

524 lines
12 KiB
C

/* MDB Tools - A library for reading MS Access database file
* Copyright (C) 2000 Brian Bruns
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <stdio.h>
#ifdef HAVE_LIBREADLINE
# if defined(HAVE_READLINE_READLINE_H)
# include <readline/readline.h>
# elif defined(HAVE_READLINE_H)
# include <readline.h>
# else
/* no readline.h */
extern char *readline ();
# endif
char *cmdline = NULL;
#endif /* HAVE_LIBREADLINE */
#ifdef HAVE_READLINE_HISTORY
# if defined(HAVE_READLINE_HISTORY_H)
# include <readline/history.h>
# elif defined(HAVE_HISTORY_H)
# include <history.h>
# else
/* no history.h */
extern void add_history ();
extern int write_history ();
extern int read_history ();
extern void clear_history ();
# endif
#endif /* HAVE_READLINE_HISTORY */
#include <string.h>
#include "mdbsql.h"
#include "mdbver.h"
void dump_results(FILE *out, MdbSQL *sql, char *delimiter);
void dump_results_pp(FILE *out, MdbSQL *sql);
int headers = 1;
int footers = 1;
int pretty_print = 1;
int showplan = 0;
int noexec = 0;
#ifdef HAVE_READLINE_HISTORY
#define HISTFILE ".mdbhistory"
#endif
#ifndef HAVE_LIBREADLINE
char *readline(char *prompt)
{
char line[1000];
int i = 0;
fputs(prompt, stdout);
if (!fgets(line, sizeof(line), stdin)) {
return NULL;
}
for (i=strlen(line);i>0;i--) {
if (line[i]=='\n') {
line[i]='\0';
break;
}
}
return g_strdup(line);
}
#endif
static int strlen_utf(const char *s) {
int len = 0;
while (*s) {
if ((*s++ & 0xc0) != 0x80)
len++;
}
return len;
}
void
do_set_cmd(MdbSQL *sql, char *s)
{
char *level1, *level2;
level1 = strtok(s, " \t\n");
if (!level1) {
printf("Usage: set [stats|showplan|noexec] [on|off]\n");
return;
}
if (!strcmp(level1,"stats")) {
level2 = strtok(NULL, " \t");
if (!level2) {
printf("Usage: set stats [on|off]\n");
return;
}
if (!strcmp(level2,"on")) {
mdb_stats_on(sql->mdb);
} else if (!strcmp(level2,"off")) {
mdb_stats_off(sql->mdb);
mdb_dump_stats(sql->mdb);
} else {
printf("Unknown stats option %s\n", level2);
printf("Usage: set stats [on|off]\n");
}
} else if (!strcmp(level1,"showplan")) {
level2 = strtok(NULL, " \t");
if (!level2) {
printf("Usage: set showplan [on|off]\n");
return;
}
if (!strcmp(level2,"on")) {
showplan=1;
} else if (!strcmp(level2,"off")) {
showplan=0;
} else {
printf("Unknown showplan option %s\n", level2);
printf("Usage: set showplan [on|off]\n");
}
} else if (!strcmp(level1,"noexec")) {
level2 = strtok(NULL, " \t");
if (!level2) {
printf("Usage: set noexec [on|off]\n");
return;
}
if (!strcmp(level2,"on")) {
noexec=1;
} else if (!strcmp(level2,"off")) {
noexec=0;
} else {
printf("Unknown noexec option %s\n", level2);
printf("Usage: set noexec [on|off]\n");
}
} else {
printf("Unknown set command %s\n", level1);
printf("Usage: set [stats|showplan|noexec] [on|off]\n");
}
}
void
run_query(FILE *out, MdbSQL *sql, char *mybuf, char *delimiter)
{
MdbTableDef *table;
mdb_sql_run_query(sql, mybuf);
if (!mdb_sql_has_error(sql)) {
if (showplan) {
table = sql->cur_table;
if (table->sarg_tree) mdb_sql_dump_node(table->sarg_tree, 0);
if (sql->cur_table->strategy == MDB_TABLE_SCAN)
printf("Table scanning %s\n", table->name);
else
printf("Index scanning %s using %s\n", table->name, table->scan_idx->name);
}
/* If noexec != on, dump results */
if (!noexec) {
if (pretty_print)
dump_results_pp(out, sql);
else
dump_results(out, sql, delimiter);
}
mdb_sql_reset(sql);
}
}
void print_value(FILE *out, char *v, int sz, int first)
{
int i;
int vlen;
if (first)
fputc('|', out);
vlen = strlen_utf(v);
fputs(v, out);
for (i=vlen;i<sz;i++)
fputc(' ', out);
fputc('|', out);
}
static void print_break(FILE *out, int sz, int first)
{
int i;
if (first)
fputc('+', out);
for (i=0;i<sz;i++)
fputc('-', out);
fputc('+', out);
}
void print_rows_retrieved(FILE *out, unsigned long row_count)
{
if (!row_count)
fprintf(out, "No Rows retrieved\n");
else if (row_count==1)
fprintf(out, "1 Row retrieved\n");
else
fprintf(out, "%lu Rows retrieved\n", row_count);
fflush(out);
}
void
dump_results(FILE *out, MdbSQL *sql, char *delimiter)
{
unsigned int j;
MdbSQLColumn *sqlcol;
if (headers) {
for (j=0;j<sql->num_columns-1;j++) {
sqlcol = g_ptr_array_index(sql->columns,j);
fprintf(out, "%s%s", sqlcol->name,
delimiter ? delimiter : "\t");
}
sqlcol = g_ptr_array_index(sql->columns,sql->num_columns-1);
fprintf(out, "%s", sqlcol->name);
fprintf(out,"\n");
fflush(out);
}
while(mdb_sql_fetch_row(sql, sql->cur_table)) {
for (j=0;j<sql->num_columns-1;j++) {
sqlcol = g_ptr_array_index(sql->columns,j);
fprintf(out, "%s%s", (char*)(g_ptr_array_index(sql->bound_values, j)),
delimiter ? delimiter : "\t");
}
sqlcol = g_ptr_array_index(sql->columns,sql->num_columns-1);
fprintf(out, "%s", (char*)(g_ptr_array_index(sql->bound_values, sql->num_columns-1)));
fprintf(out,"\n");
fflush(out);
}
if (footers) {
print_rows_retrieved(out, sql->row_count);
}
}
void
dump_results_pp(FILE *out, MdbSQL *sql)
{
unsigned int j;
MdbSQLColumn *sqlcol;
/* print header */
if (headers) {
for (j=0;j<sql->num_columns;j++) {
sqlcol = g_ptr_array_index(sql->columns,j);
if (strlen(sqlcol->name)>(size_t)sqlcol->disp_size)
sqlcol->disp_size = strlen(sqlcol->name);
print_break(out, sqlcol->disp_size, !j);
}
fprintf(out,"\n");
fflush(out);
for (j=0;j<sql->num_columns;j++) {
sqlcol = g_ptr_array_index(sql->columns,j);
print_value(out, sqlcol->name,sqlcol->disp_size,!j);
}
fprintf(out,"\n");
fflush(out);
}
for (j=0;j<sql->num_columns;j++) {
sqlcol = g_ptr_array_index(sql->columns,j);
print_break(out, sqlcol->disp_size, !j);
}
fprintf(out,"\n");
fflush(out);
/* print each row */
while(mdb_sql_fetch_row(sql, sql->cur_table)) {
for (j=0;j<sql->num_columns;j++) {
sqlcol = g_ptr_array_index(sql->columns,j);
print_value(out, (char *) g_ptr_array_index(sql->bound_values, j), sqlcol->disp_size,!j);
}
fprintf(out,"\n");
fflush(out);
}
/* footer */
for (j=0;j<sql->num_columns;j++) {
sqlcol = g_ptr_array_index(sql->columns,j);
print_break(out, sqlcol->disp_size, !j);
}
fprintf(out,"\n");
fflush(out);
if (footers) {
print_rows_retrieved(out, sql->row_count);
}
}
static char *
find_sql_terminator(char *s)
{
char *sp;
int len = strlen(s);
if (len == 0) {
return NULL;
}
sp = &s[len-1];
while (sp > s && isspace(*sp)) {
sp--;
}
if (*sp == ';') {
return sp;
}
return NULL;
}
int
main(int argc, char **argv)
{
char *s = NULL;
char prompt[20];
int line = 0;
char *mybuf;
unsigned int bufsz;
MdbSQL *sql;
FILE *in = NULL, *out = NULL;
char *filename_in=NULL, *filename_out=NULL;
#ifdef HAVE_READLINE_HISTORY
char *home = getenv("HOME");
char *histpath;
#endif
char *delimiter = NULL;
int in_from_colon_r = 0;
char *locale = NULL;
int print_mdbver = 0;
GOptionEntry entries[] = {
{ "delim", 'd', 0, G_OPTION_ARG_STRING, &delimiter, "Use this delimiter.", "char"},
{ "no-pretty-print", 'P', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &pretty_print, "Don't pretty print", NULL},
{ "no-header", 'H', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &headers, "Don't print header", NULL},
{ "no-footer", 'F', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &footers, "Don't print footer", NULL},
{ "input", 'i', 0, G_OPTION_ARG_FILENAME, &filename_in, "Read SQL from specified file", "file"},
{ "output", 'o', 0, G_OPTION_ARG_FILENAME, &filename_out, "Write result to specified file", "file"},
{"version", 0, 0, G_OPTION_ARG_NONE, &print_mdbver, "Show mdbtools version and exit", NULL},
{ NULL },
};
GError *error = NULL;
GOptionContext *opt_context;
opt_context = g_option_context_new("<file> - Run SQL");
g_option_context_add_main_entries(opt_context, entries, NULL /*i18n*/);
// g_option_context_set_strict_posix(opt_context, TRUE); /* options first, requires glib 2.44 */
locale = setlocale(LC_CTYPE, "");
if (!g_option_context_parse (opt_context, &argc, &argv, &error))
{
fprintf(stderr, "option parsing failed: %s\n", error->message);
fputs(g_option_context_get_help(opt_context, TRUE, NULL), stderr);
exit (1);
}
if (print_mdbver) {
if (argc > 1) {
fputs(g_option_context_get_help(opt_context, TRUE, NULL), stderr);
}
fprintf(stdout,"%s\n", MDB_FULL_VERSION);
exit(argc > 1);
}
setlocale(LC_CTYPE, locale);
if (argc > 2) {
fputs("Wrong number of arguments.\n\n", stderr);
fputs(g_option_context_get_help(opt_context, TRUE, NULL), stderr);
exit(1);
}
#ifdef HAVE_READLINE_HISTORY
if (home) {
histpath = (char *) g_strconcat(home, "/", HISTFILE, NULL);
read_history(histpath);
g_free(histpath);
}
#endif
/* If input is coming from a pipe */
if (!isatty(fileno(stdin))) {
in = stdin;
}
if (filename_in) {
if (!strcmp(filename_in, "stdin"))
in = stdin;
else if (!(in = fopen(filename_in, "r"))) {
fprintf(stderr, "Unable to open file %s\n", filename_in);
exit(1);
}
}
if (filename_out) {
if (!(out = fopen(filename_out, "w"))) {
fprintf(stderr,"Unable to open file %s\n", filename_out);
exit(1);
}
}
setlocale(LC_COLLATE, "");
/* initialize the SQL engine */
sql = mdb_sql_init();
if (argc == 2) {
mdb_sql_open(sql, argv[1]);
}
/* give the buffer an initial size */
bufsz = 4096;
mybuf = g_malloc(bufsz);
mybuf[0]='\0';
while (1) {
line ++;
if (s) {
free(s);
s = NULL;
}
if (in) {
s=calloc(bufsz, 1);
if (!fgets(s, bufsz, in)) {
// Backwards compatibility with older MDBTools
// Files read from the command line had an
// implicit "go" at the end
if (!in_from_colon_r && strlen(mybuf))
strcpy(s, "go");
else if (in_from_colon_r) {
line = 0;
fclose(in);
in = NULL;
in_from_colon_r = 0;
} else
break;
} else if (s[strlen(s)-1]=='\n')
s[strlen(s)-1]=0;
} else {
snprintf(prompt, sizeof(prompt), "%d => ", line);
locale = setlocale(LC_CTYPE, "");
char *l = readline(prompt);
setlocale(LC_CTYPE, locale);
if (!l)
break;
s=g_locale_to_utf8(l, -1, NULL, NULL, NULL);
free(l);
}
if (!strcmp(s,"exit") || !strcmp(s,"quit") || !strcmp(s,"bye"))
break;
if (line==1 && (!strncmp(s,"set ",4) || !strcmp(s,"set"))) {
do_set_cmd(sql, &s[3]);
line = 0;
} else if (!strcmp(s,"go")) {
line = 0;
run_query((out) ? out : stdout, sql, mybuf, delimiter);
mybuf[0]='\0';
} else if (!strcmp(s,"reset")) {
line = 0;
mybuf[0]='\0';
} else if (!strncmp(s,":r",2)) {
char *fname = &s[2];
if (in) {
fprintf(stderr, "Can not handle nested opens\n");
} else {
while (*fname && isspace(*fname))
fname++;
if (!(in = fopen(fname, "r"))) {
fprintf(stderr,"Unable to open file %s\n", fname);
mybuf[0]=0;
} else {
in_from_colon_r = 1;
}
}
} else {
char *p;
while (strlen(mybuf) + strlen(s) > bufsz) {
bufsz *= 2;
mybuf = (char *) g_realloc(mybuf, bufsz);
}
#ifdef HAVE_READLINE_HISTORY
/* don't record blank lines, or lines read from files
* specified on the command line */
if ((!in || in_from_colon_r) && strlen(s))
add_history(s);
#endif
strcat(mybuf,s);
/* preserve line numbering for the parser */
strcat(mybuf,"\n");
if ((p = find_sql_terminator(mybuf))) {
*p = '\0';
line = 0;
run_query((out) ? out : stdout, sql, mybuf, delimiter);
mybuf[0]='\0';
}
}
}
mdb_sql_exit(sql);
g_free(mybuf);
if (s) free(s);
if (out) fclose(out);
if ((in) && (in != stdin)) fclose(in);
#ifdef HAVE_READLINE_HISTORY
if (home) {
histpath = (char *) g_strconcat(home, "/", HISTFILE, NULL);
write_history(histpath);
g_free(histpath);
clear_history();
}
#endif
g_option_context_free(opt_context);
g_free(delimiter);
g_free(filename_in);
g_free(filename_out);
return 0;
}