Skip to content

Commit 7e0ee32

Browse files
committed
feat(sqlite3): add execute_json method for JSON output
- Add execute_json method to Cursor class - Update documentation and add new test file - Implement JSON result generation in C extension
1 parent d22a745 commit 7e0ee32

File tree

5 files changed

+329
-1
lines changed

5 files changed

+329
-1
lines changed

Doc/library/sqlite3.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,6 +1510,50 @@ Cursor objects
15101510

15111511
Use :meth:`executescript` to execute multiple SQL statements.
15121512

1513+
.. method:: execute_json(sql, parameters=(), /)
1514+
1515+
Execute a single SQL statement and return the result as a JSON string.
1516+
This method is similar to :meth:`execute` but returns the query results
1517+
directly as a JSON string instead of rows.
1518+
1519+
:param str sql:
1520+
A single SQL statement that returns rows.
1521+
1522+
:param parameters:
1523+
Python values to bind to placeholders in *sql*.
1524+
A :class:`!dict` if named placeholders are used.
1525+
A :term:`!sequence` if unnamed placeholders are used.
1526+
See :ref:`sqlite3-placeholders`.
1527+
:type parameters: :class:`dict` | :term:`sequence`
1528+
1529+
:raises ProgrammingError:
1530+
When *sql* contains more than one SQL statement.
1531+
When :ref:`named placeholders <sqlite3-placeholders>` are used
1532+
and *parameters* is a sequence instead of a :class:`dict`.
1533+
1534+
This method provides significant performance benefits when working with
1535+
JSON data, as it leverages SQLite's built-in JSON functions to generate
1536+
the JSON directly in the database engine rather than fetching rows and
1537+
converting them to JSON in Python.
1538+
1539+
Example:
1540+
1541+
.. testcode:: sqlite3.cursor
1542+
1543+
# Create and populate a table
1544+
cur.execute("CREATE TABLE users(id INTEGER, name TEXT, email TEXT)")
1545+
cur.executemany("INSERT INTO users VALUES(?, ?, ?)", [
1546+
(1, 'Alice', '[email protected]'),
1547+
(2, 'Bob', '[email protected]')
1548+
])
1549+
1550+
# Get results as JSON
1551+
json_result = cur.execute_json("SELECT * FROM users")
1552+
print(json_result)
1553+
# Output: [{"id":1,"name":"Alice","email":"[email protected]"},{"id":2,"name":"Bob","email":"[email protected]"}]
1554+
1555+
.. versionadded:: 3.15
1556+
15131557
.. method:: executemany(sql, parameters, /)
15141558

15151559
For every item in *parameters*,
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# pysqlite2/test/execute_json.py: tests for execute_json method
2+
#
3+
# Copyright (C) 2025 Python Software Foundation
4+
#
5+
# This file is part of pysqlite.
6+
#
7+
# This software is provided 'as-is', without any express or implied
8+
# warranty. In no event will the authors be held liable for any damages
9+
# arising from the use of this software.
10+
#
11+
# Permission is granted to anyone to use this software for any purpose,
12+
# including commercial applications, and to alter it and redistribute it
13+
# freely, subject to the following restrictions:
14+
#
15+
# 1. The origin of this software must not be misrepresented; you must not
16+
# claim that you wrote the original software. If you use this software
17+
# in a product, an acknowledgment in the product documentation would be
18+
# appreciated but is not required.
19+
# 2. Altered source versions must be plainly marked as such, and must not be
20+
# misrepresented as being the original software.
21+
# 3. This notice may not be removed or altered from any source distribution.
22+
23+
import json
24+
import sqlite3
25+
import unittest
26+
27+
28+
29+
class ExecuteJsonTests(unittest.TestCase):
30+
def setUp(self):
31+
self.cx = sqlite3.connect(":memory:")
32+
self.cu = self.cx.cursor()
33+
self.cu.execute(
34+
"create table test(id integer primary key, name text, "
35+
"income number, unique_test text unique)"
36+
)
37+
self.cu.execute("insert into test(name, income) values (?, ?)", ("foo", 100))
38+
self.cu.execute("insert into test(name, income) values (?, ?)", ("bar", 200))
39+
self.cu.execute("insert into test(name, income) values (?, ?)", ("baz", 300))
40+
41+
def tearDown(self):
42+
self.cu.close()
43+
self.cx.close()
44+
45+
def test_execute_json_basic(self):
46+
# Test basic functionality of execute_json
47+
result = self.cu.execute_json("select * from test")
48+
data = json.loads(result)
49+
self.assertIsInstance(data, list)
50+
self.assertEqual(len(data), 3)
51+
self.assertEqual(data[0]["name"], "foo")
52+
self.assertEqual(data[0]["income"], 100)
53+
self.assertEqual(data[1]["name"], "bar")
54+
self.assertEqual(data[1]["income"], 200)
55+
self.assertEqual(data[2]["name"], "baz")
56+
self.assertEqual(data[2]["income"], 300)
57+
58+
def test_execute_json_empty_result(self):
59+
# Test execute_json with empty result set
60+
result = self.cu.execute_json("select * from test where id > 1000")
61+
data = json.loads(result)
62+
self.assertIsInstance(data, list)
63+
self.assertEqual(len(data), 0)
64+
65+
def test_execute_json_with_parameters(self):
66+
# Test execute_json with parameterized queries
67+
result = self.cu.execute_json("select * from test where income > ?", (150,))
68+
data = json.loads(result)
69+
self.assertIsInstance(data, list)
70+
self.assertEqual(len(data), 2)
71+
self.assertEqual(data[0]["name"], "bar")
72+
self.assertEqual(data[0]["income"], 200)
73+
self.assertEqual(data[1]["name"], "baz")
74+
self.assertEqual(data[1]["income"], 300)
75+
76+
def test_execute_json_with_named_parameters(self):
77+
# Test execute_json with named parameters
78+
result = self.cu.execute_json("select * from test where income > :min_income",
79+
{"min_income": 150})
80+
data = json.loads(result)
81+
self.assertIsInstance(data, list)
82+
self.assertEqual(len(data), 2)
83+
self.assertEqual(data[0]["name"], "bar")
84+
self.assertEqual(data[0]["income"], 200)
85+
self.assertEqual(data[1]["name"], "baz")
86+
self.assertEqual(data[1]["income"], 300)
87+
88+
def test_execute_json_invalid_sql(self):
89+
# Test execute_json with invalid SQL
90+
with self.assertRaises(sqlite3.OperationalError):
91+
self.cu.execute_json("select asdf")
92+
93+
def test_execute_json_non_select(self):
94+
# Test execute_json with non-SELECT statement
95+
result = self.cu.execute_json("insert into test(name, income) values (?, ?)",
96+
("new_entry", 400))
97+
data = json.loads(result)
98+
self.assertIsInstance(data, list)
99+
self.assertEqual(len(data), 0)
100+
101+
102+
if __name__ == "__main__":
103+
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
JSON (output) support for sqlite3

Modules/_sqlite/clinic/cursor.c.h

Lines changed: 40 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_sqlite/cursor.c

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,146 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation
977977
return Py_NewRef((PyObject *)self);
978978
}
979979

980+
/*[clinic input]
981+
_sqlite3.Cursor.execute_json as pysqlite_cursor_execute_json
982+
983+
sql: unicode
984+
parameters: object(c_default = 'NULL') = ()
985+
/
986+
987+
Executes an SQL statement and returns the result as a JSON string.
988+
[clinic start generated code]*/
989+
990+
static PyObject *
991+
pysqlite_cursor_execute_json_impl(pysqlite_Cursor *self, PyObject *sql,
992+
PyObject *parameters)
993+
/*[clinic end generated code: output=192c296377bf8175 input=3c6c8e94ef0b53c7]*/
994+
{
995+
// First execute the query normally to get column information
996+
PyObject *result = _pysqlite_query_execute(self, 0, sql, parameters);
997+
if (!result) {
998+
return NULL;
999+
}
1000+
Py_DECREF(result);
1001+
1002+
// Check if we have a statement with results
1003+
if (!self->statement || sqlite3_column_count(self->statement->st) == 0) {
1004+
// No results to convert to JSON, return empty JSON array
1005+
return PyUnicode_FromString("[]");
1006+
}
1007+
1008+
// Build a JSON query that wraps the original query
1009+
sqlite3_stmt *stmt = self->statement->st;
1010+
int numcols = sqlite3_column_count(stmt);
1011+
1012+
// Build the json_object parameters
1013+
PyObject *column_list = PyList_New(0);
1014+
if (!column_list) {
1015+
return NULL;
1016+
}
1017+
1018+
for (int i = 0; i < numcols; i++) {
1019+
const char *colname = sqlite3_column_name(stmt, i);
1020+
if (!colname) {
1021+
Py_DECREF(column_list);
1022+
return PyErr_NoMemory();
1023+
}
1024+
1025+
// Add column name as string
1026+
PyObject *colname_obj = PyUnicode_FromString(colname);
1027+
if (!colname_obj) {
1028+
Py_DECREF(column_list);
1029+
return NULL;
1030+
}
1031+
1032+
if (PyList_Append(column_list, colname_obj) < 0) {
1033+
Py_DECREF(colname_obj);
1034+
Py_DECREF(column_list);
1035+
return NULL;
1036+
}
1037+
Py_DECREF(colname_obj);
1038+
1039+
// Add column reference
1040+
PyObject *colref_obj = PyUnicode_FromFormat("row.%s", colname);
1041+
if (!colref_obj) {
1042+
Py_DECREF(column_list);
1043+
return NULL;
1044+
}
1045+
1046+
if (PyList_Append(column_list, colref_obj) < 0) {
1047+
Py_DECREF(colref_obj);
1048+
Py_DECREF(column_list);
1049+
return NULL;
1050+
}
1051+
Py_DECREF(colref_obj);
1052+
}
1053+
1054+
// Join the column list with commas
1055+
PyObject *comma = PyUnicode_FromString(",");
1056+
if (!comma) {
1057+
Py_DECREF(column_list);
1058+
return NULL;
1059+
}
1060+
1061+
PyObject *column_str = PyUnicode_Join(comma, column_list);
1062+
Py_DECREF(comma);
1063+
if (!column_str) {
1064+
Py_DECREF(column_list);
1065+
return NULL;
1066+
}
1067+
1068+
// Complete the JSON query
1069+
PyObject *full_query = PyUnicode_FromFormat("SELECT json_group_array(json_object(%S)) FROM (%U) AS row", column_str, sql);
1070+
Py_DECREF(column_str);
1071+
Py_DECREF(column_list);
1072+
if (!full_query) {
1073+
return NULL;
1074+
}
1075+
1076+
// Execute the JSON query
1077+
pysqlite_Statement *json_stmt = pysqlite_statement_create(self->connection, full_query);
1078+
Py_DECREF(full_query);
1079+
if (!json_stmt) {
1080+
return NULL;
1081+
}
1082+
1083+
// Bind parameters if needed
1084+
if (parameters != NULL && parameters != Py_None) {
1085+
bind_parameters(self->connection->state, json_stmt, parameters);
1086+
if (PyErr_Occurred()) {
1087+
Py_DECREF(json_stmt);
1088+
return NULL;
1089+
}
1090+
}
1091+
1092+
// Execute the statement
1093+
int rc = stmt_step(json_stmt->st);
1094+
if (rc != SQLITE_ROW) {
1095+
Py_DECREF(json_stmt);
1096+
if (rc == SQLITE_DONE) {
1097+
// No rows returned, return empty JSON array
1098+
return PyUnicode_FromString("[]");
1099+
} else {
1100+
// Error occurred
1101+
set_error_from_db(self->connection->state, self->connection->db);
1102+
return NULL;
1103+
}
1104+
}
1105+
1106+
// Get the JSON result
1107+
const char *json_result = (const char*)sqlite3_column_text(json_stmt->st, 0);
1108+
PyObject *result_str = NULL;
1109+
if (json_result) {
1110+
result_str = PyUnicode_FromString(json_result);
1111+
} else {
1112+
// NULL result, return empty JSON array
1113+
result_str = PyUnicode_FromString("[]");
1114+
}
1115+
1116+
Py_DECREF(json_stmt);
1117+
return result_str;
1118+
}
1119+
9801120
/*[clinic input]
9811121
_sqlite3.Cursor.execute as pysqlite_cursor_execute
9821122
@@ -1303,6 +1443,7 @@ pysqlite_cursor_close_impl(pysqlite_Cursor *self)
13031443

13041444
static PyMethodDef cursor_methods[] = {
13051445
PYSQLITE_CURSOR_CLOSE_METHODDEF
1446+
PYSQLITE_CURSOR_EXECUTE_JSON_METHODDEF
13061447
PYSQLITE_CURSOR_EXECUTEMANY_METHODDEF
13071448
PYSQLITE_CURSOR_EXECUTESCRIPT_METHODDEF
13081449
PYSQLITE_CURSOR_EXECUTE_METHODDEF

0 commit comments

Comments
 (0)