504 lines
15 KiB
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;
|
|
}
|