usqlite/usqlite.c

504 lines
15 KiB
C

/* usqlite - minimal SQLite exec wrapper
*
* Copyright (C) 2020 /df <https://git.hpkg.tv/df/usqlite/>
*
* 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 3 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 (the file LICENSE in the source directory). If not, see
* <https://www.gnu.org/licenses/>.
*/
/* printf, scanf, stdin, stderr */
#include <stdio.h>
/* malloc, free */
#include <stdlib.h>
/* size_t */
#include <sys/types.h>
/* strdup, strtok_r, strcmp, strncmp */
#include <string.h>
/* basename */
#include <libgen.h>
/* sqlite3_* */
#include <sqlite3.h>
/* ABI version required by the code */
#ifndef REQD_SQLITE_VERSION_NUMBER
#define REQD_SQLITE_VERSION_NUMBER 3007015
#endif
/* ABI version available on target platform */
#ifndef TGT_SQLITE_VERSION_NUMBER
#define TGT_SQLITE_VERSION_NUMBER SQLITE_VERSION_NUMBER
#endif
static const char *zVersionNo = "0.3 (" __DATE__ " " __TIME__ ")";
static int fDbg = 0;
static int fCols = 0;
static char *zColSep = "|";
#if 0
/* readlink */
#include <unistd.h>
/* PAtH_MAX */
#include <limits.h>
static int get_pathname(FILE *f, char *buf, size_t bufsiz)
{
char fnmbuf[sizeof "/proc/self/fd/0123456789"];
size_t nr;
if (sizeof "/proc/self/fd/0" - 1 > snprintf(fnmbuf, sizeof fnmbuf, "/proc/self/fd/%d", fileno(f)) ||
0 > (nr = readlink(fnmbuf, buf, bufsiz)) || nr >= bufsiz) {
return -1;
}
buf[nr]='\0';
return nr;
}
static int line_callback(void *parm, int argc, char **argv, char **azColName) {
int i;
(void *) parm;
for (i=0; i<argc; i++) {
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
printf("\n");
return 0;
}
#endif
/* provide missing functions from newer ABI if needed */
#if (TGT_SQLITE_VERSION_NUMBER < REQD_SQLITE_VERSION_NUMBER)
#ifndef ArraySize
#define ArraySize(a) ((int)(sizeof(a) / sizeof(*(a))))
#endif
#ifndef ALWAYS
#define ALWAYS(a) (a)
#endif
/*
** Return a static string that describes the kind of error specified in the
** argument.
*/
static const char *sqlite3ErrStr(int rc){
static const char* const aMsg[] = {
/* SQLITE_OK */ "not an error",
/* SQLITE_ERROR */ "SQL logic error",
/* SQLITE_INTERNAL */ 0,
/* SQLITE_PERM */ "access permission denied",
/* SQLITE_ABORT */ "query aborted",
/* SQLITE_BUSY */ "database is locked",
/* SQLITE_LOCKED */ "database table is locked",
/* SQLITE_NOMEM */ "out of memory",
/* SQLITE_READONLY */ "attempt to write a readonly database",
/* SQLITE_INTERRUPT */ "interrupted",
/* SQLITE_IOERR */ "disk I/O error",
/* SQLITE_CORRUPT */ "database disk image is malformed",
/* SQLITE_NOTFOUND */ "unknown operation",
/* SQLITE_FULL */ "database or disk is full",
/* SQLITE_CANTOPEN */ "unable to open database file",
/* SQLITE_PROTOCOL */ "locking protocol",
/* SQLITE_EMPTY */ 0,
/* SQLITE_SCHEMA */ "database schema has changed",
/* SQLITE_TOOBIG */ "string or blob too big",
/* SQLITE_CONSTRAINT */ "constraint failed",
/* SQLITE_MISMATCH */ "datatype mismatch",
/* SQLITE_MISUSE */ "bad parameter or other API misuse",
#ifdef SQLITE_DISABLE_LFS
/* SQLITE_NOLFS */ "large file support is disabled",
#else
/* SQLITE_NOLFS */ 0,
#endif
/* SQLITE_AUTH */ "authorization denied",
/* SQLITE_FORMAT */ 0,
/* SQLITE_RANGE */ "column index out of range",
/* SQLITE_NOTADB */ "file is not a database",
/* SQLITE_NOTICE */ "notification message",
/* SQLITE_WARNING */ "warning message",
};
const char *zErr = "unknown error";
switch( rc ){
case SQLITE_ABORT_ROLLBACK: {
zErr = "abort due to ROLLBACK";
break;
}
case SQLITE_ROW: {
zErr = "another row available";
break;
}
case SQLITE_DONE: {
zErr = "no more rows available";
break;
}
default: {
rc &= 0xff;
if( ALWAYS(rc>=0) && rc<ArraySize(aMsg) && aMsg[rc]!=0 ){
zErr = aMsg[rc];
}
break;
}
}
return zErr;
}
#define sqlite3_errstr(a) sqlite3ErrStr(a)
#endif
static void ok(void) {
fprintf( stderr, "OK\n");
}
static void eof(void) {
fprintf( stderr, "EOF on input\n");
}
static int col_callback(void *parm, int argc, char **argv, char **azColName) {
if (parm && *(int*)parm) {
char ** azHeader = (char **) malloc(argc*sizeof(char *));
for (int i=0; i<argc; i++) {
printf("%s%s", (i > 0? zColSep:""), azColName[i]);
azHeader[i] = strdup(azColName[i]);
memset(azHeader[i],'=',strlen(azHeader[i]));
}
printf("\n");
for (int i=0; i<argc; i++) {
printf("%s%s", (i > 0? " ":""), azHeader[i]);
free(azHeader[i]);
}
free(azHeader);
printf("\n");
*(int*)parm = 0;
}
for (int i=0; i<argc; i++) {
printf("%s%s", (i > 0? zColSep:""), argv[i]);
}
printf("\n");
return 0;
}
static int sql_exec( sqlite3 * db, const char *sql ) {
char *zErrMsg = 0;
int cb = /*fDbg ||*/ fCols;
if (fDbg) fprintf( stderr, "About to run SQL ---\n%s\n---\n", sql);
int rc = sqlite3_exec(db, sql, col_callback, &cb, &zErrMsg);
if ( rc != SQLITE_OK ) {
fprintf(stderr, "SQL error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
return 1;
}
else if (fDbg) ok();
return 0;
}
static int cmdline_exec( sqlite3 *db, int argc, char *argv[]) {
int rc = 0;
for (int ii=1; ii < argc; ++ii) {
rc = sql_exec(db, argv[ii]);
if ( rc != 0 ) {
return 1;
}
}
return rc;
}
static int stream_exec( FILE * ff, sqlite3 *db) {
size_t bsiz = 4096;
char * zStmt = (char *)malloc(bsiz);
size_t nStmt;
size_t nStmtEnd = 0;
int rc = 0;
/*
* escaping " %n%<size>[^;]%n%c%n":
* maybe trim whitespace, non-; string; possible ;
*/
const char fmt[] = "%s%%n%%%u[^;]%%n%%c%%n";
char sfmt[32];
/* alloc failed? */
if (!zStmt) return 1;
/* empty text */
*zStmt = '\0';
for (char *pStmt = zStmt; rc == 0; pStmt += (nStmtEnd > 0? nStmtEnd: nStmt)) {
/* if the allocation is full (include final \0), increase */
if (pStmt+1 >= zStmt+bsiz) {
char *zz = (char *)sqlite3_realloc(zStmt,bsiz+=4096);
if (!zz) {
rc = 1; break;
}
pStmt = zz + (pStmt - zStmt);
zStmt = zz;
}
/* set the text field width, allowing for extra \0 */
rc = snprintf( sfmt, sizeof sfmt, fmt, (pStmt==zStmt? " ":""), bsiz-(pStmt-zStmt)-1 );
if (rc < 0 || rc > (int)(sizeof sfmt)) {
if (fDbg) fprintf( stderr, "Can't set format to read SQL: rc=%d\n", rc);
rc = 1; break;
}
nStmt = nStmtEnd = 0;
/* reading possible non-; text followed by possible ; */
char scln;
rc = fscanf( ff, ";%n", (int *) &nStmtEnd );
if (rc >= 0) {
if (nStmtEnd == 0) { /* not an empty statement */
int nStart = 0;
rc = fscanf( ff, sfmt, &nStart, pStmt, &nStmt, &scln, &nStmtEnd );
if (rc > 0) {
/* nStmt may randomly be set to 1024 - why?? */
if (zStmt[0]) {
if (fDbg) fprintf(stderr, "Adjusting nStmt = %u", nStmt);
nStmt = strlen(pStmt);
if (fDbg) fprintf(stderr, " to %u\n", nStmt);
}
else if (nStmtEnd > nStmt) {
nStmt -= nStart;
}
nStmtEnd -= nStart;
}
}
else
scln = ';';
}
/* err, EOF or read nothing: fail on error */
if (rc <= 0 && ferror(ff)) {
if (fDbg) {
if (EOF == rc)
eof();
else
fprintf( stderr, "SQL read error: rc=%d\n", rc);
}
rc = 1;
break;
}
if (rc == EOF) {
if (zStmt[0]) {
/* read some stuff but no ; ? */
scln = ';';
nStmtEnd = nStmt+1;
if (fDbg) fprintf( stderr, "Faking final ; for unterminated input\n");
}
else {
if (fDbg) eof();
break;
}
}
if (fDbg) fprintf( stderr, "Read %u characters of SQL (rc=%d)\n", nStmt, rc);
/* read or faked a possible ; - add it */
if (nStmtEnd > nStmt) {
if (fDbg) fprintf( stderr, "Adding final ;\n");
pStmt[nStmt] = scln;
pStmt[nStmtEnd] = '\0';
}
/* no final ;, try again */
if (nStmtEnd == 0 || pStmt[nStmt] != ';') {
if (fDbg) fprintf( stderr, "No final ;: reading more\n");
if (rc > 0) rc = 0; continue;
}
/* potential command: what does sqlite say? -> 1, 0, SQLITE_NOMEM */
if (fDbg) fprintf( stderr, "About to check SQL ---\n%s\n---\n", zStmt);
int src = sqlite3_complete(zStmt);
switch (src) {
case 1: { /* complete-ish - try it */
if (fDbg) ok();
src = sql_exec(db, zStmt);
if (rc >= 0) rc = src;
if (src != 0) break;
/* now ready for the next one */
pStmt = zStmt;
nStmt = nStmtEnd = 0;
zStmt[0] = '\0';
break;
}
case 0: { /* incomplete - get more */
int nn = 0;
sscanf( zStmt, " ;%n", &nn);
if (nn == 0) {
/* non-empty statement */
if (fDbg) fprintf( stderr, "Incomplete, reading more\n");
rc = src;
}
break;
}
default: {
if (fDbg) fprintf( stderr, "Error: %s\n", sqlite3_errstr(src));
rc = 1;
}
}
}
free(zStmt);
return rc == EOF? 0: rc;
}
#if 0
static int strcmpany( const char* str, const char* strtest[] ) {
for (size_t i = 0; strtest[i]; ++i) {
if (0 == strcmp(str, strtest[i])) return 0;
}
return 1;
}
#endif
static int optcmp( const char* str, const char* strtest[], char** val ) {
for (size_t i = 0; strtest[i]; ++i) {
if (strtest[i][1] == '-') { /* --opt */
if (val && str[1] == '-') { /* expecting --opt= */
char *save = NULL;
char *tmpStr = strdup(str);
if (tmpStr && (0 == strcmp(strtok_r(tmpStr, "=", &save), strtest[i]))) {
*val = strtok_r( NULL, "=", &save);
return 0;
}
free(tmpStr);
}
else if (0 == strcmp(str, strtest[i])) {
return 0;
}
}
else { /* -o */
if (0 == strncmp(str, strtest[i], 2)) {
if (val) *val = strdup(str+2);
return 0;
}
}
}
return 1;
}
static void usage(const char* progname) {
fprintf( stderr, "Usage: %s [OPTIONS] [\"SQL command;\" ...] [< SQL_script]\n", progname);
}
static void gpl(void) {
fprintf( stderr, "Copyright (C) 2020 /df <https://git.hpkg.tv/df/usqlite/>\n"
"\nThis program comes with ABSOLUTELY NO WARRANTY.\n"
"This is free software, and you are welcome to redistribute it under the\n"
"terms of the GNU General Public License as published by the Free Software\n"
"Foundation, either version 3 of the License, or (at your option) any later\n"
"version: see <https://www.gnu.org/licenses/>.\n\n" );
}
static void help(const char* progname) {
usage(progname);
fprintf( stderr, "\nExecute SQL commands on SQLite databases\n\n"
"Options:\n"
" -C, --show-columns Show column names (if any) as headers of \n"
" query results\n"
" -S, String to separate columns in query \n"
" --column-separator=SEP results (default \"|\")\n"
" -v, --verbose Log details to stderr\n"
" -V, --version, Print program version, licensing and \n"
" warranty details to stderr, and continue\n"
" -- Subsequent parameters are not options\n"
" -h, --help, -? Print this help to stderr and exit\n"
"\nTo cancel reading from stdin in a POSIX shell, use the input \nredirection \"<&-\".\n" );
}
int main(int argc, char *argv[]) {
const char zDummyDB[] = ":memory:";
const char *zHelp[] = { "--help", "-h", "-?", 0 };
const char *zVersion[] = { "--version", "-V", 0 };
const char *zVerbose[] = { "--verbose", "-v", 0 };
const char *zCols[] = { "--show-columns", "-C", 0 };
const char *zColSepOpt[] = { "--column-separator", "-S", 0 };
const char *zCont[] = { "--", 0 };
const char *zProgName = argv[0];
/* process options */
while (argc >= 2 && (*(argv[1]) == '-')) {
char * tmpVal = NULL;
if (0 == optcmp(argv[1], zHelp, NULL)) {
help(zProgName);
return 0;
}
else if (0 == optcmp(argv[1], zCols, NULL)) {
fCols = 1;
}
else if (0 == optcmp(argv[1], zColSepOpt, &tmpVal)) {
if (tmpVal && *tmpVal) {
zColSep = tmpVal;
}
else if ((argv[1][1] != '-') && (argc > 2)) {
tmpVal = zColSep = argv[2];
--argc; ++argv;
}
if (!(tmpVal && *tmpVal)) {
fprintf( stderr, "Bad or empty separator option value: %s\n", argv[1]);
}
}
else if (0 == optcmp(argv[1], zVersion, NULL)) {
/* basename() may modify its argument */
fprintf( stderr, "%s SQLite exec wrapper version %s\n",
basename(strdup(zProgName)), zVersionNo);
gpl();
}
else if (0 == optcmp(argv[1], zVerbose, NULL)) {
fDbg = 1;
}
else if (0 == optcmp(argv[1], zCont, NULL)) {
--argc; ++argv;
break;
}
else {
fprintf( stderr, "Unknown option: %s\n", argv[1]);
}
/* single-char option with no value: next char may be an option */
if ((argv[1][1] != '-') && !(tmpVal && *tmpVal) && argv[1][2]) {
(argv[1])++;
argv[1][0] = '-';
}
else {
--argc; ++argv;
}
}
if (fDbg) {
fprintf( stderr, "Options: %s", zVerbose[0]);
if (fCols) fprintf( stderr, " %s", zCols[0]);
fprintf( stderr, " separator = '%s'\n", zColSep);
}
if (fDbg) fprintf( stderr, "About to configure SQLITE_CONFIG_SINGLETHREAD\n");
int rc = sqlite3_config(SQLITE_CONFIG_SINGLETHREAD);
if (SQLITE_OK != rc) {
fprintf( stderr, "Can't set single-thread: %s\n", sqlite3_errstr(rc));
return 1;
}
else if (fDbg) ok();
sqlite3 *db;
if (fDbg) fprintf( stderr, "About to open dummy database %s\n", zDummyDB);
rc = sqlite3_open(zDummyDB, &db);
if (SQLITE_OK != rc) {
fprintf(stderr, "Can't open database: %s\n", sqlite3_errstr(rc));
}
else {
if (fDbg) ok();
rc = cmdline_exec( db, argc, argv);
if (rc == 0)
rc = stream_exec( stdin, db);
}
sqlite3_close(db);
return rc;
}