Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sqlite): add StatementSync.prototype.iterate method #54213

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
19 changes: 19 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,25 @@ object. If the prepared statement does not return any results, this method
returns `undefined`. The prepared statement [parameters are bound][] using the
values in `namedParameters` and `anonymousParameters`.

### `statement.iterate([namedParameters][, ...anonymousParameters])`

<!-- YAML
added: REPLACEME
-->

* `namedParameters` {Object} An optional object used to bind named parameters.
The keys of this object are used to configure the mapping.
* `...anonymousParameters` {null|number|bigint|string|Buffer|Uint8Array} Zero or
more values to bind to anonymous parameters.
* Returns: {Iterator} An iterable iterator of objects. Each object corresponds to a row
returned by executing the prepared statement. The keys and values of each
object correspond to the column names and values of the row.

This method executes a prepared statement and returns an iterator of
objects. If the prepared statement does not return any results, this method
returns an empty iterator. The prepared statement [parameters are bound][] using
the values in `namedParameters` and `anonymousParameters`.

### `statement.run([namedParameters][, ...anonymousParameters])`

<!-- YAML
Expand Down
6 changes: 6 additions & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,10 @@
V(ipv4_string, "IPv4") \
V(ipv6_string, "IPv6") \
V(isclosing_string, "isClosing") \
V(isfinished_string, "isFinished") \
V(issuer_string, "issuer") \
V(issuercert_string, "issuerCertificate") \
V(iterator_string, "Iterator") \
V(jwk_crv_string, "crv") \
V(jwk_d_string, "d") \
V(jwk_dp_string, "dp") \
Expand Down Expand Up @@ -241,6 +243,7 @@
V(nistcurve_string, "nistCurve") \
V(node_string, "node") \
V(nsname_string, "nsname") \
V(num_cols_string, "num_cols") \
V(object_string, "Object") \
V(ocsp_request_string, "OCSPRequest") \
V(oncertcb_string, "oncertcb") \
Expand Down Expand Up @@ -288,6 +291,7 @@
V(priority_string, "priority") \
V(process_string, "process") \
V(promise_string, "promise") \
V(prototype_string, "prototype") \
V(psk_string, "psk") \
V(pubkey_string, "pubkey") \
V(public_exponent_string, "publicExponent") \
Expand All @@ -309,6 +313,7 @@
V(require_string, "require") \
V(resource_string, "resource") \
V(retry_string, "retry") \
V(return_string, "return") \
V(salt_length_string, "saltLength") \
V(scheme_string, "scheme") \
V(scopeid_string, "scopeid") \
Expand All @@ -332,6 +337,7 @@
V(standard_name_string, "standardName") \
V(start_time_string, "startTime") \
V(state_string, "state") \
V(statement_string, "statement") \
V(stats_string, "stats") \
V(status_string, "status") \
V(stdio_string, "stdio") \
Expand Down
176 changes: 176 additions & 0 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ using v8::ConstructorBehavior;
using v8::Context;
using v8::DontDelete;
using v8::Exception;
using v8::External;
using v8::Function;
using v8::FunctionCallback;
using v8::FunctionCallbackInfo;
Expand Down Expand Up @@ -790,6 +791,180 @@ void StatementSync::All(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(Array::New(isolate, rows.data(), rows.size()));
}

void StatementSync::IterateReturnCallback(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
auto isolate = env->isolate();
auto context = isolate->GetCurrentContext();

auto self = args.This();
// iterator has fetch all result or break, prevent next func to return result
self->Set(context, env->isfinished_string(), Boolean::New(isolate, true))
.ToChecked();
tpoisseau marked this conversation as resolved.
Show resolved Hide resolved

auto external_stmt = Local<External>::Cast(
self->Get(context, env->statement_string()).ToLocalChecked());
auto stmt = static_cast<StatementSync*>(external_stmt->Value());
if (!stmt->IsFinalized()) {
sqlite3_reset(stmt->statement_);
}

LocalVector<Name> keys(isolate, {env->done_string(), env->value_string()});
LocalVector<Value> values(isolate,
{Boolean::New(isolate, true), Null(isolate)});

DCHECK_EQ(keys.size(), values.size());
Local<Object> result = Object::New(
isolate, Null(isolate), keys.data(), values.data(), keys.size());
args.GetReturnValue().Set(result);
}

void StatementSync::IterateNextCallback(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
auto isolate = env->isolate();
auto context = isolate->GetCurrentContext();

auto self = args.This();

// skip iteration if is_finished
auto is_finished = Local<Boolean>::Cast(
self->Get(context, env->isfinished_string()).ToLocalChecked());
if (is_finished->Value()) {
LocalVector<Name> keys(isolate, {env->done_string(), env->value_string()});
LocalVector<Value> values(isolate,
{Boolean::New(isolate, true), Null(isolate)});

DCHECK_EQ(keys.size(), values.size());
Local<Object> result = Object::New(
isolate, Null(isolate), keys.data(), values.data(), keys.size());
args.GetReturnValue().Set(result);
return;
}

auto external_stmt = Local<External>::Cast(
self->Get(context, env->statement_string()).ToLocalChecked());
auto stmt = static_cast<StatementSync*>(external_stmt->Value());
auto num_cols =
Local<Integer>::Cast(
self->Get(context, env->num_cols_string()).ToLocalChecked())
->Value();

THROW_AND_RETURN_ON_BAD_STATE(
env, stmt->IsFinalized(), "statement has been finalized");

int r = sqlite3_step(stmt->statement_);
if (r != SQLITE_ROW) {
CHECK_ERROR_OR_THROW(
env->isolate(), stmt->db_->Connection(), r, SQLITE_DONE, void());

// cleanup when no more rows to fetch
sqlite3_reset(stmt->statement_);
self->Set(context, env->isfinished_string(), Boolean::New(isolate, true))
.ToChecked();

LocalVector<Name> keys(isolate, {env->done_string(), env->value_string()});
LocalVector<Value> values(isolate,
{Boolean::New(isolate, true), Null(isolate)});

DCHECK_EQ(keys.size(), values.size());
Local<Object> result = Object::New(
isolate, Null(isolate), keys.data(), values.data(), keys.size());
args.GetReturnValue().Set(result);
return;
}

LocalVector<Name> row_keys(isolate);
row_keys.reserve(num_cols);
tpoisseau marked this conversation as resolved.
Show resolved Hide resolved
LocalVector<Value> row_values(isolate);
row_values.reserve(num_cols);
for (int i = 0; i < num_cols; ++i) {
Local<Name> key;
if (!stmt->ColumnNameToName(i).ToLocal(&key)) return;
Local<Value> val;
if (!stmt->ColumnToValue(i).ToLocal(&val)) return;
row_keys.emplace_back(key);
row_values.emplace_back(val);
}

Local<Object> row = Object::New(
isolate, Null(isolate), row_keys.data(), row_values.data(), num_cols);

LocalVector<Name> keys(isolate, {env->done_string(), env->value_string()});
LocalVector<Value> values(isolate, {Boolean::New(isolate, false), row});

DCHECK_EQ(keys.size(), values.size());
Local<Object> result = Object::New(
isolate, Null(isolate), keys.data(), values.data(), keys.size());
args.GetReturnValue().Set(result);
}

void StatementSync::Iterate(const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(
env, stmt->IsFinalized(), "statement has been finalized");
auto isolate = env->isolate();
auto context = env->context();
int r = sqlite3_reset(stmt->statement_);
CHECK_ERROR_OR_THROW(
env->isolate(), stmt->db_->Connection(), r, SQLITE_OK, void());

if (!stmt->BindParams(args)) {
return;
}

Local<Function> next_func =
Function::New(context, StatementSync::IterateNextCallback)
.ToLocalChecked();
Local<Function> return_func =
Function::New(context, StatementSync::IterateReturnCallback)
.ToLocalChecked();

LocalVector<Name> keys(isolate, {env->next_string(), env->return_string()});
LocalVector<Value> values(isolate, {next_func, return_func});

Local<Object> global = context->Global();
Local<Value> js_iterator;
Local<Value> js_iterator_prototype;
if (!global->Get(context, env->iterator_string()).ToLocal(&js_iterator))
return;
if (!js_iterator.As<Object>()
->Get(context, env->prototype_string())
.ToLocal(&js_iterator_prototype))
return;

DCHECK_EQ(keys.size(), values.size());
Local<Object> iterable_iterator = Object::New(
isolate, js_iterator_prototype, keys.data(), values.data(), keys.size());

auto num_cols_pd = v8::PropertyDescriptor(
v8::Integer::New(isolate, sqlite3_column_count(stmt->statement_)), false);
num_cols_pd.set_enumerable(false);
num_cols_pd.set_configurable(false);
iterable_iterator
->DefineProperty(context, env->num_cols_string(), num_cols_pd)
.ToChecked();

auto stmt_pd =
v8::PropertyDescriptor(v8::External::New(isolate, stmt), false);
stmt_pd.set_enumerable(false);
stmt_pd.set_configurable(false);
iterable_iterator->DefineProperty(context, env->statement_string(), stmt_pd)
.ToChecked();

auto is_finished_pd =
v8::PropertyDescriptor(v8::Boolean::New(isolate, false), true);
stmt_pd.set_enumerable(false);
stmt_pd.set_configurable(false);
iterable_iterator
->DefineProperty(context, env->isfinished_string(), is_finished_pd)
.ToChecked();

args.GetReturnValue().Set(iterable_iterator);
}

void StatementSync::Get(const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
Expand Down Expand Up @@ -987,6 +1162,7 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "StatementSync"));
tmpl->InstanceTemplate()->SetInternalFieldCount(
StatementSync::kInternalFieldCount);
SetProtoMethod(isolate, tmpl, "iterate", StatementSync::Iterate);
SetProtoMethod(isolate, tmpl, "all", StatementSync::All);
SetProtoMethod(isolate, tmpl, "get", StatementSync::Get);
SetProtoMethod(isolate, tmpl, "run", StatementSync::Run);
Expand Down
6 changes: 6 additions & 0 deletions src/node_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class StatementSync : public BaseObject {
DatabaseSync* db,
sqlite3_stmt* stmt);
static void All(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Iterate(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Get(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Run(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SourceSQLGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
Expand All @@ -118,6 +119,11 @@ class StatementSync : public BaseObject {
bool BindValue(const v8::Local<v8::Value>& value, const int index);
v8::MaybeLocal<v8::Value> ColumnToValue(const int column);
v8::MaybeLocal<v8::Name> ColumnNameToName(const int column);

static void IterateNextCallback(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void IterateReturnCallback(
const v8::FunctionCallbackInfo<v8::Value>& args);
};

using Sqlite3ChangesetGenFunc = int (*)(sqlite3_session*, int*, void**);
Expand Down
36 changes: 36 additions & 0 deletions test/parallel/test-sqlite-statement-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,42 @@ suite('StatementSync.prototype.all()', () => {
});
});

suite('StatementSync.prototype.iterate()', () => {
test('executes a query and returns an empty iterator on no results', (t) => {
const db = new DatabaseSync(nextDb());
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
t.assert.deepStrictEqual(stmt.iterate().toArray(), []);
});

test('executes a query and returns all results', (t) => {
const db = new DatabaseSync(nextDb());
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());
}
});
});

suite('StatementSync.prototype.run()', () => {
test('executes a query and returns change metadata', (t) => {
const db = new DatabaseSync(nextDb());
Expand Down
Loading