AtlatestRepositorysigil-sqlite
sigil-sqlite / tree / nativesqlite.c
1
/*2
* Sigil SQLite Bindings3
*4
* Provides SQLite database access for Sigil applications.5
* Uses SQLite amalgamation for easy embedding.6
*/8
#include <sigil/sigil.h>9
#include "../vendor/sqlite3.h"10
#include <stdio.h>11
#include <stdlib.h>12
#include <string.h>14
/*15
* Database handle structure16
*/17
typedef struct {18
sqlite3 *db;19
int closed;20
} SqliteDB;22
/*23
* Statement handle structure24
*/25
typedef struct {26
sqlite3_stmt *stmt;27
sqlite3 *db; /* Reference to parent database */28
int finalized;29
} SqliteStmt;31
/* Type tags for foreign objects */32
static Value db_type_tag = SIGIL_UNDEFINED;33
static Value stmt_type_tag = SIGIL_UNDEFINED;35
/*36
* Finalizer for database handles37
*/38
static void db_finalizer(void *data)39
{40
SqliteDB *handle = (SqliteDB *)data;41
if (handle && !handle->closed && handle->db) {42
sqlite3_close(handle->db);43
}44
free(handle);45
}47
/*48
* Finalizer for statement handles49
*/50
static void stmt_finalizer(void *data)51
{52
SqliteStmt *handle = (SqliteStmt *)data;53
if (handle && !handle->finalized && handle->stmt) {54
sqlite3_finalize(handle->stmt);55
}56
free(handle);57
}59
/*60
* Helper: Check if value is a database handle61
*/62
static int is_db(Value v)63
{64
if (!sigil_is_foreign(v)) return 0;65
return sigil_foreign_type(v) == db_type_tag;66
}68
/*69
* Helper: Check if value is a statement handle70
*/71
static int is_stmt(Value v)72
{73
if (!sigil_is_foreign(v)) return 0;74
return sigil_foreign_type(v) == stmt_type_tag;75
}77
/*78
* Helper: Get database data from value79
*/80
static SqliteDB *as_db(Value v)81
{82
return (SqliteDB *)sigil_foreign_data(v);83
}85
/*86
* Helper: Get statement data from value87
*/88
static SqliteStmt *as_stmt(Value v)89
{90
return (SqliteStmt *)sigil_foreign_data(v);91
}93
/*94
* Helper: Extract null-terminated string from Sigil string95
* Caller must free the returned string.96
*/97
static char *extract_string(Value v)98
{99
SigilString *s = (SigilString *)sigil_as_ptr(v);100
char *result = malloc(s->byte_length + 1);101
if (!result) return NULL;102
memcpy(result, s->data, s->byte_length);103
result[s->byte_length] = '\0';104
return result;105
}107
/*108
* sqlite-open path -> db | #f109
* Open a database file. Creates if it doesn't exist.110
*/111
static Value native_sqlite_open(SigilVM *vm, int argc, Value *args)112
{113
(void)argc;115
if (!sigil_is_string(args[0])) {116
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-open: expected string for path");117
return SIGIL_UNDEFINED;118
}120
char *path = extract_string(args[0]);121
if (!path) return SIGIL_FALSE;123
sqlite3 *db;124
int rc = sqlite3_open(path, &db);125
free(path);127
if (rc != SQLITE_OK) {128
if (db) sqlite3_close(db);129
return SIGIL_FALSE;130
}132
SqliteDB *handle = malloc(sizeof(SqliteDB));133
if (!handle) {134
sqlite3_close(db);135
return SIGIL_FALSE;136
}138
handle->db = db;139
handle->closed = 0;141
return sigil_make_foreign(vm, db_type_tag, handle, db_finalizer, sizeof(SqliteDB));142
}144
/*145
* sqlite-close db -> boolean146
* Close a database connection.147
*/148
static Value native_sqlite_close(SigilVM *vm, int argc, Value *args)149
{150
(void)argc;152
if (!is_db(args[0])) {153
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-close: expected database handle");154
return SIGIL_UNDEFINED;155
}157
SqliteDB *handle = as_db(args[0]);158
if (handle->closed) {159
return SIGIL_TRUE; /* Already closed */160
}162
int rc = sqlite3_close(handle->db);163
if (rc != SQLITE_OK) {164
return SIGIL_FALSE;165
}167
handle->db = NULL;168
handle->closed = 1;169
return SIGIL_TRUE;170
}172
/*173
* sqlite-db? value -> boolean174
* Check if value is a database handle.175
*/176
static Value native_sqlite_dbp(SigilVM *vm, int argc, Value *args)177
{178
(void)vm;179
(void)argc;180
return sigil_bool(is_db(args[0]));181
}183
/*184
* sqlite-stmt? value -> boolean185
* Check if value is a statement handle.186
*/187
static Value native_sqlite_stmtp(SigilVM *vm, int argc, Value *args)188
{189
(void)vm;190
(void)argc;191
return sigil_bool(is_stmt(args[0]));192
}194
/*195
* sqlite-exec db sql -> boolean196
* Execute SQL that doesn't return rows (CREATE, INSERT, UPDATE, DELETE).197
*/198
static Value native_sqlite_exec(SigilVM *vm, int argc, Value *args)199
{200
(void)argc;202
if (!is_db(args[0])) {203
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-exec: expected database handle");204
return SIGIL_UNDEFINED;205
}206
if (!sigil_is_string(args[1])) {207
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-exec: expected string for SQL");208
return SIGIL_UNDEFINED;209
}211
SqliteDB *handle = as_db(args[0]);212
if (handle->closed) {213
sigil__vm_error(vm, SIGIL_ERR_IO, "sqlite-exec: database is closed");214
return SIGIL_UNDEFINED;215
}217
char *sql = extract_string(args[1]);218
if (!sql) return SIGIL_FALSE;220
/* Force non-blocking: never let sqlite3_exec block the single cooperative221
* scheduler thread waiting on a lock. A contended lock returns SQLITE_BUSY222
* immediately, which we surface as the symbol 'busy so the Scheme wrapper223
* can yield to the scheduler + retry cooperatively (letting the lock holder224
* run and commit). This overrides any PRAGMA busy_timeout the caller set —225
* that intent is honored by the wrapper's retry budget instead. */226
sqlite3_busy_timeout(handle->db, 0);228
char *errmsg = NULL;229
int rc = sqlite3_exec(handle->db, sql, NULL, NULL, &errmsg);230
free(sql);231
if (errmsg) sqlite3_free(errmsg);233
if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED) {234
return sigil_intern_symbol(vm, "busy", 4);235
}236
if (rc != SQLITE_OK) {237
return SIGIL_FALSE;238
}239
return SIGIL_TRUE;240
}242
/*243
* sqlite-prepare db sql -> stmt | #f244
* Prepare a SQL statement for execution.245
*/246
static Value native_sqlite_prepare(SigilVM *vm, int argc, Value *args)247
{248
(void)argc;250
if (!is_db(args[0])) {251
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-prepare: expected database handle");252
return SIGIL_UNDEFINED;253
}254
if (!sigil_is_string(args[1])) {255
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-prepare: expected string for SQL");256
return SIGIL_UNDEFINED;257
}259
SqliteDB *db_handle = as_db(args[0]);260
if (db_handle->closed) {261
sigil__vm_error(vm, SIGIL_ERR_IO, "sqlite-prepare: database is closed");262
return SIGIL_UNDEFINED;263
}265
SigilString *sql_str = (SigilString *)sigil_as_ptr(args[1]);267
sqlite3_stmt *stmt;268
int rc = sqlite3_prepare_v2(db_handle->db, sql_str->data, sql_str->byte_length,269
&stmt, NULL);271
if (rc != SQLITE_OK || !stmt) {272
return SIGIL_FALSE;273
}275
SqliteStmt *handle = malloc(sizeof(SqliteStmt));276
if (!handle) {277
sqlite3_finalize(stmt);278
return SIGIL_FALSE;279
}281
handle->stmt = stmt;282
handle->db = db_handle->db;283
handle->finalized = 0;285
return sigil_make_foreign(vm, stmt_type_tag, handle, stmt_finalizer, sizeof(SqliteStmt));286
}288
/*289
* sqlite-bind stmt index value -> boolean290
* Bind a value to a parameter in a prepared statement.291
* Index is 1-based.292
*/293
static Value native_sqlite_bind(SigilVM *vm, int argc, Value *args)294
{295
(void)argc;297
if (!is_stmt(args[0])) {298
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-bind: expected statement handle");299
return SIGIL_UNDEFINED;300
}301
if (!sigil_is_fixnum(args[1])) {302
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-bind: expected integer for index");303
return SIGIL_UNDEFINED;304
}306
SqliteStmt *handle = as_stmt(args[0]);307
if (handle->finalized) {308
sigil__vm_error(vm, SIGIL_ERR_IO, "sqlite-bind: statement is finalized");309
return SIGIL_UNDEFINED;310
}312
int index = (int)sigil_as_fixnum(args[1]);313
Value value = args[2];314
int rc;316
if (sigil_is_null(value) || sigil_is_false(value)) {317
/* Bind NULL */318
rc = sqlite3_bind_null(handle->stmt, index);319
} else if (sigil_is_fixnum(value)) {320
/* Bind integer */321
rc = sqlite3_bind_int64(handle->stmt, index, sigil_as_fixnum(value));322
} else if (sigil_is_flonum(value)) {323
/* Bind double */324
rc = sqlite3_bind_double(handle->stmt, index, sigil_as_flonum(value));325
} else if (sigil_is_string(value)) {326
/* Bind text */327
SigilString *s = (SigilString *)sigil_as_ptr(value);328
rc = sqlite3_bind_text(handle->stmt, index, s->data, s->byte_length,329
SQLITE_TRANSIENT);330
} else if (sigil_is_bytevector(value)) {331
/* Bind blob */332
SigilBytevector *bv = (SigilBytevector *)sigil_as_ptr(value);333
rc = sqlite3_bind_blob(handle->stmt, index, bv->data, bv->length,334
SQLITE_TRANSIENT);335
} else {336
sigil__vm_error(vm, SIGIL_ERR_TYPE,337
"sqlite-bind: value must be null, boolean, integer, real, string, or bytevector");338
return SIGIL_UNDEFINED;339
}341
return sigil_bool(rc == SQLITE_OK);342
}344
/*345
* sqlite-step stmt -> 'row | 'done | #f346
* Execute one step of a prepared statement.347
* Returns 'row if a row is available, 'done if finished, #f on error.348
*/349
static Value native_sqlite_step(SigilVM *vm, int argc, Value *args)350
{351
(void)argc;353
if (!is_stmt(args[0])) {354
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-step: expected statement handle");355
return SIGIL_UNDEFINED;356
}358
SqliteStmt *handle = as_stmt(args[0]);359
if (handle->finalized) {360
sigil__vm_error(vm, SIGIL_ERR_IO, "sqlite-step: statement is finalized");361
return SIGIL_UNDEFINED;362
}364
/* Force non-blocking (see native_sqlite_exec): a contended lock surfaces as365
* 'busy for the Scheme wrapper to yield + retry, rather than blocking the366
* scheduler thread inside sqlite3_step. */367
sqlite3_busy_timeout(sqlite3_db_handle(handle->stmt), 0);369
int rc = sqlite3_step(handle->stmt);371
if (rc == SQLITE_ROW) {372
return sigil_intern_symbol(vm, "row", 3);373
} else if (rc == SQLITE_DONE) {374
return sigil_intern_symbol(vm, "done", 4);375
} else if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED) {376
return sigil_intern_symbol(vm, "busy", 4);377
} else {378
return SIGIL_FALSE;379
}380
}382
/*383
* sqlite-column-count stmt -> integer384
* Get the number of columns in the result set.385
*/386
static Value native_sqlite_column_count(SigilVM *vm, int argc, Value *args)387
{388
(void)argc;390
if (!is_stmt(args[0])) {391
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-column-count: expected statement handle");392
return SIGIL_UNDEFINED;393
}395
SqliteStmt *handle = as_stmt(args[0]);396
if (handle->finalized) {397
return sigil_fixnum(0);398
}400
return sigil_fixnum(sqlite3_column_count(handle->stmt));401
}403
/*404
* sqlite-column-name stmt index -> string405
* Get the name of a column (0-based index).406
*/407
static Value native_sqlite_column_name(SigilVM *vm, int argc, Value *args)408
{409
(void)argc;411
if (!is_stmt(args[0])) {412
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-column-name: expected statement handle");413
return SIGIL_UNDEFINED;414
}415
if (!sigil_is_fixnum(args[1])) {416
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-column-name: expected integer for index");417
return SIGIL_UNDEFINED;418
}420
SqliteStmt *handle = as_stmt(args[0]);421
if (handle->finalized) {422
return SIGIL_FALSE;423
}425
int index = (int)sigil_as_fixnum(args[1]);426
const char *name = sqlite3_column_name(handle->stmt, index);428
if (!name) return SIGIL_FALSE;429
return sigil_make_string(vm, name, strlen(name));430
}432
/*433
* sqlite-column stmt index -> value434
* Get the value of a column in the current row (0-based index).435
* Returns appropriate Sigil type based on SQLite column type.436
*/437
static Value native_sqlite_column(SigilVM *vm, int argc, Value *args)438
{439
(void)argc;441
if (!is_stmt(args[0])) {442
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-column: expected statement handle");443
return SIGIL_UNDEFINED;444
}445
if (!sigil_is_fixnum(args[1])) {446
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-column: expected integer for index");447
return SIGIL_UNDEFINED;448
}450
SqliteStmt *handle = as_stmt(args[0]);451
if (handle->finalized) {452
return SIGIL_FALSE;453
}455
int index = (int)sigil_as_fixnum(args[1]);456
int type = sqlite3_column_type(handle->stmt, index);458
switch (type) {459
case SQLITE_NULL:460
return SIGIL_FALSE; /* Use #f for NULL */462
case SQLITE_INTEGER:463
return sigil_fixnum(sqlite3_column_int64(handle->stmt, index));465
case SQLITE_FLOAT:466
return sigil_flonum(sqlite3_column_double(handle->stmt, index));468
case SQLITE_TEXT: {469
const char *text = (const char *)sqlite3_column_text(handle->stmt, index);470
int len = sqlite3_column_bytes(handle->stmt, index);471
return sigil_make_string(vm, text, len);472
}474
case SQLITE_BLOB: {475
const void *data = sqlite3_column_blob(handle->stmt, index);476
int len = sqlite3_column_bytes(handle->stmt, index);477
Value bv = sigil_make_bytevector(vm, (size_t)len);478
if (sigil_is_bytevector(bv)) {479
memcpy(sigil_bytevector_data(bv), data, len);480
}481
return bv;482
}484
default:485
return SIGIL_FALSE;486
}487
}489
/*490
* sqlite-reset stmt -> boolean491
* Reset a prepared statement to its initial state.492
*/493
static Value native_sqlite_reset(SigilVM *vm, int argc, Value *args)494
{495
(void)argc;497
if (!is_stmt(args[0])) {498
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-reset: expected statement handle");499
return SIGIL_UNDEFINED;500
}502
SqliteStmt *handle = as_stmt(args[0]);503
if (handle->finalized) {504
return SIGIL_FALSE;505
}507
int rc = sqlite3_reset(handle->stmt);508
return sigil_bool(rc == SQLITE_OK);509
}511
/*512
* sqlite-finalize stmt -> boolean513
* Finalize a prepared statement, freeing resources.514
*/515
static Value native_sqlite_finalize(SigilVM *vm, int argc, Value *args)516
{517
(void)argc;519
if (!is_stmt(args[0])) {520
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-finalize: expected statement handle");521
return SIGIL_UNDEFINED;522
}524
SqliteStmt *handle = as_stmt(args[0]);525
if (handle->finalized) {526
return SIGIL_TRUE; /* Already finalized */527
}529
int rc = sqlite3_finalize(handle->stmt);530
handle->stmt = NULL;531
handle->finalized = 1;533
return sigil_bool(rc == SQLITE_OK);534
}536
/*537
* sqlite-last-insert-rowid db -> integer538
* Get the rowid of the last inserted row.539
*/540
static Value native_sqlite_last_insert_rowid(SigilVM *vm, int argc, Value *args)541
{542
(void)argc;544
if (!is_db(args[0])) {545
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-last-insert-rowid: expected database handle");546
return SIGIL_UNDEFINED;547
}549
SqliteDB *handle = as_db(args[0]);550
if (handle->closed) {551
return sigil_fixnum(0);552
}554
return sigil_fixnum(sqlite3_last_insert_rowid(handle->db));555
}557
/*558
* sqlite-changes db -> integer559
* Get the number of rows changed by the last statement.560
*/561
static Value native_sqlite_changes(SigilVM *vm, int argc, Value *args)562
{563
(void)argc;565
if (!is_db(args[0])) {566
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-changes: expected database handle");567
return SIGIL_UNDEFINED;568
}570
SqliteDB *handle = as_db(args[0]);571
if (handle->closed) {572
return sigil_fixnum(0);573
}575
return sigil_fixnum(sqlite3_changes(handle->db));576
}578
/*579
* sqlite-errmsg db -> string580
* Get the error message for the most recent error.581
*/582
static Value native_sqlite_errmsg(SigilVM *vm, int argc, Value *args)583
{584
(void)argc;586
if (!is_db(args[0])) {587
sigil__vm_error(vm, SIGIL_ERR_TYPE, "sqlite-errmsg: expected database handle");588
return SIGIL_UNDEFINED;589
}591
SqliteDB *handle = as_db(args[0]);592
if (handle->closed) {593
return sigil_make_string(vm, "database is closed", 18);594
}596
const char *msg = sqlite3_errmsg(handle->db);597
return sigil_make_string(vm, msg, strlen(msg));598
}600
/*601
* Helper macro for module-scoped registration with export602
*/603
#define REGISTER_AND_EXPORT(name, func, arity, doc) \604
sigil_module_register_native(vm, name, func, arity, doc); \605
sigil_module_export(vm, name)607
/*608
* Initialize the (sigil sqlite) module.609
*/610
void sigil__init_sigil_sqlite_module(SigilVM *vm)611
{612
/* Initialize type tags */613
db_type_tag = sigil_intern_symbol(vm, "sqlite-db", 9);614
stmt_type_tag = sigil_intern_symbol(vm, "sqlite-stmt", 11);616
SigilModule *module = sigil_begin_module(vm, "(sigil sqlite)");617
if (!module) return;619
/* Type predicates */620
REGISTER_AND_EXPORT("sqlite-db?", native_sqlite_dbp,621
SIGIL_ARITY_EXACT(1), "Check if value is a database handle");622
REGISTER_AND_EXPORT("sqlite-stmt?", native_sqlite_stmtp,623
SIGIL_ARITY_EXACT(1), "Check if value is a statement handle");625
/* Database operations */626
REGISTER_AND_EXPORT("sqlite-open", native_sqlite_open,627
SIGIL_ARITY_EXACT(1), "Open database file");628
REGISTER_AND_EXPORT("sqlite-close", native_sqlite_close,629
SIGIL_ARITY_EXACT(1), "Close database connection");630
/* Internal: returns 'busy on a contended lock (never blocks). The public631
* sqlite-exec (Scheme, in sqlite.sgl) wraps this with cooperative retry. */632
REGISTER_AND_EXPORT("%sqlite-exec-native", native_sqlite_exec,633
SIGIL_ARITY_EXACT(2), "Execute SQL without results (non-blocking)");635
/* Prepared statements */636
REGISTER_AND_EXPORT("sqlite-prepare", native_sqlite_prepare,637
SIGIL_ARITY_EXACT(2), "Prepare SQL statement");638
REGISTER_AND_EXPORT("sqlite-bind", native_sqlite_bind,639
SIGIL_ARITY_EXACT(3), "Bind parameter value");640
/* Internal: returns 'busy on a contended lock (never blocks). The public641
* sqlite-step (Scheme, in sqlite.sgl) wraps this with cooperative retry. */642
REGISTER_AND_EXPORT("%sqlite-step-native", native_sqlite_step,643
SIGIL_ARITY_EXACT(1), "Execute statement step (non-blocking)");644
REGISTER_AND_EXPORT("sqlite-reset", native_sqlite_reset,645
SIGIL_ARITY_EXACT(1), "Reset statement to initial state");646
REGISTER_AND_EXPORT("sqlite-finalize", native_sqlite_finalize,647
SIGIL_ARITY_EXACT(1), "Finalize and free statement");649
/* Column access */650
REGISTER_AND_EXPORT("sqlite-column-count", native_sqlite_column_count,651
SIGIL_ARITY_EXACT(1), "Get number of result columns");652
REGISTER_AND_EXPORT("sqlite-column-name", native_sqlite_column_name,653
SIGIL_ARITY_EXACT(2), "Get column name");654
REGISTER_AND_EXPORT("sqlite-column", native_sqlite_column,655
SIGIL_ARITY_EXACT(2), "Get column value");657
/* Database info */658
REGISTER_AND_EXPORT("sqlite-last-insert-rowid", native_sqlite_last_insert_rowid,659
SIGIL_ARITY_EXACT(1), "Get last inserted rowid");660
REGISTER_AND_EXPORT("sqlite-changes", native_sqlite_changes,661
SIGIL_ARITY_EXACT(1), "Get rows changed by last statement");662
REGISTER_AND_EXPORT("sqlite-errmsg", native_sqlite_errmsg,663
SIGIL_ARITY_EXACT(1), "Get last error message");665
sigil_end_module(vm);666
}668
#undef REGISTER_AND_EXPORT