Skip to content

Commit 4b04c92

Browse files
cjihrigaduh95
authored andcommitted
sqlite: add StatementSync.prototype.columns()
This commit adds a method for retrieving column metadata from a prepared statement. Fixes: #57457 PR-URL: #57490 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Edy Silva <[email protected]>
1 parent 4729649 commit 4b04c92

File tree

7 files changed

+277
-0
lines changed

7 files changed

+277
-0
lines changed

deps/sqlite/sqlite.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
},
1515
'defines': [
1616
'SQLITE_DEFAULT_MEMSTATUS=0',
17+
'SQLITE_ENABLE_COLUMN_METADATA',
1718
'SQLITE_ENABLE_MATH_FUNCTIONS',
1819
'SQLITE_ENABLE_SESSION',
1920
'SQLITE_ENABLE_PREUPDATE_HOOK'

deps/sqlite/unofficial.gni

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ template("sqlite_gn_build") {
88
config("sqlite_config") {
99
include_dirs = [ "." ]
1010
defines = [
11+
"SQLITE_ENABLE_COLUMN_METADATA",
1112
"SQLITE_ENABLE_MATH_FUNCTIONS",
1213
"SQLITE_ENABLE_SESSION",
1314
"SQLITE_ENABLE_PREUPDATE_HOOK",

doc/api/sqlite.md

+33
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,34 @@ objects. If the prepared statement does not return any results, this method
355355
returns an empty array. The prepared statement [parameters are bound][] using
356356
the values in `namedParameters` and `anonymousParameters`.
357357

358+
### `statement.columns()`
359+
360+
<!-- YAML
361+
added: REPLACEME
362+
-->
363+
364+
* Returns: {Array} An array of objects. Each object corresponds to a column
365+
in the prepared statement, and contains the following properties:
366+
367+
* `column`: {string|null} The unaliased name of the column in the origin
368+
table, or `null` if the column is the result of an expression or subquery.
369+
This property is the result of [`sqlite3_column_origin_name()`][].
370+
* `database`: {string|null} The unaliased name of the origin database, or
371+
`null` if the column is the result of an expression or subquery. This
372+
property is the result of [`sqlite3_column_database_name()`][].
373+
* `name`: {string} The name assigned to the column in the result set of a
374+
`SELECT` statement. This property is the result of
375+
[`sqlite3_column_name()`][].
376+
* `table`: {string|null} The unaliased name of the origin table, or `null` if
377+
the column is the result of an expression or subquery. This property is the
378+
result of [`sqlite3_column_table_name()`][].
379+
* `type`: {string|null} The declared data type of the column, or `null` if the
380+
column is the result of an expression or subquery. This property is the
381+
result of [`sqlite3_column_decltype()`][].
382+
383+
This method is used to retrieve information about the columns returned by the
384+
prepared statement.
385+
358386
### `statement.expandedSQL`
359387

360388
<!-- YAML
@@ -659,6 +687,11 @@ resolution handler passed to [`database.applyChangeset()`][]. See also
659687
[`sqlite3_backup_step()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
660688
[`sqlite3_changes64()`]: https://www.sqlite.org/c3ref/changes.html
661689
[`sqlite3_close_v2()`]: https://www.sqlite.org/c3ref/close.html
690+
[`sqlite3_column_database_name()`]: https://www.sqlite.org/c3ref/column_database_name.html
691+
[`sqlite3_column_decltype()`]: https://www.sqlite.org/c3ref/column_decltype.html
692+
[`sqlite3_column_name()`]: https://www.sqlite.org/c3ref/column_name.html
693+
[`sqlite3_column_origin_name()`]: https://www.sqlite.org/c3ref/column_database_name.html
694+
[`sqlite3_column_table_name()`]: https://www.sqlite.org/c3ref/column_database_name.html
662695
[`sqlite3_create_function_v2()`]: https://www.sqlite.org/c3ref/create_function.html
663696
[`sqlite3_exec()`]: https://www.sqlite.org/c3ref/exec.html
664697
[`sqlite3_expanded_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html

src/env_properties.h

+2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
V(crypto_rsa_pss_string, "rsa-pss") \
119119
V(cwd_string, "cwd") \
120120
V(data_string, "data") \
121+
V(database_string, "database") \
121122
V(default_is_true_string, "defaultIsTrue") \
122123
V(deserialize_info_string, "deserializeInfo") \
123124
V(dest_string, "dest") \
@@ -369,6 +370,7 @@
369370
V(subject_string, "subject") \
370371
V(subjectaltname_string, "subjectaltname") \
371372
V(syscall_string, "syscall") \
373+
V(table_string, "table") \
372374
V(target_string, "target") \
373375
V(thread_id_string, "threadId") \
374376
V(ticketkeycallback_string, "onticketkeycallback") \

src/node_sqlite.cc

+78
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,16 @@ inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, int errcode) {
158158
}
159159
}
160160

161+
inline MaybeLocal<Value> NullableSQLiteStringToValue(Isolate* isolate,
162+
const char* str) {
163+
if (str == nullptr) {
164+
return Null(isolate);
165+
}
166+
167+
return String::NewFromUtf8(isolate, str, NewStringType::kInternalized)
168+
.As<Value>();
169+
}
170+
161171
class BackupJob : public ThreadPoolWork {
162172
public:
163173
explicit BackupJob(Environment* env,
@@ -1897,6 +1907,72 @@ void StatementSync::Run(const FunctionCallbackInfo<Value>& args) {
18971907
args.GetReturnValue().Set(result);
18981908
}
18991909

1910+
void StatementSync::Columns(const FunctionCallbackInfo<Value>& args) {
1911+
StatementSync* stmt;
1912+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
1913+
Environment* env = Environment::GetCurrent(args);
1914+
THROW_AND_RETURN_ON_BAD_STATE(
1915+
env, stmt->IsFinalized(), "statement has been finalized");
1916+
int num_cols = sqlite3_column_count(stmt->statement_);
1917+
Isolate* isolate = env->isolate();
1918+
LocalVector<Value> cols(isolate);
1919+
LocalVector<Name> col_keys(isolate,
1920+
{env->column_string(),
1921+
env->database_string(),
1922+
env->name_string(),
1923+
env->table_string(),
1924+
env->type_string()});
1925+
Local<Value> value;
1926+
1927+
cols.reserve(num_cols);
1928+
for (int i = 0; i < num_cols; ++i) {
1929+
LocalVector<Value> col_values(isolate);
1930+
col_values.reserve(col_keys.size());
1931+
1932+
if (!NullableSQLiteStringToValue(
1933+
isolate, sqlite3_column_origin_name(stmt->statement_, i))
1934+
.ToLocal(&value)) {
1935+
return;
1936+
}
1937+
col_values.emplace_back(value);
1938+
1939+
if (!NullableSQLiteStringToValue(
1940+
isolate, sqlite3_column_database_name(stmt->statement_, i))
1941+
.ToLocal(&value)) {
1942+
return;
1943+
}
1944+
col_values.emplace_back(value);
1945+
1946+
if (!stmt->ColumnNameToName(i).ToLocal(&value)) {
1947+
return;
1948+
}
1949+
col_values.emplace_back(value);
1950+
1951+
if (!NullableSQLiteStringToValue(
1952+
isolate, sqlite3_column_table_name(stmt->statement_, i))
1953+
.ToLocal(&value)) {
1954+
return;
1955+
}
1956+
col_values.emplace_back(value);
1957+
1958+
if (!NullableSQLiteStringToValue(
1959+
isolate, sqlite3_column_decltype(stmt->statement_, i))
1960+
.ToLocal(&value)) {
1961+
return;
1962+
}
1963+
col_values.emplace_back(value);
1964+
1965+
Local<Object> column = Object::New(isolate,
1966+
Null(isolate),
1967+
col_keys.data(),
1968+
col_values.data(),
1969+
col_keys.size());
1970+
cols.emplace_back(column);
1971+
}
1972+
1973+
args.GetReturnValue().Set(Array::New(isolate, cols.data(), cols.size()));
1974+
}
1975+
19001976
void StatementSync::SourceSQLGetter(const FunctionCallbackInfo<Value>& args) {
19011977
StatementSync* stmt;
19021978
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
@@ -2002,6 +2078,8 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
20022078
SetProtoMethod(isolate, tmpl, "all", StatementSync::All);
20032079
SetProtoMethod(isolate, tmpl, "get", StatementSync::Get);
20042080
SetProtoMethod(isolate, tmpl, "run", StatementSync::Run);
2081+
SetProtoMethodNoSideEffect(
2082+
isolate, tmpl, "columns", StatementSync::Columns);
20052083
SetSideEffectFreeGetter(isolate,
20062084
tmpl,
20072085
FIXED_ONE_BYTE_STRING(isolate, "sourceSQL"),

src/node_sqlite.h

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class StatementSync : public BaseObject {
116116
static void Iterate(const v8::FunctionCallbackInfo<v8::Value>& args);
117117
static void Get(const v8::FunctionCallbackInfo<v8::Value>& args);
118118
static void Run(const v8::FunctionCallbackInfo<v8::Value>& args);
119+
static void Columns(const v8::FunctionCallbackInfo<v8::Value>& args);
119120
static void SourceSQLGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
120121
static void ExpandedSQLGetter(
121122
const v8::FunctionCallbackInfo<v8::Value>& args);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
'use strict';
2+
require('../common');
3+
const assert = require('node:assert');
4+
const { DatabaseSync } = require('node:sqlite');
5+
const { suite, test } = require('node:test');
6+
7+
suite('StatementSync.prototype.columns()', () => {
8+
test('returns column metadata for core SQLite types', () => {
9+
const db = new DatabaseSync(':memory:');
10+
db.exec(`CREATE TABLE test (
11+
col1 INTEGER,
12+
col2 REAL,
13+
col3 TEXT,
14+
col4 BLOB,
15+
col5 NULL
16+
)`);
17+
const stmt = db.prepare('SELECT col1, col2, col3, col4, col5 FROM test');
18+
assert.deepStrictEqual(stmt.columns(), [
19+
{
20+
__proto__: null,
21+
column: 'col1',
22+
database: 'main',
23+
name: 'col1',
24+
table: 'test',
25+
type: 'INTEGER',
26+
},
27+
{
28+
__proto__: null,
29+
column: 'col2',
30+
database: 'main',
31+
name: 'col2',
32+
table: 'test',
33+
type: 'REAL',
34+
},
35+
{
36+
__proto__: null,
37+
column: 'col3',
38+
database: 'main',
39+
name: 'col3',
40+
table: 'test',
41+
type: 'TEXT',
42+
},
43+
{
44+
__proto__: null,
45+
column: 'col4',
46+
database: 'main',
47+
name: 'col4',
48+
table: 'test',
49+
type: 'BLOB',
50+
},
51+
{
52+
__proto__: null,
53+
column: 'col5',
54+
database: 'main',
55+
name: 'col5',
56+
table: 'test',
57+
type: null,
58+
},
59+
]);
60+
});
61+
62+
test('supports statements using multiple tables', () => {
63+
const db = new DatabaseSync(':memory:');
64+
db.exec(`
65+
CREATE TABLE test1 (value1 INTEGER);
66+
CREATE TABLE test2 (value2 INTEGER);
67+
`);
68+
const stmt = db.prepare('SELECT value1, value2 FROM test1, test2');
69+
assert.deepStrictEqual(stmt.columns(), [
70+
{
71+
__proto__: null,
72+
column: 'value1',
73+
database: 'main',
74+
name: 'value1',
75+
table: 'test1',
76+
type: 'INTEGER',
77+
},
78+
{
79+
__proto__: null,
80+
column: 'value2',
81+
database: 'main',
82+
name: 'value2',
83+
table: 'test2',
84+
type: 'INTEGER',
85+
},
86+
]);
87+
});
88+
89+
test('supports column aliases', () => {
90+
const db = new DatabaseSync(':memory:');
91+
db.exec(`CREATE TABLE test (value INTEGER)`);
92+
const stmt = db.prepare('SELECT value AS foo FROM test');
93+
assert.deepStrictEqual(stmt.columns(), [
94+
{
95+
__proto__: null,
96+
column: 'value',
97+
database: 'main',
98+
name: 'foo',
99+
table: 'test',
100+
type: 'INTEGER',
101+
},
102+
]);
103+
});
104+
105+
test('supports column expressions', () => {
106+
const db = new DatabaseSync(':memory:');
107+
db.exec(`CREATE TABLE test (value INTEGER)`);
108+
const stmt = db.prepare('SELECT value + 1, value FROM test');
109+
assert.deepStrictEqual(stmt.columns(), [
110+
{
111+
__proto__: null,
112+
column: null,
113+
database: null,
114+
name: 'value + 1',
115+
table: null,
116+
type: null,
117+
},
118+
{
119+
__proto__: null,
120+
column: 'value',
121+
database: 'main',
122+
name: 'value',
123+
table: 'test',
124+
type: 'INTEGER',
125+
},
126+
]);
127+
});
128+
129+
test('supports subqueries', () => {
130+
const db = new DatabaseSync(':memory:');
131+
db.exec(`CREATE TABLE test (value INTEGER)`);
132+
const stmt = db.prepare('SELECT * FROM (SELECT * FROM test)');
133+
assert.deepStrictEqual(stmt.columns(), [
134+
{
135+
__proto__: null,
136+
column: 'value',
137+
database: 'main',
138+
name: 'value',
139+
table: 'test',
140+
type: 'INTEGER',
141+
},
142+
]);
143+
});
144+
145+
test('supports statements that do not return data', () => {
146+
const db = new DatabaseSync(':memory:');
147+
db.exec('CREATE TABLE test (value INTEGER)');
148+
const stmt = db.prepare('INSERT INTO test (value) VALUES (?)');
149+
assert.deepStrictEqual(stmt.columns(), []);
150+
});
151+
152+
test('throws if the statement is finalized', () => {
153+
const db = new DatabaseSync(':memory:');
154+
db.exec('CREATE TABLE test (value INTEGER)');
155+
const stmt = db.prepare('SELECT value FROM test');
156+
db.close();
157+
assert.throws(() => {
158+
stmt.columns();
159+
}, /statement has been finalized/);
160+
});
161+
});

0 commit comments

Comments
 (0)