/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* ** Copyright (c) 2013 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** This file houses the db-related v2 APIs. */ #include "fossil/fossil2.h" #include "fsl_internal.h" #include #include /* NULL on linux */ #include /* time() */ /* Only for debugging */ #define MARKER(pfexp) \ do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \ printf pfexp; \ } while(0) #if 0 /* ** ** Helper which sets fsl_err_set() to some string ** pulled from the db connection. dbCode should be ** the result code which came from an error'd ** sqlite API call. */ static int fsl_err_repo_db( fsl_ctx * f, int dbCode ){ assert(f && f->dbRepo.dbh); return fsl_err_set(f, FSL_RC_DB, "Db error #%d: %s", dbCode, sqlite3_errmsg(f->dbRepo.dbh)); } #endif /* ** Resets db->error state based on the given code and the current ** error string from the driver. Returns FSL_RC_DB on success, some ** other non-0 value on error (most likely FSL_RC_OOM while allocating ** the error string). */ static int fsl_err_from_db( fsl_db * db, int dbCode ){ int rc; assert(db && db->dbh); db->error.msg.used =0 ; rc = fsl_buffer_appendf(&db->error.msg, "Db error #%d: %s", dbCode, sqlite3_errmsg(db->dbh)); return rc ? rc : FSL_RC_DB; } int fsl_db_close( fsl_db * db ){ if(!db) return FSL_RC_MISUSE; else{ if(db->dbh){ sqlite3_close(db->dbh) /* ignoring result in the style of "destructors may not throw". */; } if(db->filename.mem){ fsl_buffer_reserve(&db->filename, 0); } assert(0==db->openStatementCount); *db = fsl_db_empty; return 0; } } int fsl_repo_close( fsl_ctx * f ){ if(!f) return FSL_RC_MISUSE; else if(!f->dbRepo.dbh) return FSL_RC_NOT_A_REPO; else return fsl_db_close( &f->dbRepo ); } /* ** This file contains the fsl_db_xxx() and fsl_stmt_xxx() ** parts of the API. */ int fsl_stmt_preparev( fsl_db *db, fsl_stmt * tgt, char const * sql, va_list args ){ if(!db || !db->dbh || !tgt || !sql) return FSL_RC_MISUSE; else if(!*sql) return FSL_RC_RANGE; else if(tgt->stmt) return FSL_RC_ALREADY_EXISTS; else{ int rc; fsl_buffer buf = fsl_buffer_empty; fsl_stmt_t * liteStmt = NULL; fsl_ctx * f = db->f; rc = fsl_buffer_appendfv( &buf, sql, args ); if(!rc){ sql = fsl_buffer_cstr(&buf); rc = sqlite3_prepare_v2(db->dbh, sql, (int)buf.used, &liteStmt, 0); if(rc){ rc = fsl_err_set(f, rc, "Db statement preparation failed. " "SQL: [%.*s]. Error #%d: %s\n", (int)buf.used, sql, rc, sqlite3_errmsg(db->dbh)); } } if(!rc){ ++db->openStatementCount; tgt->stmt = liteStmt; tgt->db = db; tgt->sql = buf /*transfer ownership*/; tgt->colCount = sqlite3_column_count(tgt->stmt); tgt->paramCount = sqlite3_bind_parameter_count(tgt->stmt); }else{ assert(!liteStmt); fsl_buffer_reserve(&buf, 0); } return rc; } } int fsl_stmt_prepare( fsl_db *db, fsl_stmt * tgt, char const * sql, ... ){ if(!db || !db->dbh || !tgt || !sql) return FSL_RC_MISUSE; else if(!*sql) return FSL_RC_RANGE; else if(!tgt->stmt) return FSL_RC_ALREADY_EXISTS; else{ int rc; va_list args; va_start(args,sql); rc = fsl_stmt_preparev( db, tgt, sql, args ); va_end(args); return rc; } } int fsl_repo_prepare( fsl_ctx *f, fsl_stmt * tgt, char const * sql, ... ){ if(!f) return FSL_RC_MISUSE; else if(!f->dbRepo.dbh) return FSL_RC_NOT_A_REPO; else{ int rc; va_list args; va_start(args,sql); rc = fsl_stmt_preparev( &f->dbRepo, tgt, sql, args ); va_end(args); return rc; } } int fsl_repo_preparev( fsl_ctx *f, fsl_stmt * tgt, char const * sql, va_list args ){ if(!f) return FSL_RC_MISUSE; else if(!f->dbRepo.dbh) return FSL_RC_NOT_A_REPO; else return fsl_stmt_preparev(&f->dbRepo, tgt, sql, args); } int fsl_stmt_finalize( fsl_stmt * stmt ){ if(!stmt) return FSL_RC_MISUSE; else{ assert(stmt->stmt); assert(stmt->db); --stmt->db->openStatementCount; fsl_buffer_reserve(&stmt->sql, 0); sqlite3_finalize( stmt->stmt ); *stmt = fsl_stmt_empty; return 0; } } fsl_step_t fsl_stmt_step( fsl_stmt * stmt ){ if(!stmt || !stmt->stmt) return FSL_RC_MISUSE; else{ int const rc = sqlite3_step(stmt->stmt); assert(stmt->db); switch( rc ){ case SQLITE_ROW: return FSL_STEP_ROW; case SQLITE_DONE: return FSL_STEP_DONE; default: stmt->db->error.code = FSL_RC_DB; fsl_buffer_appendf(&stmt->db->error.msg, "Db error #%d: %s", rc, sqlite3_errmsg(stmt->db->dbh)); return FSL_STEP_ERROR; } } } int fsl_stmt_each( fsl_stmt * stmt, fsl_stmt_each_f callback, void * callbackState ){ if(!stmt || !callback) return FSL_RC_MISUSE; else{ fsl_step_t strc; int rc = 0; char doBreak = 0; while( !doBreak && (FSL_STEP_ROW == (strc=fsl_stmt_step(stmt)))){ rc = callback( stmt, callbackState ); switch(rc){ case 0: continue; case FSL_RC_BREAK: rc = 0; /* fall through */ default: doBreak = 1; break; } } return rc ? rc : ((FSL_STEP_ERROR==strc) ? FSL_RC_DB : 0); } } int fsl_stmt_reset( fsl_stmt * stmt ){ if(!stmt || !stmt->stmt) return FSL_RC_MISUSE; else{ int const rc = sqlite3_reset(stmt->stmt); assert(stmt->db); return rc ? fsl_err_from_db(stmt->db, rc) : 0; } } int fsl_stmt_col_count( fsl_stmt const * stmt ){ return (!stmt || !stmt->stmt) ? -1 : stmt->colCount ; } int fsl_stmt_param_count( fsl_stmt const * stmt ){ return (!stmt || !stmt->stmt) ? -1 : stmt->paramCount; } #define BIND_PARAM_CHECK \ if(!stmt || !stmt->stmt || !stmt->db) return FSL_RC_MISUSE; else int fsl_stmt_bind_null( fsl_stmt * stmt, int ndx ){ BIND_PARAM_CHECK { int const rc = sqlite3_bind_null( stmt->stmt, ndx ); return rc ? fsl_err_from_db(stmt->db, rc) : 0; } } int fsl_stmt_bind_int32( fsl_stmt * stmt, int ndx, fsl_int32_t v ){ BIND_PARAM_CHECK { int const rc = sqlite3_bind_int( stmt->stmt, ndx, (int)v ); return rc ? fsl_err_from_db(stmt->db, rc) : 0; } } int fsl_stmt_bind_int64( fsl_stmt * stmt, int ndx, fsl_int64_t v ){ BIND_PARAM_CHECK { int const rc = sqlite3_bind_int64( stmt->stmt, ndx, (sqlite3_int64)v ); return rc ? fsl_err_from_db(stmt->db, rc) : 0; } } int fsl_stmt_bind_double( fsl_stmt * stmt, int ndx, fsl_double_t v ){ BIND_PARAM_CHECK { int const rc = sqlite3_bind_double( stmt->stmt, ndx, (double)v ); return rc ? fsl_err_from_db(stmt->db, rc) : 0; } } #undef BIND_PARAM_CHECK #define GET_CHECK if(!stmt || !stmt->colCount) return FSL_RC_MISUSE; \ else if((ndx<0) || (ndx>=stmt->colCount)) return FSL_RC_RANGE int fsl_stmt_get_int32( fsl_stmt * stmt, int ndx, fsl_int32_t * v ){ GET_CHECK; else { if(v) *v = (fsl_int32_t)sqlite3_column_int(stmt->stmt, ndx); return 0; } } int fsl_stmt_get_int64( fsl_stmt * stmt, int ndx, fsl_int64_t * v ){ GET_CHECK; else { if(v) *v = (fsl_int64_t)sqlite3_column_int64(stmt->stmt, ndx); return 0; } } int fsl_stmt_get_double( fsl_stmt * stmt, int ndx, fsl_double_t * v ){ GET_CHECK; else { if(v) *v = (fsl_double_t)sqlite3_column_double(stmt->stmt, ndx); return 0; } } int fsl_stmt_get_text( fsl_stmt * stmt, int ndx, char const **out, fsl_int_t * outLen ){ GET_CHECK; else { unsigned char const * t = (out || outLen) ? sqlite3_column_text(stmt->stmt, ndx) : NULL; if(out) *out = (char const *)t; if(outLen) *outLen = t ? sqlite3_column_bytes(stmt->stmt, ndx) : 0; return 0; } } int fsl_stmt_get_blob( fsl_stmt * stmt, int ndx, void const **out, fsl_int_t * outLen ){ GET_CHECK; else { void const * t = (out || outLen) ? sqlite3_column_blob(stmt->stmt, ndx) : NULL; if(out) *out = (char const *)t; if(outLen) *outLen = t ? sqlite3_column_bytes(stmt->stmt, ndx) : 0; return 0; } } #undef GET_CHECK static void fsl_db_sql_trace(void *fslCtx, const char *zSql){ int const n = fsl_strlen(zSql); static int counter = 0; /* FIXME: in v1 this uses fossil_trace(), but don't have that functionality here yet. */ fsl_outputf((fsl_ctx*)fslCtx, "SQL TRACE #%d: %s%s\n", ++counter, zSql, (n>0 && zSql[n-1]==';') ? "" : ";"); } /* ** SQL functions for debugging. ** ** The print() function writes its arguments to fsl_output(), but only ** if the -sqlprint command-line option is turned on. */ static void fsl_db_sql_print( sqlite3_context *context, int argc, sqlite3_value **argv ){ fsl_ctx * f = (fsl_ctx*)sqlite3_user_data(context); if( f->debug.printSql ){ int i; for(i=0; i'%s'", FSL_AUX_SCHEMA); } /* ** Returns - if db appears to be a current repository, ** 1 if it appears to have an out of date schema, and ** -1 if it appears to not be a repository. */ int fsl_repo_verify_schema(fsl_db * db){ if(fsl_db_schema_is_outofdate(db)) return 1; else return fsl_db_val_exists(db, "SELECT 1 FROM config " "WHERE name='project-code'") ? 0 : -1; } int fsl_db_open( fsl_db * db, char const * repoDbFile, int openFlags ){ if(!db || !repoDbFile || !*repoDbFile) return FSL_RC_MISUSE; else if(db->dbh) return FSL_RC_MISUSE; else{ fsl_dbh_t * dbh = NULL; int rc = sqlite3_open_v2( repoDbFile, &dbh, SQLITE_OPEN_READWRITE, NULL ); if(rc){ if(dbh){ /* By some complete coincidence, FSL_RC_DB==SQLITE_CANTOPEN. */ fsl_buffer_appendf(&db->error.msg, "Opening db file [%s] failed with " "sqlite code #%d: %s", repoDbFile, rc, sqlite3_errmsg(dbh)); }else{ fsl_buffer_appendf(&db->error.msg, "Opening db file [%s] failed with " "sqlite code #%d", repoDbFile, rc); } rc = FSL_RC_DB; } if(!rc){ rc = fsl_buffer_append(&db->filename, repoDbFile, -1); if(rc){ assert((FSL_RC_OOM==rc) && "That's the only logical explanation."); } } if(rc){ db->error.code = rc; if(dbh){ sqlite3_close(dbh); } }else{ db->dbh = dbh; } return rc; } } int fsl_repo_open_db( fsl_ctx * f, char const * repoDbFile, int openFlags ){ fsl_dbh_t * dbh = NULL; if(!f || !repoDbFile || !*repoDbFile) return FSL_RC_MISUSE; else if(f->dbRepo.dbh) return FSL_RC_MISUSE; if(!(FSL_DB_OPEN_NOT_FOUND_OK & openFlags) && fsl_file_access(repoDbFile, 0)){ return FSL_RC_NOT_FOUND; } else { int rc = sqlite3_open_v2( repoDbFile, &dbh, SQLITE_OPEN_READWRITE, NULL ); if(rc){ if(dbh){ /* By some complete coincidence, FSL_RC_DB==SQLITE_CANTOPEN. */ rc = fsl_err_set(f, FSL_RC_DB, "Opening db file [%s] failed with " "sqlite code #%d: %s", repoDbFile, rc, sqlite3_errmsg(dbh)); }else{ rc = fsl_err_set(f, FSL_RC_DB, "Opening db file [%s] failed with " "sqlite code #%d", repoDbFile, rc); } rc = FSL_RC_DB; goto end; } if( f->debug.traceSql ){ sqlite3_trace(dbh, fsl_db_sql_trace, f); } if(!(FSL_DB_OPEN_NO_SCHEMA_VALIDATE & openFlags)){ int check; fsl_db dbTmp = fsl_db_empty; dbTmp.f = f; dbTmp.dbh = dbh; check = fsl_repo_verify_schema(&dbTmp); if(0 != check){ rc = (check<0) ? fsl_err_set(f, FSL_RC_NOT_A_REPO, "DB file [%s] does not appear to be " "a repository.", repoDbFile) : fsl_err_set( f, FSL_RC_REPO_NEEDS_REBUILD, "DB file [%s] appears to be a fossil " "repsitory, but is out-of-date and needs " "a rebuild.", repoDbFile) ; goto end; } } /* This all comes from db.c:db_open()... */ /* FIXME: check result codes here. */ sqlite3_busy_timeout(dbh, 5000 /* historical value */); sqlite3_wal_autocheckpoint(dbh, 1); /* Set to checkpoint frequently */ sqlite3_exec(dbh, "PRAGMA foreign_keys=OFF;", 0, 0, 0); sqlite3_create_function(dbh, "now", 0, SQLITE_ANY, f, fsl_db_now_function, 0, 0); sqlite3_create_function(dbh, "checkin_mtime", 2, SQLITE_ANY, f, fsl_db_checkin_mtime_function, 0, 0); sqlite3_create_function(dbh, "user", 0, SQLITE_ANY, f, fsl_db_sql_user, 0, 0); if( f->debug.printSql ){ sqlite3_create_function(dbh, "print", -1, SQLITE_UTF8, f, fsl_db_sql_print,0,0); } #if 0 /* functions registered in v1 by db.c:db_open(). */ /* porting cgi() requires access to the HTTP/CGI layer. i.e. this belongs downstream. */ sqlite3_create_function(dbh, "cgi", 1, SQLITE_ANY, 0, db_sql_cgi, 0, 0); sqlite3_create_function(dbh, "cgi", 2, SQLITE_ANY, 0, db_sql_cgi, 0, 0); /* i[sf]_selected() both require access to the array of file names being considered for commit: g.aCommitFile[] */ sqlite3_create_function(dbh, "is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 ); sqlite3_create_function( dbh, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0 ); re_add_sql_func(db) /* Requires the regex bits. */; #endif end: if(!rc){ f->dbRepo.dbh = dbh; f->dbRepo.f = f; rc = fsl_buffer_append(&f->dbRepo.filename, repoDbFile, -1); if(rc){ assert((FSL_RC_OOM==rc) && "That's the only logical explanation."); return rc; } }else{ if(dbh){ sqlite3_close(dbh); } } return rc; } } #undef MARKER