sqlite: add StatementSync.prototype.columns()

This commit adds a method for retrieving column metadata from
a prepared statement.

Fixes: https://github.com/nodejs/node/issues/57457
PR-URL: https://github.com/nodejs/node/pull/57490
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com>
This commit is contained in:
Colin Ihrig 2025-03-18 09:22:32 -04:00 committed by RafaelGSS
parent f4b835c734
commit dd38a47ea1
No known key found for this signature in database
GPG Key ID: 8BEAB4DFCF555EF4
7 changed files with 277 additions and 0 deletions

View File

@ -14,6 +14,7 @@
},
'defines': [
'SQLITE_DEFAULT_MEMSTATUS=0',
'SQLITE_ENABLE_COLUMN_METADATA',
'SQLITE_ENABLE_MATH_FUNCTIONS',
'SQLITE_ENABLE_SESSION',
'SQLITE_ENABLE_PREUPDATE_HOOK'

View File

@ -8,6 +8,7 @@ template("sqlite_gn_build") {
config("sqlite_config") {
include_dirs = [ "." ]
defines = [
"SQLITE_ENABLE_COLUMN_METADATA",
"SQLITE_ENABLE_MATH_FUNCTIONS",
"SQLITE_ENABLE_SESSION",
"SQLITE_ENABLE_PREUPDATE_HOOK",

View File

@ -373,6 +373,34 @@ objects. If the prepared statement does not return any results, this method
returns an empty array. The prepared statement [parameters are bound][] using
the values in `namedParameters` and `anonymousParameters`.
### `statement.columns()`
<!-- YAML
added: REPLACEME
-->
* Returns: {Array} An array of objects. Each object corresponds to a column
in the prepared statement, and contains the following properties:
* `column`: {string|null} The unaliased name of the column in the origin
table, or `null` if the column is the result of an expression or subquery.
This property is the result of [`sqlite3_column_origin_name()`][].
* `database`: {string|null} The unaliased name of the origin database, or
`null` if the column is the result of an expression or subquery. This
property is the result of [`sqlite3_column_database_name()`][].
* `name`: {string} The name assigned to the column in the result set of a
`SELECT` statement. This property is the result of
[`sqlite3_column_name()`][].
* `table`: {string|null} The unaliased name of the origin table, or `null` if
the column is the result of an expression or subquery. This property is the
result of [`sqlite3_column_table_name()`][].
* `type`: {string|null} The declared data type of the column, or `null` if the
column is the result of an expression or subquery. This property is the
result of [`sqlite3_column_decltype()`][].
This method is used to retrieve information about the columns returned by the
prepared statement.
### `statement.expandedSQL`
<!-- YAML
@ -687,6 +715,11 @@ resolution handler passed to [`database.applyChangeset()`][]. See also
[`sqlite3_backup_step()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
[`sqlite3_changes64()`]: https://www.sqlite.org/c3ref/changes.html
[`sqlite3_close_v2()`]: https://www.sqlite.org/c3ref/close.html
[`sqlite3_column_database_name()`]: https://www.sqlite.org/c3ref/column_database_name.html
[`sqlite3_column_decltype()`]: https://www.sqlite.org/c3ref/column_decltype.html
[`sqlite3_column_name()`]: https://www.sqlite.org/c3ref/column_name.html
[`sqlite3_column_origin_name()`]: https://www.sqlite.org/c3ref/column_database_name.html
[`sqlite3_column_table_name()`]: https://www.sqlite.org/c3ref/column_database_name.html
[`sqlite3_create_function_v2()`]: https://www.sqlite.org/c3ref/create_function.html
[`sqlite3_exec()`]: https://www.sqlite.org/c3ref/exec.html
[`sqlite3_expanded_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html

View File

@ -118,6 +118,7 @@
V(crypto_rsa_pss_string, "rsa-pss") \
V(cwd_string, "cwd") \
V(data_string, "data") \
V(database_string, "database") \
V(default_is_true_string, "defaultIsTrue") \
V(deserialize_info_string, "deserializeInfo") \
V(dest_string, "dest") \
@ -370,6 +371,7 @@
V(subject_string, "subject") \
V(subjectaltname_string, "subjectaltname") \
V(syscall_string, "syscall") \
V(table_string, "table") \
V(target_string, "target") \
V(thread_id_string, "threadId") \
V(ticketkeycallback_string, "onticketkeycallback") \

View File

@ -158,6 +158,16 @@ inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, int errcode) {
}
}
inline MaybeLocal<Value> NullableSQLiteStringToValue(Isolate* isolate,
const char* str) {
if (str == nullptr) {
return Null(isolate);
}
return String::NewFromUtf8(isolate, str, NewStringType::kInternalized)
.As<Value>();
}
class BackupJob : public ThreadPoolWork {
public:
explicit BackupJob(Environment* env,
@ -1897,6 +1907,72 @@ void StatementSync::Run(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(result);
}
void StatementSync::Columns(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");
int num_cols = sqlite3_column_count(stmt->statement_);
Isolate* isolate = env->isolate();
LocalVector<Value> cols(isolate);
LocalVector<Name> col_keys(isolate,
{env->column_string(),
env->database_string(),
env->name_string(),
env->table_string(),
env->type_string()});
Local<Value> value;
cols.reserve(num_cols);
for (int i = 0; i < num_cols; ++i) {
LocalVector<Value> col_values(isolate);
col_values.reserve(col_keys.size());
if (!NullableSQLiteStringToValue(
isolate, sqlite3_column_origin_name(stmt->statement_, i))
.ToLocal(&value)) {
return;
}
col_values.emplace_back(value);
if (!NullableSQLiteStringToValue(
isolate, sqlite3_column_database_name(stmt->statement_, i))
.ToLocal(&value)) {
return;
}
col_values.emplace_back(value);
if (!stmt->ColumnNameToName(i).ToLocal(&value)) {
return;
}
col_values.emplace_back(value);
if (!NullableSQLiteStringToValue(
isolate, sqlite3_column_table_name(stmt->statement_, i))
.ToLocal(&value)) {
return;
}
col_values.emplace_back(value);
if (!NullableSQLiteStringToValue(
isolate, sqlite3_column_decltype(stmt->statement_, i))
.ToLocal(&value)) {
return;
}
col_values.emplace_back(value);
Local<Object> column = Object::New(isolate,
Null(isolate),
col_keys.data(),
col_values.data(),
col_keys.size());
cols.emplace_back(column);
}
args.GetReturnValue().Set(Array::New(isolate, cols.data(), cols.size()));
}
void StatementSync::SourceSQLGetter(const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
@ -2002,6 +2078,8 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
SetProtoMethod(isolate, tmpl, "all", StatementSync::All);
SetProtoMethod(isolate, tmpl, "get", StatementSync::Get);
SetProtoMethod(isolate, tmpl, "run", StatementSync::Run);
SetProtoMethodNoSideEffect(
isolate, tmpl, "columns", StatementSync::Columns);
SetSideEffectFreeGetter(isolate,
tmpl,
FIXED_ONE_BYTE_STRING(isolate, "sourceSQL"),

View File

@ -116,6 +116,7 @@ class StatementSync : public BaseObject {
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 Columns(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SourceSQLGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ExpandedSQLGetter(
const v8::FunctionCallbackInfo<v8::Value>& args);

View File

@ -0,0 +1,161 @@
'use strict';
require('../common');
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/);
});
});