6262 * file, not in this plugin. A node that provides no `schema` gets empty
6363 * SQLite shards and runs its own DDL through `exec`. */
6464
65+ /* Per-shard prepared-statement cache entry. Statements live the process
66+ * lifetime (like the shard), so re-preparing the same SQL — which re-runs the
67+ * SQLite parser + query planner on every request, a measured hot path — is
68+ * avoided: each distinct query is parsed once, then reset+rebound per call. */
69+ struct rel_stmt {
70+ char * sql ;
71+ sqlite3_stmt * stmt ;
72+ };
73+
6574struct rel_shard {
6675 sqlite3 * db ;
6776 pthread_mutex_t mu ;
77+ struct rel_stmt * cache ; /* prepared-statement cache, guarded by `mu` */
78+ int cache_n ;
79+ int cache_cap ;
6880};
6981
7082/* One NAMED logical database == one shard set. A single relational_storage
@@ -390,8 +402,42 @@ struct rel_work {
390402 char err [256 ];
391403};
392404
393- /* Runs on a worker-pool thread: prepare + bind + step on the shard_key's shard,
394- * serialized by the shard mutex; build the JSON result. */
405+ /* Return a prepared, reset+cleared statement for `sql` from the shard's cache,
406+ * preparing (and caching) it on first sight. Caller MUST hold sh->mu. Returns
407+ * NULL and sets *errmsg on failure. The statement is owned by the cache (never
408+ * finalized by the caller — reset it after use); the cache + statements live
409+ * the process lifetime, matching the shard. */
410+ static sqlite3_stmt * rel_shard_stmt (struct rel_shard * sh , const char * sql , const char * * errmsg )
411+ {
412+ for (int i = 0 ; i < sh -> cache_n ; ++ i ) {
413+ if (strcmp (sh -> cache [i ].sql , sql ) == 0 ) {
414+ sqlite3_reset (sh -> cache [i ].stmt );
415+ sqlite3_clear_bindings (sh -> cache [i ].stmt );
416+ return sh -> cache [i ].stmt ;
417+ }
418+ }
419+ sqlite3_stmt * st = NULL ;
420+ if (sqlite3_prepare_v2 (sh -> db , sql , -1 , & st , NULL ) != SQLITE_OK ) {
421+ if (errmsg ) * errmsg = sqlite3_errmsg (sh -> db );
422+ return NULL ;
423+ }
424+ if (sh -> cache_n == sh -> cache_cap ) {
425+ int ncap = sh -> cache_cap ? sh -> cache_cap * 2 : 16 ;
426+ struct rel_stmt * nc = realloc (sh -> cache , (size_t )ncap * sizeof (* nc ));
427+ if (!nc ) { sqlite3_finalize (st ); if (errmsg ) * errmsg = "stmt cache: out of memory" ; return NULL ; }
428+ sh -> cache = nc ;
429+ sh -> cache_cap = ncap ;
430+ }
431+ char * sql_copy = strdup (sql );
432+ if (!sql_copy ) { sqlite3_finalize (st ); if (errmsg ) * errmsg = "stmt cache: out of memory" ; return NULL ; }
433+ sh -> cache [sh -> cache_n ].sql = sql_copy ;
434+ sh -> cache [sh -> cache_n ].stmt = st ;
435+ sh -> cache_n ++ ;
436+ return st ;
437+ }
438+
439+ /* Runs on a worker-pool thread: bind + step a (cached) prepared statement on
440+ * the shard_key's shard, serialized by the shard mutex; build the JSON result. */
395441static void rel_work_fn (void * arg )
396442{
397443 struct rel_work * w = arg ;
@@ -402,9 +448,12 @@ static void rel_work_fn(void *arg)
402448 struct rel_shard * sh = & s -> shards [(uint64_t )w -> shard_key % (uint64_t )s -> n ];
403449
404450 pthread_mutex_lock (& sh -> mu );
405- sqlite3_stmt * st = NULL ;
406- if (sqlite3_prepare_v2 (sh -> db , w -> sql , -1 , & st , NULL ) != SQLITE_OK ) {
407- snprintf (w -> err , sizeof (w -> err ), "prepare: %s" , sqlite3_errmsg (sh -> db ));
451+ /* Cached prepared statement (parsed once per distinct SQL, then reset+
452+ * rebound) — avoids re-running the SQLite parser/planner on every call. */
453+ const char * prep_err = NULL ;
454+ sqlite3_stmt * st = rel_shard_stmt (sh , w -> sql , & prep_err );
455+ if (!st ) {
456+ snprintf (w -> err , sizeof (w -> err ), "prepare: %s" , prep_err ? prep_err : "?" );
408457 pthread_mutex_unlock (& sh -> mu );
409458 return ;
410459 }
@@ -416,13 +465,13 @@ static void rel_work_fn(void *arg)
416465 if (want != w -> nbinds ) {
417466 snprintf (w -> err , sizeof (w -> err ),
418467 "bind count mismatch: SQL has %d parameter(s), got %d" , want , w -> nbinds );
419- sqlite3_finalize (st );
468+ sqlite3_reset (st );
420469 pthread_mutex_unlock (& sh -> mu );
421470 return ;
422471 }
423472 if (rel_bind_args (st , w -> binds , w -> nbinds ) != SQLITE_OK ) {
424473 snprintf (w -> err , sizeof (w -> err ), "bind: %s" , sqlite3_errmsg (sh -> db ));
425- sqlite3_finalize (st );
474+ sqlite3_reset (st );
426475 pthread_mutex_unlock (& sh -> mu );
427476 return ;
428477 }
@@ -441,7 +490,7 @@ static void rel_work_fn(void *arg)
441490
442491 if (w -> op == REL_QUERY ) {
443492 jw = yjson_writer_new ();
444- if (!jw ) { sqlite3_finalize (st ); pthread_mutex_unlock (& sh -> mu );
493+ if (!jw ) { sqlite3_reset (st ); pthread_mutex_unlock (& sh -> mu );
445494 snprintf (w -> err , sizeof (w -> err ), "writer alloc" ); return ; }
446495 yjson_writer_begin_array (jw );
447496 int ncol = sqlite3_column_count (st );
@@ -457,21 +506,21 @@ static void rel_work_fn(void *arg)
457506 yjson_writer_end_array (jw );
458507 if (rc != SQLITE_DONE ) {
459508 snprintf (w -> err , sizeof (w -> err ), "step: %s" , sqlite3_errmsg (sh -> db ));
460- yjson_writer_free (jw ); sqlite3_finalize (st ); pthread_mutex_unlock (& sh -> mu );
509+ yjson_writer_free (jw ); sqlite3_reset (st ); pthread_mutex_unlock (& sh -> mu );
461510 return ;
462511 }
463512 } else { /* REL_EXEC */
464513 int rc = sqlite3_step (st );
465514 if (rc != SQLITE_DONE && rc != SQLITE_ROW ) {
466515 snprintf (w -> err , sizeof (w -> err ), "exec: %s" , sqlite3_errmsg (sh -> db ));
467- sqlite3_finalize (st ); pthread_mutex_unlock (& sh -> mu );
516+ sqlite3_reset (st ); pthread_mutex_unlock (& sh -> mu );
468517 return ;
469518 }
470519 exec_changes = sqlite3_changes (sh -> db );
471520 exec_rowid = sqlite3_last_insert_rowid (sh -> db );
472521 }
473522
474- sqlite3_finalize (st );
523+ sqlite3_reset (st );
475524 pthread_mutex_unlock (& sh -> mu );
476525 /* --- DB lock released: serialize + allocate the result OUTSIDE it. --- */
477526
0 commit comments