Files
bun.sh/src/bun.js/bindings/sqlite/JSSQLStatement.cpp
SUZUKI Sosuke b04303cb23 fix(gc): fix three GC safety issues (speculative fix for BUN-Q81) (#27190)
## Speculative fix for
[BUN-Q81](https://bun-p9.sentry.io/issues/BUN-Q81)

BUN-Q81 is a long-standing `SlotVisitor::drain` segfault during GC
marking (150 occurrences since July 2025, across v1.1.10 through
v1.3.10). A full audit of the codebase for GC safety issues found three
bugs:

### 1. `JSCommonJSModule::m_overriddenCompile` not visited in
`visitChildren`

`m_overriddenCompile` is a `WriteBarrier<Unknown>` that stores the
overridden `module._compile` function (used by `ts-node`, `pirates`,
`@swc-node/register`, etc.). It was the only WriteBarrier field in the
class not visited by `visitChildrenImpl`, making it invisible to the GC.
The pointed-to function could be prematurely collected, and subsequent
GC marking would follow the dangling WriteBarrier pointer into freed
memory.

**This is the strongest candidate for BUN-Q81.**

### 2. `JSSQLStatement::userPrototype` — wrong owner in
`WriteBarrier::set()`

```cpp
// Before (wrong):
castedThis->userPrototype.set(vm, classObject, prototype.getObject());
// After (correct):
castedThis->userPrototype.set(vm, castedThis, prototype.getObject());
```

The owner parameter must be the object containing the WriteBarrier so
the GC's remembered set is updated correctly. All other `.set()` calls
in the same file correctly use `castedThis`.

### 3. `NodeVMSpecialSandbox` — missing `visitChildren` entirely

`NodeVMSpecialSandbox` has a `WriteBarrier<NodeVMGlobalObject>
m_parentGlobal` member but had no `visitChildren` implementation. Added
the standard boilerplate.
2026-02-19 12:28:49 -08:00

2832 lines
110 KiB
C++

#include "root.h"
#include "JavaScriptCore/Error.h"
#include "JavaScriptCore/JSBigInt.h"
#include "JavaScriptCore/Structure.h"
#include "JavaScriptCore/ThrowScope.h"
#include "JavaScriptCore/JSArray.h"
#include "JavaScriptCore/ExceptionScope.h"
#include "JavaScriptCore/JSArrayBufferView.h"
#include "JavaScriptCore/JSType.h"
#include "JSSQLStatement.h"
#include <JavaScriptCore/JSObjectInlines.h>
#include <limits>
#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 "GCDefferalContext.h"
#include <JavaScriptCore/DOMJITAbstractHeap.h>
#include "DOMJITIDLConvert.h"
#include "DOMJITIDLType.h"
#include "JSBuffer.h"
#include "DOMJITIDLTypeFilter.h"
#include "DOMJITHelpers.h"
#include <JavaScriptCore/DFGAbstractHeap.h>
#include "wtf/SIMDUTF.h"
#include <JavaScriptCore/ObjectPrototype.h>
#include "BunBuiltinNames.h"
#include "sqlite3_error_codes.h"
#include "wtf/BitVector.h"
#include "wtf/FastBitVector.h"
#include "wtf/Vector.h"
#include <atomic>
#include "wtf/LazyRef.h"
#include "wtf/text/StringToIntegerConversion.h"
#include <JavaScriptCore/InternalFieldTuple.h>
#include "BunString.h"
static constexpr int32_t kSafeIntegersFlag = 1 << 1;
static constexpr int32_t kStrictFlag = 1 << 2;
#ifndef BREAKING_CHANGES_BUN_1_2
#define BREAKING_CHANGES_BUN_1_2 0
#endif
/* ******************************************************************************** */
// 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
#if LAZY_LOAD_SQLITE
#include "lazy_sqlite3.h"
#else
static inline int lazyLoadSQLite()
{
return 0;
}
#endif
/* ******************************************************************************** */
#if !USE(SYSTEM_MALLOC)
#include <bmalloc/BPlatform.h>
#define ENABLE_SQLITE_FAST_MALLOC (BENABLE(MALLOC_SIZE) && BENABLE(MALLOC_GOOD_SIZE))
#endif
static std::atomic<int64_t> sqlite_malloc_amount = 0;
static void enableFastMallocForSQLite()
{
#if ENABLE(SQLITE_FAST_MALLOC)
int returnCode = sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 0, 0);
ASSERT_WITH_MESSAGE(returnCode == SQLITE_OK, "Unable to reduce lookaside buffer size");
static sqlite3_mem_methods fastMallocMethods = {
[](int n) {
auto* ret = fastMalloc(n);
sqlite_malloc_amount += fastMallocSize(ret);
return ret;
},
[](void* p) {
sqlite_malloc_amount -= fastMallocSize(p);
return fastFree(p);
},
[](void* p, int n) {
sqlite_malloc_amount -= fastMallocSize(p);
auto* out = fastRealloc(p, n);
sqlite_malloc_amount += fastMallocSize(out);
return out;
},
[](void* p) { return static_cast<int>(fastMallocSize(p)); },
[](int n) { return static_cast<int>(fastMallocGoodSize(n)); },
[](void*) { return SQLITE_OK; },
[](void*) {},
nullptr
};
returnCode = sqlite3_config(SQLITE_CONFIG_MALLOC, &fastMallocMethods);
ASSERT_WITH_MESSAGE(returnCode == SQLITE_OK, "Unable to replace SQLite malloc");
#endif
}
class AutoDestructingSQLiteStatement {
public:
sqlite3_stmt* stmt { nullptr };
~AutoDestructingSQLiteStatement()
{
sqlite3_finalize(stmt);
}
};
static void initializeSQLite()
{
static std::once_flag onceFlag;
std::call_once(onceFlag, [] {
enableFastMallocForSQLite();
});
}
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* buf, void* ctx)
{
sqlite3_free((void*)buf);
}
static constexpr int DEFAULT_SQLITE_FLAGS = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
static constexpr unsigned int DEFAULT_SQLITE_PREPARE_FLAGS = SQLITE_PREPARE_PERSISTENT;
static constexpr int MAX_SQLITE_PREPARE_FLAG = SQLITE_PREPARE_PERSISTENT | SQLITE_PREPARE_NORMALIZE | SQLITE_PREPARE_NO_VTAB;
static inline JSC::JSValue jsNumberFromSQLite(sqlite3_stmt* stmt, unsigned int i)
{
int64_t num = sqlite3_column_int64(stmt, i);
return JSC::jsNumber(num);
}
static inline JSC::JSValue jsBigIntFromSQLite(JSC::JSGlobalObject* globalObject, sqlite3_stmt* stmt, unsigned int i)
{
int64_t num = sqlite3_column_int64(stmt, i);
return JSC::JSBigInt::createFrom(globalObject, num);
}
#define CHECK_THIS \
if (!castedThis) [[unlikely]] { \
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQLStatement"_s)); \
return {}; \
}
#define DO_REBIND(param) \
if (param.isObject()) { \
JSC::JSValue reb = castedThis->rebind(lexicalGlobalObject, param, true, castedThis->version_db->db); \
RETURN_IF_EXCEPTION(scope, {}); \
if (!reb.isNumber()) [[unlikely]] { \
return JSValue::encode(reb); /* this means an error */ \
} \
} else { \
throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Expected object or array"_s)); \
return {}; \
}
#define CHECK_PREPARED \
if (castedThis->stmt == nullptr || castedThis->version_db == nullptr) [[unlikely]] { \
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Statement has finalized"_s)); \
return {}; \
}
#define CHECK_PREPARED_JIT \
if (castedThis->stmt == nullptr || castedThis->version_db == nullptr) [[unlikely]] { \
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Statement has finalized"_s)); \
return {}; \
}
DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(VersionSqlite3);
class VersionSqlite3 {
WTF_DEPRECATED_MAKE_FAST_ALLOCATED_WITH_HEAP_IDENTIFIER(VersionSqlite3, VersionSqlite3);
public:
explicit VersionSqlite3(sqlite3* db)
: db(db)
, version(0)
, reference_count(1)
{
}
sqlite3* db;
std::atomic<uint64_t> version;
size_t reference_count;
void release()
{
ASSERT(reference_count > 0);
--reference_count;
if (reference_count == 0) {
if (!db) {
return;
}
sqlite3_close_v2(db);
db = nullptr;
}
};
};
DEFINE_ALLOCATOR_WITH_HEAP_IDENTIFIER(VersionSqlite3);
class SQLiteSingleton {
public:
Vector<VersionSqlite3*> databases;
Vector<std::atomic<uint64_t>> schema_versions;
};
static SQLiteSingleton* _instance = nullptr;
static Vector<VersionSqlite3*>& databases()
{
if (!_instance) {
_instance = new SQLiteSingleton();
_instance->databases = Vector<VersionSqlite3*>();
_instance->databases.reserveInitialCapacity(4);
_instance->schema_versions = Vector<std::atomic<uint64_t>>();
}
return _instance->databases;
}
extern "C" void Bun__closeAllSQLiteDatabasesForTermination()
{
if (!_instance) {
return;
}
auto& dbs = _instance->databases;
for (auto& db : dbs) {
if (db->db)
sqlite3_close(db->db);
}
}
namespace WebCore {
using namespace JSC;
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementPrepareStatementFunction);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteFunction);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementOpenStatementFunction);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementIsInTransactionFunction);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunction);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRun);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionGet);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionAll);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionIterate);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRows);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRawRows);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnNames);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnCount);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementSerialize);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementDeserialize);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementSetPrototypeFunction);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementFunctionFinalize);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementToStringFunction);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnNames);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnCount);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetParamCount);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetHasMultipleStatements);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnTypes);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnDeclaredTypes);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetSafeIntegers);
JSC_DECLARE_CUSTOM_SETTER(jsSqlStatementSetSafeIntegers);
static JSValue createSQLiteError(JSC::JSGlobalObject* globalObject, sqlite3* db)
{
auto& vm = JSC::getVM(globalObject);
int code = sqlite3_extended_errcode(db);
int byteOffset = sqlite3_error_offset(db);
const char* msg = sqlite3_errmsg(db);
WTF::String str = WTF::String::fromUTF8(msg);
JSC::JSObject* object = JSC::createError(globalObject, str);
auto& builtinNames = WebCore::builtinNames(vm);
object->putDirect(vm, vm.propertyNames->name, jsString(vm, String("SQLiteError"_s)), JSC::PropertyAttribute::DontEnum | 0);
String codeStr;
switch (code) {
#define MACRO(SQLITE_DEF) \
case SQLITE_DEF: { \
codeStr = #SQLITE_DEF##_s; \
break; \
}
FOR_EACH_SQLITE_ERROR(MACRO)
#undef MACRO
}
if (!codeStr.isEmpty())
object->putDirect(vm, builtinNames.codePublicName(), jsString(vm, codeStr), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | 0);
object->putDirect(vm, builtinNames.errnoPublicName(), jsNumber(code), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | 0);
object->putDirect(vm, vm.propertyNames->byteOffset, jsNumber(byteOffset), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | 0);
return object;
}
class SQLiteBindingsMap {
public:
SQLiteBindingsMap() = default;
SQLiteBindingsMap(uint16_t count = 0, bool trimLeadingPrefix = false)
{
this->trimLeadingPrefix = trimLeadingPrefix;
hasLoadedNames = false;
reset(count);
}
void reset(uint16_t count = 0)
{
ASSERT(count <= std::numeric_limits<uint16_t>::max());
if (this->count != count) {
hasLoadedNames = false;
bindingNames.clear();
}
this->count = count;
}
void ensureNamesLoaded(JSC::VM& vm, sqlite3_stmt* stmt)
{
if (hasLoadedNames)
return;
hasLoadedNames = true;
hasOutOfOrderNames = false;
size_t count = this->count;
size_t prefixOffset = trimLeadingPrefix ? 1 : 0;
bindingNames.clear();
bool hasLoadedBindingNames = false;
size_t indexedCount = 0;
for (size_t i = 0; i < count; i++) {
const unsigned char* name = reinterpret_cast<const unsigned char*>(sqlite3_bind_parameter_name(stmt, i + 1));
// INSERT INTO cats (name, age) VALUES (?, ?) RETURNING name
if (name == nullptr) {
indexedCount++;
if (hasLoadedBindingNames) {
bindingNames[i] = Identifier(Identifier::EmptyIdentifierFlag::EmptyIdentifier);
}
continue;
}
if (!hasLoadedBindingNames) {
bindingNames.resize(count);
hasLoadedBindingNames = true;
}
name += prefixOffset;
size_t namelen = strlen(reinterpret_cast<const char*>(name));
if (prefixOffset == 1 && name[0] >= '0' && name[0] <= '9') {
auto integer = WTF::parseInteger<uint64_t>(StringView({ name, namelen }), 10);
if (integer.has_value()) {
hasOutOfOrderNames = true;
bindingNames.clear();
break;
}
}
WTF::String wtfString = WTF::String::fromUTF8ReplacingInvalidSequences({ name, namelen });
bindingNames[i] = Identifier::fromString(vm, wtfString);
}
isOnlyIndexed = indexedCount == count;
}
Vector<Identifier> bindingNames;
uint16_t count = 0;
bool hasLoadedNames : 1 = false;
bool isOnlyIndexed : 1 = false;
bool trimLeadingPrefix : 1 = false;
bool hasOutOfOrderNames : 1 = false;
};
class JSSQLStatement : public JSC::JSDestructibleObject {
public:
using Base = JSC::JSDestructibleObject;
static constexpr JSC::DestructionMode needsDestruction = NeedsDestruction;
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());
}
static JSSQLStatement* create(JSDOMGlobalObject* globalObject, sqlite3_stmt* stmt, VersionSqlite3* version_db, int64_t memorySizeChange = 0)
{
Structure* structure = globalObject->JSSQLStatementStructure();
JSSQLStatement* ptr = new (NotNull, JSC::allocateCell<JSSQLStatement>(globalObject->vm())) JSSQLStatement(structure, *globalObject, stmt, version_db, memorySizeChange);
if (version_db) {
++version_db->reference_count;
}
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 = std::forward<decltype(space)>(space); },
[](auto& spaces) { return spaces.m_subspaceForJSSQLStatement.get(); },
[](auto& spaces, auto&& space) { spaces.m_subspaceForJSSQLStatement = std::forward<decltype(space)>(space); });
}
DECLARE_VISIT_CHILDREN;
DECLARE_EXPORT_INFO;
template<typename Visitor> void visitAdditionalChildren(Visitor&);
template<typename Visitor> static void visitOutputConstraints(JSCell*, Visitor&);
size_t static estimatedSize(JSCell* cell, VM& vm)
{
auto* thisObject = jsCast<JSSQLStatement*>(cell);
return Base::estimatedSize(thisObject, vm) + thisObject->extraMemorySize;
}
static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&);
JSC::JSValue rebind(JSGlobalObject* globalObject, JSC::JSValue values, bool clone, sqlite3* db);
bool need_update() { return version_db->version.load() != version; }
void update_version() { version = version_db->version.load(); }
~JSSQLStatement();
sqlite3_stmt* stmt;
VersionSqlite3* version_db;
uint64_t version = 0;
// Tracks which columns are valid in the current result set. Used to handle duplicate column names.
// The bit at index i is set if the column at index i is valid.
WTF::BitVector validColumns;
std::unique_ptr<PropertyNameArrayBuilder> columnNames;
mutable JSC::WriteBarrier<JSC::JSObject> _prototype;
mutable JSC::WriteBarrier<JSC::Structure> _structure;
mutable JSC::WriteBarrier<JSC::JSObject> userPrototype;
size_t extraMemorySize = 0;
SQLiteBindingsMap m_bindingNames = { 0, false };
bool hasExecuted : 1 = false;
bool useBigInt64 : 1 = false;
protected:
JSSQLStatement(JSC::Structure* structure, JSDOMGlobalObject& globalObject, sqlite3_stmt* stmt, VersionSqlite3* version_db, int64_t memorySizeChange = 0)
: Base(globalObject.vm(), structure)
, stmt(stmt)
, version_db(version_db)
, columnNames(new PropertyNameArrayBuilder(globalObject.vm(), PropertyNameMode::Strings, PrivateSymbolMode::Exclude))
, extraMemorySize(memorySizeChange > 0 ? memorySizeChange : 0)
{
}
void finishCreation(JSC::VM& vm);
};
static JSValue toJSAsBuffer(JSC::VM& vm, JSC::JSGlobalObject* globalObject, sqlite3_stmt* stmt, int i)
{
auto scope = DECLARE_THROW_SCOPE(vm);
switch (sqlite3_column_type(stmt, i)) {
case SQLITE_INTEGER: {
int64_t value = sqlite3_column_int64(stmt, i);
JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), 8);
RETURN_IF_EXCEPTION(scope, {});
uint8_t* data = array->typedVector();
for (int j = 0; j < 8; j++) {
data[j] = (value >> (j * 8)) & 0xFF;
}
return array;
}
case SQLITE_FLOAT: {
double value = sqlite3_column_double(stmt, i);
JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), 8);
RETURN_IF_EXCEPTION(scope, {});
memcpy(array->typedVector(), &value, 8);
return array;
}
case SQLITE3_TEXT: {
size_t len = sqlite3_column_bytes(stmt, i);
const unsigned char* text = len > 0 ? sqlite3_column_text(stmt, i) : nullptr;
if (text == nullptr || len == 0) [[unlikely]] {
JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), 0);
RETURN_IF_EXCEPTION(scope, {});
return array;
}
JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), len);
RETURN_IF_EXCEPTION(scope, {});
memcpy(array->typedVector(), text, len);
return array;
}
case SQLITE_BLOB: {
size_t len = sqlite3_column_bytes(stmt, i);
const void* blob = len > 0 ? sqlite3_column_blob(stmt, i) : nullptr;
if (len > 0 && blob != nullptr) [[likely]] {
JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), len);
RETURN_IF_EXCEPTION(scope, {});
memcpy(array->vector(), blob, len);
return array;
}
JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), 0);
RETURN_IF_EXCEPTION(scope, {});
return array;
}
case SQLITE_NULL:
default:
return jsNull();
}
}
template<bool useBigInt64>
static JSValue toJS(JSC::VM& vm, JSC::JSGlobalObject* globalObject, sqlite3_stmt* stmt, int i)
{
auto throwScope = DECLARE_THROW_SCOPE(vm);
switch (sqlite3_column_type(stmt, i)) {
case SQLITE_INTEGER: {
if constexpr (!useBigInt64) {
// https://github.com/oven-sh/bun/issues/1536
return jsNumberFromSQLite(stmt, i);
} else {
// https://github.com/oven-sh/bun/issues/1536
auto bint = jsBigIntFromSQLite(globalObject, stmt, i);
RETURN_IF_EXCEPTION(throwScope, {});
return bint;
}
}
case SQLITE_FLOAT: {
return jsNumber(sqlite3_column_double(stmt, i));
}
// > Note that the SQLITE_TEXT constant was also used in SQLite version
// > 2 for a completely different meaning. Software that links against
// > both SQLite version 2 and SQLite version 3 should use SQLITE3_TEXT,
// > not SQLITE_TEXT.
case SQLITE3_TEXT: {
size_t len = sqlite3_column_bytes(stmt, i);
const unsigned char* text = len > 0 ? sqlite3_column_text(stmt, i) : nullptr;
if (text == nullptr || len == 0) [[unlikely]] {
return jsEmptyString(vm);
}
if (len < 64) {
return jsString(vm, WTF::String::fromUTF8({ text, len }));
}
auto encoded = Bun__encoding__toStringUTF8(text, len, globalObject);
RETURN_IF_EXCEPTION(throwScope, {});
return JSC::JSValue::decode(encoded);
}
case SQLITE_BLOB: {
size_t len = sqlite3_column_bytes(stmt, i);
const void* blob = len > 0 ? sqlite3_column_blob(stmt, i) : nullptr;
if (len > 0 && blob != nullptr) [[likely]] {
JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), len);
RETURN_IF_EXCEPTION(throwScope, {});
memcpy(array->vector(), blob, len);
return array;
}
auto array = JSC::JSUint8Array::create(globalObject, globalObject->m_typedArrayUint8.get(globalObject), 0);
RETURN_IF_EXCEPTION(throwScope, {});
return array;
}
default: {
break;
}
}
return jsNull();
}
static const HashTableValue JSSQLStatementPrototypeTableValues[] = {
{ "run"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementExecuteStatementFunctionRun, 1 } },
{ "get"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementExecuteStatementFunctionGet, 1 } },
{ "all"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementExecuteStatementFunctionAll, 1 } },
{ "iterate"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementExecuteStatementFunctionIterate, 1 } },
{ "as"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementSetPrototypeFunction, 1 } },
{ "values"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementExecuteStatementFunctionRows, 1 } },
{ "raw"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementExecuteStatementFunctionRawRows, 1 } },
{ "finalize"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementFunctionFinalize, 0 } },
{ "toString"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementToStringFunction, 0 } },
{ "columns"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsSqlStatementGetColumnNames, 0 } },
{ "columnsCount"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsSqlStatementGetColumnCount, 0 } },
{ "paramsCount"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsSqlStatementGetParamCount, 0 } },
{ "columnTypes"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsSqlStatementGetColumnTypes, 0 } },
{ "declaredTypes"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsSqlStatementGetColumnDeclaredTypes, 0 } },
{ "safeIntegers"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsSqlStatementGetSafeIntegers, jsSqlStatementSetSafeIntegers } },
};
class JSSQLStatementPrototype final : public JSC::JSNonFinalObject {
public:
using Base = JSC::JSNonFinalObject;
static JSSQLStatementPrototype* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::Structure* structure)
{
JSSQLStatementPrototype* ptr = new (NotNull, JSC::allocateCell<JSSQLStatementPrototype>(vm)) JSSQLStatementPrototype(vm, globalObject, structure);
ptr->finishCreation(vm, globalObject);
return ptr;
}
DECLARE_INFO;
template<typename CellType, JSC::SubspaceAccess>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSSQLStatementPrototype, Base);
return &vm.plainObjectSpace();
}
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());
}
private:
JSSQLStatementPrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
: Base(vm, structure)
{
}
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
reifyStaticProperties(vm, JSSQLStatementPrototype::info(), JSSQLStatementPrototypeTableValues, *this);
}
};
const ClassInfo JSSQLStatementPrototype::s_info = { "SQLStatement"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSSQLStatementPrototype) };
Structure* createJSSQLStatementStructure(JSGlobalObject* globalObject)
{
Structure* prototypeStructure = JSSQLStatementPrototype::createStructure(globalObject->vm(), globalObject, globalObject->objectPrototype());
prototypeStructure->setMayBePrototype(true);
JSSQLStatementPrototype* prototype = JSSQLStatementPrototype::create(globalObject->vm(), globalObject, prototypeStructure);
return JSSQLStatement::createStructure(globalObject->vm(), globalObject, prototype);
}
static void initializeColumnNames(JSC::JSGlobalObject* lexicalGlobalObject, JSSQLStatement* castedThis)
{
if (!castedThis->hasExecuted) {
castedThis->hasExecuted = true;
} else {
// reinitialize column
castedThis->columnNames.reset(new PropertyNameArrayBuilder(
castedThis->columnNames->vm(),
castedThis->columnNames->propertyNameMode(),
castedThis->columnNames->privateSymbolMode()));
}
castedThis->validColumns.clearAll();
castedThis->update_version();
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* stmt = castedThis->stmt;
castedThis->_structure.clear();
castedThis->_prototype.clear();
int count = sqlite3_column_count(stmt);
if (count < 1)
return;
// Fast path:
if (count <= JSFinalObject::maxInlineCapacity) {
// 64 is the maximum we can preallocate here
// see https://github.com/oven-sh/bun/issues/987
// also see https://github.com/oven-sh/bun/issues/1646
auto& globalObject = *lexicalGlobalObject;
auto columnNames = castedThis->columnNames.get();
bool anyHoles = false;
for (int i = count - 1; i >= 0; i--) {
const char* name = sqlite3_column_name(stmt, i);
if (name == nullptr) {
anyHoles = true;
break;
}
size_t len = strlen(name);
if (len == 0) {
anyHoles = true;
break;
}
// When joining multiple tables, the same column names can appear multiple times
// columnNames de-dupes property names internally
// We can't have two properties with the same name, so we use validColumns to track this.
auto preCount = columnNames->size();
columnNames->add(
Identifier::fromString(vm, WTF::String::fromUTF8({ name, len })));
auto curCount = columnNames->size();
if (preCount != curCount) {
castedThis->validColumns.set(i);
}
}
if (!anyHoles) [[likely]] {
PropertyOffset offset;
JSObject* prototype = castedThis->userPrototype ? castedThis->userPrototype.get() : globalObject.objectPrototype();
Structure* structure = globalObject.structureCache().emptyObjectStructureForPrototype(&globalObject, prototype, columnNames->size());
vm.writeBarrier(castedThis, structure);
// We iterated over the columns in reverse order so we need to reverse the columnNames here
// Importantly we reverse before adding the properties to the structure to ensure that index accesses
// later refer to the correct property.
columnNames->data()->propertyNameVector().reverse();
for (const auto& propertyName : *columnNames) {
structure = Structure::addPropertyTransition(vm, structure, propertyName, 0, offset);
}
castedThis->_structure.set(vm, castedThis, structure);
// We are done.
return;
} else {
// If for any reason we do not have column names, disable the fast path.
columnNames->releaseData();
castedThis->columnNames.reset(new PropertyNameArrayBuilder(
castedThis->columnNames->vm(),
castedThis->columnNames->propertyNameMode(),
castedThis->columnNames->privateSymbolMode()));
castedThis->validColumns.clearAll();
}
}
// Slow path:
// 64 is the maximum we can preallocate here
// see https://github.com/oven-sh/bun/issues/987
JSObject* prototype = castedThis->userPrototype ? castedThis->userPrototype.get() : lexicalGlobalObject->objectPrototype();
JSC::JSObject* object = JSC::constructEmptyObject(lexicalGlobalObject, prototype, std::min(static_cast<unsigned>(count), JSFinalObject::maxInlineCapacity));
for (int i = count - 1; i >= 0; i--) {
const char* name = sqlite3_column_name(stmt, i);
if (name == nullptr)
break;
size_t len = strlen(name);
if (len == 0)
break;
const auto key = Identifier::fromString(vm, WTF::String::fromUTF8({ name, len }));
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;
}
}
}
auto preCount = castedThis->columnNames->size();
castedThis->columnNames->add(key);
auto curCount = castedThis->columnNames->size();
// only put the property if it's not a duplicate
if (preCount != curCount) {
castedThis->validColumns.set(i);
object->putDirect(vm, key, primitive, 0);
}
}
// We iterated over the columns in reverse order so we need to reverse the columnNames here
castedThis->columnNames->data()->propertyNameVector().reverse();
castedThis->_prototype.set(vm, castedThis, object);
}
void JSSQLStatement::destroy(JSC::JSCell* cell)
{
JSSQLStatement* thisObject = static_cast<JSSQLStatement*>(cell);
thisObject->~JSSQLStatement();
}
static inline bool rebindValue(JSC::JSGlobalObject* lexicalGlobalObject, sqlite3* db, sqlite3_stmt* stmt, int i, JSC::JSValue value, JSC::ThrowScope& scope, bool clone, bool isSafeInteger)
{
auto throwSQLiteError = [&]() -> void {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errmsg(db))));
};
#define CHECK_BIND(param) \
int result = param; \
if (result != SQLITE_OK) [[unlikely]] { \
throwSQLiteError(); \
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 (!str) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Expected string"_s));
return false;
}
const auto roped = str->view(lexicalGlobalObject);
if (roped->isNull()) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Out of memory :("_s));
return false;
}
if (roped->is8Bit() && roped->containsOnlyASCII()) {
CHECK_BIND(sqlite3_bind_text(stmt, i, reinterpret_cast<const char*>(roped->span8().data()), roped->length(), transientOrStatic));
} else if (!roped->is8Bit()) {
CHECK_BIND(sqlite3_bind_text16(stmt, i, roped->span16().data(), roped->length() * 2, transientOrStatic));
} else {
auto utf8 = roped->utf8();
CHECK_BIND(sqlite3_bind_text(stmt, i, utf8.data(), utf8.length(), SQLITE_TRANSIENT));
}
} else if (value.isHeapBigInt()) [[unlikely]] {
if (!isSafeInteger) {
CHECK_BIND(sqlite3_bind_int64(stmt, i, JSBigInt::toBigInt64(value)));
} else {
JSBigInt* bigInt = value.asHeapBigInt();
const auto min = JSBigInt::compare(bigInt, std::numeric_limits<int64_t>::min());
const auto max = JSBigInt::compare(bigInt, std::numeric_limits<int64_t>::max());
if ((min == JSBigInt::ComparisonResult::GreaterThan || min == JSBigInt::ComparisonResult::Equal) && (max == JSBigInt::ComparisonResult::LessThan || max == JSBigInt::ComparisonResult::Equal)) [[likely]] {
CHECK_BIND(sqlite3_bind_int64(stmt, i, JSBigInt::toBigInt64(value)));
} else {
throwRangeError(lexicalGlobalObject, scope, makeString("BigInt value '"_s, bigInt->toString(lexicalGlobalObject, 10), "' is out of range"_s));
sqlite3_clear_bindings(stmt);
return false;
}
}
} 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
}
static JSC::JSValue rebindObject(JSC::JSGlobalObject* globalObject, SQLiteBindingsMap& bindings, JSC::JSObject* target, JSC::ThrowScope& scope, sqlite3* db, sqlite3_stmt* stmt, bool clone, bool safeIntegers)
{
int count = 0;
auto& vm = JSC::getVM(globalObject);
auto& structure = *target->structure();
bindings.ensureNamesLoaded(vm, stmt);
const auto& bindingNames = bindings.bindingNames;
size_t size = bindings.count;
const bool trimLeadingPrefix = bindings.trimLeadingPrefix;
const bool throwOnMissing = trimLeadingPrefix;
// Did they reorder the columns?
//
// { ?2: "foo", ?1: "bar" }
//
if (bindings.hasOutOfOrderNames) [[unlikely]] {
const auto& getValue = [&](const char* name, size_t i) -> JSValue {
JSValue value = {};
if (name == nullptr) {
return target->getDirectIndex(globalObject, i);
}
if (trimLeadingPrefix) {
name += 1;
}
const WTF::String str = WTF::String::fromUTF8ReplacingInvalidSequences({ reinterpret_cast<const unsigned char*>(name), strlen(name) });
if (trimLeadingPrefix && name[0] >= '0' && name[0] <= '9') {
auto integer = WTF::parseInteger<int32_t>(str, 10);
if (integer.has_value()) {
return target->getDirectIndex(globalObject, integer.value() - 1);
}
}
const auto identifier = Identifier::fromString(vm, str);
PropertySlot slot(target, PropertySlot::InternalMethodType::GetOwnProperty);
if (!target->getOwnNonIndexPropertySlot(vm, &structure, identifier, slot)) {
return {};
}
if (!slot.isTaintedByOpaqueObject()) [[likely]] {
return slot.getValue(globalObject, identifier);
}
return target->get(globalObject, identifier);
};
for (size_t i = 0; i < size; i++) {
auto* name = sqlite3_bind_parameter_name(stmt, i + 1);
JSValue value = getValue(name, i);
if (!value && !scope.exception()) {
if (throwOnMissing) {
throwException(globalObject, scope, createError(globalObject, makeString("Missing parameter \""_s, WTF::String::fromUTF8ReplacingInvalidSequences({ reinterpret_cast<const unsigned char*>(name), strlen(name) }), "\""_s)));
} else {
continue;
}
}
RETURN_IF_EXCEPTION(scope, {});
if (!rebindValue(globalObject, db, stmt, i + 1, value, scope, clone, safeIntegers)) {
return {};
}
RETURN_IF_EXCEPTION(scope, {});
count++;
}
}
// Does it only contain indexed properties?
//
// { 0: "foo", 1: "bar", "2": "baz" }
//
else if (bindings.isOnlyIndexed) [[unlikely]] {
for (size_t i = 0; i < size; i++) {
JSValue value = target->getDirectIndex(globalObject, i);
if (!value && !scope.exception()) {
if (throwOnMissing) {
throwException(globalObject, scope, createError(globalObject, makeString("Missing parameter \""_s, i + 1, "\""_s)));
} else {
continue;
}
}
RETURN_IF_EXCEPTION(scope, {});
if (!rebindValue(globalObject, db, stmt, i + 1, value, scope, clone, safeIntegers)) {
return {};
}
RETURN_IF_EXCEPTION(scope, {});
count++;
}
}
// Is it a simple object with no getters or setters?
//
// { foo: "bar", baz: "qux" }
//
else if (target->canUseFastGetOwnProperty(structure)) {
for (size_t i = 0; i < size; i++) {
const auto& property = bindingNames[i];
JSValue value = property.isEmpty() ? target->getDirectIndex(globalObject, i) : target->fastGetOwnProperty(vm, structure, bindingNames[i]);
if (!value && !scope.exception()) {
if (throwOnMissing) {
throwException(globalObject, scope, createError(globalObject, makeString("Missing parameter \""_s, property.isEmpty() ? String::number(i) : property.string(), "\""_s)));
} else {
continue;
}
}
RETURN_IF_EXCEPTION(scope, {});
if (!rebindValue(globalObject, db, stmt, i + 1, value, scope, clone, safeIntegers)) {
return {};
}
RETURN_IF_EXCEPTION(scope, {});
count++;
}
} else {
for (size_t i = 0; i < size; i++) {
PropertySlot slot(target, PropertySlot::InternalMethodType::GetOwnProperty);
const auto& property = bindingNames[i];
bool hasProperty = property.isEmpty() ? target->methodTable()->getOwnPropertySlotByIndex(target, globalObject, i, slot) : target->methodTable()->getOwnPropertySlot(target, globalObject, property, slot);
if (!hasProperty && !scope.exception()) {
if (throwOnMissing) {
throwException(globalObject, scope, createError(globalObject, makeString("Missing parameter \""_s, property.isEmpty() ? String::number(i) : property.string(), "\""_s)));
} else {
continue;
}
}
RETURN_IF_EXCEPTION(scope, {});
JSValue value;
if (!slot.isTaintedByOpaqueObject()) [[likely]]
value = slot.getValue(globalObject, property);
else {
value = target->get(globalObject, property);
RETURN_IF_EXCEPTION(scope, {});
}
RETURN_IF_EXCEPTION(scope, {});
if (!rebindValue(globalObject, db, stmt, i + 1, value, scope, clone, safeIntegers)) {
return {};
}
RETURN_IF_EXCEPTION(scope, {});
count++;
}
}
return jsNumber(count);
}
static JSC::JSValue rebindStatement(JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue values, JSC::ThrowScope& scope, sqlite3* db, sqlite3_stmt* stmt, bool clone, SQLiteBindingsMap& bindings, bool safeIntegers)
{
sqlite3_clear_bindings(stmt);
JSC::JSArray* array = jsDynamicCast<JSC::JSArray*>(values);
bindings.reset(sqlite3_bind_parameter_count(stmt));
if (!array) {
if (JSC::JSObject* object = values.getObject()) {
auto res = rebindObject(lexicalGlobalObject, bindings, object, scope, db, stmt, clone, safeIntegers);
RETURN_IF_EXCEPTION(scope, {});
return res;
}
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected array"_s));
return {};
}
int count = array->length();
if (count == 0) {
return jsNumber(0);
}
int required = bindings.count;
if (count != required) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, makeString("SQLite query expected "_s, required, " values, received "_s, count)));
return {};
}
int i = 0;
for (; i < count; i++) {
JSC::JSValue value = array->getIndexQuickly(i);
if (!rebindValue(lexicalGlobalObject, db, stmt, i + 1, value, scope, clone, safeIntegers)) {
return {};
}
RETURN_IF_EXCEPTION(scope, {});
}
return jsNumber(i);
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementSetCustomSQLite, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue thisValue = callFrame->thisValue();
JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject());
if (!thisObject) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQL"_s));
return {};
}
if (callFrame->argumentCount() < 1) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected 1 argument"_s));
return {};
}
JSC::JSValue sqliteStrValue = callFrame->argument(0);
if (!sqliteStrValue.isString()) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQLite path"_s));
return {};
}
#if 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 {};
}
// Use a static CString to keep the string alive for the lifetime of the process
static CString sqlite3_lib_path_storage;
sqlite3_lib_path_storage = sqliteStrValue.toWTFString(lexicalGlobalObject).utf8();
RETURN_IF_EXCEPTION(scope, {});
sqlite3_lib_path = sqlite3_lib_path_storage.data();
if (lazyLoadSQLite() == -1) {
sqlite3_handle = nullptr;
WTF::String msg = WTF::String::fromUTF8(dlerror());
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, msg));
return {};
}
#endif
initializeSQLite();
RELEASE_AND_RETURN(scope, JSValue::encode(JSC::jsBoolean(true)));
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementDeserialize, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue thisValue = callFrame->thisValue();
JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject());
JSC::JSArrayBufferView* array = jsDynamicCast<JSC::JSArrayBufferView*>(callFrame->argument(0));
unsigned int deserializeFlags = SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE;
unsigned int openFlags = DEFAULT_SQLITE_FLAGS;
JSC::EnsureStillAliveScope ensureAliveArray(array);
if (callFrame->argumentCount() > 1) {
openFlags |= callFrame->argument(1).toInt32(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
}
if (callFrame->argumentCount() > 2) {
deserializeFlags |= callFrame->argument(2).toInt32(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
}
if (!thisObject) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQL"_s));
return {};
}
if (callFrame->argumentCount() < 1) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected 1 argument"_s));
return {};
}
if (!array) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected Uint8Array or Buffer"_s));
return {};
}
if (array->isDetached()) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "TypedArray is detached"_s));
return {};
}
#if LAZY_LOAD_SQLITE
if (lazyLoadSQLite() < 0) [[unlikely]] {
WTF::String msg = WTF::String::fromUTF8(dlerror());
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, msg));
return {};
}
#endif
initializeSQLite();
size_t byteLength = array->byteLength();
void* ptr = array->vector();
if (ptr == nullptr || byteLength == 0) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "ArrayBuffer must not be empty"_s));
return {};
}
void* data = sqlite3_malloc64(byteLength);
if (data == nullptr) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Failed to allocate memory"_s));
return {};
}
if (byteLength) {
memcpy(data, ptr, byteLength);
}
sqlite3* db = nullptr;
if (sqlite3_open_v2(":memory:", &db, openFlags, nullptr) != SQLITE_OK) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Failed to open SQLite"_s));
return {};
}
int status = sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL);
if (status != SQLITE_OK) {
// TODO: log a warning here that we can't load extensions
}
status = sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL);
if (status != SQLITE_OK) {
// TODO: log a warning here that defensive mode is not enabled
}
status = sqlite3_deserialize(db, "main", reinterpret_cast<unsigned char*>(data), byteLength, byteLength, deserializeFlags);
if (status == SQLITE_BUSY) {
sqlite3_free(data);
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "SQLITE_BUSY"_s));
return {};
}
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 {};
}
auto count = databases().size();
databases().append(new VersionSqlite3(db));
RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(count)));
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementSerialize, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue thisValue = callFrame->thisValue();
JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject());
if (!thisObject) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQL"_s));
return {};
}
int32_t dbIndex = callFrame->argument(0).toInt32(lexicalGlobalObject);
if (dbIndex < 0 || dbIndex >= databases().size()) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s));
return {};
}
sqlite3* db = databases()[dbIndex]->db;
if (!db) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Can't do this on a closed database"_s));
return {};
}
WTF::String attachedName = callFrame->argument(1).toWTFString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
if (attachedName.isEmpty()) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected attached database name"_s));
return {};
}
sqlite3_int64 length = -1;
unsigned char* data = sqlite3_serialize(db, attachedName.utf8().data(), &length, 0);
if (data == nullptr && length) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Out of memory"_s));
return {};
}
RELEASE_AND_RETURN(scope, JSBuffer__bufferFromPointerAndLengthAndDeinit(lexicalGlobalObject, reinterpret_cast<char*>(data), static_cast<unsigned int>(length), NULL, sqlite_free_typed_array));
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue thisValue = callFrame->thisValue();
JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject());
if (!thisObject) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQL"_s));
return {};
}
int32_t dbIndex = callFrame->argument(0).toInt32(lexicalGlobalObject);
if (dbIndex < 0 || dbIndex >= databases().size()) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s));
return {};
}
JSC::JSValue extension = callFrame->argument(1);
if (!extension.isString()) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Expected string"_s));
return {};
}
auto extensionString = extension.toWTFString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
sqlite3* db = databases()[dbIndex]->db;
if (!db) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Can't do this on a closed database"_s));
return {};
}
if (sqlite3_compileoption_used("SQLITE_OMIT_LOAD_EXTENSION")) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "This build of sqlite3 does not support dynamic extension loading"_s));
return {};
}
auto entryPointStr = callFrame->argumentCount() > 2 && callFrame->argument(2).isString() ? callFrame->argument(2).toWTFString(lexicalGlobalObject) : String();
RETURN_IF_EXCEPTION(scope, {});
auto entryPointUtf8 = entryPointStr.utf8();
const char* entryPoint = entryPointStr.length() == 0 ? NULL : entryPointUtf8.data();
auto extensionStringUtf8 = extensionString.utf8();
char* error;
int rc = sqlite3_load_extension(db, extensionStringUtf8.data(), entryPoint, &error);
// TODO: can we disable loading extensions after this?
if (rc != SQLITE_OK) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, error ? sqliteString(error) : String::fromUTF8(sqlite3_errmsg(db))));
return {};
}
RELEASE_AND_RETURN(scope, JSValue::encode(JSC::jsUndefined()));
}
static bool isSkippedInSQLiteQuery(const char c)
{
return c == ' ' || c == ';' || (c >= '\t' && c <= '\r');
}
// 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))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue thisValue = callFrame->thisValue();
JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject());
if (!thisObject) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQL"_s));
return {};
}
if (callFrame->argumentCount() < 2) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected at least 2 arguments"_s));
return {};
}
int32_t handle = callFrame->argument(0).toInt32(lexicalGlobalObject);
if (databases().size() < handle) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s));
return {};
}
sqlite3* db = databases()[handle]->db;
if (!db) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Database has closed"_s));
return {};
}
JSC::JSValue internalFlagsValue = callFrame->argument(1);
JSC::JSValue diffValue = callFrame->argument(2);
JSC::JSValue sqlValue = callFrame->argument(3);
if (!sqlValue.isString()) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQL string"_s));
return {};
}
EnsureStillAliveScope bindingsAliveScope = callFrame->argument(4);
auto* jsSqlString = sqlValue.toString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
if (jsSqlString->length() == 0) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "SQL string mustn't be blank"_s));
return {};
}
Bun::UTF8View utf8 = Bun::UTF8View(jsSqlString->view(lexicalGlobalObject));
const char* sqlStringHead = utf8.span().data();
const char* end = utf8.span().data() + utf8.span().size();
bool didSetBindings = false;
bool didExecuteAny = false;
int rc = SQLITE_OK;
#if ASSERT_ENABLED
int maxSqlStringBytes = end - sqlStringHead;
#endif
bool strict = internalFlagsValue.isInt32() && (internalFlagsValue.asInt32() & kStrictFlag) != 0;
bool safeIntegers = internalFlagsValue.isInt32() && (internalFlagsValue.asInt32() & kSafeIntegersFlag) != 0;
const int total_changes_before = sqlite3_total_changes(db);
while (sqlStringHead && sqlStringHead < end) {
if (isSkippedInSQLiteQuery(*sqlStringHead)) [[unlikely]] {
sqlStringHead++;
while (sqlStringHead < end && isSkippedInSQLiteQuery(*sqlStringHead))
sqlStringHead++;
}
AutoDestructingSQLiteStatement sql;
const char* tail = nullptr;
// Bounds checks
ASSERT(end >= sqlStringHead);
ASSERT(end - sqlStringHead >= 0);
ASSERT(end - sqlStringHead <= maxSqlStringBytes);
rc = sqlite3_prepare_v3(db, sqlStringHead, end - sqlStringHead, 0, &sql.stmt, &tail);
if (rc != SQLITE_OK)
break;
if (!sql.stmt) {
// this happens for an empty statement
sqlStringHead = tail;
continue;
}
// First statement gets the bindings.
if (!didSetBindings && !bindingsAliveScope.value().isUndefinedOrNull()) {
if (bindingsAliveScope.value().isObject()) {
int count = sqlite3_bind_parameter_count(sql.stmt);
SQLiteBindingsMap bindings { static_cast<uint16_t>(count > -1 ? count : 0), strict };
JSC::JSValue reb = rebindStatement(lexicalGlobalObject, bindingsAliveScope.value(), scope, db, sql.stmt, false, bindings, safeIntegers);
RETURN_IF_EXCEPTION(scope, {});
if (!reb.isNumber()) [[unlikely]] {
return JSValue::encode(reb); /* this means an error */
}
} else {
throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Expected bindings to be an object or array"_s));
return {};
}
didSetBindings = true;
}
do {
rc = sqlite3_step(sql.stmt);
} while (rc == SQLITE_ROW);
didExecuteAny = true;
sqlStringHead = tail;
}
if (rc != SQLITE_OK && rc != SQLITE_DONE) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, db));
return {};
}
if (!didExecuteAny) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Query contained no valid SQL statement; likely empty query."_s));
return {};
}
if (auto* diff = JSC::jsDynamicCast<JSC::InternalFieldTuple*>(diffValue)) {
const int total_changes_after = sqlite3_total_changes(db);
int64_t last_insert_rowid = sqlite3_last_insert_rowid(db);
diff->putInternalField(vm, 0, JSC::jsNumber(total_changes_after - total_changes_before));
if (safeIntegers) {
auto* bigInt = JSBigInt::createFrom(lexicalGlobalObject, last_insert_rowid);
RETURN_IF_EXCEPTION(scope, {});
diff->putInternalField(vm, 1, bigInt);
} else {
diff->putInternalField(vm, 1, JSC::jsNumber(last_insert_rowid));
}
}
RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined()));
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementIsInTransactionFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue thisValue = callFrame->thisValue();
JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject());
if (!thisObject) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQLStatement"_s));
return {};
}
JSC::JSValue dbNumber = callFrame->argument(0);
if (!dbNumber.isNumber()) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s));
return {};
}
int32_t handle = dbNumber.toInt32(lexicalGlobalObject);
if (handle < 0 || handle > databases().size()) {
throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid database handle"_s));
return {};
}
sqlite3* db = databases()[handle]->db;
if (!db) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Database has closed"_s));
return {};
}
RELEASE_AND_RETURN(scope, JSValue::encode(jsBoolean(!sqlite3_get_autocommit(db))));
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementPrepareStatementFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue thisValue = callFrame->thisValue();
JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject());
if (!thisObject) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQLStatement"_s));
return {};
}
JSC::JSValue dbNumber = callFrame->argument(0);
JSC::JSValue sqlValue = callFrame->argument(1);
JSC::JSValue bindings = callFrame->argument(2);
JSC::JSValue prepareFlagsValue = callFrame->argument(3);
JSC::JSValue internalFlagsValue = callFrame->argument(4);
if (!dbNumber.isNumber() || !sqlValue.isString()) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "SQLStatement requires a number and a string"_s));
return {};
}
int32_t handle = dbNumber.toInt32(lexicalGlobalObject);
if (handle < 0 || handle > databases().size()) {
throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid database handle"_s));
return {};
}
sqlite3* db = databases()[handle]->db;
if (!db) {
throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Cannot use a closed database"_s));
return {};
}
auto* jsSqlString = sqlValue.toString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
if (!jsSqlString->length()) {
throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid SQL statement"_s));
return {};
}
auto sqlString = jsSqlString->view(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
Bun::UTF8View utf8 = Bun::UTF8View(sqlString);
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 {};
}
flags = static_cast<unsigned int>(prepareFlags);
}
sqlite3_stmt* statement = nullptr;
// This is inherently somewhat racy if using Worker
// but that should be okay.
int64_t currentMemoryUsage = sqlite_malloc_amount;
int rc = SQLITE_OK;
rc = sqlite3_prepare_v3(db, reinterpret_cast<const char*>(utf8.span().data()), utf8.span().size(), flags, &statement, nullptr);
if (rc != SQLITE_OK) {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, db));
return {};
}
int64_t memoryChange = sqlite_malloc_amount - currentMemoryUsage;
JSSQLStatement* sqlStatement = JSSQLStatement::create(
static_cast<Zig::GlobalObject*>(lexicalGlobalObject), statement, databases()[handle], memoryChange);
if (internalFlagsValue.isInt32()) {
const int32_t internalFlags = internalFlagsValue.asInt32();
sqlStatement->m_bindingNames.trimLeadingPrefix = (internalFlags & kStrictFlag) != 0;
sqlStatement->useBigInt64 = (internalFlags & kSafeIntegersFlag) != 0;
}
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, ImplementationVisibility::Private, 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))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
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 {};
}
if (callFrame->argumentCount() < 1) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected 1 argument"_s));
return {};
}
JSValue pathValue = callFrame->argument(0);
if (!pathValue.isString()) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected string"_s));
return {};
}
#if LAZY_LOAD_SQLITE
if (lazyLoadSQLite() < 0) [[unlikely]] {
WTF::String msg = WTF::String::fromUTF8(dlerror());
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, msg));
return {};
}
#endif
initializeSQLite();
auto topExceptionScope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
String path = pathValue.toWTFString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(topExceptionScope, JSValue::encode(jsUndefined()));
(void)topExceptionScope.tryClearException();
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 {};
}
openFlags = flags.toInt32(lexicalGlobalObject);
}
JSValue finalizationTarget = callFrame->argument(2);
sqlite3* db = nullptr;
int statusCode = sqlite3_open_v2(path.utf8().data(), &db, openFlags, nullptr);
if (statusCode != SQLITE_OK) {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, db));
return {};
}
sqlite3_extended_result_codes(db, 1);
int status = sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL);
if (status != SQLITE_OK) {
// TODO: log a warning here that extensions are unsupported.
}
status = sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL);
if (status != SQLITE_OK) {
// TODO: log a warning here that defensive mode is unsupported.
}
auto index = databases().size();
databases().append(new VersionSqlite3(db));
if (finalizationTarget.isObject()) {
vm.heap.addFinalizer(finalizationTarget.getObject(), [index](JSC::JSCell* ptr) -> void {
databases()[index]->release();
});
}
RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(index)));
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementCloseStatementFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
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 {};
}
if (callFrame->argumentCount() < 1) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected 1 argument"_s));
return {};
}
JSValue dbNumber = callFrame->argument(0);
JSValue throwOnError = callFrame->argument(1);
if (!dbNumber.isNumber()) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected number"_s));
return {};
}
int dbIndex = dbNumber.toInt32(lexicalGlobalObject);
if (dbIndex < 0 || dbIndex >= databases().size()) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s));
return {};
}
bool shouldThrowOnError = (throwOnError.isEmpty() || throwOnError.isUndefined()) ? false : throwOnError.toBoolean(lexicalGlobalObject);
sqlite3* db = databases()[dbIndex]->db;
// no-op if already closed
if (!db) {
return JSValue::encode(jsUndefined());
}
// sqlite3_close_v2 is used for automatic GC cleanup
int statusCode = shouldThrowOnError ? sqlite3_close(db) : sqlite3_close_v2(db);
if (statusCode != SQLITE_OK) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(sqlite3_errstr(statusCode))));
return {};
}
databases()[dbIndex]->db = nullptr;
return JSValue::encode(jsUndefined());
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementFcntlFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue thisValue = callFrame->thisValue();
JSSQLStatementConstructor* thisObject = jsDynamicCast<JSSQLStatementConstructor*>(thisValue.getObject());
if (!thisObject) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected SQLStatement"_s));
return {};
}
if (callFrame->argumentCount() < 2) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected 2 arguments"_s));
return {};
}
JSValue dbNumber = callFrame->argument(0);
JSValue databaseFileName = callFrame->argument(1);
JSValue opNumber = callFrame->argument(2);
JSValue resultValue = callFrame->argument(3);
if (!dbNumber.isNumber() || !opNumber.isNumber()) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected number"_s));
return {};
}
int dbIndex = dbNumber.toInt32(lexicalGlobalObject);
int op = opNumber.toInt32(lexicalGlobalObject);
if (dbIndex < 0 || dbIndex >= databases().size()) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s));
return {};
}
sqlite3* db = databases()[dbIndex]->db;
// no-op if already closed
if (!db) {
return JSValue::encode(jsUndefined());
}
CString fileNameStr;
if (databaseFileName.isString()) {
fileNameStr = databaseFileName.toWTFString(lexicalGlobalObject).utf8();
RETURN_IF_EXCEPTION(scope, {});
}
int resultInt = -1;
void* resultPtr = nullptr;
if (resultValue.isObject()) {
if (auto* view = jsDynamicCast<JSC::JSArrayBufferView*>(resultValue.getObject())) {
if (view->isDetached()) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "TypedArray is detached"_s));
return {};
}
resultPtr = view->vector();
if (resultPtr == nullptr) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected buffer"_s));
return {};
}
}
} else if (resultValue.isNumber()) {
resultInt = resultValue.toInt32(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
resultPtr = &resultInt;
} else if (resultValue.isNull()) {
} else {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Expected result to be a number, null or a TypedArray"_s));
return {};
}
int statusCode = sqlite3_file_control(db, fileNameStr.isNull() ? nullptr : fileNameStr.data(), op, resultPtr);
if (statusCode == SQLITE_ERROR) {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, db));
return {};
}
return JSValue::encode(jsNumber(statusCode));
}
/* Hash table for constructor */
static const HashTableValue JSSQLStatementConstructorTableValues[] = {
{ "open"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementOpenStatementFunction, 2 } },
{ "close"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementCloseStatementFunction, 1 } },
{ "prepare"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementPrepareStatementFunction, 2 } },
{ "run"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementExecuteFunction, 3 } },
{ "isInTransaction"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementIsInTransactionFunction, 1 } },
{ "loadExtension"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementLoadExtensionFunction, 2 } },
{ "setCustomSQLite"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementSetCustomSQLite, 1 } },
{ "serialize"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementSerialize, 1 } },
{ "deserialize"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementDeserialize, 2 } },
{ "fcntl"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementFcntlFunction, 2 } },
};
const ClassInfo JSSQLStatementConstructor::s_info = { "SQLStatement"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSSQLStatementConstructor) };
void JSSQLStatementConstructor::finishCreation(VM& vm)
{
Base::finishCreation(vm);
// TODO: use LazyClassStructure?
auto* instanceObject = JSSQLStatement::create(static_cast<Zig::GlobalObject*>(globalObject()), nullptr, nullptr);
JSValue proto = instanceObject->getPrototype(globalObject());
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();
ASSERT(inherits(info()));
}
template<bool useBigInt64>
static inline JSC::JSValue constructResultObject(JSC::JSGlobalObject* lexicalGlobalObject, JSSQLStatement* castedThis)
{
auto& columnNames = castedThis->columnNames->data()->propertyNameVector();
int count = columnNames.size();
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
// 64 is the maximum we can preallocate here
// see https://github.com/oven-sh/bun/issues/987
JSC::JSObject* result;
auto* stmt = castedThis->stmt;
if (auto* structure = castedThis->_structure.get()) {
result = JSC::constructEmptyObject(vm, structure);
// i: the index of columns returned from SQLite
// j: the index of object property
for (int i = 0, j = 0; j < count; i++, j++) {
if (!castedThis->validColumns.get(i)) {
// this column is duplicate, skip
j -= 1;
continue;
}
auto value = toJS<useBigInt64>(vm, lexicalGlobalObject, stmt, i);
RETURN_IF_EXCEPTION(scope, {});
result->putDirectOffset(vm, j, value);
}
} else {
if (count <= JSFinalObject::maxInlineCapacity) {
result = JSC::JSFinalObject::create(vm, castedThis->_prototype.get()->structure());
} else {
JSObject* prototype = castedThis->userPrototype ? castedThis->userPrototype.get() : lexicalGlobalObject->objectPrototype();
result = JSC::JSFinalObject::create(vm, JSC::JSFinalObject::createStructure(vm, lexicalGlobalObject, prototype, JSFinalObject::maxInlineCapacity));
}
for (int i = 0, j = 0; j < count; i++, j++) {
if (!castedThis->validColumns.get(i)) {
j -= 1;
continue;
}
const auto& name = columnNames[j];
auto value = toJS<useBigInt64>(vm, lexicalGlobalObject, stmt, i);
RETURN_IF_EXCEPTION(scope, {});
result->putDirect(vm, name, value, 0);
}
}
RELEASE_AND_RETURN(scope, result);
}
static inline JSC::JSArray* constructResultRowRaw(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSSQLStatement* castedThis, size_t columnCount)
{
auto throwScope = DECLARE_THROW_SCOPE(vm);
auto* stmt = castedThis->stmt;
MarkedArgumentBuffer arguments;
arguments.ensureCapacity(columnCount);
for (size_t i = 0; i < columnCount; i++) {
JSValue value = toJSAsBuffer(vm, lexicalGlobalObject, stmt, i);
RETURN_IF_EXCEPTION(throwScope, nullptr);
arguments.append(value);
}
RELEASE_AND_RETURN(throwScope, JSC::constructArray(lexicalGlobalObject, static_cast<JSC::ArrayAllocationProfile*>(nullptr), arguments));
}
static inline JSC::JSArray* constructResultRow(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSSQLStatement* castedThis, size_t columnCount)
{
auto throwScope = DECLARE_THROW_SCOPE(vm);
auto* stmt = castedThis->stmt;
MarkedArgumentBuffer arguments;
arguments.ensureCapacity(columnCount);
if (castedThis->useBigInt64) {
for (size_t i = 0; i < columnCount; i++) {
JSValue value = toJS<true>(vm, lexicalGlobalObject, stmt, i);
RETURN_IF_EXCEPTION(throwScope, nullptr);
arguments.append(value);
}
} else {
for (size_t i = 0; i < columnCount; i++) {
JSValue value = toJS<false>(vm, lexicalGlobalObject, stmt, i);
RETURN_IF_EXCEPTION(throwScope, nullptr);
arguments.append(value);
}
}
JSC::ObjectInitializationScope initializationScope(vm);
Structure* arrayStructure = lexicalGlobalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous);
JSC::JSArray* result;
if ((result = JSC::JSArray::tryCreateUninitializedRestricted(initializationScope, arrayStructure, columnCount))) [[likely]] {
for (size_t i = 0; i < columnCount; i++) {
result->initializeIndex(initializationScope, i, arguments.at(i));
}
} else {
RETURN_IF_EXCEPTION(throwScope, nullptr);
result = JSC::constructArray(lexicalGlobalObject, arrayStructure, arguments);
RETURN_IF_EXCEPTION(throwScope, {});
}
return result;
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementSetPrototypeFunction, (JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* castedThis = jsCast<JSSQLStatement*>(callFrame->thisValue());
CHECK_THIS
JSValue classValue = callFrame->argument(0);
if (classValue.isObject()) {
JSObject* classObject = classValue.getObject();
if (classObject == lexicalGlobalObject->objectConstructor()) {
castedThis->userPrototype.clear();
// Force the prototypes to be re-created
if (castedThis->version_db) {
castedThis->version_db->version++;
}
return JSValue::encode(jsUndefined());
}
if (!classObject->isConstructor()) {
throwTypeError(lexicalGlobalObject, scope, "Expected a constructor"_s);
return {};
}
JSValue prototype = classObject->getIfPropertyExists(lexicalGlobalObject, vm.propertyNames->prototype);
RETURN_IF_EXCEPTION(scope, {});
if (!prototype && !scope.exception()) [[unlikely]] {
throwTypeError(lexicalGlobalObject, scope, "Expected constructor to have a prototype"_s);
return {};
}
if (!prototype.isObject()) {
throwTypeError(lexicalGlobalObject, scope, "Expected a constructor prototype to be an object"_s);
return {};
}
castedThis->userPrototype.set(vm, castedThis, prototype.getObject());
// Force the prototypes to be re-created
if (castedThis->version_db) {
castedThis->version_db->version++;
}
} else if (classValue.isUndefined()) {
castedThis->userPrototype.clear();
// Force the prototypes to be re-created
if (castedThis->version_db) {
castedThis->version_db->version++;
}
} else {
throwTypeError(lexicalGlobalObject, scope, "Expected class to be a constructor or undefined"_s);
return {};
}
return JSValue::encode(jsUndefined());
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionIterate, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto castedThis = jsDynamicCast<JSSQLStatement*>(callFrame->thisValue());
CHECK_THIS
auto* stmt = castedThis->stmt;
CHECK_PREPARED
int busy = sqlite3_stmt_busy(stmt);
if (!busy) {
int statusCode = sqlite3_reset(stmt);
if (statusCode != SQLITE_OK) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
return {};
}
}
if (callFrame->argumentCount() > 0) {
auto arg0 = callFrame->argument(0);
DO_REBIND(arg0);
}
int status = sqlite3_step(stmt);
if (!sqlite3_stmt_readonly(stmt)) {
castedThis->version_db->version++;
}
if (!castedThis->hasExecuted || castedThis->need_update()) {
initializeColumnNames(lexicalGlobalObject, castedThis);
RETURN_IF_EXCEPTION(scope, {});
}
JSValue result = jsNull();
if (status == SQLITE_ROW) {
bool useBigInt64 = castedThis->useBigInt64;
result = useBigInt64 ? constructResultObject<true>(lexicalGlobalObject, castedThis)
: constructResultObject<false>(lexicalGlobalObject, castedThis);
RETURN_IF_EXCEPTION(scope, {});
}
if (status == SQLITE_DONE || status == SQLITE_OK || status == SQLITE_ROW) {
RELEASE_AND_RETURN(scope, JSValue::encode(result));
} else {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
sqlite3_reset(stmt);
return {};
}
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionAll, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
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 (statusCode != SQLITE_OK) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
return {};
}
int64_t currentMemoryUsage = sqlite_malloc_amount;
if (callFrame->argumentCount() > 0) {
auto arg0 = callFrame->argument(0);
DO_REBIND(arg0);
}
int status = sqlite3_step(stmt);
if (!sqlite3_stmt_readonly(stmt)) {
castedThis->version_db->version++;
}
if (!castedThis->hasExecuted || castedThis->need_update()) {
initializeColumnNames(lexicalGlobalObject, castedThis);
RETURN_IF_EXCEPTION(scope, {});
}
size_t columnCount = castedThis->columnNames->size();
JSValue result = jsUndefined();
if (status == SQLITE_ROW) {
// this is a count from UPDATE or another query like that
if (columnCount == 0) {
result = jsNumber(sqlite3_changes(castedThis->version_db->db));
while (status == SQLITE_ROW) {
status = sqlite3_step(stmt);
}
} else {
bool useBigInt64 = castedThis->useBigInt64;
JSC::JSArray* resultArray = JSC::constructEmptyArray(lexicalGlobalObject, static_cast<ArrayAllocationProfile*>(nullptr), 0);
RETURN_IF_EXCEPTION(scope, {});
if (useBigInt64) {
do {
JSC::JSValue result = constructResultObject<true>(lexicalGlobalObject, castedThis);
RETURN_IF_EXCEPTION(scope, {});
resultArray->push(lexicalGlobalObject, result);
RETURN_IF_EXCEPTION(scope, {});
status = sqlite3_step(stmt);
} while (status == SQLITE_ROW);
} else {
do {
JSC::JSValue result = constructResultObject<false>(lexicalGlobalObject, castedThis);
RETURN_IF_EXCEPTION(scope, {});
resultArray->push(lexicalGlobalObject, result);
RETURN_IF_EXCEPTION(scope, {});
status = sqlite3_step(stmt);
} while (status == SQLITE_ROW);
}
result = resultArray;
}
} else if (status == SQLITE_DONE) {
result = JSC::constructEmptyArray(lexicalGlobalObject, static_cast<ArrayAllocationProfile*>(nullptr), 0);
RETURN_IF_EXCEPTION(scope, {});
}
if (status != SQLITE_DONE && status != SQLITE_OK) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
sqlite3_reset(stmt);
return {};
}
int64_t memoryChange = sqlite_malloc_amount - currentMemoryUsage;
if (memoryChange > 255) {
vm.heap.deprecatedReportExtraMemory(memoryChange);
}
RELEASE_AND_RETURN(scope, JSC::JSValue::encode(result));
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionGet, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
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 (statusCode != SQLITE_OK) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
return {};
}
if (callFrame->argumentCount() > 0) {
auto arg0 = callFrame->argument(0);
DO_REBIND(arg0);
}
int status = sqlite3_step(stmt);
if (!sqlite3_stmt_readonly(stmt)) {
castedThis->version_db->version++;
}
if (!castedThis->hasExecuted || castedThis->need_update()) {
initializeColumnNames(lexicalGlobalObject, castedThis);
RETURN_IF_EXCEPTION(scope, {});
}
JSValue result = jsNull();
if (status == SQLITE_ROW) {
bool useBigInt64 = castedThis->useBigInt64;
result = useBigInt64 ? constructResultObject<true>(lexicalGlobalObject, castedThis)
: constructResultObject<false>(lexicalGlobalObject, castedThis);
RETURN_IF_EXCEPTION(scope, {});
while (status == SQLITE_ROW) {
status = sqlite3_step(stmt);
}
}
if (status == SQLITE_DONE || status == SQLITE_OK) {
RELEASE_AND_RETURN(scope, JSValue::encode(result));
} else {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
sqlite3_reset(stmt);
return {};
}
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRows, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
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 (statusCode != SQLITE_OK) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
sqlite3_reset(stmt);
return {};
}
int count = callFrame->argumentCount();
if (count > 0) {
auto arg0 = callFrame->argument(0);
DO_REBIND(arg0);
}
int status = sqlite3_step(stmt);
if (!sqlite3_stmt_readonly(stmt)) {
castedThis->version_db->version++;
}
if (!castedThis->hasExecuted || castedThis->need_update()) {
initializeColumnNames(lexicalGlobalObject, castedThis);
if (scope.exception()) [[unlikely]] {
// Don't forget to reset before releasing the exception.
sqlite3_reset(stmt);
RELEASE_AND_RETURN(scope, {});
}
}
size_t columnCount = castedThis->columnNames->size();
JSValue result = jsNull();
if (status == SQLITE_ROW) {
// this is a count from UPDATE or another query like that
if (columnCount == 0) {
while (status == SQLITE_ROW) {
status = sqlite3_step(stmt);
}
result = jsNumber(sqlite3_column_count(stmt));
} else {
JSC::JSArray* resultArray = JSC::constructEmptyArray(lexicalGlobalObject, static_cast<ArrayAllocationProfile*>(nullptr), 0);
RETURN_IF_EXCEPTION(scope, {});
{
size_t columnCount = sqlite3_column_count(stmt);
do {
JSC::JSArray* row = constructResultRow(vm, lexicalGlobalObject, castedThis, columnCount);
if (!row || scope.exception()) [[unlikely]] {
sqlite3_reset(stmt);
RELEASE_AND_RETURN(scope, {});
}
resultArray->push(lexicalGlobalObject, row);
RETURN_IF_EXCEPTION(scope, {});
status = sqlite3_step(stmt);
} while (status == SQLITE_ROW);
}
result = resultArray;
}
} else if (status == SQLITE_DONE && columnCount != 0) {
// breaking change in Bun v0.6.8
result = JSC::constructEmptyArray(lexicalGlobalObject, nullptr, 0);
RETURN_IF_EXCEPTION(scope, {});
}
if (status != SQLITE_DONE && status != SQLITE_OK) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
sqlite3_reset(stmt);
return {};
}
// sqlite3_reset(stmt);
RELEASE_AND_RETURN(scope, JSC::JSValue::encode(result));
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRawRows, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
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 (statusCode != SQLITE_OK) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
sqlite3_reset(stmt);
return {};
}
int count = callFrame->argumentCount();
if (count > 0) {
auto arg0 = callFrame->argument(0);
DO_REBIND(arg0);
}
int status = sqlite3_step(stmt);
if (!sqlite3_stmt_readonly(stmt)) {
castedThis->version_db->version++;
}
if (!castedThis->hasExecuted || castedThis->need_update()) {
initializeColumnNames(lexicalGlobalObject, castedThis);
if (scope.exception()) [[unlikely]] {
sqlite3_reset(stmt);
RELEASE_AND_RETURN(scope, {});
}
}
size_t columnCount = castedThis->columnNames->size();
JSValue result = jsNull();
if (status == SQLITE_ROW) {
// this is a count from UPDATE or another query like that
if (columnCount == 0) {
while (status == SQLITE_ROW) {
status = sqlite3_step(stmt);
}
result = jsNumber(sqlite3_column_count(stmt));
} else {
JSC::JSArray* resultArray = JSC::constructEmptyArray(lexicalGlobalObject, nullptr, 0);
RETURN_IF_EXCEPTION(scope, {});
{
size_t columnCount = sqlite3_column_count(stmt);
do {
JSC::JSArray* row = constructResultRowRaw(vm, lexicalGlobalObject, castedThis, columnCount);
if (!row || scope.exception()) [[unlikely]] {
sqlite3_reset(stmt);
RELEASE_AND_RETURN(scope, {});
}
resultArray->push(lexicalGlobalObject, row);
if (scope.exception()) [[unlikely]] {
sqlite3_reset(stmt);
RELEASE_AND_RETURN(scope, {});
}
status = sqlite3_step(stmt);
} while (status == SQLITE_ROW);
}
result = resultArray;
}
} else if (status == SQLITE_DONE && columnCount != 0) {
// breaking change in Bun v0.6.8
result = JSC::constructEmptyArray(lexicalGlobalObject, static_cast<ArrayAllocationProfile*>(nullptr), 0);
RETURN_IF_EXCEPTION(scope, {});
}
if (status != SQLITE_DONE && status != SQLITE_OK) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
sqlite3_reset(stmt);
return {};
}
// sqlite3_reset(stmt);
RELEASE_AND_RETURN(scope, JSC::JSValue::encode(result));
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRun, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
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 (statusCode != SQLITE_OK) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
return {};
}
JSValue diffValue = callFrame->argument(0);
if (callFrame->argumentCount() > 1) {
auto arg0 = callFrame->argument(1);
DO_REBIND(arg0);
}
if (!castedThis->version_db->db) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Database has closed"_s));
return {};
}
int total_changes_before = sqlite3_total_changes(castedThis->version_db->db);
int status = sqlite3_step(stmt);
if (!sqlite3_stmt_readonly(stmt)) {
castedThis->version_db->version++;
}
if (!castedThis->hasExecuted || castedThis->need_update()) {
initializeColumnNames(lexicalGlobalObject, castedThis);
if (scope.exception()) [[unlikely]] {
sqlite3_reset(stmt);
RELEASE_AND_RETURN(scope, {});
}
}
while (status == SQLITE_ROW) {
status = sqlite3_step(stmt);
}
if (status != SQLITE_DONE && status != SQLITE_OK) [[unlikely]] {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
sqlite3_reset(stmt);
return {};
}
if (auto* diff = JSC::jsDynamicCast<JSC::InternalFieldTuple*>(diffValue)) {
auto* db = castedThis->version_db->db;
const int total_changes_after = sqlite3_total_changes(db);
int64_t last_insert_rowid = sqlite3_last_insert_rowid(db);
diff->putInternalField(vm, 0, JSC::jsNumber(total_changes_after - total_changes_before));
if (castedThis->useBigInt64) {
JSValue lastRowIdBigInt = JSBigInt::createFrom(lexicalGlobalObject, last_insert_rowid);
RETURN_IF_EXCEPTION(scope, {});
diff->putInternalField(vm, 1, lastRowIdBigInt);
} else {
diff->putInternalField(vm, 1, JSC::jsNumber(last_insert_rowid));
}
}
RELEASE_AND_RETURN(scope, JSC::JSValue::encode(jsUndefined()));
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementToStringFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
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);
auto* 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, JSC::EncodedJSValue thisValue, PropertyName attributeName))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
JSSQLStatement* castedThis = jsDynamicCast<JSSQLStatement*>(JSValue::decode(thisValue));
auto scope = DECLARE_THROW_SCOPE(vm);
CHECK_THIS
if (!castedThis->hasExecuted || castedThis->need_update()) {
initializeColumnNames(lexicalGlobalObject, castedThis);
RETURN_IF_EXCEPTION(scope, {});
}
JSC::JSArray* array;
auto* columnNames = castedThis->columnNames.get();
if (columnNames->size() > 0) {
if (castedThis->_prototype) {
array = ownPropertyKeys(lexicalGlobalObject, castedThis->_prototype.get(), PropertyNameMode::Strings, DontEnumPropertiesMode::Exclude);
RETURN_IF_EXCEPTION(scope, {});
} else {
array = JSC::constructEmptyArray(lexicalGlobalObject, static_cast<ArrayAllocationProfile*>(nullptr), columnNames->size());
RETURN_IF_EXCEPTION(scope, {});
unsigned int i = 0;
for (const auto& column : *columnNames) {
auto* string = jsString(vm, column.string());
RETURN_IF_EXCEPTION(scope, {});
array->putDirectIndex(lexicalGlobalObject, i++, string);
}
}
} else {
array = JSC::constructEmptyArray(lexicalGlobalObject, static_cast<ArrayAllocationProfile*>(nullptr), 0);
RETURN_IF_EXCEPTION(scope, {});
}
return JSC::JSValue::encode(array);
}
JSC_DEFINE_CUSTOM_GETTER(jsSqlStatementGetColumnCount, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName attributeName))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
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, JSC::EncodedJSValue thisValue, PropertyName attributeName))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
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_CUSTOM_GETTER(jsSqlStatementGetColumnTypes, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName attributeName))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
JSSQLStatement* castedThis = jsDynamicCast<JSSQLStatement*>(JSValue::decode(thisValue));
auto scope = DECLARE_THROW_SCOPE(vm);
CHECK_THIS
CHECK_PREPARED
int count = sqlite3_column_count(castedThis->stmt);
// We need to reset and step the statement to get fresh types,
// but only do this for read-only statements to avoid side effects
bool isReadOnly = sqlite3_stmt_readonly(castedThis->stmt) != 0;
if (!isReadOnly) {
// For non-read-only statements, throw an error since column types don't make sense
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "columnTypes is not available for non-read-only statements (INSERT, UPDATE, DELETE, etc.)"_s));
return {};
}
// Reset the statement (safe for read-only statements)
int resetStatus = sqlite3_reset(castedThis->stmt);
if (resetStatus != SQLITE_OK) {
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
return {};
}
MarkedArgumentBuffer args;
// Step once to get to the first row (safe for read-only statements)
int stepStatus = sqlite3_step(castedThis->stmt);
// If we got a row, get types from it
if (stepStatus == SQLITE_ROW) {
for (int i = 0; i < count; i++) {
JSC::JSValue typeValue;
// Get the actual column type from the current row
int columnType = sqlite3_column_type(castedThis->stmt, i);
switch (columnType) {
case SQLITE_INTEGER:
typeValue = JSC::jsString(vm, makeAtomString("INTEGER"_s));
break;
case SQLITE_FLOAT:
typeValue = JSC::jsString(vm, makeAtomString("FLOAT"_s));
break;
case SQLITE3_TEXT:
typeValue = JSC::jsString(vm, makeAtomString("TEXT"_s));
break;
case SQLITE_BLOB:
typeValue = JSC::jsString(vm, makeAtomString("BLOB"_s));
break;
case SQLITE_NULL:
typeValue = JSC::jsString(vm, makeAtomString("NULL"_s));
break;
default:
typeValue = JSC::jsNull();
break;
}
args.append(typeValue);
}
} else if (stepStatus == SQLITE_DONE) {
// No data rows to read, return 'NULL' for all columns
JSC::JSValue typeValue = JSC::jsString(vm, makeAtomString("NULL"_s));
for (int i = 0; i < count; i++) {
args.append(typeValue);
}
} else {
// If there was an error stepping, throw it
throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
sqlite3_reset(castedThis->stmt);
return {};
}
// Reset the statement back to its original state
sqlite3_reset(castedThis->stmt);
JSC::JSArray* array = constructArray(lexicalGlobalObject, static_cast<ArrayAllocationProfile*>(nullptr), args);
RETURN_IF_EXCEPTION(scope, {});
RELEASE_AND_RETURN(scope, JSC::JSValue::encode(array));
}
JSC_DEFINE_CUSTOM_GETTER(jsSqlStatementGetColumnDeclaredTypes, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName attributeName))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
JSSQLStatement* castedThis = jsDynamicCast<JSSQLStatement*>(JSValue::decode(thisValue));
auto scope = DECLARE_THROW_SCOPE(vm);
CHECK_THIS
CHECK_PREPARED
// Ensure the statement has been executed at least once
if (!castedThis->hasExecuted) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Statement must be executed before accessing declaredTypes"_s));
return {};
}
int count = sqlite3_column_count(castedThis->stmt);
JSC::JSArray* array = JSC::constructEmptyArray(lexicalGlobalObject, static_cast<ArrayAllocationProfile*>(nullptr), count);
RETURN_IF_EXCEPTION(scope, {});
// Use declared types only - return raw strings as-is, null when no declared type
for (int i = 0; i < count; i++) {
const char* declType = sqlite3_column_decltype(castedThis->stmt, i);
JSC::JSValue typeValue;
if (declType != nullptr) {
String typeStr = String::fromUTF8(declType);
typeValue = JSC::jsNontrivialString(vm, typeStr);
RETURN_IF_EXCEPTION(scope, {});
} else {
// If no declared type (e.g., for expressions or results of functions)
typeValue = JSC::jsNull();
}
array->putDirectIndex(lexicalGlobalObject, i, typeValue);
}
RELEASE_AND_RETURN(scope, JSC::JSValue::encode(array));
}
JSC_DEFINE_CUSTOM_GETTER(jsSqlStatementGetSafeIntegers, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName attributeName))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
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::jsBoolean(castedThis->useBigInt64)));
}
JSC_DEFINE_CUSTOM_SETTER(jsSqlStatementSetSafeIntegers, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName attributeName))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
JSSQLStatement* castedThis = jsDynamicCast<JSSQLStatement*>(JSValue::decode(thisValue));
auto scope = DECLARE_THROW_SCOPE(vm);
CHECK_THIS
CHECK_PREPARED
bool value = JSValue::decode(encodedValue).toBoolean(lexicalGlobalObject);
castedThis->useBigInt64 = value;
return true;
}
JSC_DEFINE_HOST_FUNCTION(jsSQLStatementFunctionFinalize, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
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, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSSQLStatement) };
/* Hash table for prototype */
void JSSQLStatement::finishCreation(VM& vm)
{
Base::finishCreation(vm);
ASSERT(inherits(info()));
vm.heap.reportExtraMemoryAllocated(this, this->extraMemorySize);
}
JSSQLStatement::~JSSQLStatement()
{
if (this->stmt) {
sqlite3_finalize(this->stmt);
}
if (auto* columnNames = this->columnNames.get()) {
columnNames->releaseData();
this->columnNames = nullptr;
}
if (this->version_db) {
this->version_db->release();
}
}
void JSSQLStatement::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer)
{
auto* thisObject = jsCast<JSSQLStatement*>(cell);
if (thisObject->stmt)
analyzer.setWrappedObjectForCell(cell, thisObject->stmt);
Base::analyzeHeap(cell, analyzer);
}
JSC::JSValue JSSQLStatement::rebind(JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue values, bool clone, sqlite3* db)
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* stmt = this->stmt;
auto val = rebindStatement(lexicalGlobalObject, values, scope, this->version_db->db, stmt, clone, this->m_bindingNames, this->useBigInt64);
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.reportExtraMemoryVisited(thisObject->extraMemorySize);
visitor.append(thisObject->_structure);
visitor.append(thisObject->_prototype);
visitor.append(thisObject->userPrototype);
}
DEFINE_VISIT_CHILDREN(JSSQLStatement);
template<typename Visitor>
void JSSQLStatement::visitAdditionalChildren(Visitor& visitor)
{
JSSQLStatement* thisObject = this;
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
visitor.append(thisObject->_structure);
visitor.append(thisObject->_prototype);
visitor.append(thisObject->userPrototype);
}
template<typename Visitor>
void JSSQLStatement::visitOutputConstraints(JSCell* cell, Visitor& visitor)
{
auto* thisObject = jsCast<JSSQLStatement*>(cell);
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
Base::visitOutputConstraints(thisObject, visitor);
thisObject->visitAdditionalChildren(visitor);
}
template void JSSQLStatement::visitOutputConstraints(JSCell*, AbstractSlotVisitor&);
template void JSSQLStatement::visitOutputConstraints(JSCell*, SlotVisitor&);
JSValue createJSSQLStatementConstructor(Zig::GlobalObject* globalObject)
{
VM& vm = globalObject->vm();
JSObject* object = JSC::constructEmptyObject(globalObject);
auto* diff = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), jsUndefined(), jsUndefined());
auto* constructor = JSSQLStatementConstructor::create(
vm,
globalObject,
JSSQLStatementConstructor::createStructure(vm, globalObject, globalObject->m_functionPrototype.get()));
object->putDirectIndex(globalObject, 0, constructor);
object->putDirectIndex(globalObject, 1, diff);
return object;
}
} // namespace WebCore