├── .gitignore ├── logo.png ├── script ├── build.bat ├── clean.sh ├── check.sh ├── build.sh └── check.go ├── lib ├── app.h ├── db.h ├── io.h ├── main.c ├── utl.c ├── utl.h ├── io.c ├── db.c └── app.c ├── .github └── workflows │ ├── windows.yml │ ├── linux.yml │ ├── macos.yml │ └── macos-arm64.yml ├── LICENSE ├── README.md └── rfc.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | bin/ 3 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cvilsmeier/sqinn/HEAD/logo.png -------------------------------------------------------------------------------- /script/build.bat: -------------------------------------------------------------------------------- 1 | REM Must have MSVC Build Tools installed 2 | cl.exe /Fe:sqinn.exe lib\*.c 3 | -------------------------------------------------------------------------------- /script/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | stat lib/ > /dev/null # must be in correct dir 5 | rm -rf bin/ 6 | -------------------------------------------------------------------------------- /script/check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | stat lib/ > /dev/null # must be in correct dir 5 | go run script/check.go # run checks, needs go 6 | -------------------------------------------------------------------------------- /lib/app.h: -------------------------------------------------------------------------------- 1 | #ifndef APP_H 2 | #define APP_H 3 | 4 | /* An App reads requests, processes them, and writes responses. */ 5 | typedef struct app_s App; 6 | App *newApp(Db *db, Reader *r, Writer *w); 7 | void App_free(App *this); 8 | BOOL App_step(App *this); // TRUE if next, FALSE if not (must exit then) 9 | 10 | // 11 | // Test 12 | // 13 | 14 | void testApp(); 15 | 16 | #endif // APP_H 17 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: [ "master", "v1" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: windows-2022 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Setup MSVC 15 | uses: ilammy/msvc-dev-cmd@v1 16 | 17 | - name: Compile 18 | run: | 19 | echo "build start" 20 | dir 21 | script\build.bat 22 | dir 23 | echo "run test" 24 | .\sqinn.exe test -loglevel 1 -logstderr 25 | echo "make dist folder" 26 | md dist 27 | copy sqinn.exe dist 28 | echo "build done" 29 | 30 | - name: Upload Artifact 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: dist-windows-amd64 34 | path: dist 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: 4 | push: 5 | branches: [ "master", "v1" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-24.04 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Build 15 | run: | 16 | echo "build start" 17 | uname -a 18 | pwd 19 | ls -al 20 | chmod a+x script/*.sh 21 | script/build.sh 22 | echo "executing test" 23 | bin/sqinn version 24 | bin/sqinn sqlite 25 | bin/sqinn test 26 | echo "build done" 27 | rm -rf dist/ 28 | mkdir dist/ 29 | cp bin/sqinn dist/ 30 | 31 | - name: Upload Artifact 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: dist-linux-amd64 35 | path: dist 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: MacOS 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: macos-13 # this is a amd64 machine 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Build 15 | run: | 16 | echo "build start" 17 | uname -a 18 | pwd 19 | ls -al 20 | chmod a+x script/*.sh 21 | script/build.sh 22 | echo "executing test" 23 | bin/sqinn version 24 | bin/sqinn sqlite 25 | bin/sqinn test 26 | echo "build done" 27 | rm -rf dist/ 28 | mkdir dist/ 29 | cp bin/sqinn dist/ 30 | 31 | - name: Upload Artifact 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: dist-darwin-amd64 35 | path: dist 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/macos-arm64.yml: -------------------------------------------------------------------------------- 1 | name: MacOS-Arm64 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: macos-14 # this is a arm64 machine 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Build 15 | run: | 16 | echo "build start" 17 | uname -a 18 | pwd 19 | ls -al 20 | chmod a+x script/*.sh 21 | script/build.sh 22 | echo "executing test" 23 | bin/sqinn version 24 | bin/sqinn sqlite 25 | bin/sqinn test 26 | echo "build done" 27 | rm -rf dist/ 28 | mkdir dist/ 29 | cp bin/sqinn dist/ 30 | 31 | - name: Upload Artifact 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: dist-darwin-arm64 35 | path: dist 36 | 37 | -------------------------------------------------------------------------------- /script/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | stat lib/ > /dev/null # we must be in correct directory 5 | mkdir -p bin # bin directory will hold all buld artefacts 6 | 7 | # setup 8 | CC="gcc" 9 | CFLAGS="-std=c99 -Wall -Werror -O2" 10 | LDFLAGS="-static" 11 | if test "$(uname)" = "Darwin"; then 12 | CC="clang" 13 | LDFLAGS="" # apple does not support -static 14 | fi 15 | 16 | # compile 17 | if ! test -f bin/sqlite3.o; then 18 | $CC $CFLAGS -c lib/sqlite3.c -o bin/sqlite3.o -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_THREADSAFE=0 19 | fi 20 | $CC $CFLAGS -c lib/utl.c -o bin/utl.o 21 | $CC $CFLAGS -c lib/io.c -o bin/io.o 22 | $CC $CFLAGS -c lib/db.c -o bin/db.o 23 | $CC $CFLAGS -c lib/app.c -o bin/app.o 24 | $CC $CFLAGS -c lib/main.c -o bin/main.o 25 | 26 | # link 27 | $CC $LDFLAGS \ 28 | bin/sqlite3.o \ 29 | bin/utl.o \ 30 | bin/io.o \ 31 | bin/db.o \ 32 | bin/app.o \ 33 | bin/main.o \ 34 | -o bin/sqinn 35 | 36 | -------------------------------------------------------------------------------- /lib/db.h: -------------------------------------------------------------------------------- 1 | #ifndef DB_H 2 | #define DB_H 3 | 4 | /* A Value holds a typed value. */ 5 | typedef struct value_s { 6 | char type; // see VT_... 7 | int i32; // VT_INT32 8 | int64_t i64; // VT_INT64 9 | double d; // VT_DOUBLE 10 | const char *p; // VT_STRING and VT_BLOB 11 | size_t sz; // VT_BLOB 12 | } Value; 13 | 14 | #define VT_NULL 0 15 | #define VT_INT32 1 16 | #define VT_INT64 2 17 | #define VT_DOUBLE 3 18 | #define VT_STRING 4 19 | #define VT_BLOB 5 20 | 21 | /* A Db provides access to a SQLite database. */ 22 | typedef struct db_s Db; 23 | Db *newDb(const char *dbname, BOOL debug); 24 | void Db_free(Db *this); 25 | BOOL Db_prepare(Db *this, const char *sql); 26 | void Db_finalize(Db *this); 27 | BOOL Db_bind(Db *this, const Value *params, int nparams); 28 | BOOL Db_bind_step_reset(Db *this, const Value *params, int nparams); 29 | BOOL Db_step_fetch(Db *this, BOOL *phasRow, Value *values, int nvalues); 30 | const char *Db_errmsg(Db *this); 31 | 32 | // 33 | // Test 34 | // 35 | 36 | void testDb(); 37 | 38 | #endif // DB_H 39 | -------------------------------------------------------------------------------- /lib/io.h: -------------------------------------------------------------------------------- 1 | #ifndef IO_H 2 | #define IO_H 3 | 4 | typedef struct reader_s Reader; 5 | Reader *newStdinReader(); 6 | Reader *newMemReader(char *buf, size_t bufsz); 7 | void Reader_free(Reader* this); 8 | char Reader_readByte(Reader* this); 9 | int Reader_readInt32(Reader* this); 10 | int64_t Reader_readInt64(Reader* this); 11 | double Reader_readDouble(Reader* this); 12 | const char* Reader_readString(Reader* this); 13 | const char* Reader_readBlob(Reader* this, size_t *plen); 14 | 15 | typedef struct writer_s Writer; 16 | Writer *newStdoutWriter(); 17 | Writer *newMemWriter(char *buf, size_t bufsz); 18 | void Writer_free(Writer* this); 19 | void Writer_markFrame(Writer* this); 20 | void Writer_flush(Writer* this); 21 | void Writer_writeByte(Writer* this, char value); 22 | void Writer_writeInt32(Writer* this, int value); 23 | void Writer_writeInt64(Writer* this, int64_t value); 24 | void Writer_writeDouble(Writer* this, double value); 25 | void Writer_writeString(Writer* this, const char* str); 26 | void Writer_writeBlob(Writer* this, const char* data, size_t len); 27 | 28 | void testIo(); 29 | 30 | #endif // IO_H 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2020-2025 C.Vilsmeier 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the “Software”), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom 10 | the Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY 17 | KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 18 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 19 | AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /script/check.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func main() { 10 | log.SetFlags(0) 11 | log.SetOutput(os.Stdout) 12 | var readmeVersion string 13 | { 14 | text := readFile("README.md") 15 | _, text = mustCut(text, "\nChangelog") 16 | _, text = mustCut(text, "### v") 17 | readmeVersion, _ = mustCut(text, "\n") 18 | } 19 | var sourceVersion string 20 | { 21 | // #define SQINN_VERSION "9.9.9" 22 | text := readFile("lib/main.c") 23 | _, text = mustCut(text, "#define SQINN_VERSION \"") 24 | sourceVersion, _ = mustCut(text, "\"") 25 | } 26 | if readmeVersion != sourceVersion { 27 | log.Fatal("not OK: version mismatch") 28 | log.Printf("readmeVersion %q", readmeVersion) 29 | log.Printf("sourceVersion %q", sourceVersion) 30 | } 31 | log.Print("check.go ok") 32 | } 33 | 34 | func readFile(name string) string { 35 | return string(must(os.ReadFile(name))) 36 | } 37 | 38 | func mustCut(s, sep string) (_before string, _after string) { 39 | before, after, ok := strings.Cut(s, sep) 40 | if !ok { 41 | log.Fatalf("%q not found in %q", sep, s) 42 | } 43 | return strings.TrimSpace(before), strings.TrimSpace(after) 44 | } 45 | 46 | func must[V any](v V, err error) V { 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | return v 51 | } 52 | -------------------------------------------------------------------------------- /lib/main.c: -------------------------------------------------------------------------------- 1 | #include "utl.h" 2 | #include "io.h" 3 | #include "db.h" 4 | #include "app.h" 5 | #include "sqlite3.h" 6 | 7 | #define SQINN_NAME "sqinn" 8 | #define SQINN_VERSION "2.0.1" 9 | 10 | BOOL hasCommand(int argc, char const *argv[], const char *name) { 11 | if ( argc >= 2 ) { 12 | if (strcmp(argv[1], name) == 0) { 13 | return TRUE; 14 | } 15 | } 16 | return FALSE; 17 | } 18 | 19 | BOOL hasOption(int argc, char const *argv[], const char *name) { 20 | for (int i = 0; i < argc; i++) { 21 | if (strcmp(argv[i], name) == 0) { 22 | return TRUE; 23 | } 24 | } 25 | return FALSE; 26 | } 27 | 28 | void getOption(int argc, char const *argv[], const char *name, char *value, size_t n, const char *defaultValue) { 29 | for (int i = 0; i < argc - 1; i++) { 30 | if (strcmp(argv[i], name) == 0) { 31 | strncpy(value, argv[i + 1], n-1); 32 | value[n-1] = 0; 33 | return; 34 | } 35 | } 36 | strncpy(value, defaultValue, n-1); 37 | value[n-1] = 0; 38 | } 39 | 40 | Log* makeLog(int argc, char const *argv[]) { 41 | // -loglevel 42 | int level = LOG_LEVEL_OFF; 43 | char slevel[8]; 44 | getOption(argc, argv, "-loglevel", slevel, sizeof(slevel), "0"); 45 | level = atoi(slevel); 46 | level = level < LOG_LEVEL_OFF ? LOG_LEVEL_OFF : level; 47 | level = level > 2 ? 0 : level; 48 | // -logfile 49 | char logfile[512] = {0}; 50 | getOption(argc, argv, "-logfile", logfile, sizeof(logfile), ""); 51 | // -logstderr 52 | BOOL stdErr = hasOption(argc, argv, "-logstderr"); 53 | // build Log 54 | return newLog(level, logfile, stdErr); 55 | } 56 | 57 | Db* makeDb(int argc, char const *argv[]) { 58 | // -db 59 | char dbname[256] = {0}; 60 | getOption(argc, argv, "-db", dbname, sizeof(dbname), ":memory:"); 61 | // new Db 62 | return newDb(dbname, FALSE); 63 | } 64 | 65 | void help() { 66 | printf("%s v%s - SQLite over stdin/stdout.\n", SQINN_NAME, SQINN_VERSION); 67 | printf("\n"); 68 | printf("Usage:\n"); 69 | printf(" %s [options...]\n", SQINN_NAME); 70 | printf("\n"); 71 | printf("The commands are:\n"); 72 | printf("\n"); 73 | printf(" run Read requests from stdin and write responses to stdout.\n"); 74 | printf(" test Execute selftest and exit.\n"); 75 | printf(" version Print version and exit.\n"); 76 | printf(" sqlite Print SQLite library version and exit.\n"); 77 | printf(" help Print help page and exit.\n"); 78 | printf("\n"); 79 | printf("The options are:\n"); 80 | printf("\n"); 81 | printf(" -db Database name. Default is \":memory:\"\n"); 82 | printf(" -loglevel Log level: 0=off, 1=info, 2=debug. Default is 0 (off).\n"); 83 | printf(" -logfile Log to a file. Default is empty (no file logging).\n"); 84 | printf(" Note: Logfile is appended and will grow unlimited.\n"); 85 | printf(" -logstderr Log to stderr. Default is off (no stderr logging).\n"); 86 | printf("\n"); 87 | } 88 | 89 | int main(int argc, char const *argv[]) { 90 | if (hasCommand(argc, argv, "run")) { 91 | theLog = makeLog(argc, argv); 92 | initMem(); 93 | LOG_INFO2("--- %s v%s start ---", SQINN_NAME, SQINN_VERSION); 94 | Db *db = makeDb(argc, argv); 95 | Reader *r = newStdinReader(); 96 | Writer *w = newStdoutWriter(); 97 | App *app = newApp(db, r, w); 98 | while(App_step(app)) { 99 | ; // loop until App_step() returns FALSE 100 | } 101 | App_free(app); 102 | Writer_free(w); 103 | Reader_free(r); 104 | Db_free(db); 105 | if (mallocs != frees) { 106 | LOG_INFO2("found memory leaks: mallocs %d != frees %d", mallocs, frees); 107 | } 108 | LOG_INFO2("--- %s v%s exit ---", SQINN_NAME,SQINN_VERSION); 109 | Log_free(theLog); 110 | return 0; 111 | } else if (hasCommand(argc, argv, "test")) { 112 | theLog = makeLog(argc, argv); 113 | initMem(); 114 | LOG_INFO2("--- %s v%s test start ---", SQINN_NAME, SQINN_VERSION); 115 | testIo(); 116 | testDb(); 117 | testApp(); 118 | if (mallocs != frees) { 119 | printMem(stderr); 120 | ASSERTF(mallocs == frees, "memory leak: %d mallocs, %d frees", mallocs, frees); 121 | } 122 | LOG_INFO2("--- %s v%s test ok ---", SQINN_NAME, SQINN_VERSION); 123 | Log_free(theLog); 124 | printf("test ok\n"); 125 | return 0; 126 | } else if (hasCommand(argc, argv, "version")) { 127 | printf("%s v%s\n", SQINN_NAME, SQINN_VERSION); 128 | return 0; 129 | } else if (hasCommand(argc, argv, "sqlite")) { 130 | printf("%s\n", SQLITE_VERSION); 131 | return 0; 132 | } 133 | help(); 134 | return 0; 135 | } 136 | -------------------------------------------------------------------------------- /lib/utl.c: -------------------------------------------------------------------------------- 1 | #include "utl.h" 2 | 3 | #define MAX_BLOCKS 0 // set 0 to disable memory tracking, set 8*1024 (e.g.) for debugging memory issues 4 | #define MAX_FILE_LEN 64 5 | 6 | typedef struct block_s { 7 | char file[MAX_FILE_LEN+1]; 8 | int line; 9 | void *ptr; 10 | BOOL free; 11 | } block_s; 12 | 13 | block_s blocks[MAX_BLOCKS+1]; 14 | int nblocks = 0; 15 | int mallocs = 0; 16 | int frees = 0; 17 | 18 | void initMem() { 19 | memset(blocks, 0, sizeof(blocks)); 20 | nblocks = 0; 21 | mallocs = 0; 22 | frees = 0; 23 | } 24 | 25 | void printMem(FILE *fp) { 26 | fprintf(fp, "%d mallocs, %d frees, %d in use\n", mallocs, frees, mallocs-frees); 27 | if(MAX_BLOCKS) { 28 | fprintf(fp, "%d blocks\n", nblocks); 29 | for (int i=0 ; i 1024 ) { 93 | len = 1024; 94 | } 95 | char *buf = (char*)memAlloc(4*len, __FILE__, __LINE__); 96 | buf[0] = 0; 97 | char tmp[4]; 98 | for (size_t i=0 ; ilevel = level; 141 | this->fp = NULL; 142 | this->stdErr = stdErr; 143 | if (strlen(filename) > 0) { 144 | this->fp = fopen(filename, "a"); 145 | if (!this->fp) { 146 | fprintf(stderr, "cannot open logfile\n"); 147 | } 148 | } 149 | return this; 150 | } 151 | 152 | void Log_free(Log *this) { 153 | if (!this) { 154 | return; 155 | } 156 | if (this->fp) { 157 | fclose(this->fp); 158 | } 159 | if (this->stdErr) { 160 | fflush(stderr); 161 | } 162 | memFree(this); 163 | } 164 | 165 | int Log_level(Log * this) { 166 | if (!this) { 167 | return LOG_LEVEL_OFF; 168 | } 169 | return this->level; 170 | } 171 | 172 | void Log_print(Log *this, int level, const char *fmt, ...) { 173 | if (!this) { 174 | return; 175 | } 176 | if (this->level < level) { 177 | return; 178 | } 179 | if (this->fp) { 180 | va_list args; 181 | va_start(args, fmt); 182 | _vfprintf(this->fp, level, fmt, args); 183 | va_end(args); 184 | fflush(this->fp); 185 | } 186 | if (this->stdErr) { 187 | va_list args; 188 | va_start(args, fmt); 189 | _vfprintf(stderr, level, fmt, args); 190 | va_end(args); 191 | fflush(stderr); 192 | } 193 | } 194 | 195 | Log *theLog = NULL; 196 | -------------------------------------------------------------------------------- /lib/utl.h: -------------------------------------------------------------------------------- 1 | #ifndef UTL_H 2 | #define UTL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #ifdef _WIN32 14 | // MSVC does not have unistd.h, it has io.h 15 | #include 16 | #define STDIN_FILENO 0 17 | #define STDOUT_FILENO 1 18 | #define STDERR_FILENO 2 19 | #else 20 | #include 21 | #endif 22 | 23 | /* A BOOL is either FALSE (zero) or TRUE (not zero).*/ 24 | typedef char BOOL; 25 | 26 | #define FALSE 0 27 | #define TRUE 1 28 | 29 | 30 | // 31 | // Memory primitives: Wrap malloc() and free() 32 | // 33 | 34 | void initMem(); 35 | void printMem(FILE *fp); 36 | void *memAlloc(size_t size, const char *file, int line); 37 | void *memRealloc(void *ptr, size_t newSize); 38 | void memFree(void *ptr); 39 | 40 | /* hexdump writes a hexdump into a newly allocated buffer. The buffer must be memFree'd after use. */ 41 | char *hexdump(const char *data, size_t len); 42 | 43 | /* Global memory counters. */ 44 | extern int mallocs; 45 | extern int frees; 46 | 47 | 48 | // 49 | // Logging utilities 50 | // 51 | 52 | #define LOG_LEVEL_OFF 0 53 | #define LOG_LEVEL_INFO 1 54 | #define LOG_LEVEL_DEBUG 2 55 | 56 | /* A Log writes log mesages. */ 57 | typedef struct log_s Log; 58 | Log *newLog(int level, const char *filename, BOOL stdErr); // TODO add maxFilesize parameter 59 | void Log_free(Log *this); 60 | int Log_level(Log *this); 61 | void Log_print(Log *this, int level, const char *fmt, ...); 62 | 63 | /* The (one and only) global Log instance. */ 64 | extern Log *theLog; 65 | 66 | #define LOG_CAN_INFO (Log_level(theLog) >= LOG_LEVEL_INFO) 67 | 68 | #define LOG_CAN_DEBUG (Log_level(theLog) >= LOG_LEVEL_DEBUG) 69 | 70 | #define LOG_INFO0(msg) Log_print(theLog, LOG_LEVEL_INFO, (msg)) 71 | #define LOG_INFO1(msg,a) Log_print(theLog, LOG_LEVEL_INFO, (msg), (a)) 72 | #define LOG_INFO2(msg,a,b) Log_print(theLog, LOG_LEVEL_INFO, (msg), (a), (b)) 73 | #define LOG_INFO3(msg,a,b,c) Log_print(theLog, LOG_LEVEL_INFO, (msg), (a), (b), (c)) 74 | #define LOG_INFO4(msg,a,b,c,d) Log_print(theLog, LOG_LEVEL_INFO, (msg), (a), (b), (c), (d)) 75 | 76 | #define LOG_DEBUG0(msg) Log_print(theLog, LOG_LEVEL_DEBUG, (msg)) 77 | #define LOG_DEBUG1(msg,a) Log_print(theLog, LOG_LEVEL_DEBUG, (msg), (a)) 78 | #define LOG_DEBUG2(msg,a,b) Log_print(theLog, LOG_LEVEL_DEBUG, (msg), (a), (b)) 79 | #define LOG_DEBUG3(msg,a,b,c) Log_print(theLog, LOG_LEVEL_DEBUG, (msg), (a), (b), (c)) 80 | #define LOG_DEBUG4(msg,a,b,c,d) Log_print(theLog, LOG_LEVEL_DEBUG, (msg), (a), (b), (c), (d)) 81 | 82 | 83 | // 84 | // ASSERT macros 85 | // 86 | 87 | #define ASSERT(condition) if(!(condition)) { \ 88 | Log_print(theLog, LOG_LEVEL_INFO, "%s:%d ASSERT FAIL: " #condition "" , __FILE__, __LINE__); \ 89 | fprintf(stderr, "%s:%d ASSERT FAIL: " #condition "\n", __FILE__, __LINE__); \ 90 | exit(1); \ 91 | } 92 | 93 | #define ASSERTF(condition, fmt, ...) if(!(condition)) { \ 94 | Log_print(theLog, LOG_LEVEL_INFO, "%s:%d ASSERT FAIL: " #condition ": " #fmt "" , __FILE__, __LINE__, ##__VA_ARGS__); \ 95 | fprintf(stderr, "%s:%d ASSERT FAIL: " #condition ": " #fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \ 96 | exit(1); \ 97 | } 98 | 99 | #define ASSERT_FAIL(fmt, ...) { \ 100 | Log_print(theLog, LOG_LEVEL_INFO, "%s:%d ASSERT FAIL: " #fmt "" , __FILE__, __LINE__, ##__VA_ARGS__); \ 101 | fprintf(stderr, "%s:%d ASSERT FAIL: " #fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \ 102 | exit(1); \ 103 | } 104 | 105 | #define ASSERT_INT(want, have) { \ 106 | int w = (want); \ 107 | int h = (have); \ 108 | if(w != h) { \ 109 | Log_print(theLog, LOG_LEVEL_INFO, "%s:%d ASSERT FAIL: want %d but have %d" , __FILE__, __LINE__, w, h); \ 110 | fprintf(stderr, "%s:%d ASSERT FAIL: want %d but have %d\n", __FILE__, __LINE__, w, h); \ 111 | exit(1); \ 112 | } \ 113 | } 114 | 115 | #define ASSERT_INT64(want, have) { \ 116 | int64_t w = (want); \ 117 | int64_t h = (have); \ 118 | if(w != h) { \ 119 | Log_print(theLog, LOG_LEVEL_INFO, "%s:%d ASSERT FAIL: want %" PRId64 " but have %" PRId64 "" , __FILE__, __LINE__, w, h); \ 120 | fprintf(stderr, "%s:%d ASSERT FAIL: want %" PRId64 " but have %" PRId64 "\n", __FILE__, __LINE__, w, h); \ 121 | exit(1); \ 122 | } \ 123 | } 124 | 125 | #define ASSERT_DOUBLE(want, have) { \ 126 | double w = (want); \ 127 | double h = (have); \ 128 | if(w != h) { \ 129 | Log_print(theLog, LOG_LEVEL_INFO, "%s:%d ASSERT FAIL: want %f but have %f" , __FILE__, __LINE__, w, h); \ 130 | fprintf(stderr, "%s:%d ASSERT FAIL: want %f but have %f\n", __FILE__, __LINE__, w, h); \ 131 | exit(1); \ 132 | } \ 133 | } 134 | 135 | #define ASSERT_STR(want, have) { \ 136 | const char* w = (want); \ 137 | const char* h = (have); \ 138 | if(strcmp(w,h) != 0) { \ 139 | Log_print(theLog, LOG_LEVEL_INFO, "%s:%d ASSERT FAIL: want '%s' but have '%s'" , __FILE__, __LINE__, w, h); \ 140 | fprintf(stderr, "%s:%d ASSERT FAIL: want '%s' but have '%s'\n", __FILE__, __LINE__, w, h); \ 141 | exit(1); \ 142 | } \ 143 | } 144 | 145 | #endif // UTL_H -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Sqinn](logo.png "Sqinn") 2 | 3 | [![Build Status](https://github.com/cvilsmeier/sqinn/actions/workflows/linux.yml/badge.svg)](https://github.com/cvilsmeier/sqinn/actions/workflows/linux.yml) 4 | [![Build Status](https://github.com/cvilsmeier/sqinn/actions/workflows/windows.yml/badge.svg)](https://github.com/cvilsmeier/sqinn/actions/workflows/windows.yml) 5 | [![Build Status](https://github.com/cvilsmeier/sqinn/actions/workflows/macos.yml/badge.svg)](https://github.com/cvilsmeier/sqinn/actions/workflows/macos.yml) 6 | [![Build Status](https://github.com/cvilsmeier/sqinn/actions/workflows/macos-arm64.yml/badge.svg)](https://github.com/cvilsmeier/sqinn/actions/workflows/macos-arm64.yml) 7 | [![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/) 8 | 9 | 10 | > [!NOTE] 11 | > This work is sponsored by Monibot - Website, Server and Application Monitoring. 12 | > Try out Monibot at [https://monibot.io](https://monibot.io?ref=sqinn). 13 | 14 | 15 | Sqinn is an alternative to the SQLite C API. Sqinn reads requests from stdin, 16 | forwards the request to SQLite, and writes a response to stdout. It is used in 17 | programming environments that do not allow calling C API functions. 18 | 19 | The [SQLite database library](https://www.sqlite.org) is written in C and 20 | provides an API for using it in C/C++. There are many language bindings. If you 21 | cannot or do not want to use one of the available language bindings, and your 22 | programming language allows the creation of subprocesses (fork/exec), an option 23 | might be to communicate with SQLite over stdin/stdout, using Sqinn. 24 | 25 | Sqinn provides functions to execute SQL statements and query database rows. 26 | 27 | All function calls and the binary protocol used for sending request and 28 | receiving responses is described in [rfc.txt](rfc.txt). 29 | 30 | For the Go (Golang) language binding, see , 31 | for benchmarks, see . 32 | 33 | 34 | 35 | Compiling 36 | ------------------------------------------------------------------------------- 37 | 38 | See the included `build.sh` and `build.bat` script for compiling Sqinn. 39 | I have tested it on the following platforms: 40 | 41 | - Debian Linux 12 amd64 (gcc) 42 | - Windows 10 amd64, using MSVC Build Tools (cl.exe) 43 | - Macos 13 amd64 (clang) 44 | 45 | The releases page contains a tar file with pre-built binaries for linux-amd64, 46 | windows-amd64, macos-amd64 and macos-arm64, 47 | see . 48 | 49 | If you want to compile Sqinn, have gcc/cl.exe/clang installed and follow the steps: 50 | 51 | ```bash 52 | git clone https://github.com/cvilsmeier/sqinn 53 | cd sqinn 54 | chmod a+x script/*.sh 55 | script/build.sh 56 | ``` 57 | 58 | The build script creates a `bin` subdirectory that the build results go into. 59 | Test it with: 60 | 61 | ```bash 62 | bin/sqinn test 63 | ``` 64 | 65 | See also for build actions for 66 | Linux, Windows and MacOS. 67 | 68 | 69 | 70 | Command line usage 71 | ------------------------------------------------------------------------------- 72 | 73 | There isn't really one. Sqinn is not used by humans, it's used by other 74 | programs. That said: 75 | 76 | ``` 77 | $ sqinn 78 | sqinn v2.0.0 - SQLite over stdin/stdout. 79 | 80 | Usage: 81 | sqinn [options...] 82 | 83 | The commands are: 84 | 85 | run Read requests from stdin and write responses to stdout. 86 | test Execute selftest and exit. 87 | version Print version and exit. 88 | sqlite Print SQLite library version and exit. 89 | help Print help page and exit. 90 | 91 | The options are: 92 | 93 | -db Database name. Default is ":memory:" 94 | -loglevel Log level: 0=off, 1=info, 2=debug. Default is 0 (off). 95 | -logfile Log to a file. Default is empty (no file logging). 96 | Note: Logfile is appended and will grow unlimited. 97 | -logstderr Log to stderr. Default is off (no stderr logging). 98 | ``` 99 | 100 | 101 | 102 | Limitations 103 | ------------------------------------------------------------------------------- 104 | 105 | ### Single threaded 106 | 107 | Sqinn is single threaded. It serves requests one after another. 108 | 109 | 110 | ### API subset 111 | 112 | Sqinn supports only a subset of the many functions that the SQLite C/C++ API 113 | provides. Interruption of SQL operations, incremental blob i/o, 114 | vfs and extension functions are not supported, among others. 115 | 116 | 117 | 118 | Contributing 119 | ------------------------------------------------------------------------------- 120 | 121 | I have to reject most PRs and feature requests. Why? Because I use sqinn for my 122 | own projects, and I need it to be reliable, fast, reliable, secure, reliable 123 | and easy to maintain. Did I mention reliable? 124 | I cannot include every feature under the sun. Additionally, there are legal 125 | issues that prevent me from using source code from unknown provenance. 126 | I give it away for free, so everybody can adjust it to his or her own needs. 127 | 128 | 129 | 130 | Changelog 131 | ------------------------------------------------------------------------------- 132 | 133 | ### v2.0.1 134 | 135 | - SQLite Version 3.51.0 (2025-11-04) 136 | 137 | ### v2.0.0 138 | 139 | - New I/O protocol 140 | - SQLite Version 3.50.4 (2025-07-30) 141 | -------------------------------------------------------------------------------- /rfc.txt: -------------------------------------------------------------------------------- 1 | Network Working Group C. Vilsmeier 2 | Request for Comments: None August 2025 3 | Category: Informational 4 | 5 | Sqinn - SQLite over stdin/stdout 6 | 7 | Status of this Memo 8 | 9 | This memo provides information for the Internet community. It does 10 | not specify an Internet standard of any kind. Distribution of this 11 | memo is unlimited. 12 | 13 | Copyright Notice 14 | 15 | Copyright (C) The Public Domain. All Rights Reserved. 16 | 17 | Abstract 18 | 19 | This document describes the Sqinn protocol, a protocol for accessing 20 | a SQLite database over stdin/stdout. 21 | 22 | 1. Rationale and Scope 23 | 24 | There are SQLite databases all over the world. Increasingly, in a 25 | world in which computing is ubiquitous, the computists want to make 26 | even more SQLite databases. SQLite database files are created and 27 | modified by the SQLite database library. SQLite, the library, is 28 | small, fast, free and well tested. It is made by Richard Hipp. 29 | SQLite is, by far, the most used database in the world. 30 | The SQLite library is written in C, an ancient programming language 31 | from the medieval ages of kings and queens. 32 | 33 | Some younger computists want to use SQLite in more modern 34 | programming languages like Java, Rust or Go. Some of those languages 35 | make it unneccesarily hard to interface with a C library. The 36 | reasons are manyfold: Threading issues, unclear memory models, 37 | compiler issues, platform dependencies, dynamic library version 38 | hell, and so forth. 39 | 40 | To make it easier for the users of these languages, Sqinn defines a 41 | communication protocol that is used by 'client' processes to 42 | communicate with a SQLite 'server' process. The communication uses 43 | pipes (stdin/stdout), so that a client can spawn a Sqinn process and 44 | communicate with it by sending and receiving bytes. Readers familiar 45 | with UNIX pipes will instantly recognize this communication pattern, 46 | as they use it in their daily work to forward data from one process 47 | to another. 48 | 49 | 2. Data Types 50 | 51 | Sqinn defines the following data types: 52 | 53 | int32 A 4-byte integer value. 54 | 55 | int64 A 8-byte integer value. 56 | 57 | double A 8-byte floating point value. 58 | 59 | string A length-prefixed null-terminated byte string. 60 | 61 | blob A length-prefixed byte array. 62 | 63 | value A value of any of the above types, or NULL. 64 | 65 | 2.1. Int32 66 | 67 | An int32 is encoded as 4 bytes, MSB first (a.k.a. big-endian). 68 | 69 | A sample int32 looks like this: 70 | 71 | 00 00 00 01 // int32 value 1 72 | 73 | 00 00 01 00 // int32 value 256 74 | 75 | FF FF FF FF // int32 value -1 76 | 77 | FF FF FF FE // int32 value -2 78 | 79 | 2.2. Int64 80 | 81 | An int64 is encoded as 8 bytes, MSB first (a.k.a. big-endian). 82 | 83 | A sample int64 looks like this: 84 | 85 | 00 00 00 00 00 00 00 01 // int64 value 1 86 | 87 | 00 00 00 00 00 00 01 00 // int64 value 256 88 | 89 | FF FF FF FF FF FF FF FF // int64 value -1 90 | 91 | FF FF FF FF FF FF FF FE // int64 value -2 92 | 93 | 2.3. Double 94 | 95 | A double is encoded with the 96 | 8-byte IEEE 754 encoding of a double precision floating-point 97 | value. The sign bit comes first, then the eponent bits, then 98 | the fraction bits. 99 | 100 | A sample double looks like this: 101 | 102 | 40 60 10 00 00 00 00 00 // double value 128.5 103 | 104 | 2.4. String 105 | 106 | A string is encoded with its length (a 4-byte int32) first, then the 107 | string content, then a terminating null character. It is important 108 | to note that the length includes the terminating null character. So, 109 | an empty string has length 1, not 0. A string length of 0 is not 110 | valid. 111 | 112 | A sample string looks like this: 113 | 114 | 00 00 00 04 // int32 string length 115 | 41 42 43 00 // string data and terminating null character 116 | 117 | 2.5. Blob 118 | 119 | A blob is encoded with its length (a 4-byte int32) first, then the 120 | blob's byte content. 121 | 122 | A sample blob looks like this: 123 | 124 | 00 00 00 04 // int32 blob length 125 | AF F0 33 E2 // blob data 126 | 127 | 2.6. Value 128 | 129 | A value is encoded with its type (one byte), followed by the 130 | encoding of its int32/int64/double/string/blob content. 131 | 132 | The following value types are defined: 133 | 134 | VT_NULL 0 A NULL value. 135 | VT_INT32 1 An int32 value. 136 | VT_INT64 2 An int64 value. 137 | VT_DOUBLE 3 A double value. 138 | VT_STRING 4 A string value. 139 | VT_BLOB 5 A blob value. 140 | 141 | Sample values looks like this: 142 | 143 | 00 // VT_NULL 144 | 145 | 01 00 00 00 02 // VT_INT32 followed by a 4-byte int32 146 | // value 2. 147 | 148 | 02 00 00 00 00 00 00 00 02 // VT_INT64 followed by a 8-byte int64 149 | // value 2. 150 | 151 | 03 00 00 00 00 00 00 00 02 // VT_DOUBLE followed by a 8-byte double 152 | // value. 153 | 154 | 04 00 00 00 01 00 // VT_STRING followed by an empty 155 | // string. 156 | 157 | 05 00 00 00 01 FF // VT_BLOB followed by a 1-byte blob. 158 | 159 | 3. Requests and Responses 160 | 161 | A request is sent from the client to the server. A request has the 162 | following format: 163 | 164 | 1 byte A function code 165 | N bytes The request payload (depends on the function code) 166 | 167 | The following function codes are defined: 168 | 169 | FC_EXEC 1 Execute a parameterized SQL statement (INSERT, 170 | UPDATE, DELETE, etc.), possibly multiple times. 171 | 172 | FC_QUERY 2 Execute a parameterized SQL query (SELECT). 173 | 174 | FC_QUIT 9 Close database and quit. 175 | 176 | A response is sent from the server back to the client. It has the 177 | following format: 178 | 179 | N bytes The response payload (depends on function code) 180 | 181 | Synchronization 182 | 183 | A client, having sent a request, must read the server response 184 | completely before sending a new request. 185 | 186 | A server must not send any data, except if requested by a 187 | client. After the server has sent the response completely, it 188 | must await the next request from the client. 189 | 190 | 3.1. FC_EXEC 191 | 192 | A FC_EXEC request tells the server that it should execute a DDL 193 | (CREATE TABLE, CREATE INDEX, etc.) or a parameterized SQL statement 194 | (INSERT, UPDATE, DELETE, etc.), possibly multiple times. 195 | 196 | It has the following data objects: 197 | 198 | sql string The sql statement to be executed. 199 | 200 | niter int32 Number of iterations. If niter is 1, the 201 | statement is executed once. If it is greater than 202 | 1, the statement is executed multiple times, each 203 | time with another set of supplied parameters. 204 | This is useful, for example, for inserting many 205 | rows at once. 206 | 207 | nparams int32 The number of parameters per iteration. It can 208 | be 0. 209 | 210 | params []value An array of parameter values. The length of this 211 | array is niter x nparams. In other words, for 212 | each iteration, there are nparams values. 213 | 214 | A sample FC_EXEC request looks like this: 215 | 216 | 01 // FC_EXEC 217 | 00 00 00 2C // length of sql 218 | 41 42 43 .. .. 00 // sql, null-terminated 219 | 00 00 00 02 // 2 iterations 220 | 00 00 00 02 // 2 params per iteration 221 | 01 // iteration 0 param 0 type (VT_INT32) 222 | 00 00 00 0A // iteration 0 param 0 value 223 | 04 // iteration 0 param 1 type (VT_STRING) 224 | 00 00 00 0A // iteration 0 param 1 string length 225 | 41 42 .. .. 00 // iteration 0 param 1 string value length 226 | 00 // iteration 1 param 0 type (VT_NULL) 227 | 00 // iteration 1 param 1 type (VT_NULL) 228 | 229 | The server will prepare a statement with the provided sql, and 230 | execute it niter times, each time feeding nparams parameter values 231 | into the prepared statement. It sends a success response or an error 232 | response back to the client. 233 | 234 | A sample FC_EXEC success response looks like this: 235 | 236 | 01 // ok 237 | 238 | A sample FC_EXEC error response looks like this: 239 | 240 | 00 // not ok 241 | 00 00 00 2A // length of errmsg 242 | 41 42 43 .. .. 00 // errmsg, null-terminated 243 | 244 | 3.2. FC_QUERY 245 | 246 | A FC_QUERY request tells the server that it should execute a 247 | parameterized SQL statement (SELECT, etc.), and return result 248 | values. 249 | 250 | It has the following data objects: 251 | 252 | sql string The sql statement that should be executed. 253 | 254 | nparams int32 The number of parameters. It can be 0. 255 | 256 | params []value An array (length nparams) of parameter values. 257 | 258 | ncols int32 The number of columns per result row. 259 | 260 | coltypes []byte An array (length ncols) of column types 261 | (VT_INT32, VT_INT64, etc.). 262 | 263 | A sample FC_QUERY request looks like this: 264 | 265 | 02 // FC_QUERY 266 | 00 00 00 2C // sql string length 267 | 41 42 43 .. 00 // sql string, null-terminated 268 | 00 00 00 02 // 2 params 269 | 01 // param 0 type (VT_INT32) 270 | 00 00 00 0A // int32 value 271 | 04 // param 1 type (VT_STRING) 272 | 00 00 00 0A // string length 273 | 41 42 43 .. 00 // string, null-terminated 274 | 00 00 00 02 // 2 columns 275 | 01 // column 0 type (VT_INT32) 276 | 04 // column 1 type (VT_STRING) 277 | 278 | The server will prepare a statement with the provided sql, execute 279 | it, and fetch all result rows, and send each column value for each 280 | row back to the client. 281 | 282 | A sample FC_QUERY success response looks like this: 283 | 284 | 01 // has row 285 | 01 // value 0 type (VT_INT32) 286 | 00 00 00 02 // int32 value 287 | 00 // value 1 type (VT_NULL) 288 | 01 // has row 289 | 01 // value 0 type (VT_INT32) 290 | 00 00 00 02 // int32 value 291 | 04 // value 1 type (VT_STRING) 292 | 00 00 00 0A // string length 293 | 41 42 43 .. .. 00 // string, null-terminated 294 | 00 // no more rows 295 | 01 // ok 296 | 297 | A sample FC_QUERY error response looks like this: 298 | 299 | 01 // has row 300 | 00 // value 0 type (VT_NULL) 301 | 00 // value 1 type (VT_NULL) 302 | 00 // no more rows 303 | 00 // not ok 304 | 00 00 00 2A // errmsg length 305 | 41 42 43 .. .. 00 // errmsg, null-terminated 306 | 307 | 3.3. FC_QUIT 308 | 309 | A FC_QUIT request tells the server that the client is done. 310 | 311 | It has no further data objects. 312 | 313 | A sample FC_QUIT request looks like this: 314 | 315 | 09 // FC_QUIT 316 | 317 | A sample FC_QUIT success response looks like this: 318 | 319 | 01 // ok 320 | 321 | There is no error response for FC_QUIT. 322 | 323 | 4. Data Frames 324 | 325 | All data that is sent by a client to the server, or vice versa, is 326 | framed. Big requests and responses can be split into several frames, 327 | to help avoid excessive memory consumption in clients and servers. 328 | 329 | A frame has the following format: 330 | 331 | 4 byte The frame payload length N 332 | N bytes The payload data 333 | 334 | A sample frame looks like this: 335 | 336 | 00 00 00 07 // payload length is 7 bytes 337 | 01 00 00 00 02 41 00 // 7-byte payload data 338 | 339 | The following rules apply: 340 | 341 | (a) A request or can be contained in exactly one frame or split 342 | into several frames. 343 | 344 | (b) A single frame contains the data of exactly one request or 345 | repsonse. In other words, a frame must not contain data for more 346 | than one reqest/response. 347 | 348 | (c) Each value (int32, int64, double, string or blob) must be 349 | contained as a whole in one frame. In other words, a single 350 | value cannot be split across several frames. 351 | 352 | One consequence of rule (c) is that large strings or blobs can lead 353 | to very large frames, as a string or blob is not allowed to be split 354 | into two or more frames. 355 | 356 | 5. Conclusion 357 | 358 | We have presented the Sqinn protocol for accessing a SQLite 359 | database over stdin/stdout. 360 | 361 | The references implementations for a Sqinn server and client library 362 | can be found by following these world wide web hyperlinks: 363 | https://github.com/cvilsmeier/sqinn 364 | https://github.com/cvilsmeier/sqinn-go 365 | 366 | |----------------------------------------------------------------------| 367 | -------------------------------------------------------------------------------- /lib/io.c: -------------------------------------------------------------------------------- 1 | #include "utl.h" 2 | #include "io.h" 3 | 4 | #define MAX_LEN 0x7FFFFFFF 5 | 6 | void _readStdin(char *buf, size_t len) { 7 | size_t c = 0; 8 | while(c < len) { 9 | size_t n = read(STDIN_FILENO, buf+c, len-c); 10 | if (n<=0) { 11 | ASSERT_FAIL("_readStdin: n=%zd", n); 12 | } 13 | c += n; 14 | } 15 | ASSERT(c == len); 16 | } 17 | 18 | void _writeStdout(char *buf, size_t len) { 19 | size_t c = 0; 20 | while(c < len) { 21 | size_t n = write(STDOUT_FILENO, buf+c, len-c); 22 | if (n<=0) { 23 | ASSERT_FAIL("_writeStdout: n=%zd", n); 24 | } 25 | c += n; 26 | } 27 | ASSERT(c == len); 28 | } 29 | 30 | // class Reader 31 | 32 | struct reader_s { 33 | // for stdin reader 34 | BOOL std; 35 | char* buf; 36 | size_t bufsz; 37 | size_t rp; // read pointer 38 | }; 39 | 40 | Reader *newStdinReader(){ 41 | Reader* this = (Reader*)memAlloc(sizeof(Reader), __FILE__, __LINE__); 42 | this->std = TRUE; 43 | this->buf = NULL; 44 | this->bufsz = 0; 45 | this->rp = 0; 46 | return this; 47 | } 48 | 49 | Reader *newMemReader(char *buf, size_t bufsz) { 50 | ASSERT(buf); 51 | ASSERT(bufsz); 52 | Reader* this = (Reader*)memAlloc(sizeof(Reader), __FILE__, __LINE__); 53 | this->std = FALSE; 54 | this->buf = buf; 55 | this->bufsz = bufsz; 56 | this->rp = 0; 57 | return this; 58 | } 59 | 60 | void Reader_free(Reader* this) { 61 | if(this->std && this->buf) { 62 | memFree(this->buf); 63 | } 64 | memFree(this); 65 | } 66 | 67 | void _readNextFrameIfNeeded(Reader* this) { 68 | ASSERT(this->std); 69 | ASSERT(this->rp <= this->bufsz); 70 | if(this->rp == this->bufsz) { 71 | char tmp[4]; 72 | _readStdin(tmp, 4); 73 | size_t len0 = (size_t)(unsigned char)tmp[0] << 24; 74 | size_t len1 = (size_t)(unsigned char)tmp[1] << 16; 75 | size_t len2 = (size_t)(unsigned char)tmp[2] << 8; 76 | size_t len3 = (size_t)(unsigned char)tmp[3] << 0; 77 | size_t len = len0 + len1 + len2 + len3; 78 | ASSERT(1 <= len && len <= MAX_LEN); 79 | if (this->buf) { 80 | this->buf = memRealloc(this->buf, len); 81 | } else { 82 | this->buf = memAlloc(len, __FILE__, __LINE__); 83 | } 84 | _readStdin(this->buf, len); 85 | this->bufsz = len; 86 | this->rp = 0; 87 | if (LOG_CAN_DEBUG) { 88 | char * hx = hexdump(this->buf, this->bufsz); 89 | LOG_DEBUG2("_readNextFrameIfNeeded: %d bytes: %s", this->bufsz, hx); 90 | memFree(hx); 91 | } 92 | } 93 | } 94 | 95 | char Reader_readByte(Reader* this) { 96 | if(this->std) { 97 | _readNextFrameIfNeeded(this); 98 | } 99 | ASSERT(this->bufsz - this->rp >= 1); 100 | char v = this->buf[this->rp]; 101 | this->rp += 1; 102 | return v; 103 | } 104 | 105 | int Reader_readInt32(Reader* this) { 106 | if(this->std) { 107 | _readNextFrameIfNeeded(this); 108 | } 109 | ASSERT(this->bufsz - this->rp >= 4); 110 | uint32_t v0 = (uint32_t)(unsigned char)this->buf[this->rp + 0] << 24; 111 | uint32_t v1 = (uint32_t)(unsigned char)this->buf[this->rp + 1] << 16; 112 | uint32_t v2 = (uint32_t)(unsigned char)this->buf[this->rp + 2] << 8; 113 | uint32_t v3 = (uint32_t)(unsigned char)this->buf[this->rp + 3] << 0; 114 | this->rp += 4; 115 | return (int)(v0 + v1 + v2 + v3); 116 | } 117 | 118 | int64_t Reader_readInt64(Reader* this) { 119 | if(this->std) { 120 | _readNextFrameIfNeeded(this); 121 | } 122 | ASSERT(this->bufsz - this->rp >= 8); 123 | uint64_t v0 = (uint64_t)(unsigned char)this->buf[this->rp + 0] << 56; 124 | uint64_t v1 = (uint64_t)(unsigned char)this->buf[this->rp + 1] << 48; 125 | uint64_t v2 = (uint64_t)(unsigned char)this->buf[this->rp + 2] << 40; 126 | uint64_t v3 = (uint64_t)(unsigned char)this->buf[this->rp + 3] << 32; 127 | uint64_t v4 = (uint64_t)(unsigned char)this->buf[this->rp + 4] << 24; 128 | uint64_t v5 = (uint64_t)(unsigned char)this->buf[this->rp + 5] << 16; 129 | uint64_t v6 = (uint64_t)(unsigned char)this->buf[this->rp + 6] << 8; 130 | uint64_t v7 = (uint64_t)(unsigned char)this->buf[this->rp + 7]; 131 | this->rp += 8; 132 | return (int64_t)(v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7); 133 | } 134 | 135 | double Reader_readDouble(Reader* this) { 136 | if(this->std) { 137 | _readNextFrameIfNeeded(this); 138 | } 139 | ASSERT(this->bufsz - this->rp >= 8); 140 | double v = 0.0; 141 | char *p = (char *)(&v); 142 | p[7] = this->buf[this->rp + 0]; 143 | p[6] = this->buf[this->rp + 1]; 144 | p[5] = this->buf[this->rp + 2]; 145 | p[4] = this->buf[this->rp + 3]; 146 | p[3] = this->buf[this->rp + 4]; 147 | p[2] = this->buf[this->rp + 5]; 148 | p[1] = this->buf[this->rp + 6]; 149 | p[0] = this->buf[this->rp + 7]; 150 | this->rp += 8; 151 | return v; 152 | } 153 | 154 | const char* Reader_readString(Reader* this) { 155 | size_t len = 1; 156 | const char *p = Reader_readBlob(this, &len); 157 | ASSERTF(p[len-1] == '\0', "Reader_readString: string is not null-terminated"); 158 | return p; 159 | } 160 | 161 | const char* Reader_readBlob(Reader* this, size_t *plen) { 162 | if(this->std) { 163 | _readNextFrameIfNeeded(this); 164 | } 165 | size_t len = Reader_readInt32(this); 166 | ASSERT(len <= 0x7FFFFFFF); 167 | ASSERT(this->bufsz - this->rp >= len); 168 | *plen = len; 169 | const char *p = this->buf + this->rp; 170 | this->rp += len; 171 | return p; 172 | } 173 | 174 | // class Writer 175 | 176 | struct writer_s { 177 | BOOL std; 178 | char* buf; 179 | size_t bufsz; 180 | size_t wp; // write pointer 181 | }; 182 | 183 | void _validateWriter(Writer *this) { 184 | ASSERT(this); 185 | ASSERT(this->buf); 186 | ASSERT(this->bufsz); 187 | ASSERT(this->wp <= this->bufsz); 188 | } 189 | 190 | Writer *newStdoutWriter() { 191 | Writer *this = (Writer *)memAlloc(sizeof(Writer), __FILE__, __LINE__); 192 | this->std = TRUE; 193 | this->bufsz = 1024*1024; 194 | this->buf = memAlloc(this->bufsz, __FILE__, __LINE__); 195 | this->wp = 0; 196 | _validateWriter(this); 197 | return this; 198 | } 199 | 200 | Writer *newMemWriter(char *buf, size_t bufsz) { 201 | Writer *this = (Writer *)memAlloc(sizeof(Writer), __FILE__, __LINE__); 202 | this->std = FALSE; 203 | this->buf = buf; 204 | this->bufsz = bufsz; 205 | this->wp = 0; 206 | _validateWriter(this); 207 | return this; 208 | } 209 | 210 | void Writer_free(Writer* this) { 211 | _validateWriter(this); 212 | if (this->std) { 213 | memFree(this->buf); 214 | } 215 | memFree(this); 216 | } 217 | 218 | static void _growWriter(Writer* this, size_t minSize) { 219 | if (this->bufsz < minSize) { 220 | size_t newSize = this->bufsz; 221 | while( newSize < minSize ) { 222 | newSize = 2 * newSize; 223 | } 224 | LOG_DEBUG1("_growWriter: newSize %ld ", newSize); 225 | if (newSize > MAX_LEN) { 226 | ASSERT_FAIL("_growWriter: newSize %zd > MAX_LEN", newSize) 227 | } 228 | this->buf = memRealloc(this->buf, newSize); 229 | this->bufsz = newSize; 230 | } 231 | } 232 | 233 | void Writer_markFrame(Writer* this) { 234 | _validateWriter(this); 235 | if (this->std && this->wp > 1024*1024) { 236 | Writer_flush(this); 237 | } 238 | } 239 | 240 | void Writer_flush(Writer* this) { 241 | _validateWriter(this); 242 | if (this->std && this->wp) { 243 | if (LOG_CAN_DEBUG) { 244 | char * hx = hexdump(this->buf, this->wp); 245 | LOG_DEBUG2("Writer_flush: %d bytes: %s", this->wp, hx); 246 | memFree(hx); 247 | } 248 | char tmp[4]; 249 | tmp[0] = (char)(this->wp >> 24); 250 | tmp[1] = (char)(this->wp >> 16); 251 | tmp[2] = (char)(this->wp >> 8); 252 | tmp[3] = (char)(this->wp); 253 | _writeStdout(tmp, 4); 254 | _writeStdout(this->buf, this->wp); 255 | this->wp = 0; 256 | } 257 | } 258 | 259 | 260 | void Writer_writeByte(Writer* this, char value) { 261 | _validateWriter(this); 262 | if (this->std) { 263 | _growWriter(this, this->wp + 1); 264 | } 265 | ASSERT(this->bufsz - this->wp >= 1); 266 | this->buf[this->wp] = value; 267 | this->wp += 1; 268 | } 269 | 270 | void Writer_writeInt32(Writer* this, int value) { 271 | _validateWriter(this); 272 | if (this->std) { 273 | _growWriter(this, this->wp + 4); 274 | } 275 | ASSERT(this->bufsz - this->wp >= 4); 276 | this->buf[this->wp + 0] = (char)(value >> 24); 277 | this->buf[this->wp + 1] = (char)(value >> 16); 278 | this->buf[this->wp + 2] = (char)(value >> 8); 279 | this->buf[this->wp + 3] = (char)(value); 280 | this->wp += 4; 281 | } 282 | 283 | void Writer_writeInt64(Writer* this, int64_t value) { 284 | _validateWriter(this); 285 | if (this->std) { 286 | _growWriter(this, this->wp + 8); 287 | } 288 | ASSERTF(this->bufsz - this->wp >= 8, "this->bufsz %zd - this->wp %zd = %zd", this->bufsz, this->wp, this->bufsz - this->wp); 289 | this->buf[this->wp + 0] = (char)(value >> 56); 290 | this->buf[this->wp + 1] = (char)(value >> 48); 291 | this->buf[this->wp + 2] = (char)(value >> 40); 292 | this->buf[this->wp + 3] = (char)(value >> 32); 293 | this->buf[this->wp + 4] = (char)(value >> 24); 294 | this->buf[this->wp + 5] = (char)(value >> 16); 295 | this->buf[this->wp + 6] = (char)(value >> 8); 296 | this->buf[this->wp + 7] = (char)(value); 297 | this->wp += 8; 298 | } 299 | 300 | void Writer_writeDouble(Writer* this, double value) { 301 | _validateWriter(this); 302 | if (this->std) { 303 | _growWriter(this, this->wp + 8); 304 | } 305 | ASSERT(this->bufsz - this->wp >= 8); 306 | char* p = (char*)(&value); 307 | this->buf[this->wp + 0] = p[7]; 308 | this->buf[this->wp + 1] = p[6]; 309 | this->buf[this->wp + 2] = p[5]; 310 | this->buf[this->wp + 3] = p[4]; 311 | this->buf[this->wp + 4] = p[3]; 312 | this->buf[this->wp + 5] = p[2]; 313 | this->buf[this->wp + 6] = p[1]; 314 | this->buf[this->wp + 7] = p[0]; 315 | this->wp += 8; 316 | } 317 | 318 | void Writer_writeString(Writer* this, const char* str) { 319 | _validateWriter(this); 320 | size_t len = strlen(str); 321 | Writer_writeBlob(this, str, len + 1); 322 | } 323 | 324 | void Writer_writeBlob(Writer* this, const char* data, size_t len) { 325 | ASSERT(data); 326 | ASSERT(len < MAX_LEN); 327 | _validateWriter(this); 328 | if (this->std) { 329 | _growWriter(this, this->wp + 4 + len); 330 | } 331 | Writer_writeInt32(this, (int)len); 332 | ASSERT(this->bufsz - this->wp >= len); 333 | memcpy(this->buf + this->wp, data, len); 334 | this->wp += len; 335 | } 336 | 337 | // 338 | // Test 339 | // 340 | 341 | static void testWriteAndRead() { 342 | char buf[256]; 343 | // write 344 | Writer *w = newMemWriter(buf, sizeof(buf)); 345 | Writer_writeByte(w, 0); 346 | Writer_writeByte(w, 42); 347 | Writer_writeByte(w, -1); // same as 255 348 | Writer_writeInt32(w, 0x10203040); // 4 byte 349 | Writer_writeInt32(w, 0xF0E0D0C0); // 4 byte 350 | Writer_writeInt64(w, 0x1020304050607080); // 8 byte 351 | Writer_writeInt64(w, 0xF0E0D0C0B0A09080); // 8 byte 352 | Writer_writeDouble(w, 128.5); // 8 byte // double 128.5 = hex(40 60 10 00 00 00 00 00) 353 | Writer_writeString(w, "Alice"); // 4 byte + 5 data + 1 null-termination 354 | Writer_writeBlob(w, "12345678", 8); // 4 byte + 8 byte data 355 | // check data 356 | int i=0; 357 | // Writer_writeByte(w, 0); 358 | ASSERT_INT(0, (unsigned char)buf[i++]); 359 | // Writer_writeByte(w, 42); 360 | ASSERT_INT(42, (unsigned char)buf[i++]); 361 | // Writer_writeByte(w, 255); // same as -1 362 | ASSERT_INT(255, (unsigned char)buf[i++]); 363 | // Writer_writeInt32(w, 0x10203040); // 4 byte 364 | ASSERT_INT(0x10, (unsigned char)buf[i++]); 365 | ASSERT_INT(0x20, (unsigned char)buf[i++]); 366 | ASSERT_INT(0x30, (unsigned char)buf[i++]); 367 | ASSERT_INT(0x40, (unsigned char)buf[i++]); 368 | // Writer_writeInt32(w, 0xF0E0D0C0); // 4 byte 369 | ASSERT_INT(0xF0, (unsigned char)buf[i++]); 370 | ASSERT_INT(0xE0, (unsigned char)buf[i++]); 371 | ASSERT_INT(0xD0, (unsigned char)buf[i++]); 372 | ASSERT_INT(0xC0, (unsigned char)buf[i++]); 373 | // Writer_writeInt64(w, 0x1020304050607080); // 8 byte 374 | ASSERT_INT(0x10, (unsigned char)buf[i++]); 375 | ASSERT_INT(0x20, (unsigned char)buf[i++]); 376 | ASSERT_INT(0x30, (unsigned char)buf[i++]); 377 | ASSERT_INT(0x40, (unsigned char)buf[i++]); 378 | ASSERT_INT(0x50, (unsigned char)buf[i++]); 379 | ASSERT_INT(0x60, (unsigned char)buf[i++]); 380 | ASSERT_INT(0x70, (unsigned char)buf[i++]); 381 | ASSERT_INT(0x80, (unsigned char)buf[i++]); 382 | // Writer_writeInt64(w, 0xF0E0D0C0B0A09080); // 8 byte 383 | ASSERT_INT(0xF0, (unsigned char)buf[i++]); 384 | ASSERT_INT(0xE0, (unsigned char)buf[i++]); 385 | ASSERT_INT(0xD0, (unsigned char)buf[i++]); 386 | ASSERT_INT(0xC0, (unsigned char)buf[i++]); 387 | ASSERT_INT(0xB0, (unsigned char)buf[i++]); 388 | ASSERT_INT(0xA0, (unsigned char)buf[i++]); 389 | ASSERT_INT(0x90, (unsigned char)buf[i++]); 390 | ASSERT_INT(0x80, (unsigned char)buf[i++]); 391 | // Writer_writeDouble(w, 128.5); // 8 byte // double 128.5 = hex(40 60 10 00 00 00 00 00) 392 | ASSERT_INT(0x40, (unsigned char)buf[i++]); 393 | ASSERT_INT(0x60, (unsigned char)buf[i++]); 394 | ASSERT_INT(0x10, (unsigned char)buf[i++]); 395 | ASSERT_INT(0x00, (unsigned char)buf[i++]); 396 | ASSERT_INT(0x00, (unsigned char)buf[i++]); 397 | ASSERT_INT(0x00, (unsigned char)buf[i++]); 398 | ASSERT_INT(0x00, (unsigned char)buf[i++]); 399 | ASSERT_INT(0x00, (unsigned char)buf[i++]); 400 | // Writer_writeString(w, "Alice"); // 4 byte + 5 data + 1 null-termination 401 | ASSERT_INT(0x00, (unsigned char)buf[i++]); 402 | ASSERT_INT(0x00, (unsigned char)buf[i++]); 403 | ASSERT_INT(0x00, (unsigned char)buf[i++]); 404 | ASSERT_INT(0x06, (unsigned char)buf[i++]); 405 | ASSERT_INT('A', (unsigned char)buf[i++]); 406 | ASSERT_INT('l', (unsigned char)buf[i++]); 407 | ASSERT_INT('i', (unsigned char)buf[i++]); 408 | ASSERT_INT('c', (unsigned char)buf[i++]); 409 | ASSERT_INT('e', (unsigned char)buf[i++]); 410 | ASSERT_INT('\0', (unsigned char)buf[i++]); 411 | // Writer_writeBlob(w, "12345678", 8); // 4 byte + 8 byte data 412 | ASSERT_INT(0x00, (unsigned char)buf[i++]); 413 | ASSERT_INT(0x00, (unsigned char)buf[i++]); 414 | ASSERT_INT(0x00, (unsigned char)buf[i++]); 415 | ASSERT_INT(0x08, (unsigned char)buf[i++]); 416 | ASSERT_INT('1', (unsigned char)buf[i++]); 417 | ASSERT_INT('2', (unsigned char)buf[i++]); 418 | ASSERT_INT('3', (unsigned char)buf[i++]); 419 | ASSERT_INT('4', (unsigned char)buf[i++]); 420 | ASSERT_INT('5', (unsigned char)buf[i++]); 421 | ASSERT_INT('6', (unsigned char)buf[i++]); 422 | ASSERT_INT('7', (unsigned char)buf[i++]); 423 | ASSERT_INT('8', (unsigned char)buf[i++]); 424 | ASSERT_INT(57,i); 425 | // read 426 | Reader *r = newMemReader(buf, sizeof(buf)); 427 | ASSERT_INT(0, Reader_readByte(r)); 428 | ASSERT_INT(42, Reader_readByte(r)); 429 | ASSERT_INT(-1, Reader_readByte(r)); 430 | ASSERT_INT(0x10203040, Reader_readInt32(r)); 431 | ASSERT_INT(-253701952, Reader_readInt32(r)); 432 | ASSERT_INT(-253701952, 0xF0E0D0C0); 433 | ASSERT_INT64(0x1020304050607080, Reader_readInt64(r)); 434 | ASSERT_INT64(-1089641583808049024, Reader_readInt64(r)); 435 | ASSERT_INT64(-1089641583808049024, 0xF0E0D0C0B0A09080); 436 | ASSERT_DOUBLE(128.5, Reader_readDouble(r)); 437 | const char *str = Reader_readString(r); // no need to free 438 | ASSERT_STR("Alice", str); 439 | // free 440 | Reader_free(r); 441 | Writer_free(w); 442 | } 443 | 444 | void testIo() { 445 | LOG_INFO0("testIo testWriteAndRead"); 446 | testWriteAndRead(); 447 | } 448 | -------------------------------------------------------------------------------- /lib/db.c: -------------------------------------------------------------------------------- 1 | #include "utl.h" 2 | #include "db.h" 3 | #include "sqlite3.h" 4 | 5 | // class Db 6 | 7 | struct db_s { 8 | sqlite3 *db; 9 | sqlite3_stmt *stmt; // or NULL 10 | BOOL debug; 11 | }; 12 | 13 | Db *newDb(const char *dbname, BOOL debug) { 14 | Db *this = (Db *)memAlloc(sizeof(Db), __FILE__, __LINE__); 15 | this->stmt = NULL; 16 | this->debug = debug; 17 | int rc = sqlite3_open(dbname, &(this->db)); 18 | if (this->debug) { 19 | LOG_DEBUG2("sqlite3_open '%s' rc=%d", dbname, rc); 20 | } 21 | if (rc != SQLITE_OK) { 22 | ASSERT_FAIL("sqlite3_open rc=%d (%s), errmsg='%s'", rc, sqlite3_errstr(rc), sqlite3_errmsg(this->db)) 23 | } 24 | ASSERT(this->db); 25 | return this; 26 | } 27 | 28 | void Db_free(Db *this) { 29 | ASSERT(this); 30 | ASSERT(this->db); 31 | int rc = sqlite3_close(this->db); 32 | if (rc != SQLITE_OK) { 33 | LOG_INFO3("sqlite3_close rc=%d (%s), errmsg='%s'", rc, sqlite3_errstr(rc), sqlite3_errmsg(this->db)); 34 | } 35 | memFree(this); 36 | } 37 | 38 | BOOL Db_prepare(Db *this, const char *sql) { 39 | ASSERT(this); 40 | ASSERT(this->db); 41 | ASSERT(!this->stmt); 42 | ASSERT(sql); 43 | int rc = sqlite3_prepare_v2(this->db, sql, -1, &(this->stmt), NULL); 44 | if (this->debug) { 45 | LOG_DEBUG2("sqlite3_prepare_v2 '%s' rc=%d", sql, rc); 46 | } 47 | if (rc != SQLITE_OK) { 48 | LOG_INFO4("sqlite3_prepare_v2 sql='%s', rc=%d (%s), errmsg='%s'", sql, rc, sqlite3_errstr(rc), sqlite3_errmsg(this->db)); 49 | this->stmt = NULL; 50 | return FALSE; 51 | } 52 | return TRUE; 53 | } 54 | 55 | void Db_finalize(Db *this) { 56 | ASSERT(this); 57 | ASSERT(this->db); 58 | if(this->stmt) { 59 | int rc = sqlite3_finalize(this->stmt); 60 | if (this->debug) { 61 | LOG_DEBUG2("sqlite3_finalize rc=%d", rc, rc); 62 | } 63 | if (rc != SQLITE_OK) { 64 | LOG_INFO3("sqlite3_finalize rc=%d (%s), errmsg='%s'", rc, sqlite3_errstr(rc), sqlite3_errmsg(this->db)); 65 | } 66 | this->stmt = NULL; 67 | } 68 | } 69 | 70 | BOOL _bind(Db *this, const Value *params, int nparams) { 71 | BOOL ok = TRUE; 72 | for (int i = 0; i < nparams; i++) { 73 | Value val = params[i]; 74 | switch (val.type) { 75 | case VT_NULL: { 76 | int rc = sqlite3_bind_null(this->stmt, i + 1); 77 | if (this->debug) { 78 | LOG_DEBUG2("sqlite3_bind_null rc=%d", rc, sqlite3_errstr(rc)); 79 | } 80 | if (rc != SQLITE_OK) { 81 | LOG_INFO3("sqlite3_bind_null rc=%d (%s), errmsg='%s'", rc, sqlite3_errstr(rc), sqlite3_errmsg(this->db)); 82 | ok = FALSE; 83 | } 84 | } break; 85 | case VT_INT32: { 86 | int rc = sqlite3_bind_int(this->stmt, i + 1, val.i32); 87 | if (this->debug) { 88 | LOG_DEBUG3("sqlite3_bind_int value=%d, rc=%d (%s)", val.i32, rc, sqlite3_errstr(rc)); 89 | } 90 | if (rc != SQLITE_OK) { 91 | LOG_INFO3("sqlite3_bind_int rc=%d (%s), errmsg='%s'", rc, sqlite3_errstr(rc), sqlite3_errmsg(this->db)); 92 | ok = FALSE; 93 | } 94 | } break; 95 | case VT_INT64: { 96 | int rc = sqlite3_bind_int64(this->stmt, i + 1, val.i64); 97 | if (this->debug) { 98 | LOG_DEBUG3("sqlite3_bind_int64 value=%ld, rc=%d (%s)", val.i64, rc, sqlite3_errstr(rc)); 99 | } 100 | if (rc != SQLITE_OK) { 101 | LOG_INFO3("sqlite3_bind_int64 rc=%d (%s), errmsg='%s'", rc, sqlite3_errstr(rc), sqlite3_errmsg(this->db)); 102 | ok = FALSE; 103 | } 104 | } break; 105 | case VT_DOUBLE: { 106 | int rc = sqlite3_bind_double(this->stmt, i + 1, val.d); 107 | if (this->debug) { 108 | LOG_DEBUG3("sqlite3_bind_double value=%f, rc=%d (%s)", val.d, rc, sqlite3_errstr(rc)); 109 | } 110 | if (rc != SQLITE_OK) { 111 | LOG_INFO3("sqlite3_bind_double rc=%d (%s), errmsg='%s'", rc, sqlite3_errstr(rc), sqlite3_errmsg(this->db)); 112 | ok = FALSE; 113 | } 114 | } break; 115 | case VT_STRING: { 116 | int rc = sqlite3_bind_text(this->stmt, i + 1, val.p, -1, SQLITE_TRANSIENT); 117 | if (this->debug) { 118 | LOG_DEBUG3("sqlite3_bind_text value='%s', rc=%d (%s)", val.p, rc, sqlite3_errstr(rc)); 119 | } 120 | if (rc != SQLITE_OK) { 121 | LOG_INFO3("sqlite3_bind_text rc=%d (%s), errmsg='%s'", rc, sqlite3_errstr(rc), sqlite3_errmsg(this->db)); 122 | ok = FALSE; 123 | } 124 | } break; 125 | case VT_BLOB: { 126 | int rc = sqlite3_bind_blob(this->stmt, i + 1, val.p, val.sz, SQLITE_TRANSIENT); 127 | if (this->debug) { 128 | LOG_DEBUG3("sqlite3_bind_blob value=[%d], rc=%d (%s)", val.sz, rc, sqlite3_errstr(rc)); 129 | } 130 | if (rc != SQLITE_OK) { 131 | LOG_INFO3("sqlite3_bind_blob rc=%d (%s), errmsg='%s'", rc, sqlite3_errstr(rc), sqlite3_errmsg(this->db)); 132 | ok = FALSE; 133 | } 134 | } break; 135 | default: 136 | ASSERT_FAIL("Db_bind: invalid param type %d, i=%d", val.type, i); 137 | } 138 | } 139 | return ok; 140 | } 141 | 142 | BOOL _step(Db *this, BOOL *phasRowOrNull) { 143 | int rc = sqlite3_step(this->stmt); 144 | if (this->debug) { 145 | LOG_DEBUG2("sqlite3_step rc=%d (%s)", rc, sqlite3_errstr(rc)); 146 | } 147 | if (rc != SQLITE_DONE && rc != SQLITE_ROW) { 148 | LOG_INFO3("sqlite3_step rc=%d (%s), errmsg='%s'", rc, sqlite3_errstr(rc), sqlite3_errmsg(this->db)); 149 | return FALSE; 150 | } 151 | if (phasRowOrNull) { 152 | *phasRowOrNull = rc == SQLITE_ROW; 153 | } 154 | return TRUE; 155 | } 156 | 157 | void _reset(Db *this) { 158 | if(this->stmt){ 159 | int rc = sqlite3_reset(this->stmt); 160 | if (this->debug) { 161 | LOG_DEBUG1("sqlite3_reset rc=%d", rc); 162 | } 163 | if (rc != SQLITE_OK) { 164 | LOG_INFO3("sqlite3_reset rc=%d (%s), errmsg='%s'", rc, sqlite3_errstr(rc), sqlite3_errmsg(this->db)); 165 | } 166 | } 167 | } 168 | 169 | BOOL Db_bind(Db *this, const Value *params, int nparams) { 170 | ASSERT(this); 171 | ASSERT(this->db); 172 | if (!this->stmt) { 173 | return FALSE; 174 | } 175 | if (!nparams) { 176 | return TRUE; 177 | } 178 | ASSERT(params); 179 | return _bind(this, params, nparams); 180 | } 181 | 182 | BOOL Db_bind_step_reset(Db *this, const Value *params, int nparams) { 183 | ASSERT(this); 184 | ASSERT(this->db); 185 | if(!this->stmt){ 186 | return FALSE; 187 | } 188 | BOOL ok = _bind(this, params, nparams); 189 | if (ok) { 190 | ok = _step(this, NULL); 191 | _reset(this); 192 | } 193 | return ok; 194 | } 195 | 196 | BOOL _fetch(Db *this, Value *values, int nvalues) { 197 | for (int i = 0; i < nvalues; i++) { 198 | BOOL isNull = sqlite3_column_type(this->stmt, i) == SQLITE_NULL; 199 | if (isNull) { 200 | values[i].type = VT_NULL; 201 | if (this->debug) { 202 | LOG_DEBUG0("sqlite3_column_type = SQLITE_NULL"); 203 | } 204 | } else { 205 | switch (values[i].type) { 206 | case VT_INT32: 207 | values[i].i32 = sqlite3_column_int(this->stmt, i); 208 | if (this->debug) { 209 | LOG_DEBUG1("sqlite3_column_int v=%ld", values[i].i32); 210 | } 211 | break; 212 | case VT_INT64: 213 | values[i].i64 = (int64_t)sqlite3_column_int64(this->stmt, i); 214 | if (this->debug) { 215 | LOG_DEBUG1("sqlite3_column_int64 v=%ld", values[i].i64); 216 | } 217 | break; 218 | case VT_DOUBLE: 219 | values[i].d = sqlite3_column_double(this->stmt, i); 220 | if (this->debug) { 221 | LOG_DEBUG1("sqlite3_column_double v=%f", values[i].d); 222 | } 223 | break; 224 | case VT_STRING: 225 | values[i].p = (const char *)sqlite3_column_text(this->stmt, i); 226 | values[i].sz = sqlite3_column_bytes(this->stmt, i); 227 | if (this->debug) { 228 | LOG_DEBUG2("sqlite3_column_text v=[%d]'%s'", values[i].sz, values[i].p); 229 | } 230 | break; 231 | case VT_BLOB: 232 | values[i].p = (const char *)sqlite3_column_blob(this->stmt, i); 233 | values[i].sz = sqlite3_column_bytes(this->stmt, i); 234 | if (this->debug) { 235 | LOG_DEBUG1("sqlite3_column_blob v=[%d]", values[i].sz); 236 | } 237 | break; 238 | default: 239 | ASSERT_FAIL("_fetch: invalid values[%d].type %d", i, values[i].type); 240 | } 241 | } 242 | } 243 | return TRUE; 244 | } 245 | 246 | BOOL Db_step_fetch(Db *this, BOOL *phasRow, Value *values, int nvalues) { 247 | ASSERT(this); 248 | ASSERT(this->db); 249 | ASSERT(phasRow); 250 | if(!this->stmt){ 251 | return FALSE; 252 | } 253 | BOOL ok = _step(this, phasRow); 254 | if (ok && *phasRow){ 255 | ok = _fetch(this, values, nvalues); 256 | } 257 | return ok; 258 | } 259 | 260 | const char *Db_errmsg(Db *this){ 261 | ASSERT(this); 262 | ASSERT(this->db); 263 | return sqlite3_errmsg(this->db); 264 | } 265 | 266 | // TEST 267 | 268 | static void sq_open(const char *dbname, sqlite3 **pdb) { 269 | int rc = sqlite3_open(dbname, pdb); 270 | ASSERT(rc == SQLITE_OK); 271 | } 272 | 273 | static void sq_close(sqlite3 *db) { 274 | int rc = sqlite3_close(db); 275 | ASSERT(rc == SQLITE_OK); 276 | } 277 | 278 | static void sq_prepare(sqlite3 *db, const char *sql, sqlite3_stmt **pstmt) { 279 | int rc = sqlite3_prepare(db, sql, -1, pstmt, NULL); 280 | ASSERT_INT(SQLITE_OK, rc); 281 | } 282 | 283 | static BOOL sq_step(sqlite3_stmt *stmt) { 284 | // int sqlite3_step(sqlite3_stmt*); 285 | int rc = sqlite3_step(stmt); 286 | ASSERT(rc == SQLITE_ROW || rc == SQLITE_DONE); 287 | return rc == SQLITE_DONE; 288 | } 289 | 290 | static void sq_finalize(sqlite3_stmt *stmt) { 291 | // int sqlite3_finalize(sqlite3_stmt *pStmt); 292 | int rc = sqlite3_finalize(stmt); 293 | ASSERT(rc == SQLITE_OK); 294 | } 295 | 296 | static void sq_exec(sqlite3 *db, const char *sql) { 297 | sqlite3_stmt *stmt; 298 | sq_prepare(db, sql, &stmt); 299 | ASSERT(sq_step(stmt)); 300 | sq_finalize(stmt); 301 | } 302 | 303 | static int sq_queryInt(sqlite3 *db, const char *sql) { 304 | sqlite3_stmt *stmt; 305 | sq_prepare(db, sql, &stmt); 306 | ASSERT(!sq_step(stmt)); 307 | int value = sqlite3_column_int(stmt, 0); 308 | sq_finalize(stmt); 309 | return value; 310 | } 311 | 312 | static void testSqlite() { 313 | sqlite3 *db = NULL; 314 | sq_open(":memory:", &db); 315 | sq_exec(db, "CREATE TABLE users(name TEXT)"); 316 | { 317 | sqlite3_stmt *stmt; 318 | sq_prepare(db, "INSERT INTO users(name) VALUES (?)", &stmt); 319 | { 320 | // int sqlite3_bind_text(sqlite3_stmt*,int paramOneBased ,const char* value,int lenOrNegative, void(*)(void*) freefunc ); 321 | ASSERT(sqlite3_bind_text(stmt, 1, "Alice", -1, SQLITE_STATIC) == SQLITE_OK); 322 | ASSERT(sq_step(stmt)); 323 | } 324 | sq_finalize(stmt); 325 | } 326 | { 327 | sqlite3_stmt *stmt; 328 | sq_prepare(db, "SELECT name FROM users ORDER BY name", &stmt); 329 | ASSERT(!sq_step(stmt)); 330 | ASSERT(sqlite3_column_type(stmt, 0) == SQLITE3_TEXT); 331 | ASSERT_STR("Alice", (const char *)sqlite3_column_text(stmt, 0)); 332 | ASSERT(sqlite3_step(stmt)); 333 | sq_finalize(stmt); 334 | } 335 | sq_close(db); 336 | } 337 | 338 | static void testSqliteStmtCaching() { 339 | const int nrounds = 2; 340 | const int ninserts = 1000; 341 | for (int r = 0; r < nrounds; r++) { 342 | { 343 | clock_t t0 = clock(); 344 | sqlite3 *db = NULL; 345 | sq_open(":memory:", &db); 346 | sq_exec(db, "CREATE TABLE users(id INTEGER PRIMARY KEY NOT NULL)"); 347 | for (int i = 0; i < ninserts; i++) { 348 | sq_exec(db, "BEGIN"); 349 | sqlite3_stmt *stmt; 350 | sq_prepare(db, "INSERT INTO users(id) VALUES (?)", &stmt); 351 | ASSERT_INT(SQLITE_OK, sqlite3_bind_int(stmt, 1, i)); 352 | ASSERT(sq_step(stmt)); 353 | sq_finalize(stmt); 354 | sq_exec(db, "COMMIT"); 355 | } 356 | clock_t t1 = clock(); 357 | LOG_INFO1("testSqliteStmtCaching without caching took %4ld clocks", t1 - t0); 358 | int count = sq_queryInt(db, "SELECT COUNT(*) FROM users"); 359 | ASSERT_INT(ninserts, count); 360 | sq_close(db); 361 | } 362 | { 363 | clock_t t0 = clock(); 364 | sqlite3 *db = NULL; 365 | sq_open(":memory:", &db); 366 | sq_exec(db, "CREATE TABLE users(id INTEGER PRIMARY KEY NOT NULL)"); 367 | sqlite3_stmt *stmtBegin; 368 | sq_prepare(db, "BEGIN", &stmtBegin); 369 | sqlite3_stmt *stmtInsert; 370 | sq_prepare(db, "INSERT INTO users(id) VALUES (?)", &stmtInsert); 371 | sqlite3_stmt *stmtCommit; 372 | sq_prepare(db, "COMMIT", &stmtCommit); 373 | for (int i = 0; i < ninserts; i++) { 374 | // BEGIN 375 | sqlite3_reset(stmtBegin); 376 | ASSERT(sq_step(stmtBegin)); 377 | // INSERT INTO.. 378 | sqlite3_reset(stmtInsert); 379 | ASSERT_INT(SQLITE_OK, sqlite3_bind_int(stmtInsert, 1, i)); 380 | ASSERT(sq_step(stmtInsert)); 381 | // COMMIT 382 | sqlite3_reset(stmtCommit); 383 | ASSERT(sq_step(stmtCommit)); 384 | } 385 | sq_finalize(stmtBegin); 386 | sq_finalize(stmtInsert); 387 | sq_finalize(stmtCommit); 388 | clock_t t1 = clock(); 389 | LOG_INFO1("testSqliteStmtCaching with caching took %4ld clocks", t1 - t0); 390 | int count = sq_queryInt(db, "SELECT COUNT(*) FROM users"); 391 | ASSERT_INT(ninserts, count); 392 | sq_close(db); 393 | } 394 | } 395 | // without caching took 1658 clocks 396 | // with caching took 517 clocks 397 | // without caching took 1521 clocks 398 | // with caching took 512 clocks 399 | // 400 | // Statement caching might seem to be a thing. 401 | // But this is true only for ":memory:" databases. 402 | // As soon as sqlite hits the file system, things look different. 403 | // Real-world tests with production databases have shown average 404 | // performance improvements from 5% to 15% (percent). 405 | } 406 | 407 | static void testMemoryDb() { 408 | Db *db = newDb(":memory:", FALSE); 409 | // CREATE TABLE users 410 | { 411 | ASSERT(Db_prepare(db, "CREATE TABLE users(i INTEGER PRIMARY KEY, d FLOAT, s TEXT, b BLOB)")); 412 | ASSERT(Db_bind_step_reset(db, NULL, 0)); 413 | // ASSERT(Db_step(db, NULL)); 414 | // Db_reset(db); 415 | Db_finalize(db); 416 | } 417 | // INSERT INTO users 418 | { 419 | ASSERT(Db_prepare(db, "INSERT INTO users(i,d,s,b) VALUES(?,?,?,?)")); 420 | // user 1 421 | Value params[] = { 422 | {.type = VT_INT64, .i64 = 1}, 423 | {.type = VT_DOUBLE, .d = 13.14}, 424 | {.type = VT_STRING, .p = "Alice" }, 425 | {.type = VT_BLOB, .p = "aaaaaaaaa_aaaaaaaaa_", .sz=20 }, 426 | }; 427 | ASSERT(Db_bind_step_reset(db, params, 4)); 428 | // user 2 429 | params[0] = (Value) {.type = VT_INT64, .i64 = 2}; 430 | params[1] = (Value) {.type = VT_DOUBLE, .d = 23.14}; 431 | params[2] = (Value) {.type = VT_STRING, .p = "Bob" }; 432 | params[3] = (Value) {.type = VT_BLOB, .p = "bbbbbbbbb_bbbbbbbbb_", .sz=10 }; 433 | ASSERT(Db_bind_step_reset(db, params, 4)); 434 | Db_finalize(db); 435 | } 436 | // SELECT * FROM users 437 | { 438 | ASSERT(Db_prepare(db, "SELECT i,d,s,b FROM users WHERE i>? ORDER BY i")); 439 | { 440 | Value param = {.type = VT_INT64, .i64 = 0}; 441 | ASSERT(Db_bind(db, ¶m, 1)); 442 | } 443 | // row 444 | { 445 | BOOL hasRow; 446 | Value values[] = { 447 | {.type = VT_INT64}, 448 | {.type = VT_DOUBLE}, 449 | {.type = VT_STRING}, 450 | {.type = VT_BLOB}, 451 | }; 452 | // row 453 | ASSERT(Db_step_fetch(db, &hasRow, values, 4)); 454 | ASSERT(hasRow); 455 | ASSERT_INT(VT_INT64, values[0].type); 456 | ASSERT_INT64(1, values[0].i64); 457 | ASSERT_INT(VT_DOUBLE, values[1].type); 458 | ASSERT_DOUBLE(13.14, values[1].d); 459 | ASSERT_INT(VT_STRING, values[2].type); 460 | ASSERT_STR("Alice", values[2].p); 461 | ASSERT_INT(VT_BLOB, values[3].type); 462 | ASSERT_INT(20, values[3].sz); 463 | ASSERT_INT(0, memcmp(values[3].p, "aaaaaaaaa_aaaaaaaaa_", 20)); 464 | // row 465 | ASSERT(Db_step_fetch(db, &hasRow, values, 4)); 466 | ASSERT(hasRow); 467 | ASSERT_INT(VT_INT64, values[0].type); 468 | ASSERT_INT64(2, values[0].i64); 469 | ASSERT_INT(VT_DOUBLE, values[1].type); 470 | ASSERT_DOUBLE(23.14, values[1].d); 471 | ASSERT_INT(VT_STRING, values[2].type); 472 | ASSERT_STR("Bob", values[2].p); 473 | ASSERT_INT(VT_BLOB, values[3].type); 474 | ASSERT_INT(10, values[3].sz); 475 | ASSERT_INT(0, memcmp(values[3].p, "bbbbbbbbb_", 10)); 476 | // no more rows 477 | ASSERT(Db_step_fetch(db, &hasRow, values, 4)); 478 | ASSERT(!hasRow); 479 | } 480 | Db_finalize(db); 481 | } 482 | // done 483 | Db_free(db); 484 | } 485 | 486 | static void testErrors() { 487 | Db *db = newDb(":memory:", TRUE); 488 | // good 489 | ASSERT(Db_prepare(db, "CREATE TABLE users(i INTEGER)")); 490 | ASSERT_STR("not an error", Db_errmsg(db)); 491 | Db_finalize(db); 492 | ASSERT_STR("not an error", Db_errmsg(db)); 493 | // bad 494 | ASSERT(!Db_prepare(db, "CREATE TABLE users with blablbla")); 495 | ASSERT_STR("near \"with\": syntax error", Db_errmsg(db)); 496 | Db_finalize(db); 497 | ASSERT_STR("near \"with\": syntax error", Db_errmsg(db)); 498 | // good 499 | ASSERT(Db_prepare(db, "CREATE TABLE users(i INTEGER)")); 500 | ASSERT_STR("not an error", Db_errmsg(db)); 501 | Db_finalize(db); 502 | ASSERT_STR("not an error", Db_errmsg(db)); 503 | // bad 504 | ASSERT(!Db_prepare(db, "INSERT INTO blabla(i) VALUES(?)")); 505 | ASSERT_STR("no such table: blabla", Db_errmsg(db)); 506 | Db_finalize(db); 507 | ASSERT_STR("no such table: blabla", Db_errmsg(db)); 508 | Db_finalize(db); 509 | ASSERT_STR("no such table: blabla", Db_errmsg(db)); 510 | // 511 | Db_free(db); 512 | } 513 | 514 | void testDb() { 515 | LOG_INFO0("testDb testSqlite"); 516 | testSqlite(); 517 | LOG_INFO0("testDb testSqliteStmtCaching"); 518 | testSqliteStmtCaching(); 519 | LOG_INFO0("testDb testMemoryDb"); 520 | testMemoryDb(); 521 | LOG_INFO0("testDb testErrors"); 522 | testErrors(); 523 | } 524 | -------------------------------------------------------------------------------- /lib/app.c: -------------------------------------------------------------------------------- 1 | #include "utl.h" 2 | #include "io.h" 3 | #include "db.h" 4 | #include "app.h" 5 | 6 | // function codes 7 | 8 | #define FC_EXEC 1 9 | #define FC_QUERY 2 10 | #define FC_QUIT 9 11 | 12 | // class App 13 | 14 | struct app_s { 15 | Db *db; 16 | Reader *r; 17 | Writer *w; 18 | }; 19 | 20 | App *newApp(Db *db, Reader *r, Writer *w) { 21 | ASSERT(db); 22 | ASSERT(r); 23 | ASSERT(w); 24 | App *this = (App *)memAlloc(sizeof(App), __FILE__, __LINE__); 25 | this->db = db; 26 | this->r = r; 27 | this->w = w; 28 | return this; 29 | } 30 | 31 | void App_free(App *this) { 32 | ASSERT(this); 33 | memFree(this); 34 | } 35 | 36 | static void _fcExec(App *this) { 37 | const char *sql = Reader_readString(this->r); 38 | ASSERT(sql); 39 | BOOL ok = Db_prepare(this->db, sql); 40 | int niterations = Reader_readInt32(this->r); 41 | int nparams = Reader_readInt32(this->r); 42 | Value *params = (Value *)memAlloc(nparams * sizeof(Value), __FILE__, __LINE__); 43 | for (int i = 0; i < niterations; i++) { 44 | for (int iparam = 0; iparam < nparams; iparam++) { 45 | params[iparam].type = Reader_readByte(this->r); 46 | switch (params[iparam].type) { 47 | case VT_NULL: 48 | // NULL has no further value 49 | break; 50 | case VT_INT32: 51 | params[iparam].i32 = Reader_readInt32(this->r); 52 | break; 53 | case VT_INT64: 54 | params[iparam].i64 = Reader_readInt64(this->r); 55 | break; 56 | case VT_DOUBLE: 57 | params[iparam].d = Reader_readDouble(this->r); 58 | break; 59 | case VT_STRING: 60 | params[iparam].p = Reader_readString(this->r); // TODO optimize: get string length and pass that to bind function 61 | break; 62 | case VT_BLOB: 63 | params[iparam].p = Reader_readBlob(this->r, &(params[iparam].sz)); 64 | break; 65 | default: 66 | ASSERT_FAIL("_fcExec: invalid params[%d].type = %d", iparam, params[iparam].type); 67 | } 68 | } 69 | if (ok) { 70 | ok = Db_bind_step_reset(this->db, params, nparams); 71 | } 72 | } 73 | memFree(params); 74 | Writer_writeByte(this->w, ok); 75 | if (!ok) { 76 | Writer_writeString(this->w, Db_errmsg(this->db)); 77 | } 78 | Db_finalize(this->db); 79 | } 80 | 81 | static void _fcQuery(App *this) { 82 | const char *sql = Reader_readString(this->r); 83 | BOOL ok = Db_prepare(this->db, sql); 84 | // read and bind parameters 85 | { 86 | int nparams = Reader_readInt32(this->r); 87 | Value *params = (Value *)memAlloc(nparams * sizeof(Value), __FILE__, __LINE__); 88 | for (int iparam = 0; iparam < nparams; iparam++) { 89 | params[iparam].type = Reader_readByte(this->r); 90 | switch (params[iparam].type) { 91 | case VT_NULL: 92 | // NULL has no further value 93 | break; 94 | case VT_INT32: 95 | params[iparam].i32 = Reader_readInt32(this->r); 96 | break; 97 | case VT_INT64: 98 | params[iparam].i64 = Reader_readInt64(this->r); 99 | break; 100 | case VT_DOUBLE: 101 | params[iparam].d = Reader_readDouble(this->r); 102 | break; 103 | case VT_STRING: 104 | params[iparam].p = Reader_readString(this->r); // TODO optimize get string length and pass that to bind function 105 | break; 106 | case VT_BLOB: 107 | params[iparam].p = Reader_readBlob(this->r, ¶ms[iparam].sz); 108 | break; 109 | default: 110 | ASSERT_FAIL("_fcQuery: invalid params[%d].type = %d", iparam, params[iparam].type); 111 | } 112 | } 113 | if (ok) { 114 | ok = Db_bind(this->db, params, nparams); 115 | } 116 | memFree(params); 117 | } 118 | // fetch column values 119 | { 120 | // read column types 121 | int ncols = Reader_readInt32(this->r); 122 | char *coltypes = (char *)memAlloc(ncols, __FILE__, __LINE__); 123 | for (int icol = 0; icol < ncols; icol++) { 124 | coltypes[icol] = Reader_readByte(this->r); 125 | } 126 | Value *values = (Value *)memAlloc(ncols * sizeof(Value), __FILE__, __LINE__); 127 | // fetch all rows 128 | BOOL hasRow = TRUE; 129 | while (ok && hasRow) { 130 | for (int icol = 0; icol < ncols; icol++) { 131 | values[icol].type = coltypes[icol]; 132 | } 133 | ok = Db_step_fetch(this->db, &hasRow, values, ncols); 134 | if (ok && hasRow) { 135 | Writer_writeByte(this->w, 1); // hasRow = TRUE 136 | for (int icol = 0; icol < ncols; icol++) { 137 | Value val = values[icol]; 138 | Writer_writeByte(this->w, val.type); 139 | switch (val.type) { 140 | case VT_NULL: 141 | // no furhter data 142 | break; 143 | case VT_INT32: 144 | Writer_writeInt32(this->w, val.i32); 145 | break; 146 | case VT_INT64: 147 | Writer_writeInt64(this->w, val.i64); 148 | break; 149 | case VT_DOUBLE: 150 | Writer_writeDouble(this->w, val.d); 151 | break; 152 | case VT_STRING: 153 | Writer_writeString(this->w, val.p); 154 | break; 155 | case VT_BLOB: 156 | Writer_writeBlob(this->w, val.p, val.sz); 157 | break; 158 | default: 159 | ASSERT_FAIL("_fcQuery: unknown values[%d].type %d", icol, val.type); 160 | } 161 | } 162 | Writer_markFrame(this->w); 163 | } 164 | } // end while 165 | Writer_writeByte(this->w, 0); // hasRow = FALSE 166 | memFree(values); 167 | memFree(coltypes); 168 | } 169 | Writer_writeByte(this->w, ok); 170 | if (!ok) { 171 | Writer_writeString(this->w, Db_errmsg(this->db)); 172 | } 173 | Db_finalize(this->db); 174 | } 175 | 176 | static void _fcQuit(App *this) { 177 | Writer_writeByte(this->w, TRUE); // ok 178 | } 179 | 180 | BOOL App_step(App *this) { 181 | ASSERT(this); 182 | LOG_DEBUG0("App_step: await request"); 183 | char fc = Reader_readByte(this->r); 184 | BOOL next = TRUE; 185 | switch (fc) { 186 | case FC_EXEC: 187 | LOG_DEBUG0("App_step: FC_EXEC"); 188 | _fcExec(this); 189 | break; 190 | case FC_QUERY: 191 | LOG_DEBUG0("App_step: FC_QUERY"); 192 | _fcQuery(this); 193 | break; 194 | case FC_QUIT: 195 | LOG_DEBUG0("App_step: FC_QUIT"); 196 | _fcQuit(this); 197 | next = FALSE; 198 | break; 199 | default: 200 | ASSERT_FAIL("App_step: unknown function code %d", fc); 201 | break; 202 | } 203 | Writer_flush(this->w); 204 | return next; 205 | } 206 | 207 | // TEST 208 | 209 | static void testBasic() { 210 | // setup 211 | Db *db = newDb(":memory:", FALSE); 212 | char buf[1099]; 213 | Reader *r = newMemReader(buf, sizeof(buf)); 214 | Writer *w = newMemWriter(buf, sizeof(buf)); 215 | App *app = newApp(db, r, w); 216 | { 217 | Writer_writeByte(w, FC_EXEC); 218 | Writer_writeString(w, "CREATE TABLE users(id INTEGER PRIMARY KEY NOT NULL)"); 219 | Writer_writeInt32(w, 1); // 1 iteration 220 | Writer_writeInt32(w, 0); // 0 params per iteration 221 | // 222 | BOOL next = App_step(app); 223 | ASSERT(next); 224 | // 225 | ASSERT_INT(1, Reader_readByte(r)); // ok 226 | } 227 | { 228 | Writer_writeByte(w, FC_EXEC); 229 | Writer_writeString(w, "_this_must_fail_dfidafiaodfiuoadhfoahdf"); 230 | Writer_writeInt32(w, 1); // 1 iteration 231 | Writer_writeInt32(w, 0); // 0 params per iteration 232 | // 233 | BOOL next = App_step(app); 234 | ASSERT(next); 235 | // 236 | ASSERT_INT(0, Reader_readByte(r)); // not ok 237 | const char *p = Reader_readString(r); // errmsg 238 | ASSERT_STR("near \"_this_must_fail_dfidafiaodfiuoadhfoahdf\": syntax error", p); 239 | } 240 | { 241 | Writer_writeByte(w, FC_EXEC); 242 | Writer_writeString(w, "INSERT INTO users(id) VALUES (?)"); 243 | Writer_writeInt32(w, 2); // 2 iterations 244 | Writer_writeInt32(w, 1); // 1 param per iteration 245 | Writer_writeByte(w, VT_INT64); // params[0].type 246 | Writer_writeInt64(w, 1); // params[0].value 247 | Writer_writeByte(w, VT_INT64); // params[1].type 248 | Writer_writeInt64(w, 2); // params[1].value 249 | // 250 | BOOL next = App_step(app); 251 | ASSERT(next); 252 | // -> must be ok 253 | ASSERT(Reader_readByte(r)); // ok 254 | } 255 | { 256 | Writer_writeByte(w, FC_QUERY); 257 | Writer_writeString(w, "SELECT id FROM users WHERE id>? ORDER BY id"); 258 | Writer_writeInt32(w, 1); // 1 param 259 | Writer_writeByte(w, VT_INT64); // param 0 type 260 | Writer_writeInt64(w, 0); // param 0 value 261 | Writer_writeInt32(w, 1); // 1 column 262 | Writer_writeByte(w, VT_INT64); // column 0 type 263 | ASSERT(App_step(app)); 264 | // -> must have result rows 265 | ASSERT(Reader_readByte(r)); // has row 266 | { 267 | ASSERT(Reader_readByte(r)); // has value 268 | ASSERT_INT64(1, Reader_readInt64(r)); // int value 269 | } 270 | ASSERT(Reader_readByte(r)); // has row 271 | { 272 | ASSERT(Reader_readByte(r)); // has value 273 | ASSERT_INT64(2, Reader_readInt64(r)); // int value 274 | } 275 | ASSERT(!Reader_readByte(r)); // no more row 276 | ASSERT(Reader_readByte(r)); // ok 277 | } 278 | { 279 | Writer_writeByte(w, FC_QUIT); 280 | // 281 | BOOL next = App_step(app); 282 | ASSERT(!next); 283 | // FC_QUIT must be ok 284 | ASSERT_INT(1, Reader_readByte(r)); // ok 285 | } 286 | // free 287 | App_free(app); 288 | Writer_free(w); 289 | Reader_free(r); 290 | Db_free(db); 291 | } 292 | 293 | static void testValueTypesAndErrors() { 294 | // setup 295 | Db *db = newDb(":memory:", FALSE); 296 | char buf[4 * 1024]; 297 | memset(buf, 255, sizeof(buf)); 298 | Reader *r = newMemReader(buf, sizeof(buf)); 299 | Writer *w = newMemWriter(buf, sizeof(buf)); 300 | App *app = newApp(db, r, w); 301 | { 302 | Writer_writeByte(w, FC_EXEC); 303 | Writer_writeString(w, "CREATE TABLE users(i INTEGER, d FLOAT, s TEXT, b BLOB, UNIQUE(i))"); 304 | Writer_writeInt32(w, 1); // 1 iteration 305 | Writer_writeInt32(w, 0); // 0 params per iteration 306 | // 307 | ASSERT(App_step(app)); 308 | // 309 | ASSERT_INT(1, Reader_readByte(r)); // ok 310 | } 311 | { 312 | Writer_writeByte(w, FC_EXEC); 313 | Writer_writeString(w, "INSERT INTO users(i,d,s,b) VALUES (?,?,?,?)"); 314 | Writer_writeInt32(w, 2); // 2 iterations 315 | Writer_writeInt32(w, 4); // 4 params per iteration 316 | Writer_writeByte(w, VT_INT64); // iter 0 param 0 type 317 | Writer_writeInt64(w, 1); // iter 0 param 0 value 318 | Writer_writeByte(w, VT_DOUBLE); // iter 0 param 1 type 319 | Writer_writeDouble(w, 1.1); // iter 0 param 1 value 320 | Writer_writeByte(w, VT_STRING); // iter 0 param 2 type 321 | Writer_writeString(w, "Alice"); // iter 0 param 2 value 322 | Writer_writeByte(w, VT_BLOB); // iter 0 param 3 type 323 | Writer_writeBlob(w, "Alice", 6); // iter 0 param 3 value 324 | Writer_writeByte(w, VT_INT64); // iter 1 param 0 type 325 | Writer_writeInt64(w, 2); // iter 1 param 0 value 326 | Writer_writeByte(w, VT_DOUBLE); // iter 1 param 1 type 327 | Writer_writeDouble(w, 2.2); // iter 1 param 1 value 328 | Writer_writeByte(w, VT_STRING); // iter 1 param 2 type 329 | Writer_writeString(w, "Bob"); // iter 1 param 2 value 330 | Writer_writeByte(w, VT_BLOB); // iter 1 param 3 type 331 | Writer_writeBlob(w, "Bob", 4); // iter 1 param 3 value 332 | // 333 | ASSERT(App_step(app)); 334 | // 335 | ASSERT_INT(1, Reader_readByte(r)); // ok 336 | } 337 | { 338 | Writer_writeByte(w, FC_QUERY); 339 | Writer_writeString(w, "SELECT i FROM users WHERE i>? ORDER BY i"); 340 | Writer_writeInt32(w, 1); // 1 param 341 | Writer_writeByte(w, VT_INT64); // params[0].type 342 | Writer_writeInt64(w, 0); // params[0].value 343 | Writer_writeInt32(w, 1); // 1 column 344 | Writer_writeByte(w, VT_INT64); // columns[0].type 345 | // 346 | ASSERT(App_step(app)); 347 | // 348 | ASSERT_INT(1, Reader_readByte(r)); // has row 349 | { 350 | ASSERT_INT(VT_INT64, Reader_readByte(r)); // value type 351 | ASSERT_INT(1, Reader_readInt64(r)); // value 352 | } 353 | ASSERT_INT(1, Reader_readByte(r)); // has row 354 | { 355 | ASSERT_INT(VT_INT64, Reader_readByte(r)); // value type 356 | ASSERT_INT(2, Reader_readInt64(r)); // value 357 | } 358 | ASSERT_INT(0, Reader_readByte(r)); // no more rows 359 | ASSERT_INT(1, Reader_readByte(r)); // ok 360 | } 361 | { 362 | Writer_writeByte(w, FC_QUERY); 363 | Writer_writeString(w, "SELECT i,d,s,b FROM users WHERE i>? ORDER BY i"); 364 | Writer_writeInt32(w, 1); // 1 param 365 | Writer_writeByte(w, VT_INT64); // params[0].type 366 | Writer_writeInt64(w, 0); // params[0].value 367 | Writer_writeInt32(w, 4); // 4 columns 368 | Writer_writeByte(w, VT_INT64); // columns[0].type 369 | Writer_writeByte(w, VT_DOUBLE); // columns[1].type 370 | Writer_writeByte(w, VT_STRING); // columns[2].type 371 | Writer_writeByte(w, VT_BLOB); // columns[3].type 372 | // 373 | ASSERT(App_step(app)); 374 | // 375 | ASSERT(Reader_readByte(r)); // has row 376 | { 377 | ASSERT(Reader_readByte(r)); // has value 378 | ASSERT_INT64(1, Reader_readInt64(r)); // value 379 | ASSERT(Reader_readByte(r)); // has value 380 | ASSERT_DOUBLE(1.1, Reader_readDouble(r)); // double 1.1 381 | ASSERT(Reader_readByte(r)); // has value 382 | const char *str = Reader_readString(r); 383 | ASSERT_STR("Alice", str); 384 | size_t len; 385 | ASSERT(Reader_readByte(r)); // has value 386 | const char *p = Reader_readBlob(r, &len); 387 | ASSERT_INT(6, len); 388 | ASSERT(!memcmp(p, "Alice", 6)); 389 | } 390 | ASSERT(Reader_readByte(r)); // has row 391 | { 392 | ASSERT(Reader_readByte(r)); // has value 393 | ASSERT_INT64(2, Reader_readInt64(r)); 394 | ASSERT(Reader_readByte(r)); // has value 395 | ASSERT_DOUBLE(2.2, Reader_readDouble(r)); // double 2.2 396 | ASSERT(Reader_readByte(r)); // has value 397 | const char *str = Reader_readString(r); 398 | ASSERT_STR("Bob", str); 399 | size_t len; 400 | ASSERT(Reader_readByte(r)); // has value 401 | const char *p = Reader_readBlob(r, &len); 402 | ASSERT_INT(4, len); 403 | ASSERT(!memcmp(p, "Bob", 4)); 404 | } 405 | ASSERT(!Reader_readByte(r)); // no more rows 406 | ASSERT(Reader_readByte(r)); // ok 407 | } 408 | { 409 | // insert NULL values 410 | Writer_writeByte(w, FC_EXEC); 411 | Writer_writeString(w, "INSERT INTO users(i,d,s,b) VALUES (?,?,?,?)"); 412 | Writer_writeInt32(w, 1); // 1 iteration 413 | Writer_writeInt32(w, 4); // 4 params per iteration 414 | Writer_writeByte(w, VT_NULL); // iter0 param0 type 415 | Writer_writeByte(w, VT_NULL); // param1 type 416 | Writer_writeByte(w, VT_NULL); // param2 type 417 | Writer_writeByte(w, VT_NULL); // param3 type 418 | // 419 | ASSERT(App_step(app)); 420 | // 421 | ASSERT(Reader_readByte(r)); // ok 422 | } 423 | { 424 | // select users with NULLs 425 | Writer_writeByte(w, FC_QUERY); 426 | Writer_writeString(w, "SELECT i,d,s,b FROM users ORDER BY i"); 427 | Writer_writeInt32(w, 0); // 0 params 428 | Writer_writeInt32(w, 4); // 4 columns 429 | Writer_writeByte(w, VT_INT64); // columns[0].type 430 | Writer_writeByte(w, VT_DOUBLE); // columns[1].type 431 | Writer_writeByte(w, VT_STRING); // columns[2].type 432 | Writer_writeByte(w, VT_BLOB); // columns[3].type 433 | // 434 | ASSERT(App_step(app)); 435 | // 436 | ASSERT(Reader_readByte(r)); // has row 437 | { 438 | ASSERT(!Reader_readByte(r)); // no value 439 | ASSERT(!Reader_readByte(r)); // no value 440 | ASSERT(!Reader_readByte(r)); // no value 441 | ASSERT(!Reader_readByte(r)); // no value 442 | } 443 | ASSERT_INT(1, Reader_readByte(r)); // has row 444 | { 445 | ASSERT(Reader_readByte(r)); // has value 446 | ASSERT_INT(1, Reader_readInt64(r)); // value 447 | ASSERT(Reader_readByte(r)); // has value 448 | ASSERT_DOUBLE(1.1, Reader_readDouble(r)); // double 1.1 449 | ASSERT(Reader_readByte(r)); // has value 450 | const char *str = Reader_readString(r); 451 | ASSERT_STR("Alice", str); 452 | size_t sz; 453 | ASSERT(Reader_readByte(r)); // has value 454 | const char *p = Reader_readBlob(r, &sz); 455 | ASSERT_INT(6, sz); 456 | ASSERT(memcmp(p, "Alice", 6) == 0); 457 | } 458 | ASSERT_INT(1, Reader_readByte(r)); // has row 459 | { 460 | ASSERT(Reader_readByte(r)); // has value 461 | ASSERT_INT(2, Reader_readInt64(r)); 462 | ASSERT(Reader_readByte(r)); // has value 463 | ASSERT_DOUBLE(2.2, Reader_readDouble(r)); // double 2.2 464 | ASSERT(Reader_readByte(r)); // has value 465 | const char *str = Reader_readString(r); 466 | ASSERT_STR("Bob", str); 467 | size_t sz; 468 | ASSERT(Reader_readByte(r)); // has value 469 | const char *p = Reader_readBlob(r, &sz); 470 | ASSERT_INT(4, sz); 471 | ASSERT(memcmp(p, "Bob", 4) == 0); 472 | } 473 | ASSERT(!Reader_readByte(r)); // no more rows 474 | ASSERT(Reader_readByte(r)); // ok 475 | } 476 | { 477 | // select with NULL params 478 | Writer_writeByte(w, FC_QUERY); 479 | Writer_writeString(w, "SELECT i,d,s,b FROM users WHERE i IS NULL"); 480 | Writer_writeInt32(w, 0); // no params 481 | Writer_writeInt32(w, 4); // 4 columns 482 | Writer_writeByte(w, VT_INT64); // columns[0].type 483 | Writer_writeByte(w, VT_DOUBLE); // columns[1].type 484 | Writer_writeByte(w, VT_STRING); // columns[2].type 485 | Writer_writeByte(w, VT_BLOB); // columns[3].type 486 | // 487 | ASSERT(App_step(app)); 488 | // 489 | ASSERT_INT(1, Reader_readByte(r)); // has row 490 | ASSERT(!Reader_readByte(r)); // no value 491 | ASSERT(!Reader_readByte(r)); // no value 492 | ASSERT(!Reader_readByte(r)); // no value 493 | ASSERT(!Reader_readByte(r)); // no value 494 | ASSERT(!Reader_readByte(r)); // no more rows 495 | ASSERT(Reader_readByte(r)); // ok 496 | } 497 | { 498 | // SELECT WHERE i = 2345 must select no rows 499 | Writer_writeByte(w, FC_QUERY); 500 | Writer_writeString(w, "SELECT i,d,s,b FROM users WHERE i =?"); 501 | Writer_writeInt32(w, 1); // 1 param 502 | Writer_writeByte(w, VT_INT64); // param 0 type 503 | Writer_writeInt64(w, 2345); // param 0 value 504 | Writer_writeInt32(w, 4); // 4 columns 505 | Writer_writeByte(w, VT_INT64); // column 0 type 506 | Writer_writeByte(w, VT_DOUBLE); // column 1 type 507 | Writer_writeByte(w, VT_STRING); // column 2 type 508 | Writer_writeByte(w, VT_BLOB); // column 3 type 509 | // 510 | ASSERT(App_step(app)); 511 | // 512 | ASSERT(!Reader_readByte(r)); // no more rows 513 | ASSERT(Reader_readByte(r)); // ok 514 | } 515 | { 516 | // SELECT WHERE s = "Alice" must select one row 517 | Writer_writeByte(w, FC_QUERY); 518 | Writer_writeString(w, "SELECT i,d,s,b FROM users WHERE s = ?"); 519 | Writer_writeInt32(w, 1); // 1 param 520 | Writer_writeByte(w, VT_STRING); // param 0 type 521 | Writer_writeString(w, "Alice"); // param 0 value 522 | Writer_writeInt32(w, 4); // 4 columns 523 | Writer_writeByte(w, VT_INT64); // column 0 type 524 | Writer_writeByte(w, VT_DOUBLE); // column 1 type 525 | Writer_writeByte(w, VT_STRING); // column 2 type 526 | Writer_writeByte(w, VT_BLOB); // column 3 type 527 | // 528 | ASSERT(App_step(app)); 529 | // 530 | ASSERT_INT(1, Reader_readByte(r)); // has row 531 | ASSERT(Reader_readByte(r)); // has value 0 532 | ASSERT_INT64(1, Reader_readInt64(r)); // value 0 533 | ASSERT(Reader_readByte(r)); // has value 1 534 | ASSERT_DOUBLE(1.1, Reader_readDouble(r)); // value 1 535 | ASSERT(Reader_readByte(r)); // has value 2 536 | const char *str = Reader_readString(r); // value 2 537 | ASSERT_STR("Alice", str); 538 | ASSERT(Reader_readByte(r)); // has value 3 539 | size_t len; 540 | const char *p = Reader_readBlob(r, &len); // value 3 541 | ASSERT_INT(6, len); 542 | ASSERT_INT(0, memcmp(p, "Alice", 6)); 543 | ASSERT(!Reader_readByte(r)); // no more rows 544 | ASSERT(Reader_readByte(r)); // ok 545 | } 546 | { 547 | // SELECT WHERE b = "Bob" must select one row 548 | Writer_writeByte(w, FC_QUERY); 549 | Writer_writeString(w, "SELECT i,d FROM users WHERE b = ?"); 550 | Writer_writeInt32(w, 1); // 1 param 551 | Writer_writeByte(w, VT_BLOB); // param 0 type 552 | Writer_writeBlob(w, "Bob", 4); // param 0 value 553 | Writer_writeInt32(w, 2); // 2 columns 554 | Writer_writeByte(w, VT_INT64); // column 0 type 555 | Writer_writeByte(w, VT_DOUBLE); // column 1 type 556 | // 557 | ASSERT(App_step(app)); 558 | // 559 | ASSERT_INT(1, Reader_readByte(r)); // has row 560 | ASSERT(Reader_readByte(r)); // has value 0 561 | ASSERT_INT64(2, Reader_readInt64(r)); // value 0 562 | ASSERT(Reader_readByte(r)); // has value 1 563 | ASSERT_DOUBLE(2.2, Reader_readDouble(r)); // value 1 564 | ASSERT(!Reader_readByte(r)); // no more rows 565 | ASSERT(Reader_readByte(r)); // ok 566 | } 567 | { 568 | Writer_writeByte(w, FC_EXEC); 569 | Writer_writeString(w, "INSERT INTO unknown_table(a) VALUES (?)"); 570 | Writer_writeInt32(w, 1); // 1 iterations 571 | Writer_writeInt32(w, 1); // 1 params per iteration 572 | Writer_writeByte(w, VT_INT64); // iter 0 param 0 type 573 | Writer_writeInt64(w, 1); // iter 0 param 0 value 574 | // 575 | ASSERT(App_step(app)); 576 | // 577 | ASSERT(!Reader_readByte(r)); // not ok 578 | const char *str = Reader_readString(r); // errmsg 579 | ASSERT_STR("no such table: unknown_table", str); 580 | } 581 | { 582 | Writer_writeByte(w, FC_EXEC); 583 | Writer_writeString(w, "INSERT INTO users(i,d,s,b) VALUES (1,1,NULL,NULL)"); 584 | Writer_writeInt32(w, 1); // 1 iterations 585 | Writer_writeInt32(w, 0); // 0 params per iteration 586 | // 587 | ASSERT(App_step(app)); 588 | // 589 | ASSERT(!Reader_readByte(r)); // not ok 590 | const char *str = Reader_readString(r); // errmsg 591 | ASSERT_STR("UNIQUE constraint failed: users.i", str); 592 | } 593 | { 594 | Writer_writeByte(w, FC_EXEC); 595 | Writer_writeString(w, "INSERT INTO users(i,d,s,b) VALUES (3,3,NULL,NULL,NULL,NULL,NULL)"); 596 | Writer_writeInt32(w, 1); // 1 iterations 597 | Writer_writeInt32(w, 0); // 0 params per iteration 598 | // 599 | ASSERT(App_step(app)); 600 | // 601 | ASSERT(!Reader_readByte(r)); // not ok 602 | const char *msg = Reader_readString(r); // errmsg 603 | ASSERT_STR("7 values for 4 columns", msg); 604 | } 605 | { 606 | Writer_writeByte(w, FC_EXEC); 607 | Writer_writeString(w, "INSERT INTO users(i,d,s,b) VALUES (3,3)"); 608 | Writer_writeInt32(w, 1); // 1 iterations 609 | Writer_writeInt32(w, 0); // 0 params per iteration 610 | // 611 | ASSERT(App_step(app)); 612 | // 613 | ASSERT(!Reader_readByte(r)); // not ok 614 | const char *msg = Reader_readString(r); // errmsg 615 | ASSERT_STR("2 values for 4 columns", msg); 616 | } 617 | { 618 | Writer_writeByte(w, FC_EXEC); 619 | Writer_writeString(w, "INSERT INTO users(a,b) VALUES (1,1)"); 620 | Writer_writeInt32(w, 1); // 1 iterations 621 | Writer_writeInt32(w, 0); // 0 params per iteration 622 | // 623 | ASSERT(App_step(app)); 624 | // 625 | ASSERT(!Reader_readByte(r)); // not ok 626 | const char *msg = Reader_readString(r); // errmsg 627 | ASSERT_STR("table users has no column named a", msg); 628 | } 629 | { 630 | Writer_writeByte(w, FC_QUIT); 631 | // 632 | ASSERT(!App_step(app)); 633 | // 634 | ASSERT_INT(1, Reader_readByte(r)); // ok 635 | } 636 | // free 637 | App_free(app); 638 | Writer_free(w); 639 | Reader_free(r); 640 | Db_free(db); 641 | } 642 | 643 | void testApp() { 644 | LOG_INFO0("testApp testBasic"); 645 | testBasic(); 646 | LOG_INFO0("testApp testValueTypesAndErrors"); 647 | testValueTypesAndErrors(); 648 | } 649 | --------------------------------------------------------------------------------