Skip to main content
Glama
custom-table.cpp15 kB
class CustomTable { public: explicit CustomTable( v8::Isolate* isolate, Database* db, const char* name, v8::Local<v8::Function> factory ) : addon(db->GetAddon()), isolate(isolate), db(db), name(name), factory(isolate, factory) {} static void Destructor(void* self) { delete static_cast<CustomTable*>(self); } static sqlite3_module MODULE; static sqlite3_module EPONYMOUS_MODULE; private: // This nested class is instantiated on each CREATE VIRTUAL TABLE statement. class VTab { friend class CustomTable; explicit VTab( CustomTable* parent, v8::Local<v8::Function> generator, std::vector<std::string> parameter_names, bool safe_ints ) : parent(parent), parameter_count(parameter_names.size()), safe_ints(safe_ints), generator(parent->isolate, generator), parameter_names(parameter_names) { ((void)base); } static inline CustomTable::VTab* Upcast(sqlite3_vtab* vtab) { return reinterpret_cast<VTab*>(vtab); } inline sqlite3_vtab* Downcast() { return reinterpret_cast<sqlite3_vtab*>(this); } sqlite3_vtab base; CustomTable * const parent; const int parameter_count; const bool safe_ints; const v8::Global<v8::Function> generator; const std::vector<std::string> parameter_names; }; // This nested class is instantiated each time a virtual table is scanned. class Cursor { friend class CustomTable; static inline CustomTable::Cursor* Upcast(sqlite3_vtab_cursor* cursor) { return reinterpret_cast<Cursor*>(cursor); } inline sqlite3_vtab_cursor* Downcast() { return reinterpret_cast<sqlite3_vtab_cursor*>(this); } inline CustomTable::VTab* GetVTab() { return VTab::Upcast(base.pVtab); } sqlite3_vtab_cursor base; v8::Global<v8::Object> iterator; v8::Global<v8::Function> next; v8::Global<v8::Array> row; bool done; sqlite_int64 rowid; }; // This nested class is used by Data::ResultValueFromJS to report errors. class TempDataConverter : DataConverter { friend class CustomTable; explicit TempDataConverter(CustomTable* parent) : parent(parent), status(SQLITE_OK) {} void PropagateJSError(sqlite3_context* invocation) { status = SQLITE_ERROR; parent->PropagateJSError(); } std::string GetDataErrorPrefix() { return std::string("Virtual table module \"") + parent->name + "\" yielded"; } CustomTable * const parent; int status; }; // Although this function does nothing, we cannot use xConnect directly, // because that would cause SQLite to register an eponymous virtual table. static int xCreate(sqlite3* db_handle, void* _self, int argc, const char* const * argv, sqlite3_vtab** output, char** errOutput) { return xConnect(db_handle, _self, argc, argv, output, errOutput); } // This method uses the factory function to instantiate a new virtual table. static int xConnect(sqlite3* db_handle, void* _self, int argc, const char* const * argv, sqlite3_vtab** output, char** errOutput) { CustomTable* self = static_cast<CustomTable*>(_self); v8::Isolate* isolate = self->isolate; v8::HandleScope scope(isolate); UseContext; v8::Local<v8::Value>* args = ALLOC_ARRAY<v8::Local<v8::Value>>(argc); for (int i = 0; i < argc; ++i) { args[i] = StringFromUtf8(isolate, argv[i], -1); } // Run the factory function to receive a new virtual table definition. v8::MaybeLocal<v8::Value> maybeReturnValue = self->factory.Get(isolate)->Call(ctx, v8::Undefined(isolate), argc, args); delete[] args; if (maybeReturnValue.IsEmpty()) { self->PropagateJSError(); return SQLITE_ERROR; } // Extract each part of the virtual table definition. v8::Local<v8::Array> returnValue = maybeReturnValue.ToLocalChecked().As<v8::Array>(); v8::Local<v8::String> sqlString = returnValue->Get(ctx, 0).ToLocalChecked().As<v8::String>(); v8::Local<v8::Function> generator = returnValue->Get(ctx, 1).ToLocalChecked().As<v8::Function>(); v8::Local<v8::Array> parameterNames = returnValue->Get(ctx, 2).ToLocalChecked().As<v8::Array>(); int safe_ints = returnValue->Get(ctx, 3).ToLocalChecked().As<v8::Int32>()->Value(); bool direct_only = returnValue->Get(ctx, 4).ToLocalChecked().As<v8::Boolean>()->Value(); v8::String::Utf8Value sql(isolate, sqlString); safe_ints = safe_ints < 2 ? safe_ints : static_cast<int>(self->db->GetState()->safe_ints); // Copy the parameter names into a std::vector. std::vector<std::string> parameter_names; for (int i = 0, len = parameterNames->Length(); i < len; ++i) { v8::Local<v8::String> parameterName = parameterNames->Get(ctx, i).ToLocalChecked().As<v8::String>(); v8::String::Utf8Value parameter_name(isolate, parameterName); parameter_names.emplace_back(*parameter_name); } // Pass our SQL table definition to SQLite (this should never fail). if (sqlite3_declare_vtab(db_handle, *sql) != SQLITE_OK) { *errOutput = sqlite3_mprintf("failed to declare virtual table \"%s\"", argv[2]); return SQLITE_ERROR; } if (direct_only && sqlite3_vtab_config(db_handle, SQLITE_VTAB_DIRECTONLY) != SQLITE_OK) { *errOutput = sqlite3_mprintf("failed to configure virtual table \"%s\"", argv[2]); return SQLITE_ERROR; } // Return the successfully created virtual table. *output = (new VTab(self, generator, parameter_names, safe_ints))->Downcast(); return SQLITE_OK; } static int xDisconnect(sqlite3_vtab* vtab) { delete VTab::Upcast(vtab); return SQLITE_OK; } static int xOpen(sqlite3_vtab* vtab, sqlite3_vtab_cursor** output) { *output = (new Cursor())->Downcast(); return SQLITE_OK; } static int xClose(sqlite3_vtab_cursor* cursor) { delete Cursor::Upcast(cursor); return SQLITE_OK; } // This method uses a fresh cursor to start a new scan of a virtual table. // The args and idxNum are provided by xBestIndex (idxStr is unused). // idxNum is a bitmap that provides the proper indices of the received args. static int xFilter(sqlite3_vtab_cursor* _cursor, int idxNum, const char* idxStr, int argc, sqlite3_value** argv) { Cursor* cursor = Cursor::Upcast(_cursor); VTab* vtab = cursor->GetVTab(); CustomTable* self = vtab->parent; Addon* addon = self->addon; v8::Isolate* isolate = self->isolate; v8::HandleScope scope(isolate); UseContext; // Convert the SQLite arguments into JavaScript arguments. Note that // the values in argv may be in the wrong order, so we fix that here. v8::Local<v8::Value> args_fast[4]; v8::Local<v8::Value>* args = NULL; int parameter_count = vtab->parameter_count; if (parameter_count != 0) { args = parameter_count <= 4 ? args_fast : ALLOC_ARRAY<v8::Local<v8::Value>>(parameter_count); int argn = 0; bool safe_ints = vtab->safe_ints; for (int i = 0; i < parameter_count; ++i) { if (idxNum & 1 << i) { args[i] = Data::GetValueJS(isolate, argv[argn++], safe_ints); // If any arguments are NULL, the result set is necessarily // empty, so don't bother to run the generator function. if (args[i]->IsNull()) { if (args != args_fast) delete[] args; cursor->done = true; return SQLITE_OK; } } else { args[i] = v8::Undefined(isolate); } } } // Invoke the generator function to create a new iterator. v8::MaybeLocal<v8::Value> maybeIterator = vtab->generator.Get(isolate)->Call(ctx, v8::Undefined(isolate), parameter_count, args); if (args != args_fast) delete[] args; if (maybeIterator.IsEmpty()) { self->PropagateJSError(); return SQLITE_ERROR; } // Store the iterator and its next() method; we'll be using it a lot. v8::Local<v8::Object> iterator = maybeIterator.ToLocalChecked().As<v8::Object>(); v8::Local<v8::Function> next = iterator->Get(ctx, addon->cs.next.Get(isolate)).ToLocalChecked().As<v8::Function>(); cursor->iterator.Reset(isolate, iterator); cursor->next.Reset(isolate, next); cursor->rowid = 0; // Advance the iterator/cursor to the first row. return xNext(cursor->Downcast()); } // This method advances a virtual table's cursor to the next row. // SQLite will call this method repeatedly, driving the generator function. static int xNext(sqlite3_vtab_cursor* _cursor) { Cursor* cursor = Cursor::Upcast(_cursor); CustomTable* self = cursor->GetVTab()->parent; Addon* addon = self->addon; v8::Isolate* isolate = self->isolate; v8::HandleScope scope(isolate); UseContext; v8::Local<v8::Object> iterator = cursor->iterator.Get(isolate); v8::Local<v8::Function> next = cursor->next.Get(isolate); v8::MaybeLocal<v8::Value> maybeRecord = next->Call(ctx, iterator, 0, NULL); if (maybeRecord.IsEmpty()) { self->PropagateJSError(); return SQLITE_ERROR; } v8::Local<v8::Object> record = maybeRecord.ToLocalChecked().As<v8::Object>(); bool done = record->Get(ctx, addon->cs.done.Get(isolate)).ToLocalChecked().As<v8::Boolean>()->Value(); if (!done) { cursor->row.Reset(isolate, record->Get(ctx, addon->cs.value.Get(isolate)).ToLocalChecked().As<v8::Array>()); } cursor->done = done; cursor->rowid += 1; return SQLITE_OK; } // If this method returns 1, SQLite will stop scanning the virtual table. static int xEof(sqlite3_vtab_cursor* cursor) { return Cursor::Upcast(cursor)->done; } // This method extracts some column from the cursor's current row. static int xColumn(sqlite3_vtab_cursor* _cursor, sqlite3_context* invocation, int column) { Cursor* cursor = Cursor::Upcast(_cursor); CustomTable* self = cursor->GetVTab()->parent; TempDataConverter temp_data_converter(self); v8::Isolate* isolate = self->isolate; v8::HandleScope scope(isolate); v8::Local<v8::Array> row = cursor->row.Get(isolate); v8::MaybeLocal<v8::Value> maybeColumnValue = row->Get(OnlyContext, column); if (maybeColumnValue.IsEmpty()) { temp_data_converter.PropagateJSError(NULL); } else { Data::ResultValueFromJS(isolate, invocation, maybeColumnValue.ToLocalChecked(), &temp_data_converter); } return temp_data_converter.status; } // This method outputs the rowid of the cursor's current row. static int xRowid(sqlite3_vtab_cursor* cursor, sqlite_int64* output) { *output = Cursor::Upcast(cursor)->rowid; return SQLITE_OK; } // This method tells SQLite how to *plan* queries on our virtual table. // It gets invoked (typically multiple times) during db.prepare(). static int xBestIndex(sqlite3_vtab* vtab, sqlite3_index_info* output) { int parameter_count = VTab::Upcast(vtab)->parameter_count; int argument_count = 0; std::vector<std::pair<int, int>> forwarded; for (int i = 0, len = output->nConstraint; i < len; ++i) { auto item = output->aConstraint[i]; // The SQLITE_INDEX_CONSTRAINT_LIMIT and SQLITE_INDEX_CONSTRAINT_OFFSET // operators have no left-hand operand, and so for those operators the // corresponding item.iColumn is meaningless. // We don't care those constraints. if (item.op == SQLITE_INDEX_CONSTRAINT_LIMIT || item.op == SQLITE_INDEX_CONSTRAINT_OFFSET) { continue; } // We only care about constraints on parameters, not regular columns. if (item.iColumn >= 0 && item.iColumn < parameter_count) { if (item.op != SQLITE_INDEX_CONSTRAINT_EQ) { sqlite3_free(vtab->zErrMsg); vtab->zErrMsg = sqlite3_mprintf( "virtual table parameter \"%s\" can only be constrained by the '=' operator", VTab::Upcast(vtab)->parameter_names.at(item.iColumn).c_str()); return SQLITE_ERROR; } if (!item.usable) { // Don't allow SQLite to make plans that ignore arguments. // Otherwise, a user could pass arguments, but then they // could appear undefined in the generator function. return SQLITE_CONSTRAINT; } forwarded.emplace_back(item.iColumn, i); } } // Tell SQLite to forward arguments to xFilter. std::sort(forwarded.begin(), forwarded.end()); for (std::pair<int, int> pair : forwarded) { int bit = 1 << pair.first; if (!(output->idxNum & bit)) { output->idxNum |= bit; output->aConstraintUsage[pair.second].argvIndex = ++argument_count; output->aConstraintUsage[pair.second].omit = 1; } } // Use a very high estimated cost so SQLite is not tempted to invoke the // generator function within a loop, if it can be avoided. output->estimatedCost = output->estimatedRows = 1000000000 / (argument_count + 1); return SQLITE_OK; } void PropagateJSError() { assert(db->GetState()->was_js_error == false); db->GetState()->was_js_error = true; } Addon* const addon; v8::Isolate* const isolate; Database* const db; const std::string name; const v8::Global<v8::Function> factory; }; sqlite3_module CustomTable::MODULE = { 0, /* iVersion */ xCreate, /* xCreate */ xConnect, /* xConnect */ xBestIndex, /* xBestIndex */ xDisconnect, /* xDisconnect */ xDisconnect, /* xDestroy */ xOpen, /* xOpen */ xClose, /* xClose */ xFilter, /* xFilter */ xNext, /* xNext */ xEof, /* xEof */ xColumn, /* xColumn */ xRowid, /* xRowid */ NULL, /* xUpdate */ NULL, /* xBegin */ NULL, /* xSync */ NULL, /* xCommit */ NULL, /* xRollback */ NULL, /* xFindMethod */ NULL, /* xRename */ NULL, /* xSavepoint */ NULL, /* xRelease */ NULL, /* xRollbackTo */ NULL, /* xShadowName */ NULL /* xIntegrity */ }; sqlite3_module CustomTable::EPONYMOUS_MODULE = { 0, /* iVersion */ NULL, /* xCreate */ xConnect, /* xConnect */ xBestIndex, /* xBestIndex */ xDisconnect, /* xDisconnect */ xDisconnect, /* xDestroy */ xOpen, /* xOpen */ xClose, /* xClose */ xFilter, /* xFilter */ xNext, /* xNext */ xEof, /* xEof */ xColumn, /* xColumn */ xRowid, /* xRowid */ NULL, /* xUpdate */ NULL, /* xBegin */ NULL, /* xSync */ NULL, /* xCommit */ NULL, /* xRollback */ NULL, /* xFindMethod */ NULL, /* xRename */ NULL, /* xSavepoint */ NULL, /* xRelease */ NULL, /* xRollbackTo */ NULL, /* xShadowName */ NULL /* xIntegrity */ };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/mnehmos.synch.mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server