Skip to content

Commit c510391

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 Reviewed-By: Yagiz Nizipli Reviewed-By: Edy Silva
1 parent 4e24456 commit c510391

File tree

7 files changed

+277
-0
lines changed

7 files changed

+277
-0
lines changed

deps/sqlite/sqlite.gyp

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,34 @@ objects. If the prepared statement does not return any results, this method
383383
returns an empty array. The prepared statement [parameters are bound][] using
384384
the values in `namedParameters` and `anonymousParameters`.
385385

386+
### `statement.columns()`
387+
388+
391+
392+
* Returns: {Array} An array of objects. Each object corresponds to a column
393+
in the prepared statement, and contains the following properties:
394+
395+
* `column`: {string|null} The unaliased name of the column in the origin
396+
table, or `null` if the column is the result of an expression or subquery.
397+
This property is the result of [`sqlite3_column_origin_name()`][].
398+
* `database`: {string|null} The unaliased name of the origin database, or
399+
`null` if the column is the result of an expression or subquery. This
400+
property is the result of [`sqlite3_column_database_name()`][].
401+
* `name`: {string} The name assigned to the column in the result set of a
402+
`SELECT` statement. This property is the result of
403+
[`sqlite3_column_name()`][].
404+
* `table`: {string|null} The unaliased name of the origin table, or `null` if
405+
the column is the result of an expression or subquery. This property is the
406+
result of [`sqlite3_column_table_name()`][].
407+
* `type`: {string|null} The declared data type of the column, or `null` if the
408+
column is the result of an expression or subquery. This property is the
409+
result of [`sqlite3_column_decltype()`][].
410+
411+
This method is used to retrieve information about the columns returned by the
412+
prepared statement.
413+
386414
### `statement.expandedSQL`
387415

388416
src/env_properties.h
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
V(crypto_rsa_pss_string, "rsa-pss") \
118118
V(cwd_string, "cwd") \
119119
V(data_string, "data") \
120+
V(database_string, "database") \
120121
V(default_is_true_string, "defaultIsTrue") \
121122
V(deserialize_info_string, "deserializeInfo") \
122123
V(dest_string, "dest") \
@@ -362,6 +363,7 @@
362363
V(subject_string, "subject") \
363364
V(subjectaltname_string, "subjectaltname") \
364365
V(syscall_string, "syscall") \
366+
V(table_string, "table") \
365367
V(target_string, "target") \
366368
V(thread_id_string, "threadId") \
367369
V(ticketkeycallback_string, "onticketkeycallback") \

src/node_sqlite.cc

Lines changed: 78 additions & 0 deletions
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 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();
169+
}
170+
161171
class BackupJob : public ThreadPoolWork {
162172
public:
163173
explicit BackupJob(Environment* env,
@@ -1918,6 +1928,72 @@ void StatementSync::Run(const FunctionCallbackInfo& args) {
19181928
args.GetReturnValue().Set(result);
19191929
}
19201930

1931+
void StatementSync::Columns(const FunctionCallbackInfo& args) {
1932+
StatementSync* stmt;
1933+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
1934+
Environment* env = Environment::GetCurrent(args);
1935+
THROW_AND_RETURN_ON_BAD_STATE(
1936+
env, stmt->IsFinalized(), "statement has been finalized");
1937+
int num_cols = sqlite3_column_count(stmt->statement_);
1938+
Isolate* isolate = env->isolate();
1939+
LocalVector cols(isolate);
1940+
LocalVector col_keys(isolate,
1941+
{env->column_string(),
1942+
env->database_string(),
1943+
env->name_string(),
1944+
env->table_string(),
1945+
env->type_string()});
1946+
Local value;
1947+
1948+
cols.reserve(num_cols);
1949+
for (int i = 0; i < num_cols; ++i) {
1950+
LocalVector col_values(isolate);
1951+
col_values.reserve(col_keys.size());
1952+
1953+
if (!NullableSQLiteStringToValue(
1954+
isolate, sqlite3_column_origin_name(stmt->statement_, i))
1955+
.ToLocal(&value)) {
1956+
return;
1957+
}
1958+
col_values.emplace_back(value);
1959+
1960+
if (!NullableSQLiteStringToValue(
1961+
isolate, sqlite3_column_database_name(stmt->statement_, i))
1962+
.ToLocal(&value)) {
1963+
return;
1964+
}
1965+
col_values.emplace_back(value);
1966+
1967+
if (!stmt->ColumnNameToName(i).ToLocal(&value)) {
1968+
return;
1969+
}
1970+
col_values.emplace_back(value);
1971+
1972+
if (!NullableSQLiteStringToValue(
1973+
isolate, sqlite3_column_table_name(stmt->statement_, i))
1974+
.ToLocal(&value)) {
1975+
return;
1976+
}
1977+
col_values.emplace_back(value);
1978+
1979+
if (!NullableSQLiteStringToValue(
1980+
isolate, sqlite3_column_decltype(stmt->statement_, i))
1981+
.ToLocal(&value)) {
1982+
return;
1983+
}
1984+
col_values.emplace_back(value);
1985+
1986+
Local column = Object::New(isolate,
1987+
Null(isolate),
1988+
col_keys.data(),
1989+
col_values.data(),
1990+
col_keys.size());
1991+
cols.emplace_back(column);
1992+
}
1993+
1994+
args.GetReturnValue().Set(Array::New(isolate, cols.data(), cols.size()));
1995+
}
1996+
19211997
void StatementSync::SourceSQLGetter(const FunctionCallbackInfo& args) {
19221998
StatementSync* stmt;
19231999
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
@@ -2040,6 +2116,8 @@ Local StatementSync::GetConstructorTemplate(
20402116
SetProtoMethod(isolate, tmpl, "all", StatementSync::All);
20412117
SetProtoMethod(isolate, tmpl, "get", StatementSync::Get);
20422118
SetProtoMethod(isolate, tmpl, "run", StatementSync::Run);
2119+
SetProtoMethodNoSideEffect(
2120+
isolate, tmpl, "columns", StatementSync::Columns);
20432121
SetSideEffectFreeGetter(isolate,
20442122
tmpl,
20452123
FIXED_ONE_BYTE_STRING(isolate, "sourceSQL"),

src/node_sqlite.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class StatementSync : public BaseObject {
119119
static void Iterate(const v8::FunctionCallbackInfo& args);
120120
static void Get(const v8::FunctionCallbackInfo& args);
121121
static void Run(const v8::FunctionCallbackInfo& args);
122+
static void Columns(const v8::FunctionCallbackInfo& args);
122123
static void SourceSQLGetter(const v8::FunctionCallbackInfo& args);
123124
static void ExpandedSQLGetter(
124125
const v8::FunctionCallbackInfo& args);
Lines changed: 161 additions & 0 deletions
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)