diff options
Diffstat (limited to 'src/bun.js/bindings/sqlite/JSSQLStatement.cpp')
-rw-r--r-- | src/bun.js/bindings/sqlite/JSSQLStatement.cpp | 1409 |
1 files changed, 1409 insertions, 0 deletions
diff --git a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp new file mode 100644 index 000000000..7f740ae66 --- /dev/null +++ b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp @@ -0,0 +1,1409 @@ +#include "root.h" + +#include "JSSQLStatement.h" +#include "JavaScriptCore/JSObjectInlines.h" +#include "wtf/text/ExternalStringImpl.h" + +#include "JavaScriptCore/FunctionPrototype.h" +#include "JavaScriptCore/HeapAnalyzer.h" + +#include "JavaScriptCore/JSDestructibleObjectHeapCellType.h" +#include "JavaScriptCore/SlotVisitorMacros.h" +#include "JavaScriptCore/ObjectConstructor.h" +#include "JavaScriptCore/SubspaceInlines.h" +#include "wtf/GetPtr.h" +#include "wtf/PointerPreparations.h" +#include "wtf/URL.h" +#include "JavaScriptCore/TypedArrayInlines.h" +#include "JavaScriptCore/PropertyNameArray.h" +#include "Buffer.h" +#include "GCDefferalContext.h" +#include "Buffer.h" + +/* ******************************************************************************** */ +// Lazy Load SQLite on macOS +// This seemed to be about 3% faster on macOS +// but it might be noise +// it's kind of hard to tell +// it should be strictly better though because +// instead of two pointers, one for DYLD_STUB$$ and one for the actual library +// we only call one pointer for the actual library +// and it means there's less work for DYLD to do on startup +// i.e. it shouldn't have any impact on startup time +#ifdef LAZY_LOAD_SQLITE +#include "lazy_sqlite3.h" +#else +static inline int lazyLoadSQLite() +{ + return 0; +} + +#endif +/* ******************************************************************************** */ + +static WTF::String sqliteString(const char* str) +{ + auto res = WTF::String::fromUTF8(str); + sqlite3_free((void*)str); + return res; +} + +static void sqlite_free_typed_array(void* ctx, void* buf) +{ + sqlite3_free((void*)buf); +} + +/** This is like a 3x perf improvement for allocating objects **/ +#define SQL_USE_PROTOTYPE 1 + +static int DEFAULT_SQLITE_FLAGS + = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; +static unsigned int DEFAULT_SQLITE_PREPARE_FLAGS = SQLITE_PREPARE_PERSISTENT; +static int MAX_SQLITE_PREPARE_FLAG = SQLITE_PREPARE_PERSISTENT | SQLITE_PREPARE_NORMALIZE | SQLITE_PREPARE_NO_VTAB; + +static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementPrepareStatementFunction); +static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteFunction); +static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementOpenStatementFunction); +static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementIsInTransactionFunction); + +static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction); + +static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunction); +static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRun); +static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionGet); +static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionAll); +static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRows); + +static JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnNames); +static JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnCount); + +static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementSerialize); +static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementDeserialize); + +#define CHECK_THIS \ + if (UNLIKELY(!castedThis)) { \ + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQLStatement"_s)); \ + return JSValue::encode(jsUndefined()); \ + } + +#define DO_REBIND(param) \ + if (param.isObject()) { \ + JSC::JSValue reb = castedThis->rebind(lexicalGlobalObject, param, true); \ + if (UNLIKELY(!reb.isNumber())) { \ + return JSValue::encode(reb); /* this means an error */ \ + } \ + } else { \ + throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Expected object or array"_s)); \ + return JSValue::encode(jsUndefined()); \ + } + +#define CHECK_PREPARED \ + if (UNLIKELY(castedThis->stmt == nullptr || castedThis->db == nullptr)) { \ + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Statement has finalized"_s)); \ + return JSValue::encode(jsUndefined()); \ + } + +namespace WebCore { +using namespace JSC; + +class JSSQLStatement : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSSQLStatement* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject, sqlite3_stmt* stmt, sqlite3* db) + { + JSSQLStatement* ptr = new (NotNull, JSC::allocateCell<JSSQLStatement>(globalObject->vm())) JSSQLStatement(structure, *globalObject, stmt, db); + ptr->finishCreation(globalObject->vm()); + return ptr; + } + static void destroy(JSC::JSCell*); + template<typename, SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return WebCore::subspaceForImpl<JSSQLStatement, UseCustomHeapCellType::No>( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForJSSQLStatement.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSSQLStatement = WTFMove(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSSQLStatement.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSSQLStatement = WTFMove(space); }); + } + DECLARE_VISIT_CHILDREN; + DECLARE_EXPORT_INFO; + + // static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + + JSC::JSValue rebind(JSGlobalObject* globalObject, JSC::JSValue values, bool clone); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + + ~JSSQLStatement(); + + sqlite3_stmt* stmt; + sqlite3* db; + bool hasExecuted = false; + PropertyNameArray columnNames; + mutable WriteBarrier<JSC::JSArray> _columnNames; + mutable WriteBarrier<JSC::JSObject> _prototype; + +protected: + JSSQLStatement(JSC::Structure* structure, JSDOMGlobalObject& globalObject, sqlite3_stmt* stmt, sqlite3* db) + : Base(globalObject.vm(), structure) + , columnNames(globalObject.vm(), PropertyNameMode::Strings, PrivateSymbolMode::Exclude) + , _columnNames(globalObject.vm(), this, nullptr) + , _prototype(globalObject.vm(), this, nullptr) + + { + this->stmt = stmt; + this->db = db; + } + + void finishCreation(JSC::VM&); +}; + +void JSSQLStatement::destroy(JSC::JSCell* cell) +{ + JSSQLStatement* thisObject = static_cast<JSSQLStatement*>(cell); + sqlite3_finalize(thisObject->stmt); + thisObject->stmt = nullptr; +} + +void JSSQLStatementConstructor::destroy(JSC::JSCell* cell) +{ +} + +static inline bool rebindValue(JSC::JSGlobalObject* lexicalGlobalObject, sqlite3_stmt* stmt, int i, JSC::JSValue value, JSC::ThrowScope& scope, bool clone) +{ +#define CHECK_BIND(param) \ + int result = param; \ + if (UNLIKELY(result != SQLITE_OK)) { \ + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(result)))); \ + return false; \ + } + // only clone if necessary + // SQLite has a way to call a destructor + // but there doesn't seem to be a way to pass a pointer? + // we can't use it if there's no pointer to ref/unref + auto transientOrStatic = (void (*)(void*))(clone ? SQLITE_TRANSIENT : SQLITE_STATIC); + + if (value.isUndefinedOrNull()) { + CHECK_BIND(sqlite3_bind_null(stmt, i)); + } else if (value.isBoolean()) { + CHECK_BIND(sqlite3_bind_int(stmt, i, value.toBoolean(lexicalGlobalObject) ? 1 : 0)); + } else if (value.isAnyInt()) { + int64_t val = value.asAnyInt(); + if (val < INT_MIN || val > INT_MAX) { + CHECK_BIND(sqlite3_bind_int64(stmt, i, val)); + } else { + CHECK_BIND(sqlite3_bind_int(stmt, i, val)) + } + } else if (value.isNumber()) { + CHECK_BIND(sqlite3_bind_double(stmt, i, value.asDouble())) + } else if (value.isString()) { + auto* str = value.toStringOrNull(lexicalGlobalObject); + if (UNLIKELY(!str)) { + throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Expected string"_s)); + return false; + } + + auto roped = str->tryGetValue(lexicalGlobalObject); + if (UNLIKELY(!roped)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Out of memory :("_s)); + return false; + } + + if (roped.is8Bit()) { + CHECK_BIND(sqlite3_bind_text(stmt, i, reinterpret_cast<const char*>(roped.characters8()), roped.length(), transientOrStatic)); + } else { + CHECK_BIND(sqlite3_bind_text16(stmt, i, roped.characters16(), roped.length() * 2, transientOrStatic)); + } + + } else if (UNLIKELY(value.isHeapBigInt())) { + CHECK_BIND(sqlite3_bind_int64(stmt, i, JSBigInt::toBigInt64(value))); + } else if (JSC::JSArrayBufferView* buffer = JSC::jsDynamicCast<JSC::JSArrayBufferView*>(value)) { + CHECK_BIND(sqlite3_bind_blob(stmt, i, buffer->vector(), buffer->byteLength(), transientOrStatic)); + } else { + throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Binding expected string, TypedArray, boolean, number, bigint or null"_s)); + return false; + } + + return true; +#undef CHECK_BIND +} + +// this function does the equivalent of +// Object.entries(obj) +// except without the intermediate array of arrays +static JSC::JSValue rebindObject(JSC::JSGlobalObject* globalObject, JSC::JSValue targetValue, JSC::ThrowScope& scope, sqlite3_stmt* stmt, bool clone) +{ + JSObject* target = targetValue.toObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + JSC::VM& vm = globalObject->vm(); + PropertyNameArray properties(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude); + target->methodTable()->getOwnPropertyNames(target, globalObject, properties, DontEnumPropertiesMode::Include); + RETURN_IF_EXCEPTION(scope, {}); + int count = 0; + + for (const auto& propertyName : properties) { + PropertySlot slot(target, PropertySlot::InternalMethodType::GetOwnProperty); + bool hasProperty = target->methodTable()->getOwnPropertySlot(target, globalObject, propertyName, slot); + RETURN_IF_EXCEPTION(scope, JSValue()); + if (!hasProperty) + continue; + if (slot.attributes() & PropertyAttribute::DontEnum) + continue; + + JSValue value; + if (LIKELY(!slot.isTaintedByOpaqueObject())) + value = slot.getValue(globalObject, propertyName); + else + value = target->get(globalObject, propertyName); + + // Ensure this gets freed on scope clear + auto utf8 = WTF::String(propertyName.string()).utf8(); + + int index = sqlite3_bind_parameter_index(stmt, utf8.data()); + if (index == 0) { + throwException(globalObject, scope, createError(globalObject, "Unknown parameter \"" + propertyName.string() + "\""_s)); + return JSValue(); + } + + if (!rebindValue(globalObject, stmt, index, value, scope, clone)) + return JSValue(); + RETURN_IF_EXCEPTION(scope, {}); + count++; + } + + return jsNumber(count); +} + +static JSC::JSValue rebindStatement(JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue values, JSC::ThrowScope& scope, sqlite3_stmt* stmt, bool clone) +{ + sqlite3_clear_bindings(stmt); + JSC::JSArray* array = jsDynamicCast<JSC::JSArray*>(values); + int max = sqlite3_bind_parameter_count(stmt); + + if (!array) { + if (JSC::JSObject* object = values.getObject()) { + auto res = rebindObject(lexicalGlobalObject, object, scope, stmt, clone); + RETURN_IF_EXCEPTION(scope, {}); + return res; + } + + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected array"_s)); + return jsUndefined(); + } + + int count = array->length(); + + if (count == 0) { + return jsNumber(0); + } + + if (count != max) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected " + String::number(max) + " values, got " + String::number(count))); + return jsUndefined(); + } + + int i = 0; + for (; i < count; i++) { + JSC::JSValue value = array->getIndexQuickly(i); + rebindValue(lexicalGlobalObject, stmt, i + 1, value, scope, clone); + RETURN_IF_EXCEPTION(scope, {}); + } + + return jsNumber(i); +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementSetCustomSQLite, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject()); + if (UNLIKELY(!thisObject)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQL"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + if (callFrame->argumentCount() < 1) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected 1 argument"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + JSC::JSValue sqliteStrValue = callFrame->argument(0); + if (UNLIKELY(!sqliteStrValue.isString())) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQLite path"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + +#ifdef LAZY_LOAD_SQLITE + if (sqlite3_handle) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "SQLite already loaded\nThis function can only be called before SQLite has been loaded and exactly once. SQLite auto-loads when the first time you open a Database."_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + sqlite3_lib_path = sqliteStrValue.toWTFString(lexicalGlobalObject).utf8().data(); + if (lazyLoadSQLite() == -1) { + sqlite3_handle = nullptr; + WTF::String msg = WTF::String::fromUTF8(dlerror()); + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, msg)); + return JSValue::encode(JSC::jsUndefined()); + } +#endif + + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::jsBoolean(true))); +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementDeserialize, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject()); + if (UNLIKELY(!thisObject)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQL"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + if (callFrame->argumentCount() < 1) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected 1 argument"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + JSC::JSArrayBufferView* array = jsDynamicCast<JSC::JSArrayBufferView*>(callFrame->argument(0)); + if (UNLIKELY(!array)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected Uint8Array or Buffer"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + if (UNLIKELY(array->isDetached())) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "TypedArray is detached"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + size_t byteLength = array->byteLength(); + void* ptr = array->vector(); + if (UNLIKELY(ptr == nullptr || byteLength == 0)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "ArrayBuffer must not be empty"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + void* data = sqlite3_malloc64(byteLength); + if (UNLIKELY(data == nullptr)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Failed to allocate memory"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + if (byteLength) { + memcpy(data, ptr, byteLength); + } + + unsigned int flags = SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE; + + if (callFrame->argumentCount() > 1 and callFrame->argument(1).toBoolean(lexicalGlobalObject)) { + flags |= SQLITE_DESERIALIZE_READONLY; + } + + sqlite3* db = nullptr; + if (sqlite3_open_v2(":memory:", &db, DEFAULT_SQLITE_FLAGS, nullptr) != SQLITE_OK) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Failed to open SQLite"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + int status = sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL); + assert(status == SQLITE_OK); + status = sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL); + assert(status == SQLITE_OK); + + status = sqlite3_deserialize(db, "main", reinterpret_cast<unsigned char*>(data), byteLength, byteLength, flags); + if (status == SQLITE_BUSY) { + sqlite3_free(data); + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "SQLITE_BUSY"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + if (status != SQLITE_OK) { + sqlite3_free(data); + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, status == SQLITE_ERROR ? "unable to deserialize database"_s : sqliteString(sqlite3_errstr(status)))); + return JSValue::encode(JSC::jsUndefined()); + } + + auto count = thisObject->databases.size(); + thisObject->databases.append(db); + RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(count))); +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementSerialize, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject()); + if (UNLIKELY(!thisObject)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQL"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + int32_t dbIndex = callFrame->argument(0).toInt32(lexicalGlobalObject); + if (UNLIKELY(dbIndex < 0 || dbIndex >= thisObject->databases.size())) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + sqlite3* db = thisObject->databases[dbIndex]; + if (UNLIKELY(!db)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Can't do this on a closed database"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + WTF::String attachedName = callFrame->argument(1).toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined())); + + if (attachedName.isEmpty()) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected attached database name"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + sqlite3_int64 length = -1; + unsigned char* data = sqlite3_serialize(db, attachedName.utf8().data(), &length, 0); + if (UNLIKELY(data == nullptr && length)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Out of memory"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + RELEASE_AND_RETURN(scope, JSBuffer__bufferFromPointerAndLengthAndDeinit(lexicalGlobalObject, reinterpret_cast<char*>(data), static_cast<unsigned int>(length), data, sqlite_free_typed_array)); +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject()); + if (UNLIKELY(!thisObject)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQL"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + int32_t dbIndex = callFrame->argument(0).toInt32(lexicalGlobalObject); + if (UNLIKELY(dbIndex < 0 || dbIndex >= thisObject->databases.size())) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + JSC::JSValue extension = callFrame->argument(1); + if (UNLIKELY(!extension.isString())) { + throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Expected string"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + auto extensionString = extension.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + + sqlite3* db = thisObject->databases[dbIndex]; + if (UNLIKELY(!db)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Can't do this on a closed database"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + auto entryPointStr = callFrame->argumentCount() > 2 && callFrame->argument(2).isString() ? callFrame->argument(2).toWTFString(lexicalGlobalObject) : String(); + const char* entryPoint = entryPointStr.length() == 0 ? NULL : entryPointStr.utf8().data(); + char* error; + int rc = sqlite3_load_extension(db, extensionString.utf8().data(), entryPoint, &error); + + // TODO: can we disable loading extensions after this? + if (rc != SQLITE_OK) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, sqliteString(error != nullptr ? error : sqlite3_errmsg(db)))); + return JSValue::encode(JSC::jsUndefined()); + } + + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::jsUndefined())); +} + +// This runs a query one-off +// without the overhead of a long-lived statement object +// does not return anything +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject()); + if (UNLIKELY(!thisObject)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQL"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + if (callFrame->argumentCount() < 2) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected at least 2 arguments"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + int handle = callFrame->argument(0).toInt32(lexicalGlobalObject); + if (thisObject->databases.size() < handle) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + sqlite3* db = thisObject->databases[handle]; + + if (UNLIKELY(!db)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Database has closed"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + JSC::JSValue sqlValue = callFrame->argument(1); + if (UNLIKELY(!sqlValue.isString())) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQL string"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + EnsureStillAliveScope bindingsAliveScope = callFrame->argumentCount() > 2 ? callFrame->argument(2) : jsUndefined(); + + auto sqlString = sqlValue.toWTFString(lexicalGlobalObject); + if (UNLIKELY(sqlString.length() == 0)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "SQL string mustn't be blank"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + // TODO: trim whitespace & newlines before sending + // we don't because webkit doesn't expose a function that makes this super + // easy without using unicode whitespace definition the + // StringPrototype.trim() implementation GC allocates a new JSString* and + // potentially re-allocates the string (not 100% sure if reallocates) so we + // can't use that here + sqlite3_stmt* statement = nullptr; + + int rc = SQLITE_OK; + if (sqlString.is8Bit()) { + rc = sqlite3_prepare_v3(db, reinterpret_cast<const char*>(sqlString.characters8()), sqlString.length(), 0, &statement, nullptr); + } else { + rc = sqlite3_prepare16_v3(db, sqlString.characters16(), sqlString.length() * 2, 0, &statement, nullptr); + } + + if (rc != SQLITE_OK || statement == nullptr) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errmsg(db)))); + // sqlite3 handles when the pointer is null + sqlite3_finalize(statement); + return JSValue::encode(JSC::jsUndefined()); + } + + if (!bindingsAliveScope.value().isUndefinedOrNull()) { + if (bindingsAliveScope.value().isObject()) { + JSC::JSValue reb = rebindStatement(lexicalGlobalObject, bindingsAliveScope.value(), scope, statement, false); + if (UNLIKELY(!reb.isNumber())) { + sqlite3_finalize(statement); + return JSValue::encode(reb); /* this means an error */ + } + } else { + throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Expected bindings to be an object or array"_s)); + sqlite3_finalize(statement); + return JSValue::encode(jsUndefined()); + } + } + + rc = sqlite3_step(statement); + + // we don't care about the results, therefore the row-by-row output doesn't matter + // that's why we don't bother to loop through the results + if (rc != SQLITE_DONE and rc != SQLITE_ROW) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(rc)))); + // we finalize after just incase something about error messages in + // sqlite depends on the existence of the most recent statement i don't + // think that's actually how this works - just being cautious + sqlite3_finalize(statement); + return JSValue::encode(JSC::jsUndefined()); + } + + sqlite3_finalize(statement); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementIsInTransactionFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject()); + if (UNLIKELY(!thisObject)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQLStatement"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + JSC::JSValue dbNumber = callFrame->argument(0); + + if (!dbNumber.isNumber()) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + int handle = dbNumber.toInt32(lexicalGlobalObject); + + if (handle < 0 || handle > thisObject->databases.size()) { + throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid database handle"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + sqlite3* db = thisObject->databases[handle]; + + if (UNLIKELY(!db)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Database has closed"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + RELEASE_AND_RETURN(scope, JSValue::encode(jsBoolean(!sqlite3_get_autocommit(db)))); +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementPrepareStatementFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject()); + if (UNLIKELY(!thisObject)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQLStatement"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + JSC::JSValue dbNumber = callFrame->argument(0); + JSC::JSValue sqlValue = callFrame->argument(1); + JSC::JSValue bindings = callFrame->argument(2); + JSC::JSValue prepareFlagsValue = callFrame->argument(3); + + if (!dbNumber.isNumber() || !sqlValue.isString()) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "SQLStatement requires a number and a string"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + int handle = dbNumber.toInt32(lexicalGlobalObject); + if (handle < 0 || handle > thisObject->databases.size()) { + throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid database handle"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + sqlite3* db = thisObject->databases[handle]; + if (!db) { + throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Cannot use a closed database"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + auto sqlString = sqlValue.toWTFString(lexicalGlobalObject); + if (!sqlString.length()) { + throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid SQL statement"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + unsigned int flags = DEFAULT_SQLITE_PREPARE_FLAGS; + if (prepareFlagsValue.isNumber()) { + + int prepareFlags = prepareFlagsValue.toInt32(lexicalGlobalObject); + if (prepareFlags < 0 || prepareFlags > MAX_SQLITE_PREPARE_FLAG) { + throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid prepare flags"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + flags = static_cast<unsigned int>(prepareFlags); + } + + sqlite3_stmt* statement = nullptr; + + int rc = SQLITE_OK; + if (sqlString.is8Bit()) { + rc = sqlite3_prepare_v3(db, reinterpret_cast<const char*>(sqlString.characters8()), sqlString.length(), flags, &statement, nullptr); + } else { + rc = sqlite3_prepare16_v3(db, sqlString.characters16(), sqlString.length() * 2, flags, &statement, nullptr); + } + + if (rc != SQLITE_OK) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errmsg(db)))); + return JSValue::encode(JSC::jsUndefined()); + } + + auto* structure = JSSQLStatement::createStructure(vm, lexicalGlobalObject, lexicalGlobalObject->objectPrototype()); + // auto* structure = JSSQLStatement::createStructure(vm, globalObject(), thisObject->getDirect(vm, vm.propertyNames->prototype)); + JSSQLStatement* sqlStatement = JSSQLStatement::create(structure, reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject), statement, db); + sqlStatement->db = db; + if (bindings.isObject()) { + auto* castedThis = sqlStatement; + DO_REBIND(bindings) + } + return JSValue::encode(JSValue(sqlStatement)); +} + +JSSQLStatementConstructor* JSSQLStatementConstructor::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) +{ + NativeExecutable* executable = vm.getHostFunction(jsSQLStatementPrepareStatementFunction, callHostFunctionAsConstructor, String("SQLStatement"_s)); + JSSQLStatementConstructor* ptr = new (NotNull, JSC::allocateCell<JSSQLStatementConstructor>(vm)) JSSQLStatementConstructor(vm, executable, globalObject, structure); + ptr->finishCreation(vm); + + return ptr; +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementOpenStatementFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + JSSQLStatementConstructor* constructor = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject()); + if (!constructor) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQLStatement"_s)); + return JSValue::encode(jsUndefined()); + } + + if (callFrame->argumentCount() < 1) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected 1 argument"_s)); + return JSValue::encode(jsUndefined()); + } + + JSValue pathValue = callFrame->argument(0); + if (!pathValue.isString()) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected string"_s)); + return JSValue::encode(jsUndefined()); + } + +#if LAZY_LOAD_SQLITE + if (UNLIKELY(lazyLoadSQLite() < 0)) { + WTF::String msg = WTF::String::fromUTF8(dlerror()); + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, msg)); + return JSValue::encode(JSC::jsUndefined()); + } +#endif + + auto catchScope = DECLARE_CATCH_SCOPE(vm); + String path = pathValue.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(catchScope, JSValue::encode(jsUndefined())); + catchScope.clearException(); + int openFlags = DEFAULT_SQLITE_FLAGS; + if (callFrame->argumentCount() > 1) { + JSValue flags = callFrame->argument(1); + if (!flags.isNumber()) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected number"_s)); + return JSValue::encode(jsUndefined()); + } + + openFlags = flags.toInt32(lexicalGlobalObject); + } + + sqlite3* db = nullptr; + int statusCode = sqlite3_open_v2(path.utf8().data(), &db, openFlags, nullptr); + if (statusCode != SQLITE_OK) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errmsg(db)))); + + return JSValue::encode(jsUndefined()); + } + + int status = sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL); + assert(status == SQLITE_OK); + status = sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL); + assert(status == SQLITE_OK); + + auto count = constructor->databases.size(); + constructor->databases.append(db); + RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(count))); +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementCloseStatementFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + JSSQLStatementConstructor* constructor = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject()); + + if (!constructor) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQLStatement"_s)); + return JSValue::encode(jsUndefined()); + } + + if (callFrame->argumentCount() < 1) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected 1 argument"_s)); + return JSValue::encode(jsUndefined()); + } + + JSValue dbNumber = callFrame->argument(0); + if (!dbNumber.isNumber()) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected number"_s)); + return JSValue::encode(jsUndefined()); + } + + int dbIndex = dbNumber.toInt32(lexicalGlobalObject); + + if (dbIndex < 0 || dbIndex >= constructor->databases.size()) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); + return JSValue::encode(jsUndefined()); + } + + sqlite3* db = constructor->databases[dbIndex]; + // no-op if already closed + if (!db) { + return JSValue::encode(jsUndefined()); + } + + int statusCode = sqlite3_close_v2(db); + if (statusCode != SQLITE_OK) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errmsg(db)))); + return JSValue::encode(jsUndefined()); + } + + constructor->databases[dbIndex] = nullptr; + return JSValue::encode(jsUndefined()); +} + +/* Hash table for constructor */ +static const HashTableValue JSSQLStatementConstructorTableValues[] = { + { "open"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementOpenStatementFunction), (intptr_t)(2) } }, + { "close"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementCloseStatementFunction), (intptr_t)(1) } }, + { "prepare"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementPrepareStatementFunction), (intptr_t)(2) } }, + { "run"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementExecuteFunction), (intptr_t)(3) } }, + { "isInTransaction"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementIsInTransactionFunction), (intptr_t)(1) } }, + { "loadExtension"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementLoadExtensionFunction), (intptr_t)(2) } }, + { "setCustomSQLite"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementSetCustomSQLite), (intptr_t)(1) } }, + { "serialize"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementSerialize), (intptr_t)(1) } }, + { "deserialize"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementDeserialize), (intptr_t)(2) } }, +}; + +const ClassInfo JSSQLStatementConstructor::s_info = { "SQLStatement"_s, nullptr, nullptr, nullptr, CREATE_METHOD_TABLE(JSSQLStatementConstructor) }; + +void JSSQLStatementConstructor::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + auto* structure = JSSQLStatement::createStructure(vm, globalObject(), globalObject()->objectPrototype()); + auto* proto = JSSQLStatement::create(structure, reinterpret_cast<Zig::GlobalObject*>(globalObject()), nullptr, nullptr); + this->putDirect(vm, vm.propertyNames->prototype, proto, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); + + reifyStaticProperties(vm, JSSQLStatementConstructor::info(), JSSQLStatementConstructorTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +static inline JSC::JSValue constructResultObject(JSC::JSGlobalObject* lexicalGlobalObject, JSSQLStatement* castedThis); +static inline JSC::JSValue constructResultObject(JSC::JSGlobalObject* lexicalGlobalObject, JSSQLStatement* castedThis) +{ + auto& columnNames = castedThis->columnNames.data()->propertyNameVector(); + int count = columnNames.size(); + auto& vm = lexicalGlobalObject->vm(); + +#if SQL_USE_PROTOTYPE == 1 + JSC::JSObject* result = JSC::JSFinalObject::create(vm, castedThis->_prototype.get()->structure()); +#else + JSC::JSObject* result = JSC::JSFinalObject::create(vm, JSC::JSFinalObject::createStructure(vm, lexicalGlobalObject, lexicalGlobalObject->objectPrototype(), count)); +#endif + auto* stmt = castedThis->stmt; + + for (int i = 0; i < count; i++) { + auto name = columnNames[i]; + + switch (sqlite3_column_type(stmt, i)) { + case SQLITE_INTEGER: { + result->putDirect(vm, name, jsNumber(sqlite3_column_int(stmt, i)), 0); + break; + } + case SQLITE_FLOAT: { + result->putDirect(vm, name, jsNumber(sqlite3_column_double(stmt, i)), 0); + break; + } + case SQLITE_TEXT: { + size_t len = sqlite3_column_bytes(stmt, i); + const unsigned char* text = len > 0 ? sqlite3_column_text(stmt, i) : nullptr; + + if (len > 64) { + result->putDirect(vm, name, JSC::JSValue::decode(Bun__encoding__toStringUTF8(text, len, lexicalGlobalObject)), 0); + continue; + } + + result->putDirect(vm, name, jsString(vm, WTF::String::fromUTF8(text, len)), 0); + break; + } + case SQLITE_BLOB: { + size_t len = sqlite3_column_bytes(stmt, i); + const void* blob = len > 0 ? sqlite3_column_blob(stmt, i) : nullptr; + JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(lexicalGlobalObject, lexicalGlobalObject->m_typedArrayUint8.get(lexicalGlobalObject), len); + memcpy(array->vector(), blob, len); + result->putDirect(vm, name, array, 0); + break; + } + default: { + result->putDirect(vm, name, jsNull(), 0); + break; + } + } + } + + return JSValue(result); +} + +static inline JSC::JSArray* constructResultRow(JSC::JSGlobalObject* lexicalGlobalObject, JSSQLStatement* castedThis, ObjectInitializationScope& scope, JSC::GCDeferralContext* deferralContext); +static inline JSC::JSArray* constructResultRow(JSC::JSGlobalObject* lexicalGlobalObject, JSSQLStatement* castedThis, ObjectInitializationScope& scope, JSC::GCDeferralContext* deferralContext) +{ + int count = castedThis->columnNames.size(); + auto& vm = lexicalGlobalObject->vm(); + + JSC::JSArray* result = JSArray::create(vm, lexicalGlobalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), count); + auto* stmt = castedThis->stmt; + + for (int i = 0; i < count; i++) { + + switch (sqlite3_column_type(stmt, i)) { + case SQLITE_INTEGER: { + result->initializeIndex(scope, i, jsNumber(sqlite3_column_int(stmt, i))); + break; + } + case SQLITE_FLOAT: { + result->initializeIndex(scope, i, jsNumber(sqlite3_column_double(stmt, i))); + break; + } + case SQLITE_TEXT: { + size_t len = sqlite3_column_bytes(stmt, i); + const unsigned char* text = len > 0 ? sqlite3_column_text(stmt, i) : nullptr; + if (UNLIKELY(text == nullptr || len == 0)) { + result->initializeIndex(scope, i, jsEmptyString(vm)); + continue; + } + result->initializeIndex(scope, i, len < 64 ? jsString(vm, WTF::String::fromUTF8(text, len)) : JSC::JSValue::decode(Bun__encoding__toStringUTF8(text, len, lexicalGlobalObject))); + break; + } + case SQLITE_BLOB: { + size_t len = sqlite3_column_bytes(stmt, i); + const void* blob = len > 0 ? sqlite3_column_blob(stmt, i) : nullptr; + JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(lexicalGlobalObject, lexicalGlobalObject->m_typedArrayUint8.get(lexicalGlobalObject), len); + memcpy(array->vector(), blob, len); + result->initializeIndex(scope, i, array); + break; + } + default: { + result->initializeIndex(scope, i, jsNull()); + break; + } + } + } + + return result; +} + +static inline JSC::JSArray* constructResultRow(JSC::JSGlobalObject* lexicalGlobalObject, JSSQLStatement* castedThis, ObjectInitializationScope& scope) +{ + return constructResultRow(lexicalGlobalObject, castedThis, scope, nullptr); +} + +static void initializeColumnNames(JSC::JSGlobalObject* lexicalGlobalObject, JSSQLStatement* castedThis) +{ + castedThis->hasExecuted = true; + JSC::VM& vm = lexicalGlobalObject->vm(); + auto& names = castedThis->columnNames; + + auto* stmt = castedThis->stmt; + + int count = sqlite3_column_count(stmt); + if (count == 0) + return; + JSC::ObjectInitializationScope initializationScope(vm); + JSC::JSObject* object = JSC::constructEmptyObject(lexicalGlobalObject, lexicalGlobalObject->objectPrototype(), count); + + for (int i = 0; i < count; i++) { + const char* name = sqlite3_column_name(stmt, i); + + if (name == nullptr) + break; + + size_t len = strlen(name); + if (len == 0) + break; + + auto wtfString = WTF::String::fromUTF8(name, len); + auto str = JSValue(jsString(vm, wtfString)); + auto key = str.toPropertyKey(lexicalGlobalObject); + JSC::JSValue primitive = JSC::jsUndefined(); + auto decl = sqlite3_column_decltype(stmt, i); + if (decl != nullptr) { + switch (decl[0]) { + case 'F': + case 'D': + case 'I': { + primitive = jsNumber(0); + break; + } + case 'V': + case 'T': { + primitive = jsEmptyString(vm); + break; + } + } + } + + object->putDirect(vm, key, primitive, 0); + names.add(key); + } + castedThis->_prototype.set(vm, castedThis, object); +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionAll, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto castedThis = jsDynamicCast<JSSQLStatement*>(callFrame->thisValue()); + + CHECK_THIS + + auto* stmt = castedThis->stmt; + CHECK_PREPARED + int statusCode = sqlite3_reset(stmt); + + if (UNLIKELY(statusCode != SQLITE_OK)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(statusCode)))); + return JSValue::encode(jsUndefined()); + } + + if (callFrame->argumentCount() > 0) { + auto arg0 = callFrame->argument(0); + DO_REBIND(arg0); + } + + if (!castedThis->hasExecuted) { + initializeColumnNames(lexicalGlobalObject, castedThis); + } + + auto& columnNames = castedThis->columnNames; + + int status = sqlite3_step(stmt); + + size_t columnCount = columnNames.size(); + int counter = 0; + + if (status == SQLITE_ROW) { + // this is a count from UPDATE or another query like that + if (columnCount == 0) { + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(jsNumber(sqlite3_changes(castedThis->db)))); + } + + JSC::JSArray* resultArray = JSC::constructEmptyArray(lexicalGlobalObject, nullptr, 0); + { + JSC::ObjectInitializationScope initializationScope(vm); + JSC::GCDeferralContext deferralContext(vm); + + while (status == SQLITE_ROW) { + JSC::JSValue result = constructResultObject(lexicalGlobalObject, castedThis); + resultArray->push(lexicalGlobalObject, result); + status = sqlite3_step(stmt); + } + } + + if (UNLIKELY(status != SQLITE_DONE)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(status)))); + sqlite3_reset(stmt); + return JSValue::encode(jsUndefined()); + } + + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(resultArray)); + } else if (status == SQLITE_DONE) { + if (columnCount == 0) { + RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(0))); + } + + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::constructEmptyArray(lexicalGlobalObject, nullptr, 0))); + } else { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(status)))); + sqlite3_reset(stmt); + return JSValue::encode(jsUndefined()); + } +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionGet, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto castedThis = jsDynamicCast<JSSQLStatement*>(callFrame->thisValue()); + + CHECK_THIS + + auto* stmt = castedThis->stmt; + CHECK_PREPARED + + int statusCode = sqlite3_reset(stmt); + if (UNLIKELY(statusCode != SQLITE_OK)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(statusCode)))); + return JSValue::encode(jsUndefined()); + } + + if (callFrame->argumentCount() > 0) { + auto arg0 = callFrame->argument(0); + DO_REBIND(arg0); + } + + if (!castedThis->hasExecuted) { + initializeColumnNames(lexicalGlobalObject, castedThis); + } + + auto& columnNames = castedThis->columnNames; + // { + // JSC::ObjectInitializationScope initializationScope(vm); + // array = + // } + int status = sqlite3_step(stmt); + + size_t columnCount = columnNames.size(); + int counter = 0; + + if (status == SQLITE_ROW) { + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(constructResultObject(lexicalGlobalObject, castedThis))); + } else if (status == SQLITE_DONE) { + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::jsNull())); + } else { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(status)))); + sqlite3_reset(stmt); + return JSValue::encode(jsUndefined()); + } +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRows, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto castedThis = jsDynamicCast<JSSQLStatement*>(callFrame->thisValue()); + + CHECK_THIS; + + auto* stmt = castedThis->stmt; + CHECK_PREPARED + + int statusCode = sqlite3_reset(stmt); + if (UNLIKELY(statusCode != SQLITE_OK)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(statusCode)))); + sqlite3_reset(stmt); + return JSValue::encode(jsUndefined()); + } + + int count = callFrame->argumentCount(); + if (count > 0) { + auto arg0 = callFrame->argument(0); + DO_REBIND(arg0); + } + + if (!castedThis->hasExecuted) { + initializeColumnNames(lexicalGlobalObject, castedThis); + } + + auto& columnNames = castedThis->columnNames; + int status = sqlite3_step(stmt); + + size_t columnCount = columnNames.size(); + int counter = 0; + + if (status == SQLITE_ROW) { + // this is a count from UPDATE or another query like that + if (columnCount == 0) { + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(jsNumber(sqlite3_changes(castedThis->db)))); + } + + JSC::ObjectInitializationScope initializationScope(vm); + JSC::GCDeferralContext deferralContext(vm); + + JSC::JSArray* resultArray = JSC::constructEmptyArray(lexicalGlobalObject, nullptr, 0); + { + + while (status == SQLITE_ROW) { + JSC::JSValue result = constructResultRow(lexicalGlobalObject, castedThis, initializationScope, &deferralContext); + resultArray->push(lexicalGlobalObject, result); + status = sqlite3_step(stmt); + } + } + + if (UNLIKELY(status != SQLITE_DONE)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(status)))); + sqlite3_reset(stmt); + return JSValue::encode(jsUndefined()); + } + + // sqlite3_reset(stmt); + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(resultArray)); + } else if (status == SQLITE_DONE) { + if (columnCount == 0) { + RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(0))); + } + + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::constructEmptyArray(lexicalGlobalObject, nullptr, 0))); + } else { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(status)))); + sqlite3_reset(stmt); + return JSValue::encode(jsUndefined()); + } +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRun, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto castedThis = jsDynamicCast<JSSQLStatement*>(callFrame->thisValue()); + + CHECK_THIS + + auto* stmt = castedThis->stmt; + CHECK_PREPARED + + int statusCode = sqlite3_reset(stmt); + if (UNLIKELY(statusCode != SQLITE_OK)) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(statusCode)))); + return JSValue::encode(jsUndefined()); + } + + if (callFrame->argumentCount() > 0) { + auto arg0 = callFrame->argument(0); + DO_REBIND(arg0); + } + + if (!castedThis->hasExecuted) { + initializeColumnNames(lexicalGlobalObject, castedThis); + } + + int status = sqlite3_step(stmt); + + if (status == SQLITE_ROW || status == SQLITE_DONE) { + // sqlite3_reset(stmt); + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(jsUndefined())); + } else { + sqlite3_reset(stmt); + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(status)))); + return JSValue::encode(jsUndefined()); + } +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementToStringFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + JSSQLStatement* castedThis = jsDynamicCast<JSSQLStatement*>(callFrame->thisValue()); + auto scope = DECLARE_THROW_SCOPE(vm); + + CHECK_THIS + + char* string = sqlite3_expanded_sql(castedThis->stmt); + if (!string) { + RELEASE_AND_RETURN(scope, JSValue::encode(jsEmptyString(vm))); + } + size_t length = strlen(string); + JSString* jsString = JSC::jsString(vm, WTF::String::fromUTF8(string, length)); + sqlite3_free(string); + + RELEASE_AND_RETURN(scope, JSValue::encode(jsString)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsSqlStatementGetColumnNames, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + JSSQLStatement* castedThis = jsDynamicCast<JSSQLStatement*>(JSValue::decode(thisValue)); + auto scope = DECLARE_THROW_SCOPE(vm); + CHECK_THIS + + if (!castedThis->hasExecuted) { + initializeColumnNames(lexicalGlobalObject, castedThis); + } + + auto* array = castedThis->_columnNames.get(); + if (array == nullptr) { + if (castedThis->columnNames.size() > 0) { + array = ownPropertyKeys(lexicalGlobalObject, castedThis->_prototype.get(), PropertyNameMode::Strings, DontEnumPropertiesMode::Exclude, CachedPropertyNamesKind::Keys); + } else { + array = JSC::constructEmptyArray(lexicalGlobalObject, nullptr, 0); + } + + castedThis->_columnNames.set(vm, castedThis, array); + } + + return JSC::JSValue::encode(array); +} + +JSC_DEFINE_CUSTOM_GETTER(jsSqlStatementGetColumnCount, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + JSSQLStatement* castedThis = jsDynamicCast<JSSQLStatement*>(JSValue::decode(thisValue)); + auto scope = DECLARE_THROW_SCOPE(vm); + CHECK_THIS + CHECK_PREPARED + + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::jsNumber(sqlite3_column_count(castedThis->stmt)))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsSqlStatementGetParamCount, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + JSSQLStatement* castedThis = jsDynamicCast<JSSQLStatement*>(JSValue::decode(thisValue)); + auto scope = DECLARE_THROW_SCOPE(vm); + CHECK_THIS + CHECK_PREPARED + + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSC::jsNumber(sqlite3_bind_parameter_count(castedThis->stmt)))); +} + +JSC_DEFINE_HOST_FUNCTION(jsSQLStatementFunctionFinalize, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + JSSQLStatement* castedThis = jsDynamicCast<JSSQLStatement*>(callFrame->thisValue()); + auto scope = DECLARE_THROW_SCOPE(vm); + CHECK_THIS + + if (castedThis->stmt) { + sqlite3_finalize(castedThis->stmt); + castedThis->stmt = nullptr; + } + + RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined())); +} + +const ClassInfo JSSQLStatement::s_info = { "SQLStatement"_s, nullptr, nullptr, nullptr, CREATE_METHOD_TABLE(JSSQLStatement) }; + +/* Hash table for prototype */ +static const HashTableValue JSSQLStatementTableValues[] = { + { "run"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementExecuteStatementFunctionRun), (intptr_t)(1) } }, + { "get"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementExecuteStatementFunctionGet), (intptr_t)(1) } }, + { "all"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementExecuteStatementFunctionAll), (intptr_t)(1) } }, + { "values"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementExecuteStatementFunctionRows), (intptr_t)(1) } }, + { "finalize"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementFunctionFinalize), (intptr_t)(0) } }, + { "toString"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsSQLStatementToStringFunction), (intptr_t)(0) } }, + { "columns"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { (intptr_t) static_cast<PropertySlot::GetValueFunc>(jsSqlStatementGetColumnNames), (intptr_t) static_cast<PutPropertySlot::PutValueFunc>(0) } }, + { "columnsCount"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { (intptr_t) static_cast<PropertySlot::GetValueFunc>(jsSqlStatementGetColumnCount), (intptr_t) static_cast<PutPropertySlot::PutValueFunc>(0) } }, + { "paramsCount"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { (intptr_t) static_cast<PropertySlot::GetValueFunc>(jsSqlStatementGetParamCount), (intptr_t) static_cast<PutPropertySlot::PutValueFunc>(0) } }, +}; + +void JSSQLStatement::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSSQLStatement::info(), JSSQLStatementTableValues, *this); +} + +JSSQLStatement::~JSSQLStatement() +{ + if (this->stmt) { + sqlite3_finalize(this->stmt); + } +} + +JSC::JSValue JSSQLStatement::rebind(JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue values, bool clone) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stmt = this->stmt; + auto val = rebindStatement(lexicalGlobalObject, values, scope, stmt, clone); + if (val.isNumber()) { + RELEASE_AND_RETURN(scope, val); + } else { + return val; + } +} + +template<typename Visitor> +void JSSQLStatement::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSSQLStatement* thisObject = jsCast<JSSQLStatement*>(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->_columnNames); + visitor.append(thisObject->_prototype); +} + +DEFINE_VISIT_CHILDREN(JSSQLStatement); +} |