Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,64 @@ Opens the database specified in the `path` argument of the `DatabaseSync`
constructor. This method should only be used when the database is not opened via
the constructor. An exception is thrown if the database is already open.

### `database.serialize([dbName])`

<!-- YAML
added: REPLACEME
-->

* `dbName` {string} Name of the database to serialize. This can be `'main'`
(the default primary database) or any other database that has been added with
[`ATTACH DATABASE`][]. **Default:** `'main'`.
* Returns: {Uint8Array} A binary representation of the database.

Serializes the database into a binary representation, returned as a
`Uint8Array`. This is useful for saving, cloning, or transferring an in-memory
database. This method is a wrapper around [`sqlite3_serialize()`][].

```cjs
const { DatabaseSync } = require('node:sqlite');

const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE t(key INTEGER PRIMARY KEY, value TEXT)');
db.exec("INSERT INTO t VALUES (1, 'hello')");
const buffer = db.serialize();
console.log(buffer.length); // Prints the byte length of the database
```

### `database.deserialize(buffer[, options])`

<!-- YAML
added: REPLACEME
-->

* `buffer` {Uint8Array} A binary representation of a database, such as the
output of [`database.serialize()`][].
* `options` {Object} Optional configuration for the deserialization.
* `dbName` {string} Name of the database to deserialize into.
**Default:** `'main'`.

Loads a serialized database into this connection, replacing the current
database. The deserialized database is writable. Existing prepared statements
are finalized before deserialization is attempted, even if the operation
subsequently fails. This method is a wrapper around
[`sqlite3_deserialize()`][].

```cjs
const { DatabaseSync } = require('node:sqlite');

const original = new DatabaseSync(':memory:');
original.exec('CREATE TABLE t(key INTEGER PRIMARY KEY, value TEXT)');
original.exec("INSERT INTO t VALUES (1, 'hello')");
const buffer = original.serialize();
original.close();

const clone = new DatabaseSync(':memory:');
clone.deserialize(buffer);
console.log(clone.prepare('SELECT value FROM t').get());
// Prints: { value: 'hello' }
```

### `database.prepare(sql[, options])`

<!-- YAML
Expand Down Expand Up @@ -1545,6 +1603,7 @@ callback function to indicate what type of operation is being authorized.
[`SQLTagStore`]: #class-sqltagstore
[`database.applyChangeset()`]: #databaseapplychangesetchangeset-options
[`database.createTagStore()`]: #databasecreatetagstoremaxsize
[`database.serialize()`]: #databaseserializedbname
[`database.setAuthorizer()`]: #databasesetauthorizercallback
[`sqlite3_backup_finish()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish
[`sqlite3_backup_init()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit
Expand All @@ -1559,12 +1618,14 @@ callback function to indicate what type of operation is being authorized.
[`sqlite3_create_function_v2()`]: https://www.sqlite.org/c3ref/create_function.html
[`sqlite3_create_window_function()`]: https://www.sqlite.org/c3ref/create_function.html
[`sqlite3_db_filename()`]: https://sqlite.org/c3ref/db_filename.html
[`sqlite3_deserialize()`]: https://sqlite.org/c3ref/deserialize.html
[`sqlite3_exec()`]: https://www.sqlite.org/c3ref/exec.html
[`sqlite3_expanded_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
[`sqlite3_get_autocommit()`]: https://sqlite.org/c3ref/get_autocommit.html
[`sqlite3_last_insert_rowid()`]: https://www.sqlite.org/c3ref/last_insert_rowid.html
[`sqlite3_load_extension()`]: https://www.sqlite.org/c3ref/load_extension.html
[`sqlite3_prepare_v2()`]: https://www.sqlite.org/c3ref/prepare.html
[`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
Expand Down
133 changes: 133 additions & 0 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace sqlite {
using v8::Array;
using v8::ArrayBuffer;
using v8::BackingStoreInitializationMode;
using v8::BackingStoreOnFailureMode;
using v8::BigInt;
using v8::Boolean;
using v8::ConstructorBehavior;
Expand Down Expand Up @@ -1719,6 +1720,136 @@ void DatabaseSync::Location(const FunctionCallbackInfo<Value>& args) {
}
}

void DatabaseSync::Serialize(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");

std::string db_name = "main";
if (!args[0]->IsUndefined()) {
if (!args[0]->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"dbName\" argument must be a string.");
return;
}
db_name = Utf8Value(env->isolate(), args[0].As<String>()).ToString();
}

sqlite3_int64 size = 0;
unsigned char* data =
sqlite3_serialize(db->connection_, db_name.c_str(), &size, 0);

if (data == nullptr) {
if (size == 0) {
Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(), 0);
args.GetReturnValue().Set(Uint8Array::New(ab, 0, 0));
return;
}
THROW_ERR_SQLITE_ERROR(env->isolate(), db);
return;
}

// V8 sandbox forbids external backing stores so allocate inside the
// sandbox and copy. Without sandbox wrap the output directly using
// sqlite3_free as the destructor to avoid the copy.
#ifdef V8_ENABLE_SANDBOX
auto free_data = OnScopeLeave([&] { sqlite3_free(data); });
auto store = ArrayBuffer::NewBackingStore(
env->isolate(),
size,
BackingStoreInitializationMode::kUninitialized,
BackingStoreOnFailureMode::kReturnNull);
if (!store) {
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
return;
}
memcpy(store->Data(), data, size);
Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(), std::move(store));
#else
auto store = ArrayBuffer::NewBackingStore(
data, size, [](void* ptr, size_t, void*) { sqlite3_free(ptr); }, nullptr);
Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(), std::move(store));
#endif

args.GetReturnValue().Set(Uint8Array::New(ab, 0, size));
}

void DatabaseSync::Deserialize(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");

if (!args[0]->IsUint8Array()) {
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"buffer\" argument must be a Uint8Array.");
return;
}

Local<Uint8Array> input = args[0].As<Uint8Array>();
size_t byte_length = input->ByteLength();

if (byte_length == 0) {
THROW_ERR_INVALID_ARG_VALUE(env,
"The \"buffer\" argument must not be empty.");
return;
}

std::string db_name = "main";
if (args.Length() > 1 && !args[1]->IsUndefined()) {
if (!args[1]->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"options\" argument must be an object.");
return;
}

Local<Object> options = args[1].As<Object>();
Local<String> db_name_key = FIXED_ONE_BYTE_STRING(env->isolate(), "dbName");
Local<Value> db_name_value;
if (!options->Get(env->context(), db_name_key).ToLocal(&db_name_value)) {
return;
}

if (!db_name_value->IsUndefined()) {
if (!db_name_value->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.dbName\" argument must be a string.");
return;
}
db_name =
Utf8Value(env->isolate(), db_name_value.As<String>()).ToString();
}
}

// sqlite3_malloc64 is required because SQLITE_DESERIALIZE_FREEONCLOSE
// transfers ownership to SQLite, which calls sqlite3_free() on close.
// See: https://www.sqlite.org/c3ref/deserialize.html
unsigned char* buf =
static_cast<unsigned char*>(sqlite3_malloc64(byte_length));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this freed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sqlite3_deserialize() takes ownership of the buffer when passed SQLITE_DESERIALIZE_FREEONCLOSE

sqlite3_deserialize docs:
"If the SQLITE_DESERIALIZE_FREEONCLOSE bit is set in F, then SQLite will invoke sqlite3_free() on the serialization buffer when the database connection closes. If the SQLITE_DESERIALIZE_RESIZEABLE bit is set, then SQLite will try to increase the buffer size using sqlite3_realloc64() if writes on the database cause it to grow larger than M bytes."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment to that effect would be helpful.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

if (buf == nullptr) {
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
return;
}

input->CopyContents(buf, byte_length);

db->FinalizeStatements();

int r = sqlite3_deserialize(
db->connection_,
db_name.c_str(),
buf,
byte_length,
byte_length,
SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE);
if (r != SQLITE_OK) {
THROW_ERR_SQLITE_ERROR(env->isolate(), db);
return;
}
}

void DatabaseSync::AggregateFunction(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
Expand Down Expand Up @@ -3766,6 +3897,8 @@ static void Initialize(Local<Object> target,
isolate, db_tmpl, "enableDefensive", DatabaseSync::EnableDefensive);
SetProtoMethod(
isolate, db_tmpl, "loadExtension", DatabaseSync::LoadExtension);
SetProtoMethod(isolate, db_tmpl, "serialize", DatabaseSync::Serialize);
SetProtoMethod(isolate, db_tmpl, "deserialize", DatabaseSync::Deserialize);
SetProtoMethod(
isolate, db_tmpl, "setAuthorizer", DatabaseSync::SetAuthorizer);
SetSideEffectFreeGetter(isolate,
Expand Down
2 changes: 2 additions & 0 deletions src/node_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ class DatabaseSync : public BaseObject {
static void EnableDefensive(const v8::FunctionCallbackInfo<v8::Value>& args);
static void LimitsGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
static void LoadExtension(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Serialize(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Deserialize(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetAuthorizer(const v8::FunctionCallbackInfo<v8::Value>& args);
static int AuthorizerCallback(void* user_data,
int action_code,
Expand Down
Loading
Loading