#include "libsql_ext.h" /** * Copyright (c) 2023 Jeremy Hinegardner * All rights reserved. See LICENSE and/or COPYING for details. * * vim: shiftwidth=4 */ VALUE cLS_Database; /* class Libsql::SQLite3::Database */ VALUE cLS_Database_Stat; /* class Libsql::SQLite3::Database::Stat */ /** * Document-method: open * * call-seq: * Libsql::SQLite3::Database.open( filename, flags = READWRITE | CREATE ) -> Database * * Create a new SQLite2 database with a UTF-8 encoding. * */ VALUE libsql_ext_sqlite3_database_open(int argc, VALUE *argv, VALUE class) { VALUE self = libsql_ext_sqlite3_database_alloc(class); VALUE rFlags; VALUE rFilename; int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; char* filename; int rc; libsql_ext_sqlite3* libsql_ext_db; /* at least a filename argument is required */ rb_scan_args( argc, argv, "11", &rFilename, &rFlags ); /* convert flags to the sqlite version */ flags = ( Qnil == rFlags ) ? flags : FIX2INT(rFlags); filename = StringValuePtr(rFilename); /* extract the sqlite3 wrapper struct */ Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); /* open the sqlite3 database */ rc = sqlite3_open_v2( filename, &(libsql_ext_db->db), flags, 0); if ( SQLITE_OK != rc ) { rb_raise(eLS_Error, "Failure to open database %s : [SQLITE_ERROR %d] : %s\n", filename, rc, sqlite3_errmsg(libsql_ext_db->db)); } /* by default turn on the extended result codes */ rc = sqlite3_extended_result_codes( libsql_ext_db->db, 1); if ( SQLITE_OK != rc ) { rb_raise(eLS_Error, "Failure to set extended result codes %s : [SQLITE_ERROR %d] : %s\n", filename, rc, sqlite3_errmsg(libsql_ext_db->db)); } return self; } /** * call-seq: * Libsql::SQLite3::Database.open16( filename ) -> SQLite3::Database * * Create a new SQLite3 database with a UTF-16 encoding * */ VALUE libsql_ext_sqlite3_database_open16(VALUE class, VALUE rFilename) { VALUE self = libsql_ext_sqlite3_database_alloc(class); char* filename = StringValuePtr(rFilename); libsql_ext_sqlite3* libsql_ext_db; int rc; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); rc = sqlite3_open16( filename, &(libsql_ext_db->db) ); if ( SQLITE_OK != rc ) { rb_raise(eLS_Error, "Failure to open UTF-16 database %s : [SQLITE_ERROR %d] : %s\n", filename, rc, sqlite3_errmsg( libsql_ext_db->db )); } /* by default turn on the extended result codes */ rc = sqlite3_extended_result_codes( libsql_ext_db->db, 1); if ( SQLITE_OK != rc ) { rb_raise(eLS_Error, "Failure to set extended result codes on UTF-16 database %s : [SQLITE_ERROR %d] : %s\n", filename, rc, (char*)sqlite3_errmsg16(libsql_ext_db->db)); } return self; } /** * call-seq: * database.close * * Close the database */ VALUE libsql_ext_sqlite3_database_close(VALUE self) { libsql_ext_sqlite3 *libsql_ext_db; int rc = 0; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); rc = sqlite3_close( libsql_ext_db->db ); libsql_ext_db->db = NULL; if ( SQLITE_OK != rc ) { rb_raise(eLS_Error, "Failure to close database : [SQLITE_ERROR %d] : %s\n", rc, sqlite3_errmsg( libsql_ext_db->db )); } return self; } /** * call-seq: * database.last_insert_rowid -> Integer * * Return the rowid of the last row inserted into the database from this * database connection. */ VALUE libsql_ext_sqlite3_database_last_insert_rowid(VALUE self) { libsql_ext_sqlite3 *libsql_ext_db; sqlite3_int64 last_id; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); last_id = sqlite3_last_insert_rowid( libsql_ext_db->db ); return SQLINT64_2NUM( last_id ); } /** * call-seq: * database.autocommit? -> true or false * * return true if the database is in autocommit mode, otherwise return false * */ VALUE libsql_ext_sqlite3_database_is_autocommit(VALUE self) { libsql_ext_sqlite3 *libsql_ext_db; int rc; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); rc = sqlite3_get_autocommit( libsql_ext_db->db ); return ( 0 == rc ) ? Qfalse : Qtrue ; } /** * call-seq: * database.row_changes -> Integer * * return the number of rows changed with the most recent INSERT, UPDATE or * DELETE statement. * */ VALUE libsql_ext_sqlite3_database_row_changes(VALUE self) { libsql_ext_sqlite3 *libsql_ext_db; int rc; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); rc = sqlite3_changes( libsql_ext_db->db ); return INT2FIX(rc); } /** * call-seq: * database.last_error_code -> Integer * * return the last error code that happened in the database * */ VALUE libsql_ext_sqlite3_database_last_error_code(VALUE self) { libsql_ext_sqlite3 *libsql_ext_db; int code; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); code = sqlite3_errcode( libsql_ext_db->db ); return INT2FIX( code ); } /** * call-seq: * database.last_error_message -> String * * return the last error message that happened in the database * */ VALUE libsql_ext_sqlite3_database_last_error_message(VALUE self) { libsql_ext_sqlite3 *libsql_ext_db; const char *message; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); message = sqlite3_errmsg( libsql_ext_db->db ); return rb_str_new2( message ); } /** * call-seq: * database.total_changes -> Integer * * return the number of rows changed by INSERT, UPDATE or DELETE statements * in the database connection since the connection was opened. * */ VALUE libsql_ext_sqlite3_database_total_changes(VALUE self) { libsql_ext_sqlite3 *libsql_ext_db; int rc; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); rc = sqlite3_total_changes( libsql_ext_db->db ); return INT2FIX(rc); } /* * call-seq: * stat.update!( reset = false ) -> nil * * Populates the _@current_ and _@higwater_ instance variables of the given * Database Stat object with the values from the sqlite3_db_status call. * If reset it true then the highwater mark for the stat is reset * */ VALUE libsql_ext_sqlite3_database_stat_update_bang( int argc, VALUE *argv, VALUE self ) { int current = -1; int highwater = -1; int reset_flag = 0; int status_op = FIX2INT( rb_iv_get( self, "@code" ) ); int rc; libsql_ext_sqlite3 *libsql_ext_db; VALUE reset = Qfalse; VALUE db = rb_iv_get( self, "@api_db" ); Data_Get_Struct(db, libsql_ext_sqlite3, libsql_ext_db); if ( argc > 0 ) { reset = argv[0]; reset_flag = ( Qtrue == reset ) ? 1 : 0 ; } rc = sqlite3_db_status( libsql_ext_db->db, status_op, ¤t, &highwater, reset_flag ); if ( SQLITE_OK != rc ) { VALUE n = rb_iv_get( self, "@name"); char* name = StringValuePtr( n ); rb_raise(eLS_Error, "Failure to retrieve database status for %s : [SQLITE_ERROR %d] \n", name, rc); } rb_iv_set( self, "@current", INT2NUM( current ) ); rb_iv_set( self, "@highwater", INT2NUM( highwater) ); return Qnil; } /** * call-seq: * database.prepare( sql ) -> SQLite3::Statement * * Create a new SQLite3 statement. */ VALUE libsql_ext_sqlite3_database_prepare(VALUE self, VALUE rSQL) { VALUE sql = StringValue( rSQL ); VALUE stmt = libsql_ext_sqlite3_statement_alloc(cLS_Statement); libsql_ext_sqlite3 *libsql_ext_db; libsql_ext_sqlite3_stmt *libsql_ext_stmt; const char *tail; int rc; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); Data_Get_Struct(stmt, libsql_ext_sqlite3_stmt, libsql_ext_stmt); rc = sqlite3_prepare_v2( libsql_ext_db->db, RSTRING_PTR(sql), (int)RSTRING_LEN(sql), &(libsql_ext_stmt->stmt), &tail); if ( SQLITE_OK != rc) { rb_raise(eLS_Error, "Failure to prepare statement %s : [SQLITE_ERROR %d] : %s\n", RSTRING_PTR(sql), rc, sqlite3_errmsg(libsql_ext_db->db)); libsql_ext_sqlite3_statement_free( libsql_ext_stmt ); } if ( tail != NULL ) { libsql_ext_stmt->remaining_sql = rb_str_new2( tail ); rb_gc_register_address( &(libsql_ext_stmt->remaining_sql) ); } else { libsql_ext_stmt->remaining_sql = Qnil; } return stmt; } /** * call-seqL * database.execute_batch( sqls ) -> Boolean * * Execute the statements in a batch. */ VALUE libsql_ext_sqlite3_database_exec(VALUE self, VALUE rSQL) { VALUE sql = StringValue( rSQL ); libsql_ext_sqlite3 *libsql_ext_db; int rc; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); rc = sqlite3_exec( libsql_ext_db->db, RSTRING_PTR(sql), NULL, NULL, NULL ); if ( SQLITE_OK != rc ){ rb_raise( eLS_Error, "Failed to execute bulk statements: [SQLITE_ERROR %d] : %s\n", rc, sqlite3_errmsg(libsql_ext_db->db)); } /* Presume that nobody will want to batch execute more than a Fixnum's worth of statements */ return Qtrue; } /** * This function is registered with a sqlite3 database using the * sqlite3_trace_v2 function. During the registration process a handle on a * VALUE is also registered. * * When this function is called, it calls the 'trace' method on the tap object, * which is the VALUE that was registered during the sqlite3_trace_v2 call. * * This function corresponds to the SQLite xCallback function specification. * * https://sqlite.org/c3ref/c_trace.html * */ int libsql_ext_xTraceCallback(unsigned trace_type, void* tap, void* prepared_statement, void* extra) { VALUE trace_obj = (VALUE) tap; char* msg; sqlite3_uint64 time; switch(trace_type) { case SQLITE_TRACE_STMT: msg = (char*)extra; /* The callback can compute the same text that would have been returned by the * legacy sqlite3_trace() interface by using the X argument when X begins with * "--" and invoking sqlite3_expanded_sql(P) otherwise. */ if (0 != strncmp(msg, "--", 2)) { msg = sqlite3_expanded_sql(prepared_statement); } rb_funcall( trace_obj, rb_intern("trace"), 1, rb_str_new2( msg ) ); break; case SQLITE_TRACE_PROFILE: msg = sqlite3_expanded_sql(prepared_statement); time = *(sqlite3_uint64*)extra; rb_funcall( trace_obj, rb_intern("profile"), 2, rb_str_new2( msg ), SQLUINT64_2NUM(time) ); break; case SQLITE_TRACE_ROW: /* not implemented */ break; case SQLITE_TRACE_CLOSE: /* not implemented */ break; default: /* nothing */ break; } return 0; } /** * call-seq: * database.register_trace_tap( tap_obj ) * * This registers an object to be called for all the trace objects in the sqlite * system. From an SQLite perspective, the trace object is registered for both * SQLITE_TRACE_STMT and SQLITE_TRACE_PROFILE. * * The object must respond to both `trace` and `profile` methods. See * Libsql::Trace:: * This is an experimental api and is subject to change, or removal. * */ VALUE libsql_ext_sqlite3_database_register_trace_tap(VALUE self, VALUE tap ) { libsql_ext_sqlite3 *libsql_ext_db; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); /* Qnil, unregister the item and tell the garbage collector we are done with * it. */ if ( Qnil == tap ) { sqlite3_trace_v2( libsql_ext_db->db, 0, NULL, NULL ); rb_gc_unregister_address( &(libsql_ext_db->trace_obj) ); libsql_ext_db->trace_obj = Qnil; /* register the item and store the reference to the object in the libsql_ext_db * structure. We also have to tell the Ruby garbage collector that we * point to the Ruby object from C. */ } else { libsql_ext_db->trace_obj = tap; rb_gc_register_address( &(libsql_ext_db->trace_obj) ); sqlite3_trace_v2( libsql_ext_db->db, SQLITE_TRACE_STMT | SQLITE_TRACE_PROFILE, libsql_ext_xTraceCallback, (void *)libsql_ext_db->trace_obj ); } return Qnil; } /** * invoke a ruby function. This is here to be used by rb_protect. */ VALUE libsql_ext_wrap_funcall2( VALUE arg ) { libsql_ext_protected_t *protected = (libsql_ext_protected_t*) arg; return rb_funcall2( protected->instance, protected->method, protected->argc, protected->argv ); } /** * Set the context result on the sqlite3_context based upon the ruby VALUE. * This converts the ruby value to the appropriate C-type and makes the * appropriate call sqlite3_result_* call */ void libsql_ext_set_context_result( sqlite3_context* context, VALUE result ) { switch( TYPE(result) ) { case T_FIXNUM: case T_BIGNUM: sqlite3_result_int64( context, NUM2SQLINT64(result) ); break; case T_FLOAT: sqlite3_result_double( context, NUM2DBL(result) ); break; case T_NIL: sqlite3_result_null( context ); break; case T_TRUE: sqlite3_result_int64( context, 1); break; case T_FALSE: sqlite3_result_int64( context, 0); break; case T_STRING: sqlite3_result_text( context, RSTRING_PTR(result), (int)RSTRING_LEN(result), NULL); break; default: sqlite3_result_error( context, "Unable to convert ruby object to an SQL function result", -1 ); sqlite3_result_error_code( context, 42 ); break; } return; } /** * Convert from a protected sqlite3_value to a ruby object */ VALUE sqlite3_value_to_ruby_value( sqlite3_value* s_value ) { VALUE rb_value = Qnil; sqlite3_int64 i64; switch( sqlite3_value_type( s_value) ) { case SQLITE_NULL: rb_value = Qnil; break; case SQLITE_INTEGER: i64 = sqlite3_value_int64( s_value); rb_value = SQLINT64_2NUM(i64); break; case SQLITE_FLOAT: rb_value = rb_float_new( sqlite3_value_double( s_value ) ); break; case SQLITE_TEXT: case SQLITE_BLOB: rb_value = rb_str_new2((const char*) sqlite3_value_text( s_value ) ); break; } return rb_value; } /** * the libsql_ext xBusy handler that is used to invoke the ruby function for * doing busy callbacks. * * This function conforms to the xBusy function specification for * sqlite3_busy_handler. */ int libsql_ext_xBusy( void *pArg , int nArg) { VALUE *args = ALLOCA_N( VALUE, 1 ); VALUE result = Qnil; int state; int busy = 1; libsql_ext_protected_t protected; args[0] = INT2FIX(nArg); protected.instance = (VALUE)pArg; protected.method = rb_intern("call"); protected.argc = 1; protected.argv = args; result = rb_protect( libsql_ext_wrap_funcall2, (VALUE)&protected, &state ); if ( state || ( Qnil == result || Qfalse == result ) ){ busy = 0; } return busy; } /** * call-seq: * database.busy_handler( proc_like or nil ) * * register a busy handler. If the argument is nil, then an existing busy * handler is removed. Otherwise the argument is registered as the busy * handler. */ VALUE libsql_ext_sqlite3_database_busy_handler( VALUE self, VALUE handler ) { libsql_ext_sqlite3 *libsql_ext_db; int rc; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); /* Removing a busy handler case, remove it from sqlite and then remove it * from the garbage collector if it existed */ if ( Qnil == handler ) { rc = sqlite3_busy_handler( libsql_ext_db->db, NULL, NULL ); if ( SQLITE_OK != rc ) { rb_raise(eLS_Error, "Failure removing busy handler : [SQLITE_ERROR %d] : %s\n", rc, sqlite3_errmsg( libsql_ext_db->db )); } if ( Qnil != libsql_ext_db->busy_handler_obj ) { rb_gc_unregister_address( &(libsql_ext_db->busy_handler_obj) ); } } else { /* installing a busy handler * - register it with sqlite * - keep a reference for ourselves with our database handle * - registere the handler reference with the garbage collector */ rc = sqlite3_busy_handler( libsql_ext_db->db, libsql_ext_xBusy, (void*)handler ); if ( SQLITE_OK != rc ) { rb_raise(eLS_Error, "Failure setting busy handler : [SQLITE_ERROR %d] : %s\n", rc, sqlite3_errmsg( libsql_ext_db->db )); } libsql_ext_db->busy_handler_obj = handler; rb_gc_register_address( &(libsql_ext_db->busy_handler_obj) ); } return Qnil; } /** * the libsql_ext xProgress handler that is used to invoke the ruby function for * doing progress handler callbacks. * * This function conforms to the xProgress function specification for * sqlite3_progress_handler. */ int libsql_ext_xProgress( void *pArg ) { VALUE result = Qnil; int state; int cancel = 0; libsql_ext_protected_t protected; protected.instance = (VALUE)pArg; protected.method = rb_intern("call"); protected.argc = 0; protected.argv = NULL; result = rb_protect( libsql_ext_wrap_funcall2, (VALUE)&protected, &state ); if ( state || ( Qnil == result || Qfalse == result ) ){ cancel = 1; } return cancel; } /** * call-seq: * database.progress_handler( op_count, proc_like or nil ) * * register a progress handler. If the argument is nil, then an existing * progress handler is removed. Otherwise the argument is registered as the * progress handler. */ VALUE libsql_ext_sqlite3_database_progress_handler( VALUE self, VALUE op_count, VALUE handler ) { libsql_ext_sqlite3 *libsql_ext_db; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); /* Removing a progress handler, remove it from sqlite and then remove it * from the garbage collector if it existed */ if ( Qnil == handler ) { sqlite3_progress_handler( libsql_ext_db->db, -1, NULL, (void*)NULL ); if ( Qnil != libsql_ext_db->progress_handler_obj ) { rb_gc_unregister_address( &(libsql_ext_db->progress_handler_obj) ); } } else { int op_codes = FIX2INT( op_count ); /* installing a progress handler * - register it with sqlite * - keep a reference for ourselves with our database handle * - register the handler reference with the garbage collector */ sqlite3_progress_handler( libsql_ext_db->db, op_codes, libsql_ext_xProgress, (void*)handler ); libsql_ext_db->progress_handler_obj = handler; rb_gc_register_address( &(libsql_ext_db->progress_handler_obj) ); } return Qnil; } /** * the libsql_ext xFunc callback that is used to invoke the ruby function for * doing scalar SQL functions. * * This function conforms to the xFunc function specification for * sqlite3_create_function */ void libsql_ext_xFunc( sqlite3_context* context, int argc, sqlite3_value** argv ) { VALUE *args = ALLOCA_N( VALUE, argc ); VALUE result; int state; int i; libsql_ext_protected_t protected; /* convert each item in argv to a VALUE object based upon its type via * sqlite3_value_type( argv[n] ) */ for( i = 0 ; i < argc ; i++) { args[i] = sqlite3_value_to_ruby_value( argv[i] ); } /* gather all the data to make the protected call */ protected.instance = (VALUE) sqlite3_user_data( context ); protected.method = rb_intern("call"); protected.argc = argc; protected.argv = args; result = rb_protect( libsql_ext_wrap_funcall2, (VALUE)&protected, &state ); /* check the results */ if ( state ) { VALUE msg = ERROR_INFO_MESSAGE(); sqlite3_result_error( context, RSTRING_PTR(msg), (int)RSTRING_LEN(msg) ); } else { libsql_ext_set_context_result( context, result ); } return; } /** * call-seq: * database.define_function( name, proc_like ) * * register the given function to be invoked as an sql function. */ VALUE libsql_ext_sqlite3_database_define_function( VALUE self, VALUE name, VALUE proc_like ) { libsql_ext_sqlite3 *libsql_ext_db; int rc; VALUE arity = rb_funcall( proc_like, rb_intern( "arity" ), 0 ); char* zFunctionName = RSTRING_PTR(name); int nArg = FIX2INT( arity ); Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); rc = sqlite3_create_function( libsql_ext_db->db, zFunctionName, nArg, SQLITE_UTF8, (void *)proc_like, libsql_ext_xFunc, NULL, NULL); if ( SQLITE_OK != rc ) { /* in the case of SQLITE_MISUSE the error message in the database may * not be set. In this case, hardcode the error. * http://sqlite.org/c3ref/errcode.html * * This is a result of 3.6.15 which has sqlite3_create_function return * SQLITE_MISUSE intead of SQLITE_ERROR if called with incorrect * parameters. */ if ( SQLITE_MISUSE == rc ) { rb_raise(eLS_Error, "Failure defining SQL function '%s' with arity '%d' : [SQLITE_ERROR %d] : Library used incorrectly\n", zFunctionName, nArg, rc); } else { rb_raise(eLS_Error, "Failure defining SQL function '%s' with arity '%d' : [SQLITE_ERROR %d] : %s\n", zFunctionName, nArg, rc, sqlite3_errmsg( libsql_ext_db->db )); } } rb_gc_register_address( &proc_like ); return Qnil; } /** * call-seq: * database.remove_function( name, proc_like ) * * remove the given function from availability in SQL. */ VALUE libsql_ext_sqlite3_database_remove_function( VALUE self, VALUE name, VALUE proc_like ) { libsql_ext_sqlite3 *libsql_ext_db; int rc; VALUE arity = rb_funcall( proc_like, rb_intern( "arity" ), 0 ); char* zFunctionName = RSTRING_PTR(name); int nArg = FIX2INT( arity ); Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); rc = sqlite3_create_function( libsql_ext_db->db, zFunctionName, nArg, SQLITE_UTF8, NULL, NULL, NULL, NULL); if ( SQLITE_OK != rc ) { rb_raise(eLS_Error, "Failure removing SQL function '%s' with arity '%d' : [SQLITE_ERROR %d] : %s\n", zFunctionName, nArg, rc, sqlite3_errmsg( libsql_ext_db->db )); } rb_gc_unregister_address( &proc_like ); return Qnil; } /* wrap rb_class_new_instance so it can be called from within an rb_protect */ VALUE libsql_ext_wrap_new_aggregate( VALUE arg ) { return rb_class_new_instance( 0, 0, arg ); } /** * the libsql_ext xStep callback that is used to invoke the ruby method for * doing aggregate step oprations as part of an aggregate SQL function. * * This function conforms to the xStep function specification for * sqlite3_create_function. */ void libsql_ext_xStep( sqlite3_context* context, int argc, sqlite3_value** argv ) { VALUE *args = ALLOCA_N( VALUE, argc ); VALUE result; int state; int i; libsql_ext_protected_t protected; VALUE *aggregate_context = (VALUE*)sqlite3_aggregate_context( context, sizeof( VALUE ) ); if ( 0 == aggregate_context ) { sqlite3_result_error_nomem( context ); return; } /* instantiate an instance of the aggregate function class if the * aggregate context is zero'd out . * * If there is an error in initialization of the aggregate, set the error * context */ if ( *aggregate_context == T_NONE ) { VALUE klass = (VALUE) sqlite3_user_data( context ); result = rb_protect( libsql_ext_wrap_new_aggregate, klass, &state ); /* exception was raised during initialization */ if ( state ) { *aggregate_context = rb_gv_get("$!"); rb_gc_register_address( aggregate_context ); VALUE msg = rb_obj_as_string( *aggregate_context ); sqlite3_result_error( context, RSTRING_PTR(msg), (int)RSTRING_LEN(msg)); return; } else { *aggregate_context = result; /* mark the instance as protected from collection */ rb_gc_register_address( aggregate_context ); rb_iv_set( *aggregate_context, "@_exception", Qnil ); } } /* convert each item in argv to a VALUE object based upon its type via * sqlite3_value_type( argv[n] ) */ for( i = 0 ; i < argc ; i++) { args[i] = sqlite3_value_to_ruby_value( argv[i] ); } /* gather all the data to make the protected call */ protected.instance = *aggregate_context; protected.method = rb_intern("step"); protected.argc = argc; protected.argv = args; result = rb_protect( libsql_ext_wrap_funcall2, (VALUE)&protected, &state ); /* check the results, if there is an error, set the @exception ivar */ if ( state ) { VALUE msg = ERROR_INFO_MESSAGE(); sqlite3_result_error( context, RSTRING_PTR(msg), (int)RSTRING_LEN(msg)); rb_iv_set( *aggregate_context, "@_exception", rb_gv_get("$!" )); } return ; } /** * the libsql_ext xFinal callback that is used to invoke the ruby method for * doing aggregate final operations as part of an aggregate SQL function. * * This function conforms to the xFinal function specification for * sqlite3_create_function. */ void libsql_ext_xFinal( sqlite3_context* context ) { VALUE result; int state; libsql_ext_protected_t protected; VALUE exception = Qnil; VALUE *aggregate_context = (VALUE*)sqlite3_aggregate_context( context, sizeof( VALUE ) ); /** * check and see if an exception had been throw at some point during the * initialization of hte aggregate or during the step function call * */ if (TYPE(*aggregate_context) == T_OBJECT) { /* if there is a @_exception value and it has a value then there was an * exception during step function execution */ if (rb_ivar_defined( *aggregate_context, rb_intern("@_exception") )) { exception = rb_iv_get( *aggregate_context, "@_exception" ); } else { /* if the aggregate context itself is an exception, then there was * an error during teh initialization of the aggregate context */ if (rb_obj_is_kind_of( *aggregate_context, rb_eException )) { exception = *aggregate_context; } } } if ( Qnil == exception ) { /* gather all the data to make the protected call */ protected.instance = *aggregate_context; protected.method = rb_intern("finalize"); protected.argc = 0; protected.argv = NULL; result = rb_protect( libsql_ext_wrap_funcall2, (VALUE)&protected, &state ); /* check the results */ if ( state ) { VALUE msg = ERROR_INFO_MESSAGE(); sqlite3_result_error( context, RSTRING_PTR(msg), (int)RSTRING_LEN(msg) ); } else { libsql_ext_set_context_result( context, result ); } } else { VALUE msg = rb_obj_as_string( exception ); sqlite3_result_error( context, RSTRING_PTR(msg), (int)RSTRING_LEN(msg) ); } /* release the aggregate instance from garbage collector protection */ rb_gc_unregister_address( aggregate_context ); return ; } /** * call-seq: * database.define_aggregate( name, arity, klass ) * * register the given klass to be invoked as an sql aggregate. */ VALUE libsql_ext_sqlite3_database_define_aggregate( VALUE self, VALUE name, VALUE arity, VALUE klass ) { libsql_ext_sqlite3 *libsql_ext_db; int rc; char* zFunctionName = RSTRING_PTR(name); int nArg = FIX2INT( arity ); Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); rc = sqlite3_create_function( libsql_ext_db->db, zFunctionName, nArg, SQLITE_UTF8, (void *)klass, NULL, /* for scalar functions, not used here */ libsql_ext_xStep, libsql_ext_xFinal); if ( SQLITE_OK != rc ) { /* in the case of SQLITE_MISUSE the error message in the database may * not be set. In this case, hardcode the error. * http://sqlite.org/c3ref/errcode.html * * This is a result of 3.6.15 which has sqlite3_create_function return * SQLITE_MISUSE intead of SQLITE_ERROR if called with incorrect * parameters. */ if ( SQLITE_MISUSE == rc ) { rb_raise(eLS_Error, "Failure defining SQL function '%s' with arity '%d' : [SQLITE_ERROR %d] : Library used incorrectly\n", zFunctionName, nArg, rc); } else { rb_raise(eLS_Error, "Failure defining SQL function '%s' with arity '%d' : [SQLITE_ERROR %d] : %s\n", zFunctionName, nArg, rc, sqlite3_errmsg( libsql_ext_db->db )); } } rb_gc_register_address( &klass ); return Qnil; } /** * call-seq: * database.remove_aggregate( name, arity, klass ) * * remove the given klass from availability in SQL as an aggregate. */ VALUE libsql_ext_sqlite3_database_remove_aggregate( VALUE self, VALUE name, VALUE arity, VALUE klass ) { libsql_ext_sqlite3 *libsql_ext_db; int rc; char* zFunctionName = RSTRING_PTR(name); int nArg = FIX2INT( arity ); Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); rc = sqlite3_create_function( libsql_ext_db->db, zFunctionName, nArg, SQLITE_UTF8, NULL, NULL, NULL, NULL); if ( SQLITE_OK != rc ) { rb_raise(eLS_Error, "Failure removing SQL aggregate '%s' with arity '%d' : [SQLITE_ERROR %d] : %s\n", zFunctionName, nArg, rc, sqlite3_errmsg( libsql_ext_db->db )); } rb_gc_unregister_address( &klass ); return Qnil; } /** * call-seq: * database.interrupt! * * Cause another thread with a handle on this database to be interrupted and * return at the earliest opportunity as interrupted. */ VALUE libsql_ext_sqlite3_database_interrupt_bang( VALUE self ) { libsql_ext_sqlite3 *libsql_ext_db; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); sqlite3_interrupt( libsql_ext_db->db ); return Qnil; } /** * call-seq: * database.replicate_to( other_db ) -> other_db * * Replicates the current database to the database passed in using the * sqlite3_backup api * */ VALUE libsql_ext_sqlite3_database_replicate_to( VALUE self, VALUE other ) { libsql_ext_sqlite3 *libsql_ext_src_db; libsql_ext_sqlite3 *libsql_ext_dest_db; sqlite3_backup *backup; sqlite3 *src; sqlite3 *dest; int rc_s; int rc_f; /* source database */ Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_src_db); src = libsql_ext_src_db->db; /* destination database */ Data_Get_Struct(other, libsql_ext_sqlite3, libsql_ext_dest_db); dest = libsql_ext_dest_db->db; backup = sqlite3_backup_init( dest, "main", src, "main" ); if ( NULL == backup ) { rb_raise(eLS_Error, "Failure to initialize replication: [SQLITE_ERROR %d] : %s\n", sqlite3_errcode( dest ), sqlite3_errmsg( dest )); } rc_s = sqlite3_backup_step( backup, -1 ); /* copy the whole thing at once */ rc_f = sqlite3_backup_finish( backup ); /* report the rc_s error if that one is bad, * else raise the rc_f error, or nothing */ if ( SQLITE_DONE != rc_s ) { rb_raise(eLS_Error, "Failure in replication : [SQLITE_ERROR %d] : %s\n", sqlite3_errcode( dest ), sqlite3_errmsg( dest ) ); } else if ( SQLITE_OK != rc_f ) { rb_raise(eLS_Error, "Failure in finishing replication: [SQLITE_ERROR %d] : %s\n", sqlite3_errcode( dest ), sqlite3_errmsg( dest ) ); } return other; } /** * call-seq: * database.table_column_metadata( db_name, table_name, column_name) -> Hash * * Returns a hash containing the meta information about the column. The * available keys are: * * declared_data_type:: the declared data type of the column * collation_sequence_name:: the name of the collation sequence for the column * not_null_constraint:: True if the column has a NOT NULL constraint * primary_key:: True if the column is part of a primary key * auto_increment:: True if the column is AUTO INCREMENT * */ VALUE libsql_ext_sqlite3_database_table_column_metadata(VALUE self, VALUE db_name, VALUE tbl_name, VALUE col_name) { libsql_ext_sqlite3 *libsql_ext_db; int rc; /* input */ const char *zDbName = StringValuePtr( db_name ); const char *zTableName = StringValuePtr( tbl_name ); const char *zColumnName = StringValuePtr( col_name ); /* output */ const char *pzDataType = NULL; const char *pzCollSeq = NULL; int pNotNull, pPrimaryKey, pAutoinc; VALUE rHash = rb_hash_new(); VALUE rStr = Qnil; Data_Get_Struct(self, libsql_ext_sqlite3, libsql_ext_db); rc = sqlite3_table_column_metadata( libsql_ext_db->db, zDbName, zTableName, zColumnName, &pzDataType, &pzCollSeq, &pNotNull, &pPrimaryKey, &pAutoinc); if ( SQLITE_OK != rc ) { rb_raise(eLS_Error, "Failure retrieveing column meta data for table '%s' column '%s' : [SQLITE_ERROR %d] : %s\n", zTableName, zColumnName, rc, sqlite3_errmsg( libsql_ext_db-> db )); } rStr = ( NULL == pzDataType) ? Qnil : rb_str_new2( pzDataType ); rb_hash_aset( rHash, rb_str_new2("declared_data_type"), rStr ); rStr = ( NULL == pzCollSeq) ? Qnil : rb_str_new2( pzCollSeq ); rb_hash_aset( rHash, rb_str_new2("collation_sequence_name"), rStr ); rb_hash_aset( rHash, rb_str_new2("not_null_constraint"), ( pNotNull ? Qtrue : Qfalse )); rb_hash_aset( rHash, rb_str_new2("primary_key"), ( pPrimaryKey ? Qtrue : Qfalse )); rb_hash_aset( rHash, rb_str_new2("auto_increment"), ( pAutoinc ? Qtrue : Qfalse )); return rHash; } /*********************************************************************** * Ruby life cycle methods ***********************************************************************/ /* * garbage collector free method for the libsql_ext_data structure. Make sure to un * register the trace and profile objects if they are not Qnil */ void libsql_ext_sqlite3_database_free(libsql_ext_sqlite3* libsql_ext_db) { if ( Qnil != libsql_ext_db->trace_obj ) { rb_gc_unregister_address( &(libsql_ext_db->trace_obj) ); libsql_ext_db->trace_obj = Qnil; } if ( Qnil != libsql_ext_db->profile_obj) { rb_gc_unregister_address( &(libsql_ext_db->profile_obj) ); libsql_ext_db->profile_obj = Qnil; } if ( Qnil != libsql_ext_db->busy_handler_obj ) { rb_gc_unregister_address( &(libsql_ext_db->busy_handler_obj) ); libsql_ext_db->busy_handler_obj = Qnil; } if ( Qnil != libsql_ext_db->progress_handler_obj ) { rb_gc_unregister_address( &(libsql_ext_db->progress_handler_obj) ); libsql_ext_db->progress_handler_obj = Qnil; } libsql_ext_db->db = NULL; free(libsql_ext_db); return; } /* * allocate the libsql_ext_data structure */ VALUE libsql_ext_sqlite3_database_alloc(VALUE klass) { libsql_ext_sqlite3* libsql_ext_db = ALLOC(libsql_ext_sqlite3); VALUE obj ; libsql_ext_db->trace_obj = Qnil; libsql_ext_db->profile_obj = Qnil; libsql_ext_db->busy_handler_obj = Qnil; libsql_ext_db->progress_handler_obj = Qnil; libsql_ext_db->db = NULL; obj = Data_Wrap_Struct(klass, NULL, libsql_ext_sqlite3_database_free, libsql_ext_db); return obj; } /** * Document-class: Libsql::SQLite3::Database * * The ruby extension wrapper around the core sqlite3 database object. * */ void Init_libsql_ext_database( ) { VALUE ma = rb_define_module("Libsql"); VALUE mas = rb_define_module_under(ma, "SQLite3"); /* * Encapsulate an SQLite3 database */ cLS_Database = rb_define_class_under( mas, "Database", rb_cObject); rb_define_alloc_func(cLS_Database, libsql_ext_sqlite3_database_alloc); rb_define_singleton_method(cLS_Database, "open", libsql_ext_sqlite3_database_open, -1); rb_define_singleton_method(cLS_Database, "open16", libsql_ext_sqlite3_database_open16, 1); rb_define_method(cLS_Database, "prepare", libsql_ext_sqlite3_database_prepare, 1); rb_define_method(cLS_Database, "close", libsql_ext_sqlite3_database_close, 0); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "last_insert_rowid", libsql_ext_sqlite3_database_last_insert_rowid, 0); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "autocommit?", libsql_ext_sqlite3_database_is_autocommit, 0); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "register_trace_tap", libsql_ext_sqlite3_database_register_trace_tap, 1); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "table_column_metadata", libsql_ext_sqlite3_database_table_column_metadata, 3); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "row_changes", libsql_ext_sqlite3_database_row_changes, 0); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "total_changes", libsql_ext_sqlite3_database_total_changes, 0); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "last_error_code", libsql_ext_sqlite3_database_last_error_code, 0); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "last_error_message", libsql_ext_sqlite3_database_last_error_message, 0); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "define_function", libsql_ext_sqlite3_database_define_function, 2); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "remove_function", libsql_ext_sqlite3_database_remove_function, 2); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "define_aggregate", libsql_ext_sqlite3_database_define_aggregate, 3); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "remove_aggregate", libsql_ext_sqlite3_database_remove_aggregate, 3); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "busy_handler", libsql_ext_sqlite3_database_busy_handler, 1); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "progress_handler", libsql_ext_sqlite3_database_progress_handler, 2); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "interrupt!", libsql_ext_sqlite3_database_interrupt_bang, 0); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "replicate_to", libsql_ext_sqlite3_database_replicate_to, 1); /* in libsql_ext_database.c */ rb_define_method(cLS_Database, "execute_batch", libsql_ext_sqlite3_database_exec, 1); /* in libsql_ext_database.c */ /* * Ecapuslate a SQLite3 Database stat */ cLS_Database_Stat = rb_define_class_under( cLS_Database, "Stat", rb_cObject ); rb_define_method(cLS_Database_Stat, "update!", libsql_ext_sqlite3_database_stat_update_bang, -1); /* in libsql_ext_database.c */ }