Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
715413adc0 Initial commit 2025-08-06 03:20:43 +00:00
24 changed files with 4493 additions and 0 deletions

View File

@@ -2649,6 +2649,7 @@ pub const HardcodedModule = enum {
@"node:child_process",
@"node:console",
@"node:constants",
@"node:sqlite",
@"node:crypto",
@"node:dns",
@"node:dns/promises",
@@ -2739,6 +2740,7 @@ pub const HardcodedModule = enum {
.{ "node:cluster", .@"node:cluster" },
.{ "node:console", .@"node:console" },
.{ "node:constants", .@"node:constants" },
.{ "node:sqlite", .@"node:sqlite" },
.{ "node:crypto", .@"node:crypto" },
.{ "node:dgram", .@"node:dgram" },
.{ "node:diagnostics_channel", .@"node:diagnostics_channel" },
@@ -2849,6 +2851,7 @@ pub const HardcodedModule = enum {
nodeEntry("node:cluster"),
nodeEntry("node:console"),
nodeEntry("node:constants"),
nodeEntry("node:sqlite"),
nodeEntry("node:crypto"),
nodeEntry("node:dgram"),
nodeEntry("node:diagnostics_channel"),

View File

@@ -79,6 +79,9 @@
#include "JSBroadcastChannel.h"
#include "JSBuffer.h"
#include "JSBufferList.h"
#include "sqlite/JSNodeSQLiteDatabaseSync.h"
#include "sqlite/JSNodeSQLiteStatementSync.h"
#include "sqlite/NodeSQLiteModule.h"
#include "webcore/JSMIMEBindings.h"
#include "JSByteLengthQueuingStrategy.h"
#include "JSCloseEvent.h"
@@ -3422,6 +3425,7 @@ void GlobalObject::finishCreation(VM& vm)
init.setConstructor(constructor);
});
m_JSCryptoKey.initLater(
[](const JSC::LazyProperty<JSC::JSGlobalObject, JSC::Structure>::Initializer& init) {
Zig::GlobalObject* globalObject = static_cast<Zig::GlobalObject*>(init.owner);

View File

@@ -215,6 +215,14 @@ public:
JSC::Structure* JSBufferStructure() const { return m_JSBufferClassStructure.getInitializedOnMainThread(this); }
JSC::JSObject* JSBufferConstructor() const { return m_JSBufferClassStructure.constructorInitializedOnMainThread(this); }
JSC::JSValue JSBufferPrototype() const { return m_JSBufferClassStructure.prototypeInitializedOnMainThread(this); }
JSC::Structure* JSNodeSQLiteDatabaseSyncStructure() const { return m_JSNodeSQLiteDatabaseSyncClassStructure.getInitializedOnMainThread(this); }
JSC::JSObject* JSNodeSQLiteDatabaseSyncConstructor() const { return m_JSNodeSQLiteDatabaseSyncClassStructure.constructorInitializedOnMainThread(this); }
JSC::JSValue JSNodeSQLiteDatabaseSyncPrototype() const { return m_JSNodeSQLiteDatabaseSyncClassStructure.prototypeInitializedOnMainThread(this); }
JSC::Structure* JSNodeSQLiteStatementSyncStructure() const { return m_JSNodeSQLiteStatementSyncClassStructure.getInitializedOnMainThread(this); }
JSC::JSObject* JSNodeSQLiteStatementSyncConstructor() const { return m_JSNodeSQLiteStatementSyncClassStructure.constructorInitializedOnMainThread(this); }
JSC::JSValue JSNodeSQLiteStatementSyncPrototype() const { return m_JSNodeSQLiteStatementSyncClassStructure.prototypeInitializedOnMainThread(this); }
JSC::Structure* JSBufferSubclassStructure() const { return m_JSBufferSubclassStructure.getInitializedOnMainThread(this); }
JSC::Structure* JSResizableOrGrowableSharedBufferSubclassStructure() const { return m_JSResizableOrGrowableSharedBufferSubclassStructure.getInitializedOnMainThread(this); }
@@ -534,6 +542,8 @@ public:
V(private, LazyClassStructure, m_NapiClassStructure) \
V(private, LazyClassStructure, m_callSiteStructure) \
V(public, LazyClassStructure, m_JSBufferClassStructure) \
V(public, LazyClassStructure, m_JSNodeSQLiteDatabaseSyncClassStructure) \
V(public, LazyClassStructure, m_JSNodeSQLiteStatementSyncClassStructure) \
V(public, LazyClassStructure, m_NodeVMScriptClassStructure) \
V(public, LazyClassStructure, m_NodeVMSourceTextModuleClassStructure) \
V(public, LazyClassStructure, m_NodeVMSyntheticModuleClassStructure) \

View File

@@ -0,0 +1,271 @@
/*
* Copyright (C) 2024 Codeblog Corp
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "root.h"
#include "JSNodeSQLiteDatabaseSync.h"
#include "JSNodeSQLiteStatementSync.h"
#include "JSSQLStatement.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 <JavaScriptCore/JSObjectInlines.h>
#include <JavaScriptCore/FunctionPrototype.h>
#include <JavaScriptCore/HeapAnalyzer.h>
#include <JavaScriptCore/JSDestructibleObjectHeapCellType.h>
#include <JavaScriptCore/SlotVisitorMacros.h>
#include <JavaScriptCore/AbstractSlotVisitorInlines.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"
namespace Bun {
using namespace JSC;
using namespace WebCore;
const ClassInfo JSNodeSQLiteDatabaseSync::s_info = { "DatabaseSync"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSNodeSQLiteDatabaseSync) };
JSNodeSQLiteDatabaseSync* JSNodeSQLiteDatabaseSync::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, const String& filename, JSObject* options)
{
auto* instance = new (NotNull, allocateCell<JSNodeSQLiteDatabaseSync>(vm)) JSNodeSQLiteDatabaseSync(vm, structure);
instance->finishCreation(vm, globalObject, filename, options);
return instance;
}
Structure* JSNodeSQLiteDatabaseSync::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
{
return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
}
JSNodeSQLiteDatabaseSync::JSNodeSQLiteDatabaseSync(VM& vm, Structure* structure)
: Base(vm, structure)
{
}
void JSNodeSQLiteDatabaseSync::finishCreation(VM& vm, JSGlobalObject* globalObject, const String& filename, JSObject* options)
{
auto throwScope = DECLARE_THROW_SCOPE(vm);
Base::finishCreation(vm);
ASSERT(inherits(info()));
// Create options for underlying Database
JSObject* bunOptions = constructEmptyObject(globalObject, globalObject->objectPrototype(), 2);
if (options) {
// Convert Node.js options to Bun options
JSValue readOnlyValue = options->get(globalObject, PropertyName(Identifier::fromString(vm, "readOnly"_s)));
if (readOnlyValue.isBoolean()) {
bunOptions->putDirect(vm, PropertyName(Identifier::fromString(vm, "readonly"_s)), readOnlyValue);
}
// TODO: Process Node.js options like timeout, enableForeignKeys, readBigInts, returnArrays
// For now, we just pass along the readOnly flag to Bun
RETURN_IF_EXCEPTION(throwScope, );
}
// Create the underlying Bun Database
auto* zigGlobalObject = reinterpret_cast<Zig::GlobalObject*>(globalObject);
// Get the BunSql module
JSValue sqliteModule = zigGlobalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::BunSql);
RETURN_IF_EXCEPTION(throwScope, );
// Get the Database constructor from the default export
JSValue databaseConstructor = sqliteModule.get(globalObject, vm.propertyNames->defaultKeyword);
RETURN_IF_EXCEPTION(throwScope, );
if (!databaseConstructor.isConstructor()) {
throwTypeError(globalObject, throwScope, "Database is not a constructor"_s);
return;
}
if (databaseConstructor.isConstructor()) {
MarkedArgumentBuffer args;
args.append(jsString(vm, filename));
args.append(bunOptions);
JSValue result = JSC::construct(globalObject, databaseConstructor, args, "Database constructor"_s);
RETURN_IF_EXCEPTION(throwScope, );
if (result.isObject()) {
m_database.set(vm, this, result.getObject());
}
}
}
DEFINE_VISIT_CHILDREN(JSNodeSQLiteDatabaseSync);
template<typename Visitor>
void JSNodeSQLiteDatabaseSync::visitChildrenImpl(JSCell* cell, Visitor& visitor)
{
auto* thisObject = jsCast<JSNodeSQLiteDatabaseSync*>(cell);
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
Base::visitChildren(cell, visitor);
visitor.append(thisObject->m_database);
}
JSC_DEFINE_HOST_FUNCTION(jsNodeSQLiteDatabaseSyncPrototypeFunctionPrepare, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSNodeSQLiteDatabaseSync*>(callFrame->thisValue());
if (!thisObject) {
return throwVMTypeError(globalObject, scope, "Expected DatabaseSync"_s);
}
if (callFrame->argumentCount() < 1) {
return throwVMTypeError(globalObject, scope, "Missing required argument: sql"_s);
}
JSValue sqlValue = callFrame->argument(0);
if (!sqlValue.isString()) {
return throwVMTypeError(globalObject, scope, "SQL must be a string"_s);
}
String sql = sqlValue.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
// Call the underlying database.query() method
JSObject* database = thisObject->database();
if (!database) {
return throwVMError(globalObject, scope, "Database is not open"_s);
}
JSValue queryMethod = database->get(globalObject, PropertyName(Identifier::fromString(vm, "query"_s)));
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
if (!queryMethod.isCallable()) {
return throwVMError(globalObject, scope, "Database query method is not callable"_s);
}
MarkedArgumentBuffer args;
args.append(sqlValue);
JSValue statement = JSC::call(globalObject, queryMethod, args, "Database prepare"_s);
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
// Wrap the statement in JSNodeSQLiteStatementSync
auto* zigGlobalObject = reinterpret_cast<Zig::GlobalObject*>(globalObject);
Structure* structure = zigGlobalObject->JSNodeSQLiteStatementSyncStructure();
auto* statementSync = JSNodeSQLiteStatementSync::create(vm, globalObject, structure, statement.getObject(), thisObject);
RELEASE_AND_RETURN(scope, JSValue::encode(statementSync));
}
JSC_DEFINE_HOST_FUNCTION(jsNodeSQLiteDatabaseSyncPrototypeFunctionExec, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSNodeSQLiteDatabaseSync*>(callFrame->thisValue());
if (!thisObject) {
return throwVMTypeError(globalObject, scope, "Expected DatabaseSync"_s);
}
if (callFrame->argumentCount() < 1) {
return throwVMTypeError(globalObject, scope, "Missing required argument: sql"_s);
}
JSValue sqlValue = callFrame->argument(0);
if (!sqlValue.isString()) {
return throwVMTypeError(globalObject, scope, "SQL must be a string"_s);
}
// Call the underlying database.exec() method
JSObject* database = thisObject->database();
if (!database) {
return throwVMError(globalObject, scope, "Database is not open"_s);
}
JSValue execMethod = database->get(globalObject, PropertyName(Identifier::fromString(vm, "exec"_s)));
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
if (!execMethod.isCallable()) {
return throwVMError(globalObject, scope, "Database exec method is not callable"_s);
}
MarkedArgumentBuffer args;
args.append(sqlValue);
JSValue result = JSC::call(globalObject, execMethod, args, "Database exec"_s);
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
RELEASE_AND_RETURN(scope, JSValue::encode(result));
}
JSC_DEFINE_HOST_FUNCTION(jsNodeSQLiteDatabaseSyncPrototypeFunctionClose, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSNodeSQLiteDatabaseSync*>(callFrame->thisValue());
if (!thisObject) {
return throwVMTypeError(globalObject, scope, "Expected DatabaseSync"_s);
}
// Call the underlying database.close() method
JSObject* database = thisObject->database();
if (database) {
JSValue closeMethod = database->get(globalObject, PropertyName(Identifier::fromString(vm, "close"_s)));
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
if (closeMethod.isCallable()) {
MarkedArgumentBuffer args;
JSC::call(globalObject, closeMethod, args, "Database close"_s);
}
thisObject->m_database.clear();
}
RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined()));
}
} // namespace Bun

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2024 Codeblog Corp
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "root.h"
#include "ZigGlobalObject.h"
#include <JavaScriptCore/JSFunction.h>
#include <JavaScriptCore/VM.h>
#include <JavaScriptCore/JSDestructibleObject.h>
#include "headers-handwritten.h"
#include "BunClientData.h"
#include <JavaScriptCore/CallFrame.h>
namespace Bun {
using namespace JSC;
using namespace WebCore;
class JSNodeSQLiteStatementSync;
class JSNodeSQLiteDatabaseSync final : public JSC::JSDestructibleObject {
using Base = JSC::JSDestructibleObject;
public:
static constexpr unsigned StructureFlags = Base::StructureFlags | HasStaticPropertyTable;
static constexpr bool needsDestruction = true;
static JSNodeSQLiteDatabaseSync* create(VM& vm, JSGlobalObject* globalObject, Structure* structure, const String& filename, JSObject* options);
static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype);
DECLARE_INFO;
template<typename CellType, SubspaceAccess>
static CompleteSubspace* subspaceFor(VM& vm)
{
return &vm.destructibleObjectSpace();
}
JSObject* database() const { return m_database.get(); }
private:
JSNodeSQLiteDatabaseSync(VM& vm, Structure* structure);
void finishCreation(VM& vm, JSGlobalObject* globalObject, const String& filename, JSObject* options);
DECLARE_VISIT_CHILDREN;
WriteBarrier<JSObject> m_database;
};
JSC_DECLARE_HOST_FUNCTION(jsNodeSQLiteDatabaseSyncPrototypeFunctionPrepare);
JSC_DECLARE_HOST_FUNCTION(jsNodeSQLiteDatabaseSyncPrototypeFunctionExec);
JSC_DECLARE_HOST_FUNCTION(jsNodeSQLiteDatabaseSyncPrototypeFunctionClose);
} // namespace Bun

View File

@@ -0,0 +1,290 @@
/*
* Copyright (C) 2024 Codeblog Corp
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "root.h"
#include "JSNodeSQLiteStatementSync.h"
#include "JSNodeSQLiteDatabaseSync.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 <JavaScriptCore/JSObjectInlines.h>
#include <JavaScriptCore/FunctionPrototype.h>
#include <JavaScriptCore/HeapAnalyzer.h>
#include <JavaScriptCore/JSDestructibleObjectHeapCellType.h>
#include <JavaScriptCore/SlotVisitorMacros.h>
#include <JavaScriptCore/AbstractSlotVisitorInlines.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"
namespace Bun {
using namespace JSC;
using namespace WebCore;
const ClassInfo JSNodeSQLiteStatementSync::s_info = { "StatementSync"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSNodeSQLiteStatementSync) };
JSNodeSQLiteStatementSync* JSNodeSQLiteStatementSync::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSObject* statement, JSNodeSQLiteDatabaseSync* database)
{
auto* instance = new (NotNull, allocateCell<JSNodeSQLiteStatementSync>(vm)) JSNodeSQLiteStatementSync(vm, structure);
instance->finishCreation(vm, globalObject, statement, database);
return instance;
}
Structure* JSNodeSQLiteStatementSync::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
{
return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
}
JSNodeSQLiteStatementSync::JSNodeSQLiteStatementSync(VM& vm, Structure* structure)
: Base(vm, structure)
{
}
void JSNodeSQLiteStatementSync::finishCreation(VM& vm, JSGlobalObject* globalObject, JSObject* statement, JSNodeSQLiteDatabaseSync* database)
{
Base::finishCreation(vm);
ASSERT(inherits(info()));
m_statement.set(vm, this, statement);
m_database.set(vm, this, database);
}
DEFINE_VISIT_CHILDREN(JSNodeSQLiteStatementSync);
template<typename Visitor>
void JSNodeSQLiteStatementSync::visitChildrenImpl(JSCell* cell, Visitor& visitor)
{
auto* thisObject = jsCast<JSNodeSQLiteStatementSync*>(cell);
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
Base::visitChildren(cell, visitor);
visitor.append(thisObject->m_statement);
visitor.append(thisObject->m_database);
}
static JSValue callStatementMethod(JSGlobalObject* globalObject, JSObject* statement, const String& methodName, const MarkedArgumentBuffer& args, bool expectsReturn = true)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue method = statement->get(globalObject, PropertyName(Identifier::fromString(vm, methodName)));
RETURN_IF_EXCEPTION(scope, JSValue());
if (!method.isCallable()) {
throwVMError(globalObject, scope, "Statement method is not callable"_s);
return JSValue();
}
JSValue result = JSC::call(globalObject, method, args, "Statement method call"_s);
RETURN_IF_EXCEPTION(scope, JSValue());
return result;
}
JSC_DEFINE_HOST_FUNCTION(jsNodeSQLiteStatementSyncPrototypeFunctionRun, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSNodeSQLiteStatementSync*>(callFrame->thisValue());
if (!thisObject) {
return throwVMTypeError(globalObject, scope, "Expected StatementSync"_s);
}
JSObject* statement = thisObject->statement();
if (!statement) {
return throwVMError(globalObject, scope, "Statement is not valid"_s);
}
MarkedArgumentBuffer args;
for (size_t i = 0; i < callFrame->argumentCount(); i++) {
args.append(callFrame->argument(i));
}
JSValue result = callStatementMethod(globalObject, statement, "run"_s, args);
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
RELEASE_AND_RETURN(scope, JSValue::encode(result));
}
JSC_DEFINE_HOST_FUNCTION(jsNodeSQLiteStatementSyncPrototypeFunctionGet, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSNodeSQLiteStatementSync*>(callFrame->thisValue());
if (!thisObject) {
return throwVMTypeError(globalObject, scope, "Expected StatementSync"_s);
}
JSObject* statement = thisObject->statement();
if (!statement) {
return throwVMError(globalObject, scope, "Statement is not valid"_s);
}
MarkedArgumentBuffer args;
for (size_t i = 0; i < callFrame->argumentCount(); i++) {
args.append(callFrame->argument(i));
}
JSValue result = callStatementMethod(globalObject, statement, "get"_s, args);
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
RELEASE_AND_RETURN(scope, JSValue::encode(result));
}
JSC_DEFINE_HOST_FUNCTION(jsNodeSQLiteStatementSyncPrototypeFunctionAll, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSNodeSQLiteStatementSync*>(callFrame->thisValue());
if (!thisObject) {
return throwVMTypeError(globalObject, scope, "Expected StatementSync"_s);
}
JSObject* statement = thisObject->statement();
if (!statement) {
return throwVMError(globalObject, scope, "Statement is not valid"_s);
}
MarkedArgumentBuffer args;
for (size_t i = 0; i < callFrame->argumentCount(); i++) {
args.append(callFrame->argument(i));
}
JSValue result = callStatementMethod(globalObject, statement, "all"_s, args);
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
RELEASE_AND_RETURN(scope, JSValue::encode(result));
}
JSC_DEFINE_HOST_FUNCTION(jsNodeSQLiteStatementSyncPrototypeFunctionValues, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSNodeSQLiteStatementSync*>(callFrame->thisValue());
if (!thisObject) {
return throwVMTypeError(globalObject, scope, "Expected StatementSync"_s);
}
JSObject* statement = thisObject->statement();
if (!statement) {
return throwVMError(globalObject, scope, "Statement is not valid"_s);
}
MarkedArgumentBuffer args;
for (size_t i = 0; i < callFrame->argumentCount(); i++) {
args.append(callFrame->argument(i));
}
JSValue result = callStatementMethod(globalObject, statement, "values"_s, args);
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
RELEASE_AND_RETURN(scope, JSValue::encode(result));
}
JSC_DEFINE_HOST_FUNCTION(jsNodeSQLiteStatementSyncPrototypeFunctionFinalize, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSNodeSQLiteStatementSync*>(callFrame->thisValue());
if (!thisObject) {
return throwVMTypeError(globalObject, scope, "Expected StatementSync"_s);
}
JSObject* statement = thisObject->statement();
if (!statement) {
return throwVMError(globalObject, scope, "Statement is not valid"_s);
}
MarkedArgumentBuffer args;
callStatementMethod(globalObject, statement, "finalize"_s, args, false);
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
// Clear the statement reference
thisObject->m_statement.clear();
RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined()));
}
JSC_DEFINE_CUSTOM_GETTER(jsNodeSQLiteStatementSyncPrototype_sourceSQL, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSNodeSQLiteStatementSync*>(JSValue::decode(thisValue));
if (!thisObject) {
return throwVMTypeError(globalObject, scope, "Expected StatementSync"_s);
}
JSObject* statement = thisObject->statement();
if (!statement) {
return JSValue::encode(jsNull());
}
JSValue result = statement->get(globalObject, PropertyName(Identifier::fromString(vm, "native"_s)));
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsNull()));
if (result.isObject()) {
JSValue sql = result.getObject()->get(globalObject, PropertyName(Identifier::fromString(vm, "toString"_s)));
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsNull()));
if (sql.isCallable()) {
MarkedArgumentBuffer args;
JSValue sqlResult = JSC::call(globalObject, sql, args, "SQL toString"_s);
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsNull()));
return JSValue::encode(sqlResult);
}
}
RELEASE_AND_RETURN(scope, JSValue::encode(jsNull()));
}
} // namespace Bun

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2024 Codeblog Corp
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "root.h"
#include "ZigGlobalObject.h"
#include <JavaScriptCore/JSFunction.h>
#include <JavaScriptCore/VM.h>
#include <JavaScriptCore/JSDestructibleObject.h>
#include "headers-handwritten.h"
#include "BunClientData.h"
#include <JavaScriptCore/CallFrame.h>
namespace Bun {
using namespace JSC;
using namespace WebCore;
class JSNodeSQLiteDatabaseSync;
class JSNodeSQLiteStatementSync final : public JSC::JSDestructibleObject {
using Base = JSC::JSDestructibleObject;
public:
static constexpr unsigned StructureFlags = Base::StructureFlags | HasStaticPropertyTable;
static constexpr bool needsDestruction = true;
static JSNodeSQLiteStatementSync* create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSObject* statement, JSNodeSQLiteDatabaseSync* database);
static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype);
DECLARE_INFO;
template<typename CellType, SubspaceAccess>
static CompleteSubspace* subspaceFor(VM& vm)
{
return &vm.destructibleObjectSpace();
}
JSObject* statement() const { return m_statement.get(); }
JSNodeSQLiteDatabaseSync* database() const { return m_database.get(); }
private:
JSNodeSQLiteStatementSync(VM& vm, Structure* structure);
void finishCreation(VM& vm, JSGlobalObject* globalObject, JSObject* statement, JSNodeSQLiteDatabaseSync* database);
DECLARE_VISIT_CHILDREN;
WriteBarrier<JSObject> m_statement;
WriteBarrier<JSNodeSQLiteDatabaseSync> m_database;
};
JSC_DECLARE_HOST_FUNCTION(jsNodeSQLiteStatementSyncPrototypeFunctionRun);
JSC_DECLARE_HOST_FUNCTION(jsNodeSQLiteStatementSyncPrototypeFunctionGet);
JSC_DECLARE_HOST_FUNCTION(jsNodeSQLiteStatementSyncPrototypeFunctionAll);
JSC_DECLARE_HOST_FUNCTION(jsNodeSQLiteStatementSyncPrototypeFunctionValues);
JSC_DECLARE_HOST_FUNCTION(jsNodeSQLiteStatementSyncPrototypeFunctionFinalize);
JSC_DECLARE_CUSTOM_GETTER(jsNodeSQLiteStatementSyncPrototype_sourceSQL);
} // namespace Bun

View File

@@ -0,0 +1,197 @@
/*
* Copyright (C) 2024 Codeblog Corp
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "root.h"
#include "NodeSQLiteModule.h"
#include "JSNodeSQLiteDatabaseSync.h"
#include "JSNodeSQLiteStatementSync.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 <JavaScriptCore/JSObjectInlines.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"
namespace Bun {
using namespace JSC;
using namespace WebCore;
// DatabaseSync Constructor
JSC_DEFINE_HOST_FUNCTION(jsDatabaseSyncConstructor, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
if (callFrame->argumentCount() < 1) {
return throwVMTypeError(globalObject, scope, "Missing required argument: filename"_s);
}
JSValue filenameValue = callFrame->argument(0);
if (!filenameValue.isString()) {
return throwVMTypeError(globalObject, scope, "Filename must be a string"_s);
}
String filename = filenameValue.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
JSObject* options = nullptr;
if (callFrame->argumentCount() > 1 && callFrame->argument(1).isObject()) {
options = callFrame->argument(1).getObject();
}
auto* zigGlobalObject = reinterpret_cast<Zig::GlobalObject*>(globalObject);
Structure* structure = zigGlobalObject->JSNodeSQLiteDatabaseSyncStructure();
auto* database = JSNodeSQLiteDatabaseSync::create(vm, globalObject, structure, filename, options);
RELEASE_AND_RETURN(scope, JSValue::encode(database));
}
// StatementSync Constructor (should not be called directly)
JSC_DEFINE_HOST_FUNCTION(jsStatementSyncConstructor, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
return throwVMTypeError(globalObject, scope, "StatementSync cannot be constructed directly"_s);
}
// backup function
JSC_DEFINE_HOST_FUNCTION(jsNodeSQLiteBackup, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
// TODO: Implement backup functionality
return throwVMError(globalObject, scope, "backup() is not yet implemented"_s);
}
// Create DatabaseSync prototype
static JSObject* createDatabaseSyncPrototype(VM& vm, JSGlobalObject* globalObject)
{
auto* prototype = constructEmptyObject(globalObject, globalObject->objectPrototype(), 3);
// Add methods to prototype
prototype->putDirect(vm, PropertyName(Identifier::fromString(vm, "prepare"_s)),
JSFunction::create(vm, globalObject, 1, "prepare"_s, jsNodeSQLiteDatabaseSyncPrototypeFunctionPrepare, ImplementationVisibility::Public, NoIntrinsic, jsNodeSQLiteDatabaseSyncPrototypeFunctionPrepare));
prototype->putDirect(vm, PropertyName(Identifier::fromString(vm, "exec"_s)),
JSFunction::create(vm, globalObject, 1, "exec"_s, jsNodeSQLiteDatabaseSyncPrototypeFunctionExec, ImplementationVisibility::Public, NoIntrinsic, jsNodeSQLiteDatabaseSyncPrototypeFunctionExec));
prototype->putDirect(vm, PropertyName(Identifier::fromString(vm, "close"_s)),
JSFunction::create(vm, globalObject, 0, "close"_s, jsNodeSQLiteDatabaseSyncPrototypeFunctionClose, ImplementationVisibility::Public, NoIntrinsic, jsNodeSQLiteDatabaseSyncPrototypeFunctionClose));
return prototype;
}
// Create StatementSync prototype
static JSObject* createStatementSyncPrototype(VM& vm, JSGlobalObject* globalObject)
{
auto* prototype = constructEmptyObject(globalObject, globalObject->objectPrototype(), 3);
// Add methods to prototype
prototype->putDirect(vm, PropertyName(Identifier::fromString(vm, "run"_s)),
JSFunction::create(vm, globalObject, 0, "run"_s, jsNodeSQLiteStatementSyncPrototypeFunctionRun, ImplementationVisibility::Public, NoIntrinsic, jsNodeSQLiteStatementSyncPrototypeFunctionRun));
prototype->putDirect(vm, PropertyName(Identifier::fromString(vm, "get"_s)),
JSFunction::create(vm, globalObject, 0, "get"_s, jsNodeSQLiteStatementSyncPrototypeFunctionGet, ImplementationVisibility::Public, NoIntrinsic, jsNodeSQLiteStatementSyncPrototypeFunctionGet));
prototype->putDirect(vm, PropertyName(Identifier::fromString(vm, "all"_s)),
JSFunction::create(vm, globalObject, 0, "all"_s, jsNodeSQLiteStatementSyncPrototypeFunctionAll, ImplementationVisibility::Public, NoIntrinsic, jsNodeSQLiteStatementSyncPrototypeFunctionAll));
prototype->putDirect(vm, PropertyName(Identifier::fromString(vm, "values"_s)),
JSFunction::create(vm, globalObject, 0, "values"_s, jsNodeSQLiteStatementSyncPrototypeFunctionValues, ImplementationVisibility::Public, NoIntrinsic, jsNodeSQLiteStatementSyncPrototypeFunctionValues));
prototype->putDirect(vm, PropertyName(Identifier::fromString(vm, "finalize"_s)),
JSFunction::create(vm, globalObject, 0, "finalize"_s, jsNodeSQLiteStatementSyncPrototypeFunctionFinalize, ImplementationVisibility::Public, NoIntrinsic, jsNodeSQLiteStatementSyncPrototypeFunctionFinalize));
// Add sourceSQL getter
auto* getterSetter = CustomGetterSetter::create(vm, jsNodeSQLiteStatementSyncPrototype_sourceSQL, nullptr);
prototype->putDirectCustomAccessor(vm, PropertyName(Identifier::fromString(vm, "sourceSQL"_s)), getterSetter, PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor);
return prototype;
}
void generateNodeSQLiteModule(JSGlobalObject* globalObject, JSObject* moduleExports)
{
VM& vm = globalObject->vm();
auto* zigGlobalObject = reinterpret_cast<Zig::GlobalObject*>(globalObject);
// Use the LazyClassStructure that was set up in ZigGlobalObject.h
auto* databaseSyncConstructor = zigGlobalObject->JSNodeSQLiteDatabaseSyncConstructor();
auto* statementSyncConstructor = zigGlobalObject->JSNodeSQLiteStatementSyncConstructor();
// Export DatabaseSync and StatementSync
moduleExports->putDirect(vm, PropertyName(Identifier::fromString(vm, "DatabaseSync"_s)), databaseSyncConstructor);
moduleExports->putDirect(vm, PropertyName(Identifier::fromString(vm, "StatementSync"_s)), statementSyncConstructor);
// Export backup function
auto* backupFunction = JSFunction::create(vm, globalObject, 2, "backup"_s, jsNodeSQLiteBackup, ImplementationVisibility::Public, NoIntrinsic, jsNodeSQLiteBackup);
moduleExports->putDirect(vm, PropertyName(Identifier::fromString(vm, "backup"_s)), backupFunction);
// Export constants
auto* constants = constructEmptyObject(globalObject, globalObject->objectPrototype(), 6);
// SQLite changeset constants
constants->putDirect(vm, PropertyName(Identifier::fromString(vm, "SQLITE_CHANGESET_OMIT"_s)), jsNumber(0));
constants->putDirect(vm, PropertyName(Identifier::fromString(vm, "SQLITE_CHANGESET_REPLACE"_s)), jsNumber(1));
constants->putDirect(vm, PropertyName(Identifier::fromString(vm, "SQLITE_CHANGESET_ABORT"_s)), jsNumber(2));
constants->putDirect(vm, PropertyName(Identifier::fromString(vm, "SQLITE_CHANGESET_DATA"_s)), jsNumber(1));
constants->putDirect(vm, PropertyName(Identifier::fromString(vm, "SQLITE_CHANGESET_NOTFOUND"_s)), jsNumber(2));
constants->putDirect(vm, PropertyName(Identifier::fromString(vm, "SQLITE_CHANGESET_CONFLICT"_s)), jsNumber(3));
constants->putDirect(vm, PropertyName(Identifier::fromString(vm, "SQLITE_CHANGESET_CONSTRAINT"_s)), jsNumber(4));
constants->putDirect(vm, PropertyName(Identifier::fromString(vm, "SQLITE_CHANGESET_FOREIGN_KEY"_s)), jsNumber(5));
moduleExports->putDirect(vm, PropertyName(Identifier::fromString(vm, "constants"_s)), constants);
}
} // namespace Bun

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2024 Codeblog Corp
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "root.h"
#include "ZigGlobalObject.h"
#include <JavaScriptCore/JSFunction.h>
#include <JavaScriptCore/VM.h>
#include <JavaScriptCore/JSObject.h>
#include "headers-handwritten.h"
#include "BunClientData.h"
#include <JavaScriptCore/CallFrame.h>
namespace Bun {
using namespace JSC;
using namespace WebCore;
JSC_DECLARE_HOST_FUNCTION(jsDatabaseSyncConstructor);
JSC_DECLARE_HOST_FUNCTION(jsStatementSyncConstructor);
JSC_DECLARE_HOST_FUNCTION(jsNodeSQLiteBackup);
void generateNodeSQLiteModule(JSGlobalObject* globalObject, JSObject* moduleExports);
} // namespace Bun

View File

@@ -0,0 +1,32 @@
#pragma once
#include "root.h"
#include "_NativeModule.h"
namespace Zig {
using namespace WebCore;
using namespace JSC;
DEFINE_NATIVE_MODULE(NodeSQLite)
{
INIT_NATIVE_MODULE(4);
// For now, just export placeholder functions until we get the build working
put(JSC::Identifier::fromString(vm, "DatabaseSync"_s), JSC::jsUndefined());
put(JSC::Identifier::fromString(vm, "StatementSync"_s), JSC::jsUndefined());
auto* constants = JSC::constructEmptyObject(lexicalGlobalObject, globalObject->objectPrototype(), 6);
constants->putDirect(vm, JSC::Identifier::fromString(vm, "SQLITE_CHANGESET_OMIT"_s), JSC::jsNumber(0));
constants->putDirect(vm, JSC::Identifier::fromString(vm, "SQLITE_CHANGESET_REPLACE"_s), JSC::jsNumber(1));
constants->putDirect(vm, JSC::Identifier::fromString(vm, "SQLITE_CHANGESET_ABORT"_s), JSC::jsNumber(2));
constants->putDirect(vm, JSC::Identifier::fromString(vm, "SQLITE_CHANGESET_DATA"_s), JSC::jsNumber(1));
constants->putDirect(vm, JSC::Identifier::fromString(vm, "SQLITE_CHANGESET_NOTFOUND"_s), JSC::jsNumber(2));
constants->putDirect(vm, JSC::Identifier::fromString(vm, "SQLITE_CHANGESET_CONFLICT"_s), JSC::jsNumber(3));
put(JSC::Identifier::fromString(vm, "constants"_s), constants);
put(JSC::Identifier::fromString(vm, "backup"_s), JSC::jsUndefined());
RETURN_NATIVE_MODULE();
}
} // namespace Zig

View File

@@ -29,6 +29,7 @@
macro("bun:jsc"_s, BunJSC) \
macro("node:buffer"_s, NodeBuffer) \
macro("node:constants"_s, NodeConstants) \
macro("node:sqlite"_s, NodeSQLite) \
macro("node:string_decoder"_s, NodeStringDecoder) \
macro("node:util/types"_s, NodeUtilTypes) \
macro("utf-8-validate"_s, UTF8Validate) \

View File

@@ -0,0 +1,415 @@
import { skipIfSQLiteMissing } from '../common/index.mjs';
import { describe, test } from 'node:test';
skipIfSQLiteMissing();
const { DatabaseSync } = await import('node:sqlite');
describe('DatabaseSync.prototype.aggregate()', () => {
describe('input validation', () => {
const db = new DatabaseSync(':memory:');
test('throws if options.start is not provided', (t) => {
t.assert.throws(() => {
db.aggregate('sum', {
result: (total) => total
});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.start" argument must be a function or a primitive value.'
});
});
test('throws if options.step is not a function', (t) => {
t.assert.throws(() => {
db.aggregate('sum', {
start: 0,
result: (total) => total
});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.step" argument must be a function.'
});
});
test('throws if options.useBigIntArguments is not a boolean', (t) => {
t.assert.throws(() => {
db.aggregate('sum', {
start: 0,
step: () => null,
useBigIntArguments: ''
});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.useBigIntArguments" argument must be a boolean/,
});
});
test('throws if options.varargs is not a boolean', (t) => {
t.assert.throws(() => {
db.aggregate('sum', {
start: 0,
step: () => null,
varargs: ''
});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.varargs" argument must be a boolean/,
});
});
test('throws if options.directOnly is not a boolean', (t) => {
t.assert.throws(() => {
db.aggregate('sum', {
start: 0,
step: () => null,
directOnly: ''
});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.directOnly" argument must be a boolean/,
});
});
});
});
describe('varargs', () => {
test('supports variable number of arguments when true', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
db.exec('CREATE TABLE data (value INTEGER)');
db.exec('INSERT INTO data VALUES (1), (2), (3)');
db.aggregate('sum_int', {
start: 0,
step: (_acc, _value, var1, var2, var3) => {
return var1 + var2 + var3;
},
varargs: true,
});
const result = db.prepare('SELECT sum_int(value, 1, 2, 3) as total FROM data').get();
t.assert.deepStrictEqual(result, { __proto__: null, total: 6 });
});
test('uses the max between step.length and inverse.length when false', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
db.exec(`
CREATE TABLE t3(x, y);
INSERT INTO t3 VALUES ('a', 1),
('b', 2),
('c', 3);
`);
db.aggregate('sumint', {
start: 0,
step: (acc, var1) => {
return var1 + acc;
},
inverse: (acc, var1, var2) => {
return acc - var1 - var2;
},
varargs: false,
});
const result = db.prepare(`
SELECT x, sumint(y, 10) OVER (
ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS sum_y
FROM t3 ORDER BY x;
`).all();
t.assert.deepStrictEqual(result, [
{ __proto__: null, x: 'a', sum_y: 3 },
{ __proto__: null, x: 'b', sum_y: 6 },
{ __proto__: null, x: 'c', sum_y: -5 },
]);
t.assert.throws(() => {
db.prepare(`
SELECT x, sumint(y) OVER (
ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS sum_y
FROM t3 ORDER BY x;
`);
}, {
code: 'ERR_SQLITE_ERROR',
message: 'wrong number of arguments to function sumint()'
});
});
test('throws if an incorrect number of arguments is provided when false', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
db.aggregate('sum_int', {
start: 0,
step: (_acc, var1, var2, var3) => {
return var1 + var2 + var3;
},
varargs: false,
});
t.assert.throws(() => {
db.prepare('SELECT sum_int(1, 2, 3, 4)').get();
}, {
code: 'ERR_SQLITE_ERROR',
message: 'wrong number of arguments to function sum_int()'
});
});
});
describe('directOnly', () => {
test('is false by default', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
db.aggregate('func', {
start: 0,
step: (acc, value) => acc + value,
inverse: (acc, value) => acc - value,
});
db.exec(`
CREATE TABLE t3(x, y);
INSERT INTO t3 VALUES ('a', 4),
('b', 5),
('c', 3);
`);
db.exec(`
CREATE TRIGGER test_trigger
AFTER INSERT ON t3
BEGIN
SELECT func(1) OVER ();
END;
`);
// TRIGGER will work fine with the window function
db.exec('INSERT INTO t3 VALUES(\'d\', 6)');
});
test('set SQLITE_DIRECT_ONLY flag when true', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
db.aggregate('func', {
start: 0,
step: (acc, value) => acc + value,
inverse: (acc, value) => acc - value,
directOnly: true,
});
db.exec(`
CREATE TABLE t3(x, y);
INSERT INTO t3 VALUES ('a', 4),
('b', 5),
('c', 3);
`);
db.exec(`
CREATE TRIGGER test_trigger
AFTER INSERT ON t3
BEGIN
SELECT func(1) OVER ();
END;
`);
t.assert.throws(() => {
db.exec('INSERT INTO t3 VALUES(\'d\', 6)');
}, {
code: 'ERR_SQLITE_ERROR',
message: /unsafe use of func\(\)/
});
});
});
describe('start', () => {
test('start option as a value', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
db.exec('CREATE TABLE data (value INTEGER)');
db.exec('INSERT INTO data VALUES (1), (2), (3)');
db.aggregate('sum_int', {
start: 0,
step: (acc, value) => acc + value,
});
const result = db.prepare('SELECT sum_int(value) as total FROM data').get();
t.assert.deepStrictEqual(result, { __proto__: null, total: 6 });
});
test('start option as a function', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
db.exec('CREATE TABLE data (value INTEGER)');
db.exec('INSERT INTO data VALUES (1), (2), (3)');
db.aggregate('sum_int', {
start: () => 0,
step: (acc, value) => acc + value,
});
const result = db.prepare('SELECT sum_int(value) as total FROM data').get();
t.assert.deepStrictEqual(result, { __proto__: null, total: 6 });
});
test('start option can hold any js value', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
db.exec('CREATE TABLE data (value INTEGER)');
db.exec('INSERT INTO data VALUES (1), (2), (3)');
db.aggregate('sum_int', {
start: () => [],
step: (acc, value) => {
return [...acc, value];
},
result: (acc) => acc.join(', '),
});
const result = db.prepare('SELECT sum_int(value) as total FROM data').get();
t.assert.deepStrictEqual(result, { __proto__: null, total: '1, 2, 3' });
});
test('throws if start throws an error', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
db.exec('CREATE TABLE data (value INTEGER)');
db.exec('INSERT INTO data VALUES (1), (2), (3)');
db.aggregate('agg', {
start: () => {
throw new Error('start error');
},
step: () => null,
});
t.assert.throws(() => {
db.prepare('SELECT agg()').get();
}, {
message: 'start error'
});
});
});
describe('step', () => {
test('throws if step throws an error', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
db.exec('CREATE TABLE data (value INTEGER)');
db.exec('INSERT INTO data VALUES (1), (2), (3)');
db.aggregate('agg', {
start: 0,
step: () => {
throw new Error('step error');
},
});
t.assert.throws(() => {
db.prepare('SELECT agg()').get();
}, {
message: 'step error'
});
});
});
describe('result', () => {
test('throws if result throws an error', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
db.exec('CREATE TABLE data (value INTEGER)');
db.exec('INSERT INTO data VALUES (1), (2), (3)');
db.aggregate('sum_int', {
start: 0,
step: (acc, value) => {
return acc + value;
},
result: () => {
throw new Error('result error');
},
});
t.assert.throws(() => {
db.prepare('SELECT sum_int(value) as result FROM data').get();
}, {
message: 'result error'
});
});
test('executes once when options.inverse is not present', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
const mockFn = t.mock.fn(() => 'overridden');
db.exec('CREATE TABLE data (value INTEGER)');
db.exec('INSERT INTO data VALUES (1), (2), (3)');
db.aggregate('sum_int', {
start: 0,
step: (acc, value) => {
return acc + value;
},
result: mockFn
});
const result = db.prepare('SELECT sum_int(value) as result FROM data').get();
t.assert.deepStrictEqual(result, { __proto__: null, result: 'overridden' });
t.assert.strictEqual(mockFn.mock.calls.length, 1);
t.assert.deepStrictEqual(mockFn.mock.calls[0].arguments, [6]);
});
test('executes once per row when options.inverse is present', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
const mockFn = t.mock.fn((acc) => acc);
db.exec(`
CREATE TABLE t3(x, y);
INSERT INTO t3 VALUES ('a', 4),
('b', 5),
('c', 3);
`);
db.aggregate('sumint', {
start: 0,
step: (acc, value) => {
return acc + value;
},
inverse: (acc, value) => {
return acc - value;
},
result: mockFn
});
db.prepare(`
SELECT x, sumint(y) OVER (
ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS sum_y
FROM t3 ORDER BY x;
`).all();
t.assert.strictEqual(mockFn.mock.calls.length, 3);
t.assert.deepStrictEqual(mockFn.mock.calls[0].arguments, [9]);
t.assert.deepStrictEqual(mockFn.mock.calls[1].arguments, [12]);
t.assert.deepStrictEqual(mockFn.mock.calls[2].arguments, [8]);
});
});
test('throws an error when trying to use as windown function but didn\'t provide options.inverse', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => db.close());
db.exec(`
CREATE TABLE t3(x, y);
INSERT INTO t3 VALUES ('a', 4),
('b', 5),
('c', 3);
`);
db.aggregate('sumint', {
start: 0,
step: (total, nextValue) => total + nextValue,
});
t.assert.throws(() => {
db.prepare(`
SELECT x, sumint(y) OVER (
ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS sum_y
FROM t3 ORDER BY x;
`);
}, {
code: 'ERR_SQLITE_ERROR',
message: 'sumint() may not be used as a window function'
});
});

View File

@@ -0,0 +1,316 @@
import { isWindows, skipIfSQLiteMissing } from '../common/index.mjs';
import tmpdir from '../common/tmpdir.js';
import { join } from 'node:path';
import { describe, test } from 'node:test';
import { writeFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
skipIfSQLiteMissing();
const { backup, DatabaseSync } = await import('node:sqlite');
const isRoot = !isWindows && process.getuid() === 0;
let cnt = 0;
tmpdir.refresh();
function nextDb() {
return join(tmpdir.path, `database-${cnt++}.db`);
}
function makeSourceDb(dbPath = ':memory:') {
const database = new DatabaseSync(dbPath);
database.exec(`
CREATE TABLE data(
key INTEGER PRIMARY KEY,
value TEXT
) STRICT
`);
const insert = database.prepare('INSERT INTO data (key, value) VALUES (?, ?)');
for (let i = 1; i <= 2; i++) {
insert.run(i, `value-${i}`);
}
return database;
}
describe('backup()', () => {
test('throws if the source database is not provided', (t) => {
t.assert.throws(() => {
backup();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "sourceDb" argument must be an object.'
});
});
test('throws if path is not a string, URL, or Buffer', (t) => {
const database = makeSourceDb();
t.assert.throws(() => {
backup(database);
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.'
});
t.assert.throws(() => {
backup(database, {});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.'
});
});
test('throws if the database path contains null bytes', (t) => {
const database = makeSourceDb();
t.assert.throws(() => {
backup(database, Buffer.from('l\0cation'));
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.'
});
t.assert.throws(() => {
backup(database, 'l\0cation');
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.'
});
});
test('throws if options is not an object', (t) => {
const database = makeSourceDb();
t.assert.throws(() => {
backup(database, 'hello.db', 'invalid');
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options" argument must be an object.'
});
});
test('throws if any of provided options is invalid', (t) => {
const database = makeSourceDb();
t.assert.throws(() => {
backup(database, 'hello.db', {
source: 42
});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.source" argument must be a string.'
});
t.assert.throws(() => {
backup(database, 'hello.db', {
target: 42
});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.target" argument must be a string.'
});
t.assert.throws(() => {
backup(database, 'hello.db', {
rate: 'invalid'
});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.rate" argument must be an integer.'
});
t.assert.throws(() => {
backup(database, 'hello.db', {
progress: 'invalid'
});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.progress" argument must be a function.'
});
});
});
test('database backup', async (t) => {
const progressFn = t.mock.fn();
const database = makeSourceDb();
const destDb = nextDb();
await backup(database, destDb, {
rate: 1,
progress: progressFn,
});
const backupDb = new DatabaseSync(destDb);
const rows = backupDb.prepare('SELECT * FROM data').all();
// The source database has two pages - using the default page size -,
// so the progress function should be called once (the last call is not made since
// the promise resolves)
t.assert.strictEqual(progressFn.mock.calls.length, 1);
t.assert.deepStrictEqual(progressFn.mock.calls[0].arguments, [{ totalPages: 2, remainingPages: 1 }]);
t.assert.deepStrictEqual(rows, [
{ __proto__: null, key: 1, value: 'value-1' },
{ __proto__: null, key: 2, value: 'value-2' },
]);
t.after(() => {
database.close();
backupDb.close();
});
});
test('backup database using location as URL', async (t) => {
const database = makeSourceDb();
const destDb = pathToFileURL(nextDb());
t.after(() => { database.close(); });
await backup(database, destDb);
const backupDb = new DatabaseSync(destDb);
t.after(() => { backupDb.close(); });
const rows = backupDb.prepare('SELECT * FROM data').all();
t.assert.deepStrictEqual(rows, [
{ __proto__: null, key: 1, value: 'value-1' },
{ __proto__: null, key: 2, value: 'value-2' },
]);
});
test('backup database using location as Buffer', async (t) => {
const database = makeSourceDb();
const destDb = Buffer.from(nextDb());
t.after(() => { database.close(); });
await backup(database, destDb);
const backupDb = new DatabaseSync(destDb);
t.after(() => { backupDb.close(); });
const rows = backupDb.prepare('SELECT * FROM data').all();
t.assert.deepStrictEqual(rows, [
{ __proto__: null, key: 1, value: 'value-1' },
{ __proto__: null, key: 2, value: 'value-2' },
]);
});
test('database backup in a single call', async (t) => {
const progressFn = t.mock.fn();
const database = makeSourceDb();
const destDb = nextDb();
// Let rate to be default (100) to backup in a single call
await backup(database, destDb, {
progress: progressFn,
});
const backupDb = new DatabaseSync(destDb);
const rows = backupDb.prepare('SELECT * FROM data').all();
t.assert.strictEqual(progressFn.mock.calls.length, 0);
t.assert.deepStrictEqual(rows, [
{ __proto__: null, key: 1, value: 'value-1' },
{ __proto__: null, key: 2, value: 'value-2' },
]);
t.after(() => {
database.close();
backupDb.close();
});
});
test('throws exception when trying to start backup from a closed database', (t) => {
t.assert.throws(() => {
const database = new DatabaseSync(':memory:');
database.close();
backup(database, 'backup.db');
}, {
code: 'ERR_INVALID_STATE',
message: 'database is not open'
});
});
test('throws if URL is not file: scheme', (t) => {
const database = new DatabaseSync(':memory:');
t.after(() => { database.close(); });
t.assert.throws(() => {
backup(database, new URL('http://example.com/backup.db'));
}, {
code: 'ERR_INVALID_URL_SCHEME',
message: 'The URL must be of scheme file:',
});
});
test('database backup fails when dest file is not writable', { skip: isRoot }, async (t) => {
const readonlyDestDb = nextDb();
writeFileSync(readonlyDestDb, '', { mode: 0o444 });
const database = makeSourceDb();
await t.assert.rejects(async () => {
await backup(database, readonlyDestDb);
}, {
code: 'ERR_SQLITE_ERROR',
message: 'attempt to write a readonly database'
});
});
test('backup fails when progress function throws', async (t) => {
const database = makeSourceDb();
const destDb = nextDb();
const progressFn = t.mock.fn(() => {
throw new Error('progress error');
});
await t.assert.rejects(async () => {
await backup(database, destDb, {
rate: 1,
progress: progressFn,
});
}, {
message: 'progress error'
});
});
test('backup fails when source db is invalid', async (t) => {
const database = makeSourceDb();
const destDb = nextDb();
await t.assert.rejects(async () => {
await backup(database, destDb, {
rate: 1,
source: 'invalid',
});
}, {
message: 'unknown database invalid'
});
});
test('backup fails when path cannot be opened', async (t) => {
const database = makeSourceDb();
await t.assert.rejects(async () => {
await backup(database, `${tmpdir.path}/invalid/backup.db`);
}, {
message: 'unable to open database file'
});
});
test('backup has correct name and length', (t) => {
t.assert.strictEqual(backup.name, 'backup');
t.assert.strictEqual(backup.length, 2);
});

View File

@@ -0,0 +1,414 @@
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const assert = require('node:assert');
const { DatabaseSync } = require('node:sqlite');
const { suite, test } = require('node:test');
suite('DatabaseSync.prototype.function()', () => {
suite('input validation', () => {
const db = new DatabaseSync(':memory:');
test('throws if name is not a string', () => {
assert.throws(() => {
db.function();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "name" argument must be a string/,
});
});
test('throws if function is not a function', () => {
assert.throws(() => {
db.function('foo');
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "function" argument must be a function/,
});
});
test('throws if options is not an object', () => {
assert.throws(() => {
db.function('foo', null, () => {});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options" argument must be an object/,
});
});
test('throws if options.useBigIntArguments is not a boolean', () => {
assert.throws(() => {
db.function('foo', { useBigIntArguments: null }, () => {});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.useBigIntArguments" argument must be a boolean/,
});
});
test('throws if options.varargs is not a boolean', () => {
assert.throws(() => {
db.function('foo', { varargs: null }, () => {});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.varargs" argument must be a boolean/,
});
});
test('throws if options.deterministic is not a boolean', () => {
assert.throws(() => {
db.function('foo', { deterministic: null }, () => {});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.deterministic" argument must be a boolean/,
});
});
test('throws if options.directOnly is not a boolean', () => {
assert.throws(() => {
db.function('foo', { directOnly: null }, () => {});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.directOnly" argument must be a boolean/,
});
});
});
suite('useBigIntArguments', () => {
test('converts arguments to BigInts when true', () => {
const db = new DatabaseSync(':memory:');
let value;
const r = db.function('custom', { useBigIntArguments: true }, (arg) => {
value = arg;
});
assert.strictEqual(r, undefined);
db.prepare('SELECT custom(5) AS custom').get();
assert.strictEqual(value, 5n);
});
test('uses number primitives when false', () => {
const db = new DatabaseSync(':memory:');
let value;
const r = db.function('custom', { useBigIntArguments: false }, (arg) => {
value = arg;
});
assert.strictEqual(r, undefined);
db.prepare('SELECT custom(5) AS custom').get();
assert.strictEqual(value, 5);
});
test('defaults to false', () => {
const db = new DatabaseSync(':memory:');
let value;
const r = db.function('custom', (arg) => {
value = arg;
});
assert.strictEqual(r, undefined);
db.prepare('SELECT custom(5) AS custom').get();
assert.strictEqual(value, 5);
});
test('throws if value cannot fit in a number', () => {
const db = new DatabaseSync(':memory:');
const value = Number.MAX_SAFE_INTEGER + 1;
db.function('custom', (arg) => {});
assert.throws(() => {
db.prepare(`SELECT custom(${value}) AS custom`).get();
}, {
code: 'ERR_OUT_OF_RANGE',
message: /Value is too large to be represented as a JavaScript number: 9007199254740992/,
});
});
});
suite('varargs', () => {
test('supports variable number of arguments when true', () => {
const db = new DatabaseSync(':memory:');
let value;
const r = db.function('custom', { varargs: true }, (...args) => {
value = args;
});
assert.strictEqual(r, undefined);
db.prepare('SELECT custom(5, 4, 3, 2, 1) AS custom').get();
assert.deepStrictEqual(value, [5, 4, 3, 2, 1]);
});
test('uses function.length when false', () => {
const db = new DatabaseSync(':memory:');
let value;
const r = db.function('custom', { varargs: false }, (a, b, c) => {
value = [a, b, c];
});
assert.strictEqual(r, undefined);
db.prepare('SELECT custom(1, 2, 3) AS custom').get();
assert.deepStrictEqual(value, [1, 2, 3]);
});
test('defaults to false', () => {
const db = new DatabaseSync(':memory:');
let value;
const r = db.function('custom', (a, b, c) => {
value = [a, b, c];
});
assert.strictEqual(r, undefined);
db.prepare('SELECT custom(7, 8, 9) AS custom').get();
assert.deepStrictEqual(value, [7, 8, 9]);
});
test('throws if an incorrect number of arguments is provided', () => {
const db = new DatabaseSync(':memory:');
db.function('custom', (a, b, c, d) => {});
assert.throws(() => {
db.prepare('SELECT custom(1, 2, 3) AS custom').get();
}, {
code: 'ERR_SQLITE_ERROR',
message: /wrong number of arguments to function custom\(\)/,
});
});
});
suite('deterministic', () => {
test('creates a deterministic function when true', () => {
const db = new DatabaseSync(':memory:');
db.function('isDeterministic', { deterministic: true }, () => {
return 42;
});
const r = db.exec(`
CREATE TABLE t1 (
a INTEGER PRIMARY KEY,
b INTEGER GENERATED ALWAYS AS (isDeterministic()) VIRTUAL
)
`);
assert.strictEqual(r, undefined);
});
test('creates a non-deterministic function when false', () => {
const db = new DatabaseSync(':memory:');
db.function('isNonDeterministic', { deterministic: false }, () => {
return 42;
});
assert.throws(() => {
db.exec(`
CREATE TABLE t1 (
a INTEGER PRIMARY KEY,
b INTEGER GENERATED ALWAYS AS (isNonDeterministic()) VIRTUAL
)
`);
}, {
code: 'ERR_SQLITE_ERROR',
message: /non-deterministic functions prohibited in generated columns/,
});
});
test('deterministic defaults to false', () => {
const db = new DatabaseSync(':memory:');
db.function('isNonDeterministic', () => {
return 42;
});
assert.throws(() => {
db.exec(`
CREATE TABLE t1 (
a INTEGER PRIMARY KEY,
b INTEGER GENERATED ALWAYS AS (isNonDeterministic()) VIRTUAL
)
`);
}, {
code: 'ERR_SQLITE_ERROR',
message: /non-deterministic functions prohibited in generated columns/,
});
});
});
suite('directOnly', () => {
test('sets SQLite direct only flag when true', () => {
const db = new DatabaseSync(':memory:');
db.function('fn', { deterministic: true, directOnly: true }, () => {
return 42;
});
assert.throws(() => {
db.exec(`
CREATE TABLE t1 (
a INTEGER PRIMARY KEY,
b INTEGER GENERATED ALWAYS AS (fn()) VIRTUAL
)
`);
}, {
code: 'ERR_SQLITE_ERROR',
message: /unsafe use of fn\(\)/
});
});
test('does not set SQLite direct only flag when false', () => {
const db = new DatabaseSync(':memory:');
db.function('fn', { deterministic: true, directOnly: false }, () => {
return 42;
});
const r = db.exec(`
CREATE TABLE t1 (
a INTEGER PRIMARY KEY,
b INTEGER GENERATED ALWAYS AS (fn()) VIRTUAL
)
`);
assert.strictEqual(r, undefined);
});
test('directOnly defaults to false', () => {
const db = new DatabaseSync(':memory:');
db.function('fn', { deterministic: true }, () => {
return 42;
});
const r = db.exec(`
CREATE TABLE t1 (
a INTEGER PRIMARY KEY,
b INTEGER GENERATED ALWAYS AS (fn()) VIRTUAL
)
`);
assert.strictEqual(r, undefined);
});
});
suite('return types', () => {
test('supported return types', () => {
const db = new DatabaseSync(':memory:');
db.function('retUndefined', () => {});
db.function('retNull', () => { return null; });
db.function('retNumber', () => { return 3; });
db.function('retString', () => { return 'foo'; });
db.function('retBigInt', () => { return 5n; });
db.function('retUint8Array', () => { return new Uint8Array([1, 2, 3]); });
db.function('retArrayBufferView', () => {
const arrayBuffer = new Uint8Array([1, 2, 3]).buffer;
return new DataView(arrayBuffer);
});
const stmt = db.prepare(`SELECT
retUndefined() AS retUndefined,
retNull() AS retNull,
retNumber() AS retNumber,
retString() AS retString,
retBigInt() AS retBigInt,
retUint8Array() AS retUint8Array,
retArrayBufferView() AS retArrayBufferView
`);
assert.deepStrictEqual(stmt.get(), {
__proto__: null,
retUndefined: null,
retNull: null,
retNumber: 3,
retString: 'foo',
retBigInt: 5,
retUint8Array: new Uint8Array([1, 2, 3]),
retArrayBufferView: new Uint8Array([1, 2, 3]),
});
});
test('throws if returned BigInt is too large for SQLite', () => {
const db = new DatabaseSync(':memory:');
db.function('retBigInt', () => {
return BigInt(Number.MAX_SAFE_INTEGER + 1);
});
const stmt = db.prepare('SELECT retBigInt() AS retBigInt');
assert.throws(() => {
stmt.get();
}, {
code: 'ERR_OUT_OF_RANGE',
});
});
test('does not support Promise return values', () => {
const db = new DatabaseSync(':memory:');
db.function('retPromise', async () => {});
const stmt = db.prepare('SELECT retPromise() AS retPromise');
assert.throws(() => {
stmt.get();
}, {
code: 'ERR_SQLITE_ERROR',
message: /Asynchronous user-defined functions are not supported/,
});
});
test('throws on unsupported return types', () => {
const db = new DatabaseSync(':memory:');
db.function('retFunction', () => {
return () => {};
});
const stmt = db.prepare('SELECT retFunction() AS retFunction');
assert.throws(() => {
stmt.get();
}, {
code: 'ERR_SQLITE_ERROR',
message: /Returned JavaScript value cannot be converted to a SQLite value/,
});
});
});
suite('handles conflicting errors from SQLite and JavaScript', () => {
test('throws if value cannot fit in a number', () => {
const db = new DatabaseSync(':memory:');
const expected = { __proto__: null, id: 5, data: 'foo' };
db.function('custom', (arg) => {});
db.exec('CREATE TABLE test (id NUMBER NOT NULL PRIMARY KEY, data TEXT)');
db.prepare('INSERT INTO test (id, data) VALUES (?, ?)').run(5, 'foo');
assert.deepStrictEqual(db.prepare('SELECT * FROM test').get(), expected);
assert.throws(() => {
db.exec(`UPDATE test SET data = CUSTOM(${Number.MAX_SAFE_INTEGER + 1})`);
}, {
code: 'ERR_OUT_OF_RANGE',
message: /Value is too large to be represented as a JavaScript number: 9007199254740992/,
});
assert.deepStrictEqual(db.prepare('SELECT * FROM test').get(), expected);
});
test('propagates JavaScript errors', () => {
const db = new DatabaseSync(':memory:');
const expected = { __proto__: null, id: 5, data: 'foo' };
const err = new Error('boom');
db.function('throws', () => {
throw err;
});
db.exec('CREATE TABLE test (id NUMBER NOT NULL PRIMARY KEY, data TEXT)');
db.prepare('INSERT INTO test (id, data) VALUES (?, ?)').run(5, 'foo');
assert.deepStrictEqual(db.prepare('SELECT * FROM test').get(), expected);
assert.throws(() => {
db.exec('UPDATE test SET data = THROWS()');
}, err);
assert.deepStrictEqual(db.prepare('SELECT * FROM test').get(), expected);
});
});
test('supported argument types', () => {
const db = new DatabaseSync(':memory:');
db.function('arguments', (i, f, s, n, b) => {
assert.strictEqual(i, 5);
assert.strictEqual(f, 3.14);
assert.strictEqual(s, 'foo');
assert.strictEqual(n, null);
assert.deepStrictEqual(b, new Uint8Array([254]));
return 42;
});
const stmt = db.prepare(
'SELECT arguments(5, 3.14, \'foo\', null, x\'fe\') as result'
);
assert.deepStrictEqual(stmt.get(), { __proto__: null, result: 42 });
});
test('propagates thrown errors', () => {
const db = new DatabaseSync(':memory:');
const err = new Error('boom');
db.function('throws', () => {
throw err;
});
const stmt = db.prepare('SELECT throws()');
assert.throws(() => {
stmt.get();
}, err);
});
test('throws if database is not open', () => {
const db = new DatabaseSync(':memory:', { open: false });
assert.throws(() => {
db.function('foo', () => {});
}, {
code: 'ERR_INVALID_STATE',
message: /database is not open/,
});
});
});

View File

@@ -0,0 +1,161 @@
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const tmpdir = require('../common/tmpdir');
const { join } = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { suite, test } = require('node:test');
let cnt = 0;
tmpdir.refresh();
function nextDb() {
return join(tmpdir.path, `database-${cnt++}.db`);
}
suite('data binding and mapping', () => {
test('supported data types', (t) => {
const u8a = new TextEncoder().encode('a☃b☃c');
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE types(
key INTEGER PRIMARY KEY,
int INTEGER,
double REAL,
text TEXT,
buf BLOB
) STRICT;
`);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO types (key, int, double, text, buf) ' +
'VALUES (?, ?, ?, ?, ?)');
t.assert.deepStrictEqual(
stmt.run(1, 42, 3.14159, 'foo', u8a),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.deepStrictEqual(
stmt.run(2, null, null, null, null),
{ changes: 1, lastInsertRowid: 2 }
);
t.assert.deepStrictEqual(
stmt.run(3, Number(8), Number(2.718), String('bar'), Buffer.from('x☃y☃')),
{ changes: 1, lastInsertRowid: 3 },
);
t.assert.deepStrictEqual(
stmt.run(4, 99n, 0xf, '', new Uint8Array()),
{ changes: 1, lastInsertRowid: 4 },
);
const query = db.prepare('SELECT * FROM types WHERE key = ?');
t.assert.deepStrictEqual(query.get(1), {
__proto__: null,
key: 1,
int: 42,
double: 3.14159,
text: 'foo',
buf: u8a,
});
t.assert.deepStrictEqual(query.get(2), {
__proto__: null,
key: 2,
int: null,
double: null,
text: null,
buf: null,
});
t.assert.deepStrictEqual(query.get(3), {
__proto__: null,
key: 3,
int: 8,
double: 2.718,
text: 'bar',
buf: new TextEncoder().encode('x☃y☃'),
});
t.assert.deepStrictEqual(query.get(4), {
__proto__: null,
key: 4,
int: 99,
double: 0xf,
text: '',
buf: new Uint8Array(),
});
});
test('unsupported data types', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
[
undefined,
() => {},
Symbol(),
/foo/,
Promise.resolve(),
new Map(),
new Set(),
].forEach((val) => {
t.assert.throws(() => {
db.prepare('INSERT INTO types (key, val) VALUES (?, ?)').run(1, val);
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /Provided value cannot be bound to SQLite parameter 2/,
});
});
t.assert.throws(() => {
const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)');
stmt.run({ $k: 1, $v: () => {} });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /Provided value cannot be bound to SQLite parameter 2/,
});
});
test('throws when binding a BigInt that is too large', (t) => {
const max = 9223372036854775807n; // Largest 64-bit signed integer value.
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO types (key, val) VALUES (?, ?)');
t.assert.deepStrictEqual(
stmt.run(1, max),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.throws(() => {
stmt.run(1, max + 1n);
}, {
code: 'ERR_INVALID_ARG_VALUE',
message: /BigInt value is too large to bind/,
});
});
test('statements are unbound on each call', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
t.assert.deepStrictEqual(
stmt.run(1, 5),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.deepStrictEqual(
stmt.run(),
{ changes: 1, lastInsertRowid: 2 },
);
t.assert.deepStrictEqual(
db.prepare('SELECT * FROM data ORDER BY key').all(),
[{ __proto__: null, key: 1, val: 5 }, { __proto__: null, key: 2, val: null }],
);
});
});

View File

@@ -0,0 +1,33 @@
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const tmpdir = require('../common/tmpdir');
const assert = require('node:assert');
const { join } = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { suite, test } = require('node:test');
let cnt = 0;
tmpdir.refresh();
function nextDb() {
return join(tmpdir.path, `database-${cnt++}.db`);
}
suite('DatabaseSync.prototype[Symbol.dispose]()', () => {
test('closes an open database', () => {
const db = new DatabaseSync(nextDb());
db[Symbol.dispose]();
assert.throws(() => {
db.close();
}, /database is not open/);
});
test('supports databases that are not open', () => {
const db = new DatabaseSync(nextDb(), { open: false });
db[Symbol.dispose]();
assert.throws(() => {
db.close();
}, /database is not open/);
});
});

View File

@@ -0,0 +1,523 @@
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const tmpdir = require('../common/tmpdir');
const { existsSync } = require('node:fs');
const { join } = require('node:path');
const { DatabaseSync, StatementSync } = require('node:sqlite');
const { suite, test } = require('node:test');
let cnt = 0;
tmpdir.refresh();
function nextDb() {
return join(tmpdir.path, `database-${cnt++}.db`);
}
suite('DatabaseSync() constructor', () => {
test('throws if called without new', (t) => {
t.assert.throws(() => {
DatabaseSync();
}, {
code: 'ERR_CONSTRUCT_CALL_REQUIRED',
message: /Cannot call constructor without `new`/,
});
});
test('throws if database path is not a string, Uint8Array, or URL', (t) => {
t.assert.throws(() => {
new DatabaseSync();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "path" argument must be a string, Uint8Array, or URL without null bytes/,
});
});
test('throws if the database location as Buffer contains null bytes', (t) => {
t.assert.throws(() => {
new DatabaseSync(Buffer.from('l\0cation'));
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.',
});
});
test('throws if the database location as string contains null bytes', (t) => {
t.assert.throws(() => {
new DatabaseSync('l\0cation');
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.',
});
});
test('throws if options is provided but is not an object', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', null);
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options" argument must be an object/,
});
});
test('throws if options.open is provided but is not a boolean', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', { open: 5 });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.open" argument must be a boolean/,
});
});
test('throws if options.readOnly is provided but is not a boolean', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', { readOnly: 5 });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.readOnly" argument must be a boolean/,
});
});
test('throws if options.timeout is provided but is not an integer', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', { timeout: .99 });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.timeout" argument must be an integer/,
});
});
test('is not read-only by default', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath);
db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)');
});
test('is read-only if readOnly is set', (t) => {
const dbPath = nextDb();
{
const db = new DatabaseSync(dbPath);
db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)');
db.close();
}
{
const db = new DatabaseSync(dbPath, { readOnly: true });
t.assert.throws(() => {
db.exec('CREATE TABLE bar (id INTEGER PRIMARY KEY)');
}, {
code: 'ERR_SQLITE_ERROR',
message: /attempt to write a readonly database/,
});
}
});
test('throws if options.enableForeignKeyConstraints is provided but is not a boolean', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', { enableForeignKeyConstraints: 5 });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.enableForeignKeyConstraints" argument must be a boolean/,
});
});
test('enables foreign key constraints by default', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath);
db.exec(`
CREATE TABLE foo (id INTEGER PRIMARY KEY);
CREATE TABLE bar (foo_id INTEGER REFERENCES foo(id));
`);
t.after(() => { db.close(); });
t.assert.throws(() => {
db.exec('INSERT INTO bar (foo_id) VALUES (1)');
}, {
code: 'ERR_SQLITE_ERROR',
message: 'FOREIGN KEY constraint failed',
});
});
test('allows disabling foreign key constraints', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath, { enableForeignKeyConstraints: false });
db.exec(`
CREATE TABLE foo (id INTEGER PRIMARY KEY);
CREATE TABLE bar (foo_id INTEGER REFERENCES foo(id));
`);
t.after(() => { db.close(); });
db.exec('INSERT INTO bar (foo_id) VALUES (1)');
});
test('throws if options.enableDoubleQuotedStringLiterals is provided but is not a boolean', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', { enableDoubleQuotedStringLiterals: 5 });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.enableDoubleQuotedStringLiterals" argument must be a boolean/,
});
});
test('disables double-quoted string literals by default', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath);
t.after(() => { db.close(); });
t.assert.throws(() => {
db.exec('SELECT "foo";');
}, {
code: 'ERR_SQLITE_ERROR',
message: /no such column: "?foo"?/,
});
});
test('allows enabling double-quoted string literals', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath, { enableDoubleQuotedStringLiterals: true });
t.after(() => { db.close(); });
db.exec('SELECT "foo";');
});
test('throws if options.readBigInts is provided but is not a boolean', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', { readBigInts: 42 });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.readBigInts" argument must be a boolean.',
});
});
test('allows reading big integers', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath, { readBigInts: true });
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
INSERT INTO data (key, val) VALUES (1, 42);
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare('SELECT val FROM data');
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42n });
const insert = db.prepare('INSERT INTO data (key) VALUES (?)');
t.assert.deepStrictEqual(
insert.run(20),
{ changes: 1n, lastInsertRowid: 20n },
);
});
test('throws if options.returnArrays is provided but is not a boolean', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', { returnArrays: 42 });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.returnArrays" argument must be a boolean.',
});
});
test('allows returning arrays', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath, { returnArrays: true });
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
INSERT INTO data (key, val) VALUES (1, 'one');
INSERT INTO data (key, val) VALUES (2, 'two');
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare('SELECT key, val FROM data WHERE key = 1');
t.assert.deepStrictEqual(query.get(), [1, 'one']);
});
test('throws if options.allowBareNamedParameters is provided but is not a boolean', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', { allowBareNamedParameters: 42 });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.allowBareNamedParameters" argument must be a boolean.',
});
});
test('throws if bare named parameters are used when option is false', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath, { allowBareNamedParameters: false });
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
t.assert.throws(() => {
stmt.run({ k: 2, v: 4 });
}, {
code: 'ERR_INVALID_STATE',
message: /Unknown named parameter 'k'/,
});
});
test('throws if options.allowUnknownNamedParameters is provided but is not a boolean', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', { allowUnknownNamedParameters: 42 });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.allowUnknownNamedParameters" argument must be a boolean.',
});
});
test('allows unknown named parameters', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath, { allowUnknownNamedParameters: true });
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
const params = { $a: 1, $b: 2, $k: 42, $y: 25, $v: 84, $z: 99 };
t.assert.deepStrictEqual(
stmt.run(params),
{ changes: 1, lastInsertRowid: 1 },
);
});
});
suite('DatabaseSync.prototype.open()', () => {
test('opens a database connection', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath, { open: false });
t.after(() => { db.close(); });
t.assert.strictEqual(db.isOpen, false);
t.assert.strictEqual(existsSync(dbPath), false);
t.assert.strictEqual(db.open(), undefined);
t.assert.strictEqual(db.isOpen, true);
t.assert.strictEqual(existsSync(dbPath), true);
});
test('throws if database is already open', (t) => {
const db = new DatabaseSync(nextDb(), { open: false });
t.after(() => { db.close(); });
t.assert.strictEqual(db.isOpen, false);
db.open();
t.assert.strictEqual(db.isOpen, true);
t.assert.throws(() => {
db.open();
}, {
code: 'ERR_INVALID_STATE',
message: /database is already open/,
});
t.assert.strictEqual(db.isOpen, true);
});
});
suite('DatabaseSync.prototype.close()', () => {
test('closes an open database connection', (t) => {
const db = new DatabaseSync(nextDb());
t.assert.strictEqual(db.isOpen, true);
t.assert.strictEqual(db.close(), undefined);
t.assert.strictEqual(db.isOpen, false);
});
test('throws if database is not open', (t) => {
const db = new DatabaseSync(nextDb(), { open: false });
t.assert.strictEqual(db.isOpen, false);
t.assert.throws(() => {
db.close();
}, {
code: 'ERR_INVALID_STATE',
message: /database is not open/,
});
t.assert.strictEqual(db.isOpen, false);
});
});
suite('DatabaseSync.prototype.prepare()', () => {
test('returns a prepared statement', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const stmt = db.prepare('CREATE TABLE webstorage(key TEXT)');
t.assert.ok(stmt instanceof StatementSync);
});
test('throws if database is not open', (t) => {
const db = new DatabaseSync(nextDb(), { open: false });
t.assert.throws(() => {
db.prepare();
}, {
code: 'ERR_INVALID_STATE',
message: /database is not open/,
});
});
test('throws if sql is not a string', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
t.assert.throws(() => {
db.prepare();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "sql" argument must be a string/,
});
});
});
suite('DatabaseSync.prototype.exec()', () => {
test('executes SQL', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const result = db.exec(`
CREATE TABLE data(
key INTEGER PRIMARY KEY,
val INTEGER
) STRICT;
INSERT INTO data (key, val) VALUES (1, 2);
INSERT INTO data (key, val) VALUES (8, 9);
`);
t.assert.strictEqual(result, undefined);
const stmt = db.prepare('SELECT * FROM data ORDER BY key');
t.assert.deepStrictEqual(stmt.all(), [
{ __proto__: null, key: 1, val: 2 },
{ __proto__: null, key: 8, val: 9 },
]);
});
test('reports errors from SQLite', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
t.assert.throws(() => {
db.exec('CREATE TABLEEEE');
}, {
code: 'ERR_SQLITE_ERROR',
message: /syntax error/,
});
});
test('throws if the URL does not have the file: scheme', (t) => {
t.assert.throws(() => {
new DatabaseSync(new URL('http://example.com'));
}, {
code: 'ERR_INVALID_URL_SCHEME',
message: 'The URL must be of scheme file:',
});
});
test('throws if database is not open', (t) => {
const db = new DatabaseSync(nextDb(), { open: false });
t.assert.throws(() => {
db.exec();
}, {
code: 'ERR_INVALID_STATE',
message: /database is not open/,
});
});
test('throws if sql is not a string', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
t.assert.throws(() => {
db.exec();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "sql" argument must be a string/,
});
});
});
suite('DatabaseSync.prototype.isTransaction', () => {
test('correctly detects a committed transaction', (t) => {
const db = new DatabaseSync(':memory:');
t.assert.strictEqual(db.isTransaction, false);
db.exec('BEGIN');
t.assert.strictEqual(db.isTransaction, true);
db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)');
t.assert.strictEqual(db.isTransaction, true);
db.exec('COMMIT');
t.assert.strictEqual(db.isTransaction, false);
});
test('correctly detects a rolled back transaction', (t) => {
const db = new DatabaseSync(':memory:');
t.assert.strictEqual(db.isTransaction, false);
db.exec('BEGIN');
t.assert.strictEqual(db.isTransaction, true);
db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)');
t.assert.strictEqual(db.isTransaction, true);
db.exec('ROLLBACK');
t.assert.strictEqual(db.isTransaction, false);
});
test('throws if database is not open', (t) => {
const db = new DatabaseSync(nextDb(), { open: false });
t.assert.throws(() => {
return db.isTransaction;
}, {
code: 'ERR_INVALID_STATE',
message: /database is not open/,
});
});
});
suite('DatabaseSync.prototype.location()', () => {
test('throws if database is not open', (t) => {
const db = new DatabaseSync(nextDb(), { open: false });
t.assert.throws(() => {
db.location();
}, {
code: 'ERR_INVALID_STATE',
message: /database is not open/,
});
});
test('throws if provided dbName is not string', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
t.assert.throws(() => {
db.location(null);
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "dbName" argument must be a string/,
});
});
test('returns null when connected to in-memory database', (t) => {
const db = new DatabaseSync(':memory:');
t.assert.strictEqual(db.location(), null);
});
test('returns db path when connected to a persistent database', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath);
t.after(() => { db.close(); });
t.assert.strictEqual(db.location(), dbPath);
});
test('returns that specific db path when attached', (t) => {
const dbPath = nextDb();
const otherPath = nextDb();
const db = new DatabaseSync(dbPath);
t.after(() => { db.close(); });
const other = new DatabaseSync(dbPath);
t.after(() => { other.close(); });
// Adding this escape because the test with unusual chars have a single quote which breaks the query
const escapedPath = otherPath.replace("'", "''");
db.exec(`ATTACH DATABASE '${escapedPath}' AS other`);
t.assert.strictEqual(db.location('other'), otherPath);
});
});

View File

@@ -0,0 +1,121 @@
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const tmpdir = require('../common/tmpdir');
const { join } = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { suite, test } = require('node:test');
let cnt = 0;
tmpdir.refresh();
function nextDb() {
return join(tmpdir.path, `database-${cnt++}.db`);
}
suite('named parameters', () => {
test('throws on unknown named parameters', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
t.assert.throws(() => {
const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)');
stmt.run({ $k: 1, $unknown: 1 });
}, {
code: 'ERR_INVALID_STATE',
message: /Unknown named parameter '\$unknown'/,
});
});
test('bare named parameters are supported', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
stmt.run({ k: 1, v: 9 });
t.assert.deepStrictEqual(
db.prepare('SELECT * FROM data').get(),
{ __proto__: null, key: 1, val: 9 },
);
});
test('duplicate bare named parameters are supported', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $k)');
stmt.run({ k: 1 });
t.assert.deepStrictEqual(
db.prepare('SELECT * FROM data').get(),
{ __proto__: null, key: 1, val: 1 },
);
});
test('bare named parameters throw on ambiguous names', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, @k)');
t.assert.throws(() => {
stmt.run({ k: 1 });
}, {
code: 'ERR_INVALID_STATE',
message: 'Cannot create bare named parameter \'k\' because of ' +
'conflicting names \'$k\' and \'@k\'.',
});
});
});
suite('StatementSync.prototype.setAllowUnknownNamedParameters()', () => {
test('unknown named parameter support can be toggled', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
t.assert.strictEqual(stmt.setAllowUnknownNamedParameters(true), undefined);
const params = { $a: 1, $b: 2, $k: 42, $y: 25, $v: 84, $z: 99 };
t.assert.deepStrictEqual(
stmt.run(params),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.strictEqual(stmt.setAllowUnknownNamedParameters(false), undefined);
t.assert.throws(() => {
stmt.run(params);
}, {
code: 'ERR_INVALID_STATE',
message: /Unknown named parameter '\$a'/,
});
});
test('throws when input is not a boolean', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
t.assert.throws(() => {
stmt.setAllowUnknownNamedParameters();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "enabled" argument must be a boolean/,
});
});
});

View File

@@ -0,0 +1,542 @@
// Flags: --experimental-sqlite
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const {
DatabaseSync,
constants,
} = require('node:sqlite');
const { test, suite } = require('node:test');
/**
* Convenience wrapper around assert.deepStrictEqual that sets a null
* prototype to the expected object.
* @returns {boolean}
*/
function deepStrictEqual(t) {
return (actual, expected, message) => {
if (Array.isArray(expected)) {
expected = expected.map((obj) => ({ ...obj, __proto__: null }));
} else if (typeof expected === 'object') {
expected = { ...expected, __proto__: null };
}
t.assert.deepStrictEqual(actual, expected, message);
};
}
test('creating and applying a changeset', (t) => {
const createDataTableSql = `
CREATE TABLE data(
key INTEGER PRIMARY KEY,
value TEXT
) STRICT`;
const createDatabase = () => {
const database = new DatabaseSync(':memory:');
database.exec(createDataTableSql);
return database;
};
const databaseFrom = createDatabase();
const session = databaseFrom.createSession();
const select = 'SELECT * FROM data ORDER BY key';
const insert = databaseFrom.prepare('INSERT INTO data (key, value) VALUES (?, ?)');
insert.run(1, 'hello');
insert.run(2, 'world');
const databaseTo = createDatabase();
t.assert.strictEqual(databaseTo.applyChangeset(session.changeset()), true);
deepStrictEqual(t)(
databaseFrom.prepare(select).all(),
databaseTo.prepare(select).all()
);
});
test('database.createSession() - closed database results in exception', (t) => {
const database = new DatabaseSync(':memory:');
database.close();
t.assert.throws(() => {
database.createSession();
}, {
name: 'Error',
message: 'database is not open',
});
});
test('session.changeset() - closed database results in exception', (t) => {
const database = new DatabaseSync(':memory:');
const session = database.createSession();
database.close();
t.assert.throws(() => {
session.changeset();
}, {
name: 'Error',
message: 'database is not open',
});
});
test('database.applyChangeset() - closed database results in exception', (t) => {
const database = new DatabaseSync(':memory:');
const session = database.createSession();
const changeset = session.changeset();
database.close();
t.assert.throws(() => {
database.applyChangeset(changeset);
}, {
name: 'Error',
message: 'database is not open',
});
});
test('database.createSession() - use table option to track specific table', (t) => {
const database1 = new DatabaseSync(':memory:');
const database2 = new DatabaseSync(':memory:');
const createData1TableSql = `CREATE TABLE data1 (
key INTEGER PRIMARY KEY,
value TEXT
) STRICT
`;
const createData2TableSql = `CREATE TABLE data2 (
key INTEGER PRIMARY KEY,
value TEXT
) STRICT
`;
database1.exec(createData1TableSql);
database1.exec(createData2TableSql);
database2.exec(createData1TableSql);
database2.exec(createData2TableSql);
const session = database1.createSession({
table: 'data1'
});
const insert1 = database1.prepare('INSERT INTO data1 (key, value) VALUES (?, ?)');
insert1.run(1, 'hello');
insert1.run(2, 'world');
const insert2 = database1.prepare('INSERT INTO data2 (key, value) VALUES (?, ?)');
insert2.run(1, 'hello');
insert2.run(2, 'world');
const select1 = 'SELECT * FROM data1 ORDER BY key';
const select2 = 'SELECT * FROM data2 ORDER BY key';
t.assert.strictEqual(database2.applyChangeset(session.changeset()), true);
deepStrictEqual(t)(
database1.prepare(select1).all(),
database2.prepare(select1).all()); // data1 table should be equal
deepStrictEqual(t)(database2.prepare(select2).all(), []); // data2 should be empty in database2
t.assert.strictEqual(database1.prepare(select2).all().length, 2); // data1 should have values in database1
});
suite('conflict resolution', () => {
const createDataTableSql = `CREATE TABLE data (
key INTEGER PRIMARY KEY,
value TEXT UNIQUE
) STRICT`;
const prepareConflict = () => {
const database1 = new DatabaseSync(':memory:');
const database2 = new DatabaseSync(':memory:');
database1.exec(createDataTableSql);
database2.exec(createDataTableSql);
const insertSql = 'INSERT INTO data (key, value) VALUES (?, ?)';
const session = database1.createSession();
database1.prepare(insertSql).run(1, 'hello');
database1.prepare(insertSql).run(2, 'foo');
database2.prepare(insertSql).run(1, 'world');
return {
database2,
changeset: session.changeset()
};
};
const prepareDataConflict = () => {
const database1 = new DatabaseSync(':memory:');
const database2 = new DatabaseSync(':memory:');
database1.exec(createDataTableSql);
database2.exec(createDataTableSql);
const insertSql = 'INSERT INTO data (key, value) VALUES (?, ?)';
database1.prepare(insertSql).run(1, 'hello');
database2.prepare(insertSql).run(1, 'othervalue');
const session = database1.createSession();
database1.prepare('UPDATE data SET value = ? WHERE key = ?').run('foo', 1);
return {
database2,
changeset: session.changeset()
};
};
const prepareNotFoundConflict = () => {
const database1 = new DatabaseSync(':memory:');
const database2 = new DatabaseSync(':memory:');
database1.exec(createDataTableSql);
database2.exec(createDataTableSql);
const insertSql = 'INSERT INTO data (key, value) VALUES (?, ?)';
database1.prepare(insertSql).run(1, 'hello');
const session = database1.createSession();
database1.prepare('DELETE FROM data WHERE key = 1').run();
return {
database2,
changeset: session.changeset()
};
};
const prepareFkConflict = () => {
const database1 = new DatabaseSync(':memory:');
const database2 = new DatabaseSync(':memory:');
database1.exec(createDataTableSql);
database2.exec(createDataTableSql);
const fkTableSql = `CREATE TABLE other (
key INTEGER PRIMARY KEY,
ref REFERENCES data(key)
)`;
database1.exec(fkTableSql);
database2.exec(fkTableSql);
const insertDataSql = 'INSERT INTO data (key, value) VALUES (?, ?)';
const insertOtherSql = 'INSERT INTO other (key, ref) VALUES (?, ?)';
database1.prepare(insertDataSql).run(1, 'hello');
database2.prepare(insertDataSql).run(1, 'hello');
database1.prepare(insertOtherSql).run(1, 1);
database2.prepare(insertOtherSql).run(1, 1);
database1.exec('DELETE FROM other WHERE key = 1'); // So we don't get a fk violation in database1
const session = database1.createSession();
database1.prepare('DELETE FROM data WHERE key = 1').run(); // Changeset with fk violation
database2.exec('PRAGMA foreign_keys = ON'); // Needs to be supported, otherwise will fail here
return {
database2,
changeset: session.changeset()
};
};
const prepareConstraintConflict = () => {
const database1 = new DatabaseSync(':memory:');
const database2 = new DatabaseSync(':memory:');
database1.exec(createDataTableSql);
database2.exec(createDataTableSql);
const insertSql = 'INSERT INTO data (key, value) VALUES (?, ?)';
const session = database1.createSession();
database1.prepare(insertSql).run(1, 'hello');
database2.prepare(insertSql).run(2, 'hello'); // database2 already constains hello
return {
database2,
changeset: session.changeset()
};
};
test('database.applyChangeset() - SQLITE_CHANGESET_CONFLICT conflict with default behavior (abort)', (t) => {
const { database2, changeset } = prepareConflict();
// When changeset is aborted due to a conflict, applyChangeset should return false
t.assert.strictEqual(database2.applyChangeset(changeset), false);
deepStrictEqual(t)(
database2.prepare('SELECT value from data').all(),
[{ value: 'world' }]); // unchanged
});
test('database.applyChangeset() - SQLITE_CHANGESET_CONFLICT conflict handled with SQLITE_CHANGESET_ABORT', (t) => {
const { database2, changeset } = prepareConflict();
let conflictType = null;
const result = database2.applyChangeset(changeset, {
onConflict: (conflictType_) => {
conflictType = conflictType_;
return constants.SQLITE_CHANGESET_ABORT;
}
});
// When changeset is aborted due to a conflict, applyChangeset should return false
t.assert.strictEqual(result, false);
t.assert.strictEqual(conflictType, constants.SQLITE_CHANGESET_CONFLICT);
deepStrictEqual(t)(
database2.prepare('SELECT value from data').all(),
[{ value: 'world' }]); // unchanged
});
test('database.applyChangeset() - SQLITE_CHANGESET_DATA conflict handled with SQLITE_CHANGESET_REPLACE', (t) => {
const { database2, changeset } = prepareDataConflict();
let conflictType = null;
const result = database2.applyChangeset(changeset, {
onConflict: (conflictType_) => {
conflictType = conflictType_;
return constants.SQLITE_CHANGESET_REPLACE;
}
});
// Not aborted due to conflict, so should return true
t.assert.strictEqual(result, true);
t.assert.strictEqual(conflictType, constants.SQLITE_CHANGESET_DATA);
deepStrictEqual(t)(
database2.prepare('SELECT value from data ORDER BY key').all(),
[{ value: 'foo' }]); // replaced
});
test('database.applyChangeset() - SQLITE_CHANGESET_NOTFOUND conflict with SQLITE_CHANGESET_OMIT', (t) => {
const { database2, changeset } = prepareNotFoundConflict();
let conflictType = null;
const result = database2.applyChangeset(changeset, {
onConflict: (conflictType_) => {
conflictType = conflictType_;
return constants.SQLITE_CHANGESET_OMIT;
}
});
// Not aborted due to conflict, so should return true
t.assert.strictEqual(result, true);
t.assert.strictEqual(conflictType, constants.SQLITE_CHANGESET_NOTFOUND);
deepStrictEqual(t)(database2.prepare('SELECT value from data').all(), []);
});
test('database.applyChangeset() - SQLITE_CHANGESET_FOREIGN_KEY conflict', (t) => {
const { database2, changeset } = prepareFkConflict();
let conflictType = null;
const result = database2.applyChangeset(changeset, {
onConflict: (conflictType_) => {
conflictType = conflictType_;
return constants.SQLITE_CHANGESET_OMIT;
}
});
// Not aborted due to conflict, so should return true
t.assert.strictEqual(result, true);
t.assert.strictEqual(conflictType, constants.SQLITE_CHANGESET_FOREIGN_KEY);
deepStrictEqual(t)(database2.prepare('SELECT value from data').all(), []);
});
test('database.applyChangeset() - SQLITE_CHANGESET_CONSTRAINT conflict', (t) => {
const { database2, changeset } = prepareConstraintConflict();
let conflictType = null;
const result = database2.applyChangeset(changeset, {
onConflict: (conflictType_) => {
conflictType = conflictType_;
return constants.SQLITE_CHANGESET_OMIT;
}
});
// Not aborted due to conflict, so should return true
t.assert.strictEqual(result, true);
t.assert.strictEqual(conflictType, constants.SQLITE_CHANGESET_CONSTRAINT);
deepStrictEqual(t)(database2.prepare('SELECT key, value from data').all(), [{ key: 2, value: 'hello' }]);
});
test('conflict resolution handler returns invalid value', (t) => {
const invalidHandlers = [
() => -1,
() => ({}),
() => null,
async () => constants.SQLITE_CHANGESET_ABORT,
];
for (const invalidHandler of invalidHandlers) {
const { database2, changeset } = prepareConflict();
t.assert.throws(() => {
database2.applyChangeset(changeset, {
onConflict: invalidHandler
});
}, {
name: 'Error',
message: 'bad parameter or other API misuse',
errcode: 21,
code: 'ERR_SQLITE_ERROR'
}, `Did not throw expected exception when using invalid onConflict handler: ${invalidHandler}`);
}
});
test('conflict resolution handler throws', (t) => {
const { database2, changeset } = prepareConflict();
t.assert.throws(() => {
database2.applyChangeset(changeset, {
onConflict: () => {
throw new Error('some error');
}
});
}, {
name: 'Error',
message: 'some error'
});
});
});
test('database.createSession() - filter changes', (t) => {
const database1 = new DatabaseSync(':memory:');
const database2 = new DatabaseSync(':memory:');
const createTableSql = 'CREATE TABLE data1(key INTEGER PRIMARY KEY); CREATE TABLE data2(key INTEGER PRIMARY KEY);';
database1.exec(createTableSql);
database2.exec(createTableSql);
const session = database1.createSession();
database1.exec('INSERT INTO data1 (key) VALUES (1), (2), (3)');
database1.exec('INSERT INTO data2 (key) VALUES (1), (2), (3), (4), (5)');
database2.applyChangeset(session.changeset(), {
filter: (tableName) => tableName === 'data2'
});
const data1Rows = database2.prepare('SELECT * FROM data1').all();
const data2Rows = database2.prepare('SELECT * FROM data2').all();
// Expect no rows since all changes were filtered out
t.assert.strictEqual(data1Rows.length, 0);
// Expect 5 rows since these changes were not filtered out
t.assert.strictEqual(data2Rows.length, 5);
});
test('database.createSession() - specify other database', (t) => {
const database = new DatabaseSync(':memory:');
const session = database.createSession();
const sessionMain = database.createSession({
db: 'main'
});
const sessionTest = database.createSession({
db: 'test'
});
database.exec('CREATE TABLE data (key INTEGER PRIMARY KEY)');
database.exec('INSERT INTO data (key) VALUES (1)');
t.assert.notStrictEqual(session.changeset().length, 0);
t.assert.notStrictEqual(sessionMain.changeset().length, 0);
// Since this session is attached to a different database, its changeset should be empty
t.assert.strictEqual(sessionTest.changeset().length, 0);
});
test('database.createSession() - wrong arguments', (t) => {
const database = new DatabaseSync(':memory:');
t.assert.throws(() => {
database.createSession(null);
}, {
name: 'TypeError',
message: 'The "options" argument must be an object.'
});
t.assert.throws(() => {
database.createSession({
table: 123
});
}, {
name: 'TypeError',
message: 'The "options.table" argument must be a string.'
});
t.assert.throws(() => {
database.createSession({
db: 123
});
}, {
name: 'TypeError',
message: 'The "options.db" argument must be a string.'
});
});
test('database.applyChangeset() - wrong arguments', (t) => {
const database = new DatabaseSync(':memory:');
const session = database.createSession();
t.assert.throws(() => {
database.applyChangeset(null);
}, {
name: 'TypeError',
message: 'The "changeset" argument must be a Uint8Array.'
});
t.assert.throws(() => {
database.applyChangeset(session.changeset(), null);
}, {
name: 'TypeError',
message: 'The "options" argument must be an object.'
});
t.assert.throws(() => {
database.applyChangeset(session.changeset(), {
filter: null
}, null);
}, {
name: 'TypeError',
message: 'The "options.filter" argument must be a function.'
});
t.assert.throws(() => {
database.applyChangeset(session.changeset(), {
onConflict: null
}, null);
}, {
name: 'TypeError',
message: 'The "options.onConflict" argument must be a function.'
});
});
test('session.patchset()', (t) => {
const database = new DatabaseSync(':memory:');
database.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)');
database.exec("INSERT INTO data VALUES ('1', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.')");
const session = database.createSession();
database.exec("UPDATE data SET value = 'hi' WHERE key = 1");
const patchset = session.patchset();
const changeset = session.changeset();
t.assert.ok(patchset instanceof Uint8Array);
t.assert.ok(changeset instanceof Uint8Array);
t.assert.deepStrictEqual(patchset, session.patchset());
t.assert.deepStrictEqual(changeset, session.changeset());
t.assert.ok(
patchset.length < changeset.length,
'expected patchset to be smaller than changeset');
});
test('session.close() - using session after close throws exception', (t) => {
const database = new DatabaseSync(':memory:');
database.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)');
database.exec("INSERT INTO data VALUES ('1', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.')");
const session = database.createSession();
database.exec("UPDATE data SET value = 'hi' WHERE key = 1");
session.close();
database.exec("UPDATE data SET value = 'world' WHERE key = 1");
t.assert.throws(() => {
session.changeset();
}, {
name: 'Error',
message: 'session is not open'
});
});
test('session.close() - after closing database throws exception', (t) => {
const database = new DatabaseSync(':memory:');
database.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)');
database.exec("INSERT INTO data VALUES ('1', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.')");
const session = database.createSession();
database.close();
t.assert.throws(() => {
session.close();
}, {
name: 'Error',
message: 'database is not open'
});
});
test('session.close() - closing twice', (t) => {
const database = new DatabaseSync(':memory:');
const session = database.createSession();
session.close();
t.assert.throws(() => {
session.close();
}, {
name: 'Error',
message: 'session is not open'
});
});

View File

@@ -0,0 +1,162 @@
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const assert = require('node:assert');
const { DatabaseSync } = require('node:sqlite');
const { suite, test } = require('node:test');
suite('StatementSync.prototype.columns()', () => {
test('returns column metadata for core SQLite types', () => {
const db = new DatabaseSync(':memory:');
db.exec(`CREATE TABLE test (
col1 INTEGER,
col2 REAL,
col3 TEXT,
col4 BLOB,
col5 NULL
)`);
const stmt = db.prepare('SELECT col1, col2, col3, col4, col5 FROM test');
assert.deepStrictEqual(stmt.columns(), [
{
__proto__: null,
column: 'col1',
database: 'main',
name: 'col1',
table: 'test',
type: 'INTEGER',
},
{
__proto__: null,
column: 'col2',
database: 'main',
name: 'col2',
table: 'test',
type: 'REAL',
},
{
__proto__: null,
column: 'col3',
database: 'main',
name: 'col3',
table: 'test',
type: 'TEXT',
},
{
__proto__: null,
column: 'col4',
database: 'main',
name: 'col4',
table: 'test',
type: 'BLOB',
},
{
__proto__: null,
column: 'col5',
database: 'main',
name: 'col5',
table: 'test',
type: null,
},
]);
});
test('supports statements using multiple tables', () => {
const db = new DatabaseSync(':memory:');
db.exec(`
CREATE TABLE test1 (value1 INTEGER);
CREATE TABLE test2 (value2 INTEGER);
`);
const stmt = db.prepare('SELECT value1, value2 FROM test1, test2');
assert.deepStrictEqual(stmt.columns(), [
{
__proto__: null,
column: 'value1',
database: 'main',
name: 'value1',
table: 'test1',
type: 'INTEGER',
},
{
__proto__: null,
column: 'value2',
database: 'main',
name: 'value2',
table: 'test2',
type: 'INTEGER',
},
]);
});
test('supports column aliases', () => {
const db = new DatabaseSync(':memory:');
db.exec(`CREATE TABLE test (value INTEGER)`);
const stmt = db.prepare('SELECT value AS foo FROM test');
assert.deepStrictEqual(stmt.columns(), [
{
__proto__: null,
column: 'value',
database: 'main',
name: 'foo',
table: 'test',
type: 'INTEGER',
},
]);
});
test('supports column expressions', () => {
const db = new DatabaseSync(':memory:');
db.exec(`CREATE TABLE test (value INTEGER)`);
const stmt = db.prepare('SELECT value + 1, value FROM test');
assert.deepStrictEqual(stmt.columns(), [
{
__proto__: null,
column: null,
database: null,
name: 'value + 1',
table: null,
type: null,
},
{
__proto__: null,
column: 'value',
database: 'main',
name: 'value',
table: 'test',
type: 'INTEGER',
},
]);
});
test('supports subqueries', () => {
const db = new DatabaseSync(':memory:');
db.exec(`CREATE TABLE test (value INTEGER)`);
const stmt = db.prepare('SELECT * FROM (SELECT * FROM test)');
assert.deepStrictEqual(stmt.columns(), [
{
__proto__: null,
column: 'value',
database: 'main',
name: 'value',
table: 'test',
type: 'INTEGER',
},
]);
});
test('supports statements that do not return data', () => {
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE test (value INTEGER)');
const stmt = db.prepare('INSERT INTO test (value) VALUES (?)');
assert.deepStrictEqual(stmt.columns(), []);
});
test('throws if the statement is finalized', () => {
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE test (value INTEGER)');
const stmt = db.prepare('SELECT value FROM test');
db.close();
assert.throws(() => {
stmt.columns();
}, /statement has been finalized/);
});
});

View File

@@ -0,0 +1,581 @@
// Flags: --expose-gc
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const tmpdir = require('../common/tmpdir');
const { join } = require('node:path');
const { DatabaseSync, StatementSync } = require('node:sqlite');
const { suite, test } = require('node:test');
let cnt = 0;
tmpdir.refresh();
function nextDb() {
return join(tmpdir.path, `database-${cnt++}.db`);
}
suite('StatementSync() constructor', () => {
test('StatementSync cannot be constructed directly', (t) => {
t.assert.throws(() => {
new StatementSync();
}, {
code: 'ERR_ILLEGAL_CONSTRUCTOR',
message: /Illegal constructor/,
});
});
});
suite('StatementSync.prototype.get()', () => {
test('executes a query and returns undefined on no results', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
t.assert.strictEqual(stmt.get(), undefined);
stmt = db.prepare('SELECT * FROM storage');
t.assert.strictEqual(stmt.get(), undefined);
});
test('executes a query and returns the first result', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
t.assert.strictEqual(stmt.get(), undefined);
stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
t.assert.strictEqual(stmt.get('key1', 'val1'), undefined);
t.assert.strictEqual(stmt.get('key2', 'val2'), undefined);
stmt = db.prepare('SELECT * FROM storage ORDER BY key');
t.assert.deepStrictEqual(stmt.get(), { __proto__: null, key: 'key1', val: 'val1' });
});
test('executes a query that returns special columns', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const stmt = db.prepare('SELECT 1 as __proto__, 2 as constructor, 3 as toString');
t.assert.deepStrictEqual(stmt.get(), { __proto__: null, ['__proto__']: 1, constructor: 2, toString: 3 });
});
});
suite('StatementSync.prototype.all()', () => {
test('executes a query and returns an empty array on no results', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
t.assert.deepStrictEqual(stmt.all(), []);
});
test('executes a query and returns all results', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
t.assert.deepStrictEqual(stmt.run(), { changes: 0, lastInsertRowid: 0 });
stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
t.assert.deepStrictEqual(
stmt.run('key1', 'val1'),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.deepStrictEqual(
stmt.run('key2', 'val2'),
{ changes: 1, lastInsertRowid: 2 },
);
stmt = db.prepare('SELECT * FROM storage ORDER BY key');
t.assert.deepStrictEqual(stmt.all(), [
{ __proto__: null, key: 'key1', val: 'val1' },
{ __proto__: null, key: 'key2', val: 'val2' },
]);
});
});
suite('StatementSync.prototype.iterate()', () => {
test('executes a query and returns an empty iterator on no results', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
const iter = stmt.iterate();
t.assert.strictEqual(iter instanceof globalThis.Iterator, true);
t.assert.ok(iter[Symbol.iterator]);
t.assert.deepStrictEqual(iter.toArray(), []);
});
test('executes a query and returns all results', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
t.assert.deepStrictEqual(stmt.run(), { changes: 0, lastInsertRowid: 0 });
stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
t.assert.deepStrictEqual(
stmt.run('key1', 'val1'),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.deepStrictEqual(
stmt.run('key2', 'val2'),
{ changes: 1, lastInsertRowid: 2 },
);
const items = [
{ __proto__: null, key: 'key1', val: 'val1' },
{ __proto__: null, key: 'key2', val: 'val2' },
];
stmt = db.prepare('SELECT * FROM storage ORDER BY key');
t.assert.deepStrictEqual(stmt.iterate().toArray(), items);
const itemsLoop = items.slice();
for (const item of stmt.iterate()) {
t.assert.deepStrictEqual(item, itemsLoop.shift());
}
});
test('iterator keeps the prepared statement from being collected', (t) => {
const db = new DatabaseSync(':memory:');
db.exec(`
CREATE TABLE test(key TEXT, val TEXT);
INSERT INTO test (key, val) VALUES ('key1', 'val1');
INSERT INTO test (key, val) VALUES ('key2', 'val2');
`);
// Do not keep an explicit reference to the prepared statement.
const iterator = db.prepare('SELECT * FROM test').iterate();
const results = [];
global.gc();
for (const item of iterator) {
results.push(item);
}
t.assert.deepStrictEqual(results, [
{ __proto__: null, key: 'key1', val: 'val1' },
{ __proto__: null, key: 'key2', val: 'val2' },
]);
});
test('iterator can be exited early', (t) => {
const db = new DatabaseSync(':memory:');
db.exec(`
CREATE TABLE test(key TEXT, val TEXT);
INSERT INTO test (key, val) VALUES ('key1', 'val1');
INSERT INTO test (key, val) VALUES ('key2', 'val2');
`);
const iterator = db.prepare('SELECT * FROM test').iterate();
const results = [];
for (const item of iterator) {
results.push(item);
break;
}
t.assert.deepStrictEqual(results, [
{ __proto__: null, key: 'key1', val: 'val1' },
]);
t.assert.deepStrictEqual(
iterator.next(),
{ __proto__: null, done: true, value: null },
);
});
});
suite('StatementSync.prototype.run()', () => {
test('executes a query and returns change metadata', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE storage(key TEXT, val TEXT);
INSERT INTO storage (key, val) VALUES ('foo', 'bar');
`);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('SELECT * FROM storage');
t.assert.deepStrictEqual(stmt.run(), { changes: 1, lastInsertRowid: 1 });
});
test('SQLite throws when trying to bind too many parameters', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
t.assert.throws(() => {
stmt.run(1, 2, 3);
}, {
code: 'ERR_SQLITE_ERROR',
message: 'column index out of range',
errcode: 25,
errstr: 'column index out of range',
});
});
test('SQLite defaults to NULL for unbound parameters', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
t.assert.throws(() => {
stmt.run(1);
}, {
code: 'ERR_SQLITE_ERROR',
message: 'NOT NULL constraint failed: data.val',
errcode: 1299,
errstr: 'constraint failed',
});
});
test('returns correct metadata when using RETURNING', (t) => {
const db = new DatabaseSync(':memory:');
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const sql = 'INSERT INTO data (key, val) VALUES ($k, $v) RETURNING key';
const stmt = db.prepare(sql);
t.assert.deepStrictEqual(
stmt.run({ k: 1, v: 10 }), { changes: 1, lastInsertRowid: 1 }
);
t.assert.deepStrictEqual(
stmt.run({ k: 2, v: 20 }), { changes: 1, lastInsertRowid: 2 }
);
t.assert.deepStrictEqual(
stmt.run({ k: 3, v: 30 }), { changes: 1, lastInsertRowid: 3 }
);
});
});
suite('StatementSync.prototype.sourceSQL', () => {
test('equals input SQL', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const sql = 'INSERT INTO types (key, val) VALUES ($k, $v)';
const stmt = db.prepare(sql);
t.assert.strictEqual(stmt.sourceSQL, sql);
});
});
suite('StatementSync.prototype.expandedSQL', () => {
test('equals expanded SQL', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const sql = 'INSERT INTO types (key, val) VALUES ($k, ?)';
const expanded = 'INSERT INTO types (key, val) VALUES (\'33\', \'42\')';
const stmt = db.prepare(sql);
t.assert.deepStrictEqual(
stmt.run({ $k: '33' }, '42'),
{ changes: 1, lastInsertRowid: 33 },
);
t.assert.strictEqual(stmt.expandedSQL, expanded);
});
});
suite('StatementSync.prototype.setReadBigInts()', () => {
test('BigInts support can be toggled', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
INSERT INTO data (key, val) VALUES (1, 42);
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare('SELECT val FROM data');
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42 });
t.assert.strictEqual(query.setReadBigInts(true), undefined);
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42n });
t.assert.strictEqual(query.setReadBigInts(false), undefined);
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42 });
const insert = db.prepare('INSERT INTO data (key) VALUES (?)');
t.assert.deepStrictEqual(
insert.run(10),
{ changes: 1, lastInsertRowid: 10 },
);
t.assert.strictEqual(insert.setReadBigInts(true), undefined);
t.assert.deepStrictEqual(
insert.run(20),
{ changes: 1n, lastInsertRowid: 20n },
);
t.assert.strictEqual(insert.setReadBigInts(false), undefined);
t.assert.deepStrictEqual(
insert.run(30),
{ changes: 1, lastInsertRowid: 30 },
);
});
test('throws when input is not a boolean', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)');
t.assert.throws(() => {
stmt.setReadBigInts();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "readBigInts" argument must be a boolean/,
});
});
test('BigInt is required for reading large integers', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const bad = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`);
t.assert.throws(() => {
bad.get();
}, {
code: 'ERR_OUT_OF_RANGE',
message: /^Value is too large to be represented as a JavaScript number: 9007199254740992$/,
});
const good = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`);
good.setReadBigInts(true);
t.assert.deepStrictEqual(good.get(), {
__proto__: null,
[`${Number.MAX_SAFE_INTEGER} + 1`]: 2n ** 53n,
});
});
});
suite('StatementSync.prototype.setReturnArrays()', () => {
test('throws when input is not a boolean', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('SELECT key, val FROM data');
t.assert.throws(() => {
stmt.setReturnArrays();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "returnArrays" argument must be a boolean/,
});
});
});
suite('StatementSync.prototype.get() with array output', () => {
test('returns array row when setReturnArrays is true', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
INSERT INTO data (key, val) VALUES (1, 'one');
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare('SELECT key, val FROM data WHERE key = 1');
t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' });
query.setReturnArrays(true);
t.assert.deepStrictEqual(query.get(), [1, 'one']);
query.setReturnArrays(false);
t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' });
});
test('returns array rows with BigInts when both flags are set', (t) => {
const expected = [1n, 9007199254740992n];
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE big_data(id INTEGER, big_num INTEGER);
INSERT INTO big_data VALUES (1, 9007199254740992);
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare('SELECT id, big_num FROM big_data');
query.setReturnArrays(true);
query.setReadBigInts(true);
const row = query.get();
t.assert.deepStrictEqual(row, expected);
});
});
suite('StatementSync.prototype.all() with array output', () => {
test('returns array rows when setReturnArrays is true', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
INSERT INTO data (key, val) VALUES (1, 'one');
INSERT INTO data (key, val) VALUES (2, 'two');
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare('SELECT key, val FROM data ORDER BY key');
t.assert.deepStrictEqual(query.all(), [
{ __proto__: null, key: 1, val: 'one' },
{ __proto__: null, key: 2, val: 'two' },
]);
query.setReturnArrays(true);
t.assert.deepStrictEqual(query.all(), [
[1, 'one'],
[2, 'two'],
]);
query.setReturnArrays(false);
t.assert.deepStrictEqual(query.all(), [
{ __proto__: null, key: 1, val: 'one' },
{ __proto__: null, key: 2, val: 'two' },
]);
});
test('handles array rows with many columns', (t) => {
const expected = [
1,
'text1',
1.1,
new Uint8Array([0xde, 0xad, 0xbe, 0xef]),
5,
'text2',
2.2,
new Uint8Array([0xbe, 0xef, 0xca, 0xfe]),
9,
'text3',
];
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE wide_table(
col1 INTEGER, col2 TEXT, col3 REAL, col4 BLOB, col5 INTEGER,
col6 TEXT, col7 REAL, col8 BLOB, col9 INTEGER, col10 TEXT
);
INSERT INTO wide_table VALUES (
1, 'text1', 1.1, X'DEADBEEF', 5,
'text2', 2.2, X'BEEFCAFE', 9, 'text3'
);
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare('SELECT * FROM wide_table');
query.setReturnArrays(true);
const results = query.all();
t.assert.strictEqual(results.length, 1);
t.assert.deepStrictEqual(results[0], expected);
});
});
suite('StatementSync.prototype.iterate() with array output', () => {
test('iterates array rows when setReturnArrays is true', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
INSERT INTO data (key, val) VALUES (1, 'one');
INSERT INTO data (key, val) VALUES (2, 'two');
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare('SELECT key, val FROM data ORDER BY key');
// Test with objects first
const objectRows = [];
for (const row of query.iterate()) {
objectRows.push(row);
}
t.assert.deepStrictEqual(objectRows, [
{ __proto__: null, key: 1, val: 'one' },
{ __proto__: null, key: 2, val: 'two' },
]);
// Test with arrays
query.setReturnArrays(true);
const arrayRows = [];
for (const row of query.iterate()) {
arrayRows.push(row);
}
t.assert.deepStrictEqual(arrayRows, [
[1, 'one'],
[2, 'two'],
]);
// Test toArray() method
t.assert.deepStrictEqual(query.iterate().toArray(), [
[1, 'one'],
[2, 'two'],
]);
});
test('iterator can be exited early with array rows', (t) => {
const db = new DatabaseSync(':memory:');
db.exec(`
CREATE TABLE test(key TEXT, val TEXT);
INSERT INTO test (key, val) VALUES ('key1', 'val1');
INSERT INTO test (key, val) VALUES ('key2', 'val2');
`);
const stmt = db.prepare('SELECT key, val FROM test');
stmt.setReturnArrays(true);
const iterator = stmt.iterate();
const results = [];
for (const item of iterator) {
results.push(item);
break;
}
t.assert.deepStrictEqual(results, [
['key1', 'val1'],
]);
t.assert.deepStrictEqual(
iterator.next(),
{ __proto__: null, done: true, value: null },
);
});
});
suite('StatementSync.prototype.setAllowBareNamedParameters()', () => {
test('bare named parameter support can be toggled', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
t.assert.deepStrictEqual(
stmt.run({ k: 1, v: 2 }),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.strictEqual(stmt.setAllowBareNamedParameters(false), undefined);
t.assert.throws(() => {
stmt.run({ k: 2, v: 4 });
}, {
code: 'ERR_INVALID_STATE',
message: /Unknown named parameter 'k'/,
});
t.assert.strictEqual(stmt.setAllowBareNamedParameters(true), undefined);
t.assert.deepStrictEqual(
stmt.run({ k: 3, v: 6 }),
{ changes: 1, lastInsertRowid: 3 },
);
});
test('throws when input is not a boolean', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
t.assert.throws(() => {
stmt.setAllowBareNamedParameters();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "allowBareNamedParameters" argument must be a boolean/,
});
});
});

View File

@@ -0,0 +1,73 @@
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const tmpdir = require('../common/tmpdir');
const { join } = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const { once } = require('node:events');
const { Worker } = require('node:worker_threads');
let cnt = 0;
tmpdir.refresh();
function nextDb() {
return join(tmpdir.path, `database-${cnt++}.db`);
}
test('waits to acquire lock', async (t) => {
const DB_PATH = nextDb();
const conn = new DatabaseSync(DB_PATH);
t.after(() => {
try {
conn.close();
} catch {
// Ignore.
}
});
conn.exec('CREATE TABLE IF NOT EXISTS data (value TEXT)');
conn.exec('BEGIN EXCLUSIVE;');
const worker = new Worker(`
'use strict';
const { DatabaseSync } = require('node:sqlite');
const { workerData } = require('node:worker_threads');
const conn = new DatabaseSync(workerData.database, { timeout: 30000 });
conn.exec('SELECT * FROM data');
conn.close();
`, {
eval: true,
workerData: {
database: DB_PATH,
}
});
await once(worker, 'online');
conn.exec('COMMIT;');
await once(worker, 'exit');
});
test('throws if the lock cannot be acquired before timeout', (t) => {
const DB_PATH = nextDb();
const conn1 = new DatabaseSync(DB_PATH);
t.after(() => {
try {
conn1.close();
} catch {
// Ignore.
}
});
const conn2 = new DatabaseSync(DB_PATH, { timeout: 1 });
t.after(() => {
try {
conn2.close();
} catch {
// Ignore.
}
});
conn1.exec('CREATE TABLE IF NOT EXISTS data (value TEXT)');
conn1.exec('PRAGMA locking_mode = EXCLUSIVE; BEGIN EXCLUSIVE;');
t.assert.throws(() => {
conn2.exec('SELECT * FROM data');
}, /database is locked/);
});

View File

@@ -0,0 +1,67 @@
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const tmpdir = require('../common/tmpdir');
const { join } = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { suite, test } = require('node:test');
let cnt = 0;
tmpdir.refresh();
function nextDb() {
return join(tmpdir.path, `database-${cnt++}.db`);
}
suite('manual transactions', () => {
test('a transaction is committed', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(
key INTEGER PRIMARY KEY
) STRICT;
`);
t.assert.strictEqual(setup, undefined);
t.assert.deepStrictEqual(
db.prepare('BEGIN').run(),
{ changes: 0, lastInsertRowid: 0 },
);
t.assert.deepStrictEqual(
db.prepare('INSERT INTO data (key) VALUES (100)').run(),
{ changes: 1, lastInsertRowid: 100 },
);
t.assert.deepStrictEqual(
db.prepare('COMMIT').run(),
{ changes: 1, lastInsertRowid: 100 },
);
t.assert.deepStrictEqual(
db.prepare('SELECT * FROM data').all(),
[{ __proto__: null, key: 100 }],
);
});
test('a transaction is rolled back', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(
key INTEGER PRIMARY KEY
) STRICT;
`);
t.assert.strictEqual(setup, undefined);
t.assert.deepStrictEqual(
db.prepare('BEGIN').run(),
{ changes: 0, lastInsertRowid: 0 },
);
t.assert.deepStrictEqual(
db.prepare('INSERT INTO data (key) VALUES (100)').run(),
{ changes: 1, lastInsertRowid: 100 },
);
t.assert.deepStrictEqual(
db.prepare('ROLLBACK').run(),
{ changes: 1, lastInsertRowid: 100 },
);
t.assert.deepStrictEqual(db.prepare('SELECT * FROM data').all(), []);
});
});

View File

@@ -0,0 +1,62 @@
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const tmpdir = require('../common/tmpdir');
const { join } = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { suite, test } = require('node:test');
let cnt = 0;
tmpdir.refresh();
function nextDb() {
return join(tmpdir.path, `database-${cnt++}.db`);
}
const arrayBuffer = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]).buffer;
const TypedArrays = [
['Int8Array', Int8Array],
['Uint8Array', Uint8Array],
['Uint8ClampedArray', Uint8ClampedArray],
['Int16Array', Int16Array],
['Uint16Array', Uint16Array],
['Int32Array', Int32Array],
['Uint32Array', Uint32Array],
['Float32Array', Float32Array],
['Float64Array', Float64Array],
['BigInt64Array', BigInt64Array],
['BigUint64Array', BigUint64Array],
['DataView', DataView],
];
suite('StatementSync with TypedArray/DataView', () => {
for (const [displayName, TypedArray] of TypedArrays) {
test(displayName, (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
db.exec('CREATE TABLE test (data BLOB)');
// insert
{
const stmt = db.prepare('INSERT INTO test VALUES (?)');
stmt.run(new TypedArray(arrayBuffer));
}
// select all
{
const stmt = db.prepare('SELECT * FROM test');
const row = stmt.get();
t.assert.ok(row.data instanceof Uint8Array);
t.assert.strictEqual(row.data.length, 8);
t.assert.deepStrictEqual(row.data, new Uint8Array(arrayBuffer));
}
// query
{
const stmt = db.prepare('SELECT * FROM test WHERE data = ?');
const rows = stmt.all(new TypedArray(arrayBuffer));
t.assert.strictEqual(rows.length, 1);
t.assert.ok(rows[0].data instanceof Uint8Array);
t.assert.strictEqual(rows[0].data.length, 8);
t.assert.deepStrictEqual(rows[0].data, new Uint8Array(arrayBuffer));
}
});
}
});