Skip to content

Commit 5dd04fc

Browse files
authored
feat: lazily initialize engines from saved settings (#10)
Reuse persisted provider/model settings by creating missing local, remote, or custom engines on first embedding use instead of requiring memory_set_model on every connection. Keep memory_set_model eager so callers can preload and validate engines explicitly, while memory_set_apikey remains connection-scoped and lazy for saved remote models. Update API/README documentation and add regression coverage for saved local, remote, and custom provider settings. Verification: build/unittest with TEST_SQLITE_EXTENSION passed 157 tests.
1 parent dd50ee5 commit 5dd04fc

6 files changed

Lines changed: 219 additions & 8 deletions

File tree

API.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,9 @@ Configures the embedding model to use.
136136
- When `provider` is `"local"`, the extension uses the built-in llama.cpp engine and verifies the model file exists
137137
- When `provider` is anything other than `"local"`, the extension uses the [vectors.space](https://vectors.space) remote embedding service
138138
- Remote embedding requires a free API key from [vectors.space](https://vectors.space) (set via `memory_set_apikey`)
139-
- Settings are persisted in `dbmem_settings` table
140-
- For local models, the embedding engine is initialized immediately
139+
- Provider/model settings are persisted in the `dbmem_settings` table and reused by new connections
140+
- Calling `memory_set_model()` initializes the embedding engine immediately, which can be used to preload and validate the engine
141+
- When provider/model settings are loaded by a new connection, the engine is initialized lazily on first embedding use
141142
- **Automatic reindex**: If a model was previously configured and the new provider/model differs, all existing content is automatically re-embedded with the new model. File-based entries are re-read from disk; text-based entries are re-embedded from stored content. Errors on individual entries are silently skipped (best-effort)
142143

143144
**Example:**
@@ -164,7 +165,7 @@ Sets the API key for the [vectors.space](https://vectors.space) remote embedding
164165
**Returns:** INTEGER - 1 on success
165166

166167
**Notes:**
167-
- API key is stored in memory only, not persisted to disk
168+
- API key is stored in memory only, not persisted to disk, and must be set per connection for remote embeddings
168169
- Required when using any provider other than `"local"`
169170
- Get a free API key by creating an account at [vectors.space](https://vectors.space)
170171

@@ -623,7 +624,7 @@ Generates or refreshes local embeddings for stored content.
623624
**Returns:** INTEGER - Number of content rows reindexed or realigned
624625

625626
**Notes:**
626-
- Requires an embedding model configured with `memory_set_model()`
627+
- Requires an embedding model configured with `memory_set_model()` or loaded from persisted provider/model settings
627628
- Processes rows in `dbmem_content` that have stored `value`
628629
- Skips rows whose `dbmem_content.hash` already matches `value` and whose local `dbmem_vault` entries already exist
629630
- After sync merges remote changes into `dbmem_content.value`, recomputes stale hashes, refreshes missing embeddings, and removes old local index rows
@@ -714,7 +715,7 @@ int sqlite3_memory_register_provider(
714715
);
715716
```
716717

717-
Registers a custom embedding engine for a specific database connection. Once registered, calling `memory_set_model(provider_name, model)` from SQL will use your engine instead of the built-in local or remote engines.
718+
Registers a custom embedding engine for a specific database connection. Once registered, calling `memory_set_model(provider_name, model)` from SQL will use your engine instead of the built-in local or remote engines. If provider/model settings were already loaded from `dbmem_settings`, the custom engine is initialized lazily on first embedding use after registration.
718719

719720
**Parameters:**
720721
| Parameter | Type | Description |
@@ -728,7 +729,8 @@ Registers a custom embedding engine for a specific database connection. Once reg
728729
**`dbmem_provider_t` struct:**
729730
```c
730731
typedef struct {
731-
// Called when memory_set_model(provider_name, model) is executed.
732+
// Called when memory_set_model(provider_name, model) is executed, or lazily
733+
// on first embedding use when provider/model were loaded from settings.
732734
// api_key is the value set via memory_set_apikey() (may be NULL).
733735
// xdata is the user pointer from this struct.
734736
// Return an opaque engine pointer on success, or NULL on error (fill err_msg).

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ SELECT memory_set_model('local', '/path/to/nomic-embed-text-v1.5.Q8_0.gguf');
128128
-- SELECT memory_set_apikey('your-vectorspace-api-key');
129129
-- SELECT memory_set_model('openai', 'text-embedding-3-small');
130130

131+
-- Provider/model settings are persisted. New connections reuse them and
132+
-- initialize the engine lazily on first embedding use. Remote API keys are
133+
-- connection-scoped, so call memory_set_apikey() on each remote connection.
134+
131135
-- Add some knowledge
132136
SELECT memory_add_text('SQLite is a C-language library that implements a small, fast,
133137
self-contained, high-reliability, full-featured, SQL database engine. SQLite is the
@@ -182,7 +186,8 @@ conn.enable_load_extension(True)
182186
conn.load_extension('./vector')
183187
conn.load_extension('./memory')
184188

185-
# One-time setup
189+
# One-time setup. Later connections reuse the saved provider/model and lazily
190+
# load the engine on first embedding use.
186191
conn.execute("SELECT memory_set_model('local', './models/nomic-embed-text-v1.5.Q8_0.gguf')")
187192

188193
# Store conversation context

src/dbmem-search.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,12 @@ static int vMemorySearchCursorFilter (sqlite3_vtab_cursor *cur, int idxNum, cons
656656
if (rc != SQLITE_OK) return SQLITE_NOMEM;
657657

658658
// perform semantic search
659+
rc = dbmem_context_ensure_engine(ctx);
660+
if (rc != SQLITE_OK) {
661+
sqlvTab->zErrMsg = sqlite3_mprintf("%s", dbmem_context_errmsg(ctx));
662+
return SQLITE_ERROR;
663+
}
664+
659665
// retrieve engine
660666
bool is_local;
661667
void *engine = dbmem_context_engine(ctx, &is_local);

src/sqlite-memory.c

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,56 @@ void *dbmem_context_engine (dbmem_context *ctx, bool *is_local) {
10671067
return (ctx->is_local) ? (void *)ctx->l_engine : (void *)ctx->r_engine;
10681068
}
10691069

1070+
int dbmem_context_ensure_engine (dbmem_context *ctx) {
1071+
if (!ctx || !ctx->provider || !ctx->model) {
1072+
if (ctx) dbmem_context_set_error(ctx, "memory_set_model must be called before adding content");
1073+
return SQLITE_ERROR;
1074+
}
1075+
1076+
bool is_local_provider = (strcasecmp(ctx->provider, DBMEM_LOCAL_PROVIDER) == 0);
1077+
bool is_custom_provider = (ctx->custom_provider_name && ctx->custom_provider.compute &&
1078+
strcasecmp(ctx->provider, ctx->custom_provider_name) == 0);
1079+
1080+
ctx->is_local = is_custom_provider ? false : is_local_provider;
1081+
ctx->is_custom = is_custom_provider;
1082+
1083+
if (is_custom_provider) {
1084+
if (ctx->custom_engine) return SQLITE_OK;
1085+
ctx->custom_engine = ctx->custom_provider.init(ctx->model, ctx->api_key, ctx->custom_provider.xdata, ctx->error_msg);
1086+
return ctx->custom_engine ? SQLITE_OK : SQLITE_ERROR;
1087+
}
1088+
1089+
#ifndef DBMEM_OMIT_LOCAL_ENGINE
1090+
if (is_local_provider) {
1091+
if (ctx->l_engine) return SQLITE_OK;
1092+
if (dbmem_file_exists(ctx->model) == false) {
1093+
dbmem_context_set_error(ctx, "Local model not found in the specified path");
1094+
return SQLITE_ERROR;
1095+
}
1096+
1097+
int max_context_tokens = (int)(ctx->max_tokens + ctx->overlay_tokens);
1098+
ctx->l_engine = dbmem_local_engine_init(ctx, ctx->model, max_context_tokens, ctx->error_msg);
1099+
if (!ctx->l_engine) return SQLITE_ERROR;
1100+
if (ctx->engine_warmup) dbmem_local_engine_warmup(ctx->l_engine);
1101+
return SQLITE_OK;
1102+
}
1103+
#else
1104+
if (is_local_provider) {
1105+
dbmem_context_set_error(ctx, "Local provider cannot be set because SQLite-memory was compiled without local provider support");
1106+
return SQLITE_ERROR;
1107+
}
1108+
#endif
1109+
1110+
#ifndef DBMEM_OMIT_REMOTE_ENGINE
1111+
if (ctx->r_engine) return SQLITE_OK;
1112+
ctx->r_engine = dbmem_remote_engine_init(ctx, ctx->provider, ctx->model, ctx->error_msg);
1113+
return ctx->r_engine ? SQLITE_OK : SQLITE_ERROR;
1114+
#else
1115+
dbmem_context_set_error(ctx, "Remote provider cannot be set because SQLite-memory was compiled without remote provider support");
1116+
return SQLITE_ERROR;
1117+
#endif
1118+
}
1119+
10701120
bool dbmem_context_is_custom (dbmem_context *ctx) {
10711121
return ctx->is_custom;
10721122
}
@@ -2565,6 +2615,8 @@ static int dbmem_process_callback (const char *text, size_t len, size_t offset,
25652615
dbmem_context_set_error(ctx, "memory_set_model must be called before adding content");
25662616
return SQLITE_ERROR;
25672617
}
2618+
rc = dbmem_context_ensure_engine(ctx);
2619+
if (rc != SQLITE_OK) return rc;
25682620

25692621
// compute embedding
25702622
if (ctx->is_custom) {

src/sqlite-memory.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ typedef struct {
4545
} dbmem_embedding_result_t;
4646

4747
typedef struct {
48-
// Called when memory_set_model(provider, model) matches this provider.
48+
// Called when memory_set_model(provider, model) matches this provider, or
49+
// lazily on first embedding use when provider/model were loaded from settings.
4950
// api_key is the value set via memory_set_apikey() (may be NULL).
5051
// xdata is the user-supplied generic pointer from the struct.
5152
// Return opaque engine pointer, or NULL on error (fill err_msg).
@@ -67,6 +68,7 @@ typedef struct {
6768
SQLITE_DBMEMORY_API int sqlite3_memory_register_provider (sqlite3 *db, const char *provider_name, const dbmem_provider_t *provider);
6869

6970
void *dbmem_context_engine (dbmem_context *ctx, bool *is_local);
71+
int dbmem_context_ensure_engine (dbmem_context *ctx);
7072
bool dbmem_context_is_custom (dbmem_context *ctx);
7173
bool dbmem_context_load_vector (dbmem_context *ctx);
7274
bool dbmem_context_sync_available (dbmem_context *ctx);

test/unittest.c

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,19 @@ static sqlite3 *open_test_db(void) {
14901490
return db;
14911491
}
14921492

1493+
static sqlite3 *open_test_db_path(const char *path) {
1494+
sqlite3 *db = NULL;
1495+
int rc = sqlite3_open(path, &db);
1496+
if (rc != SQLITE_OK) return NULL;
1497+
1498+
rc = sqlite3_memory_init(db, NULL, NULL);
1499+
if (rc != SQLITE_OK) {
1500+
sqlite3_close(db);
1501+
return NULL;
1502+
}
1503+
return db;
1504+
}
1505+
14931506
// Helper to execute SQL and get integer result
14941507
static int exec_get_int(sqlite3 *db, const char *sql, sqlite3_int64 *result) {
14951508
sqlite3_stmt *stmt = NULL;
@@ -3275,10 +3288,12 @@ typedef struct {
32753288
} dummy_engine_t;
32763289

32773290
static int dummy_compute_calls = 0;
3291+
static int dummy_init_calls = 0;
32783292

32793293
static void *dummy_init(const char *model, const char *api_key, void *xdata, char err_msg[1024]) {
32803294
UNUSED_PARAM(model);
32813295
UNUSED_PARAM(xdata);
3296+
dummy_init_calls++;
32823297
dummy_engine_t *e = (dummy_engine_t *)calloc(1, sizeof(dummy_engine_t));
32833298
if (!e) { snprintf(err_msg, 1024, "alloc failed"); return NULL; }
32843299
e->dimension = 4;
@@ -4069,6 +4084,128 @@ TEST(sqlite_memory_add_text_requires_model) {
40694084
sqlite3_close(db);
40704085
}
40714086

4087+
#ifndef DBMEM_OMIT_REMOTE_ENGINE
4088+
TEST(sqlite_saved_remote_model_initializes_lazily_after_apikey) {
4089+
const char *path = TEST_TMP_DIR "/dbmem_saved_remote_model.sqlite";
4090+
remove_test_file(path);
4091+
4092+
sqlite3 *db = open_test_db_path(path);
4093+
ASSERT(db != NULL);
4094+
4095+
int rc = sqlite3_exec(db,
4096+
"INSERT OR REPLACE INTO dbmem_settings (key, value) VALUES ('provider', 'openai');"
4097+
"INSERT OR REPLACE INTO dbmem_settings (key, value) VALUES ('model', 'text-embedding-3-small');",
4098+
NULL, NULL, NULL);
4099+
ASSERT_EQ(rc, SQLITE_OK);
4100+
sqlite3_close(db);
4101+
4102+
db = open_test_db_path(path);
4103+
ASSERT(db != NULL);
4104+
4105+
dbmem_context *ctx = get_test_ctx(db);
4106+
ASSERT(ctx != NULL);
4107+
rc = dbmem_context_ensure_engine(ctx);
4108+
ASSERT_EQ(rc, SQLITE_ERROR);
4109+
ASSERT(strstr(dbmem_context_errmsg(ctx), "memory_set_apikey must be called") != NULL);
4110+
4111+
sqlite3_int64 result = 0;
4112+
rc = exec_get_int(db, "SELECT memory_set_apikey('test-key');", &result);
4113+
ASSERT_EQ(rc, SQLITE_OK);
4114+
4115+
rc = dbmem_context_ensure_engine(ctx);
4116+
ASSERT_EQ(rc, SQLITE_OK);
4117+
4118+
bool is_local = true;
4119+
ASSERT(dbmem_context_engine(ctx, &is_local) != NULL);
4120+
ASSERT_EQ(is_local, false);
4121+
4122+
sqlite3_close(db);
4123+
remove_test_file(path);
4124+
}
4125+
#endif
4126+
4127+
#ifndef DBMEM_OMIT_LOCAL_ENGINE
4128+
TEST(sqlite_saved_local_model_initializes_lazily) {
4129+
const char *path = TEST_TMP_DIR "/dbmem_saved_local_model.sqlite";
4130+
const char *model_path = "models/embeddinggemma-300M-Q8_0.gguf";
4131+
remove_test_file(path);
4132+
4133+
if (access(model_path, F_OK) != 0) return;
4134+
4135+
sqlite3 *db = open_test_db_path(path);
4136+
ASSERT(db != NULL);
4137+
4138+
sqlite3_int64 result = 0;
4139+
int rc = exec_get_int(db, "SELECT memory_set_model('local', 'models/embeddinggemma-300M-Q8_0.gguf');", &result);
4140+
ASSERT_EQ(rc, SQLITE_OK);
4141+
sqlite3_close(db);
4142+
4143+
db = open_test_db_path(path);
4144+
ASSERT(db != NULL);
4145+
4146+
dbmem_context *ctx = get_test_ctx(db);
4147+
ASSERT(ctx != NULL);
4148+
4149+
bool is_local = false;
4150+
ASSERT(dbmem_context_engine(ctx, &is_local) == NULL);
4151+
4152+
rc = exec_get_int(db, "SELECT memory_add_text('Saved local model settings should load lazily.');", &result);
4153+
ASSERT_EQ(rc, SQLITE_OK);
4154+
ASSERT(result >= 1);
4155+
4156+
ASSERT(dbmem_context_engine(ctx, &is_local) != NULL);
4157+
ASSERT_EQ(is_local, true);
4158+
4159+
sqlite3_close(db);
4160+
remove_test_file(path);
4161+
}
4162+
#endif
4163+
4164+
TEST(sqlite_saved_custom_model_initializes_lazily_after_register) {
4165+
const char *path = TEST_TMP_DIR "/dbmem_saved_custom_model.sqlite";
4166+
remove_test_file(path);
4167+
4168+
sqlite3 *db = open_test_db_path(path);
4169+
ASSERT(db != NULL);
4170+
4171+
dbmem_provider_t prov = { .init = dummy_init, .compute = dummy_compute, .free = dummy_free };
4172+
int rc = sqlite3_memory_register_provider(db, "dummy", &prov);
4173+
ASSERT_EQ(rc, SQLITE_OK);
4174+
4175+
sqlite3_int64 result = 0;
4176+
rc = exec_get_int(db, "SELECT memory_set_model('dummy', 'test-model');", &result);
4177+
ASSERT_EQ(rc, SQLITE_OK);
4178+
sqlite3_close(db);
4179+
4180+
dummy_init_calls = 0;
4181+
dummy_compute_calls = 0;
4182+
4183+
db = open_test_db_path(path);
4184+
ASSERT(db != NULL);
4185+
4186+
dbmem_context *ctx = get_test_ctx(db);
4187+
ASSERT(ctx != NULL);
4188+
bool is_local = true;
4189+
ASSERT(dbmem_context_engine(ctx, &is_local) == NULL);
4190+
ASSERT_EQ(dummy_init_calls, 0);
4191+
4192+
rc = sqlite3_memory_register_provider(db, "dummy", &prov);
4193+
ASSERT_EQ(rc, SQLITE_OK);
4194+
ASSERT_EQ(dummy_init_calls, 0);
4195+
4196+
rc = exec_get_int(db, "SELECT memory_add_text('Saved custom provider settings should load lazily.');", &result);
4197+
ASSERT_EQ(rc, SQLITE_OK);
4198+
ASSERT(result >= 1);
4199+
ASSERT_EQ(dummy_init_calls, 1);
4200+
ASSERT(dummy_compute_calls >= 1);
4201+
4202+
ASSERT(dbmem_context_engine(ctx, &is_local) != NULL);
4203+
ASSERT_EQ(is_local, false);
4204+
4205+
sqlite3_close(db);
4206+
remove_test_file(path);
4207+
}
4208+
40724209
TEST(sqlite_custom_provider_add_text) {
40734210
sqlite3 *db = open_test_db();
40744211
ASSERT(db != NULL);
@@ -4662,6 +4799,13 @@ int main(int argc, char *argv[]) {
46624799
RUN_TEST(sqlite_custom_provider_register);
46634800
RUN_TEST(sqlite_custom_provider_set_model);
46644801
RUN_TEST(sqlite_memory_add_text_requires_model);
4802+
#ifndef DBMEM_OMIT_REMOTE_ENGINE
4803+
RUN_TEST(sqlite_saved_remote_model_initializes_lazily_after_apikey);
4804+
#endif
4805+
#ifndef DBMEM_OMIT_LOCAL_ENGINE
4806+
RUN_TEST(sqlite_saved_local_model_initializes_lazily);
4807+
#endif
4808+
RUN_TEST(sqlite_saved_custom_model_initializes_lazily_after_register);
46654809
RUN_TEST(sqlite_custom_provider_add_text);
46664810
RUN_TEST(sqlite_memory_reindex_refreshes_synced_value_changes);
46674811
RUN_TEST(sqlite_custom_provider_skips_whitespace_only_text);

0 commit comments

Comments
 (0)