diff --git a/Doc/includes/sqlite3/blob.py b/Doc/includes/sqlite3/blob.py new file mode 100644 index 00000000000000..61994fb82dd72a --- /dev/null +++ b/Doc/includes/sqlite3/blob.py @@ -0,0 +1,12 @@ +import sqlite3 + +con = sqlite3.connect(":memory:") +con.execute("create table test(blob_col blob)") +con.execute("insert into test(blob_col) values (zeroblob(10))") + +blob = con.blobopen("test", "blob_col", 1) +blob.write(b"Hello") +blob.write(b"World") +blob.seek(0) +print(blob.read()) # will print b"HelloWorld" +blob.close() diff --git a/Doc/includes/sqlite3/blob_with.py b/Doc/includes/sqlite3/blob_with.py new file mode 100644 index 00000000000000..d489bd632d867d --- /dev/null +++ b/Doc/includes/sqlite3/blob_with.py @@ -0,0 +1,12 @@ +import sqlite3 + +con = sqlite3.connect(":memory:") + +con.execute("create table test(blob_col blob)") +con.execute("insert into test(blob_col) values (zeroblob(10))") + +with con.blobopen("test", "blob_col", 1) as blob: + blob.write(b"Hello") + blob.write(b"World") + blob.seek(0) + print(blob.read()) # will print b"HelloWorld" diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index d213933ba5827f..8c860e15a618b4 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -394,6 +394,20 @@ Connection Objects supplied, this must be a callable returning an instance of :class:`Cursor` or its subclasses. + .. method:: blobopen(table, column, row, /, *, readonly=False, name="main") + + On success, a :class:`Blob` handle to the :abbr:`BLOB (Binary Large + OBject)` located in row *row*, column *column*, table *table* in database + *name* will be returned. When *readonly* is :const:`True` the blob is + opened without write permissions. + + .. note:: + + The BLOB size cannot be changed using the :class:`Blob` class. Use the + SQL function ``zeroblob`` to create a blob with a fixed size. + + .. versionadded:: 3.11 + .. method:: commit() This method commits the current transaction. If you don't call this method, @@ -1021,6 +1035,65 @@ Exceptions transactions turned off. It is a subclass of :exc:`DatabaseError`. +.. _sqlite3-blob-objects: + +Blob Objects +------------ + +.. versionadded:: 3.11 + +.. class:: Blob + + A :class:`Blob` instance can read and write the data in a :abbr:`BLOB + (Binary Large OBject)`. The :class:`Blob` class implements the file and + mapping protocols. + + .. method:: Blob.close() + + Close the BLOB. + + The BLOB will be unusable from this point forward. An + :class:`~sqlite3.Error` (or subclass) exception will be raised if any + further operation is attempted with the BLOB. + + .. method:: Blob.__len__() + + Return the BLOB size as length in bytes. + + .. method:: Blob.read(length=-1, /) + + Read *length* bytes of data from the BLOB at the current offset position. + If the end of the BLOB is reached we will return the data up to end of + file. When *size* is not specified or is negative, :meth:`~Blob.read` + will read till the end of the BLOB. + + .. method:: Blob.write(data, /) + + Write *data* to the BLOB at the current offset. This function cannot + change the BLOB length. Writing beyond the end of the blob will result in + an exception being raised. + + .. method:: Blob.tell() + + Return the current access position of the BLOB. + + .. method:: Blob.seek(offset, origin=sqlite3.BLOB_SEEK_START, /) + + Set the current access position of the BLOB to *offset*. The *origin* + argument defaults to :data:`os.SEEK_SET` (absolute BLOB positioning). + Other values for *origin* are :data:`os.SEEK_CUR` (seek relative to the + current position) and :data:`os.SEEK_END` (seek relative to the BLOB’s + end). + + :class:`Blob` example: + + .. literalinclude:: ../includes/sqlite3/blob.py + + A :class:`Blob` can also be used as a :term:`context manager`: + + .. literalinclude:: ../includes/sqlite3/blob_with.py + + .. _sqlite3-types: SQLite and Python types diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 5563e3d84de6d1..01f03bfbdf4011 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -301,6 +301,10 @@ sqlite3 Instead we leave it to the SQLite library to handle these cases. (Contributed by Erlend E. Aasland in :issue:`44092`.) +* Add :meth:`~sqlite3.Connection.blobopen` to :class:`sqlite3.Connection`. + :class:`sqlite3.Blob` allows incremental I/O operations on blobs. + (Contributed by Aviv Palivoda and Erlend E. Aasland in :issue:`24905`) + sys --- diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 4eb4e180bf117e..b25e6b125f9095 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -20,7 +20,9 @@ # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. +import _testcapi import contextlib +import os import sqlite3 as sqlite import subprocess import sys @@ -989,11 +991,284 @@ def test_same_query_in_multiple_cursors(self): self.assertEqual(cu.fetchall(), [(1,)]) +class BlobTests(unittest.TestCase): + def setUp(self): + self.cx = sqlite.connect(":memory:") + self.cx.execute("create table test(b blob)") + self.data = b"this blob data string is exactly fifty bytes long!" + self.cx.execute("insert into test(b) values (?)", (self.data,)) + self.blob = self.cx.blobopen("test", "b", 1) + + def tearDown(self): + self.blob.close() + self.cx.close() + + def test_blob_length(self): + self.assertEqual(len(self.blob), 50) + + def test_blob_seek_and_tell(self): + self.blob.seek(10) + self.assertEqual(self.blob.tell(), 10) + + self.blob.seek(10, os.SEEK_SET) + self.assertEqual(self.blob.tell(), 10) + + self.blob.seek(10, os.SEEK_CUR) + self.assertEqual(self.blob.tell(), 20) + + self.blob.seek(-10, os.SEEK_END) + self.assertEqual(self.blob.tell(), 40) + + def test_blob_seek_error(self): + msg_oor = "offset out of blob range" + msg_orig = "'origin' should be 0, 1, or 2" + msg_of = "seek offset result in overflow" + + dataset = ( + (ValueError, msg_oor, lambda: self.blob.seek(1000)), + (ValueError, msg_oor, lambda: self.blob.seek(-10)), + (ValueError, msg_orig, lambda: self.blob.seek(10, -1)), + (ValueError, msg_orig, lambda: self.blob.seek(10, 3)), + ) + for exc, msg, fn in dataset: + with self.subTest(exc=exc, msg=msg, fn=fn): + self.assertRaisesRegex(exc, msg, fn) + + n = len(self.data) // 2 + self.blob.seek(n, os.SEEK_SET) + with self.assertRaisesRegex(OverflowError, msg_of): + self.blob.seek(_testcapi.INT_MAX, os.SEEK_CUR) + with self.assertRaisesRegex(OverflowError, msg_of): + self.blob.seek( _testcapi.INT_MAX, os.SEEK_END) + + def test_blob_read(self): + buf = self.blob.read() + self.assertEqual(buf, self.data) + self.assertEqual(len(buf), len(self.data)) + + def test_blob_read_too_much(self): + buf = self.blob.read(len(self.data) * 2) + self.assertEqual(buf, self.data) + self.assertEqual(len(buf), len(self.data)) + + def test_blob_read_advance_offset(self): + n = 10 + buf = self.blob.read(n) + self.assertEqual(buf, self.data[:n]) + self.assertEqual(self.blob.tell(), n) + + def test_blob_read_start_at_offset(self): + new_data = b"b" * 50 + self.blob.seek(10) + self.blob.write(new_data[:10]) + self.blob.seek(10) + self.assertEqual(self.blob.read(10), new_data[:10]) + + def test_blob_read_after_row_change(self): + self.cx.execute("update test set b='aaaa' where rowid=1") + with self.assertRaises(sqlite.OperationalError): + self.blob.read() + + def test_blob_write(self): + new_data = b"new data".ljust(50) + self.blob.write(new_data) + row = self.cx.execute("select b from test").fetchone() + self.assertEqual(row[0], new_data) + + def test_blob_write_at_offset(self): + new_data = b"c" * 50 + self.blob.seek(25) + self.blob.write(new_data[:25]) + row = self.cx.execute("select b from test").fetchone() + self.assertEqual(row[0], self.data[:25] + new_data[:25]) + + def test_blob_write_advance_offset(self): + new_data = b"d" * 50 + self.blob.write(new_data[:25]) + self.assertEqual(self.blob.tell(), 25) + + def test_blob_write_error_length(self): + with self.assertRaisesRegex(ValueError, "data longer than blob"): + self.blob.write(b"a" * 1000) + + def test_blob_write_error_row_changed(self): + self.cx.execute("update test set b='aaaa' where rowid=1") + with self.assertRaises(sqlite.OperationalError): + self.blob.write(b"aaa") + + def test_blob_write_error_readonly(self): + ro_blob = self.cx.blobopen("test", "b", 1, readonly=True) + with self.assertRaisesRegex(sqlite.OperationalError, "readonly"): + ro_blob.write(b"aaa") + ro_blob.close() + + def test_blob_open_error(self): + dataset = ( + (("test", "b", 1), {"name": "notexisting"}), + (("notexisting", "b", 1), {}), + (("test", "notexisting", 1), {}), + (("test", "b", 2), {}), + ) + regex = "no such" + for args, kwds in dataset: + with self.subTest(args=args, kwds=kwds): + with self.assertRaisesRegex(sqlite.OperationalError, regex): + self.cx.blobopen(*args, **kwds) + + def test_blob_get_item(self): + self.assertEqual(self.blob[5], b"b") + self.assertEqual(self.blob[6], b"l") + self.assertEqual(self.blob[7], b"o") + self.assertEqual(self.blob[8], b"b") + self.assertEqual(self.blob[-1], b"!") + + def test_blob_get_item_error(self): + dataset = ( + (b"", TypeError, "Blob indices must be integers"), + (105, IndexError, "Blob index out of range"), + (-105, IndexError, "Blob index out of range"), + (_testcapi.ULLONG_MAX, IndexError, "cannot fit 'int'"), + (len(self.blob), IndexError, "Blob index out of range"), + ) + for idx, exc, regex in dataset: + with self.subTest(idx=idx, exc=exc, regex=regex): + with self.assertRaisesRegex(exc, regex): + self.blob[idx] + + def test_blob_get_slice(self): + self.assertEqual(self.blob[5:14], b"blob data") + + def test_blob_get_slice_negative_index(self): + self.assertEqual(self.blob[5:-5], self.data[5:-5]) + + def test_blob_get_slice_invalid_index(self): + with self.assertRaisesRegex(TypeError, "indices must be integers"): + self.blob[5:b"a"] + + def test_blob_get_slice_with_skip(self): + self.assertEqual(self.blob[0:10:2], b"ti lb") + + def test_blob_set_item(self): + self.blob[0] = b"b" + expected = b"b" + self.data[1:] + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, expected) + + def test_blob_set_item(self): + self.blob[-1] = b"z" + self.assertEqual(self.blob[-1], b"z") + + def test_blob_set_item_error(self): + with self.assertRaisesRegex(TypeError, "indices must be integers"): + self.blob["a"] = b"b" + with self.assertRaisesRegex(ValueError, "must be a single byte"): + self.blob[0] = b"abc" + with self.assertRaisesRegex(TypeError, "doesn't support.*deletion"): + del self.blob[0] + with self.assertRaisesRegex(IndexError, "Blob index out of range"): + self.blob[1000] = b"a" + + def test_blob_set_slice(self): + self.blob[0:5] = b"bbbbb" + expected = b"bbbbb" + self.data[5:] + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, expected) + + def test_blob_set_empty_slice(self): + self.blob[0:0] = b"" + self.assertEqual(self.blob[:], self.data) + + def test_blob_set_slice_with_skip(self): + self.blob[0:10:2] = b"bbbbb" + actual = self.cx.execute("select b from test").fetchone()[0] + expected = b"bhbsbbbob " + self.data[10:] + self.assertEqual(actual, expected) + + def test_blob_get_empty_slice(self): + self.assertEqual(self.blob[5:5], b"") + + def test_blob_set_slice_error(self): + with self.assertRaises(IndexError): + self.blob[5:10] = b"a" + with self.assertRaises(IndexError): + self.blob[5:10] = b"a" * 1000 + with self.assertRaises(TypeError): + del self.blob[5:10] + with self.assertRaises(BufferError): + self.blob[5:10] = memoryview(b"abcde")[::2] + with self.assertRaises(ValueError): + self.blob[5:10:0] = b"12345" + + def test_blob_sequence_not_supported(self): + ops = ( + lambda: self.blob + self.blob, + lambda: self.blob * 5, + lambda: b"a" in self.blob, + ) + for op in ops: + with self.subTest(op=op): + self.assertRaises(TypeError, op) + + def test_blob_context_manager(self): + data = b"a" * 50 + with self.cx.blobopen("test", "b", 1) as blob: + blob.write(data) + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, data) + + def test_blob_closed(self): + with memory_database() as cx: + cx.execute("create table test(b blob)") + cx.execute("insert into test values (zeroblob(100))") + blob = cx.blobopen("test", "b", 1) + blob.close() + + def assign(): blob[0] = b"" + ops = [ + lambda: blob.read(), + lambda: blob.write(b""), + lambda: blob.seek(0), + lambda: blob.tell(), + lambda: blob.__enter__(), + lambda: blob.__exit__(None, None, None), + lambda: len(blob), + lambda: blob[0], + lambda: blob[0:1], + assign, + ] + msg = "Cannot operate on a closed blob" + for op in ops: + with self.subTest(op=op): + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + op() + + def test_blob_close_bad_connection(self): + with memory_database() as cx: + cx.execute("create table test(b blob)") + cx.execute("insert into test values(zeroblob(1))") + blob = cx.blobopen("test", "b", 1) + cx.close() + self.assertRaisesRegex(sqlite.ProgrammingError, + "Cannot operate on a closed database", + blob.close) + + def test_closed_blob_read(self): + with memory_database() as cx: + cx.execute("create table test(b blob)") + cx.execute("insert into test(b) values (zeroblob(100))") + blob = cx.blobopen("test", "b", 1) + cx.close() + self.assertRaisesRegex(sqlite.ProgrammingError, + "Cannot operate on a closed database", + blob.read) + + class ThreadTests(unittest.TestCase): def setUp(self): self.con = sqlite.connect(":memory:") self.cur = self.con.cursor() - self.cur.execute("create table test(name text)") + self.cur.execute("create table test(name text, b blob)") + self.cur.execute("insert into test values('blob', zeroblob(1))") def tearDown(self): self.cur.close() @@ -1028,6 +1303,7 @@ def test_check_connection_thread(self): lambda: self.con.create_collation("foo", None), lambda: self.con.setlimit(sqlite.SQLITE_LIMIT_LENGTH, -1), lambda: self.con.getlimit(sqlite.SQLITE_LIMIT_LENGTH), + lambda: self.con.blobopen("test", "b", 1), ] for fn in fns: with self.subTest(fn=fn): diff --git a/Misc/NEWS.d/next/Library/2018-04-18-16-15-55.bpo-24905.jYqjYx.rst b/Misc/NEWS.d/next/Library/2018-04-18-16-15-55.bpo-24905.jYqjYx.rst new file mode 100644 index 00000000000000..0a57f90c12378f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-04-18-16-15-55.bpo-24905.jYqjYx.rst @@ -0,0 +1,3 @@ +Add :meth:`~sqlite3.Connection.blobopen` to :class:`sqlite3.Connection`. +:class:`sqlite3.Blob` allows incremental I/O operations on blobs. +Patch by Aviv Palivoda and Erlend E. Aasland. diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c new file mode 100644 index 00000000000000..5fa4cd3f98b63d --- /dev/null +++ b/Modules/_sqlite/blob.c @@ -0,0 +1,570 @@ +#include "blob.h" +#include "util.h" + +#define clinic_state() (pysqlite_get_state_by_type(Py_TYPE(self))) +#include "clinic/blob.c.h" +#undef clinic_state + +/*[clinic input] +module _sqlite3 +class _sqlite3.Blob "pysqlite_Blob *" "clinic_state()->BlobType" +[clinic start generated code]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=908d3e16a45f8da7]*/ + +static void +close_blob(pysqlite_Blob *self) +{ + if (self->blob) { + sqlite3_blob *blob = self->blob; + self->blob = NULL; + + Py_BEGIN_ALLOW_THREADS + sqlite3_blob_close(blob); + Py_END_ALLOW_THREADS + } +} + +static int +blob_traverse(pysqlite_Blob *self, visitproc visit, void *arg) +{ + Py_VISIT(Py_TYPE(self)); + Py_VISIT(self->connection); + return 0; +} + +static int +blob_clear(pysqlite_Blob *self) +{ + Py_CLEAR(self->connection); + return 0; +} + +static void +blob_dealloc(pysqlite_Blob *self) +{ + PyTypeObject *tp = Py_TYPE(self); + PyObject_GC_UnTrack(self); + + close_blob(self); + + if (self->in_weakreflist != NULL) { + PyObject_ClearWeakRefs((PyObject*)self); + } + tp->tp_clear((PyObject *)self); + tp->tp_free(self); + Py_DECREF(tp); +} + +// Return 1 if the blob object is usable, 0 if not. +static int +check_blob(pysqlite_Blob *self) +{ + if (!pysqlite_check_connection(self->connection) || + !pysqlite_check_thread(self->connection)) { + return 0; + } + if (self->blob == NULL) { + pysqlite_state *state = self->connection->state; + PyErr_SetString(state->ProgrammingError, + "Cannot operate on a closed blob."); + return 0; + } + return 1; +} + + +/*[clinic input] +_sqlite3.Blob.close as blob_close + +Close blob. +[clinic start generated code]*/ + +static PyObject * +blob_close_impl(pysqlite_Blob *self) +/*[clinic end generated code: output=848accc20a138d1b input=56c86df5cab22490]*/ +{ + if (!pysqlite_check_connection(self->connection) || + !pysqlite_check_thread(self->connection)) + { + return NULL; + } + close_blob(self); + Py_RETURN_NONE; +}; + +void +pysqlite_close_all_blobs(pysqlite_Connection *self) +{ + for (int i = 0; i < PyList_GET_SIZE(self->blobs); i++) { + PyObject *weakref = PyList_GET_ITEM(self->blobs, i); + PyObject *blob = PyWeakref_GetObject(weakref); + if (!Py_IsNone(blob)) { + close_blob((pysqlite_Blob *)blob); + } + } +} + +static Py_ssize_t +blob_length(pysqlite_Blob *self) +{ + if (!check_blob(self)) { + return -1; + } + return sqlite3_blob_bytes(self->blob); +}; + +static void +blob_seterror(pysqlite_Blob *self, int rc) +{ + assert(self->connection != NULL); +#if SQLITE_VERSION_NUMBER < 3008008 + // SQLite pre 3.8.8 does not set this blob error on the connection + if (rc == SQLITE_ABORT) { + PyErr_SetString(self->connection->OperationalError, + "Cannot operate on an expired blob handle"); + return; + } +#endif + _pysqlite_seterror(self->connection->state, self->connection->db); +} + +static PyObject * +inner_read(pysqlite_Blob *self, int length, int offset) +{ + PyObject *buffer = PyBytes_FromStringAndSize(NULL, length); + if (buffer == NULL) { + return NULL; + } + + char *raw_buffer = PyBytes_AS_STRING(buffer); + int rc; + Py_BEGIN_ALLOW_THREADS + rc = sqlite3_blob_read(self->blob, raw_buffer, length, offset); + Py_END_ALLOW_THREADS + + if (rc != SQLITE_OK) { + Py_DECREF(buffer); + blob_seterror(self, rc); + return NULL; + } + return buffer; +} + + +/*[clinic input] +_sqlite3.Blob.read as blob_read + + length: int = -1 + / + +Read data from blob. +[clinic start generated code]*/ + +static PyObject * +blob_read_impl(pysqlite_Blob *self, int length) +/*[clinic end generated code: output=1fc99b2541360dde input=b4b443e99af5548f]*/ +{ + if (!check_blob(self)) { + return NULL; + } + + /* Make sure we never read past "EOB". Also read the rest of the blob if a + * negative length is specified. */ + int blob_len = sqlite3_blob_bytes(self->blob); + int max_read_len = blob_len - self->offset; + if (length < 0 || length > max_read_len) { + length = max_read_len; + } + + PyObject *buffer = inner_read(self, length, self->offset); + if (buffer == NULL) { + return NULL; + } + self->offset += length; + return buffer; +}; + +static int +inner_write(pysqlite_Blob *self, const void *buf, Py_ssize_t len, int offset) +{ + int remaining_len = sqlite3_blob_bytes(self->blob) - self->offset; + if (len > remaining_len) { + PyErr_SetString(PyExc_ValueError, "data longer than blob length"); + return -1; + } + + int rc; + Py_BEGIN_ALLOW_THREADS + rc = sqlite3_blob_write(self->blob, buf, len, offset); + Py_END_ALLOW_THREADS + + if (rc != SQLITE_OK) { + blob_seterror(self, rc); + return -1; + } + return 0; +} + + +/*[clinic input] +_sqlite3.Blob.write as blob_write + + data: Py_buffer + / + +Write data to blob. +[clinic start generated code]*/ + +static PyObject * +blob_write_impl(pysqlite_Blob *self, Py_buffer *data) +/*[clinic end generated code: output=b34cf22601b570b2 input=0dcf4018286f55d2]*/ +{ + if (!check_blob(self)) { + return NULL; + } + + int rc = inner_write(self, data->buf, data->len, self->offset); + if (rc < 0) { + return NULL; + } + self->offset += (int)data->len; + Py_RETURN_NONE; +} + + +/*[clinic input] +_sqlite3.Blob.seek as blob_seek + + offset: int + origin: int = 0 + / + +Change the access position for a blob. +[clinic start generated code]*/ + +static PyObject * +blob_seek_impl(pysqlite_Blob *self, int offset, int origin) +/*[clinic end generated code: output=854c5a0e208547a5 input=cc33da6f28af0561]*/ +{ + if (!check_blob(self)) { + return NULL; + } + + int blob_len = sqlite3_blob_bytes(self->blob); + switch (origin) { + case 0: + break; + case 1: + if (offset > INT_MAX - self->offset) { + goto overflow; + } + offset += self->offset; + break; + case 2: + if (offset > INT_MAX - blob_len) { + goto overflow; + } + offset += blob_len; + break; + default: + PyErr_SetString(PyExc_ValueError, + "'origin' should be 0, 1, or 2"); + return NULL; + } + + if (offset < 0 || offset > blob_len) { + PyErr_SetString(PyExc_ValueError, "offset out of blob range"); + return NULL; + } + + self->offset = offset; + Py_RETURN_NONE; + +overflow: + PyErr_SetString(PyExc_OverflowError, "seek offset result in overflow"); + return NULL; +} + + +/*[clinic input] +_sqlite3.Blob.tell as blob_tell + +Return current access position for a blob. +[clinic start generated code]*/ + +static PyObject * +blob_tell_impl(pysqlite_Blob *self) +/*[clinic end generated code: output=3d3ba484a90b3a99 input=aa1660f9aee18be4]*/ +{ + if (!check_blob(self)) { + return NULL; + } + return PyLong_FromLong(self->offset); +} + + +/*[clinic input] +_sqlite3.Blob.__enter__ as blob_enter + +Blob context manager enter. +[clinic start generated code]*/ + +static PyObject * +blob_enter_impl(pysqlite_Blob *self) +/*[clinic end generated code: output=4fd32484b071a6cd input=fe4842c3c582d5a7]*/ +{ + if (!check_blob(self)) { + return NULL; + } + return Py_NewRef(self); +} + + +/*[clinic input] +_sqlite3.Blob.__exit__ as blob_exit + + type: object + val: object + tb: object + / + +Blob context manager exit. +[clinic start generated code]*/ + +static PyObject * +blob_exit_impl(pysqlite_Blob *self, PyObject *type, PyObject *val, + PyObject *tb) +/*[clinic end generated code: output=fc86ceeb2b68c7b2 input=575d9ecea205f35f]*/ +{ + if (!check_blob(self)) { + return NULL; + } + close_blob(self); + Py_RETURN_FALSE; +} + +static int +get_subscript_index(pysqlite_Blob *self, PyObject *item) +{ + Py_ssize_t i = PyNumber_AsSsize_t(item, PyExc_IndexError); + if (i == -1 && PyErr_Occurred()) { + return -1; + } + int blob_len = sqlite3_blob_bytes(self->blob); + if (i < 0) { + i += blob_len; + } + if (i < 0 || i >= blob_len) { + PyErr_SetString(PyExc_IndexError, "Blob index out of range"); + return -1; + } + return i; +} + +static PyObject * +subscript_index(pysqlite_Blob *self, PyObject *item) +{ + int i = get_subscript_index(self, item); + if (i < 0) { + return NULL; + } + return inner_read(self, 1, i); +} + +static PyObject * +subscript_slice(pysqlite_Blob *self, PyObject *item) +{ + Py_ssize_t start, stop, step, slicelen; + if (PySlice_Unpack(item, &start, &stop, &step) < 0) { + return NULL; + } + int blob_len = sqlite3_blob_bytes(self->blob); + slicelen = PySlice_AdjustIndices(blob_len, &start, &stop, step); + + if (slicelen <= 0) { + return PyBytes_FromStringAndSize("", 0); + } + else if (step == 1) { + return inner_read(self, slicelen, start); + } + + PyObject *blob = inner_read(self, stop - start, start); + if (blob == NULL) { + return NULL; + } + + PyObject *result = PyBytes_FromStringAndSize(NULL, slicelen); + if (result == NULL) { + goto exit; + } + + char *blob_buf = PyBytes_AS_STRING(blob); + char *res_buf = PyBytes_AS_STRING(result); + for (Py_ssize_t i = 0, j = 0; i < slicelen; i++, j += step) { + res_buf[i] = blob_buf[j]; + } + +exit: + Py_DECREF(blob); + return result; +} + +static PyObject * +blob_subscript(pysqlite_Blob *self, PyObject *item) +{ + if (!check_blob(self)) { + return NULL; + } + + if (PyIndex_Check(item)) { + return subscript_index(self, item); + } + if (PySlice_Check(item)) { + return subscript_slice(self, item); + } + + PyErr_SetString(PyExc_TypeError, "Blob indices must be integers"); + return NULL; +} + +static int +ass_subscript_index(pysqlite_Blob *self, PyObject *item, PyObject *value) +{ + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, + "Blob doesn't support item deletion"); + return -1; + } + if (!PyBytes_Check(value) || PyBytes_Size(value) != 1) { + PyErr_SetString(PyExc_ValueError, + "Blob assignment must be a single byte"); + return -1; + } + int i = get_subscript_index(self, item); + if (i < 0) { + return -1; + } + const char *buf = PyBytes_AS_STRING(value); + return inner_write(self, buf, 1, i); +} + +static int +ass_subscript_slice(pysqlite_Blob *self, PyObject *item, PyObject *value) +{ + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, + "Blob doesn't support slice deletion"); + return -1; + } + + Py_ssize_t start, stop, step, slicelen; + if (PySlice_Unpack(item, &start, &stop, &step) < 0) { + return -1; + } + int blob_len = sqlite3_blob_bytes(self->blob); + slicelen = PySlice_AdjustIndices(blob_len, &start, &stop, step); + + Py_buffer vbuf; + if (PyObject_GetBuffer(value, &vbuf, PyBUF_SIMPLE) < 0) { + return -1; + } + if (vbuf.len != slicelen) { + PyErr_SetString(PyExc_IndexError, + "Blob slice assignment is wrong size"); + PyBuffer_Release(&vbuf); + return -1; + } + + int rc; + if (slicelen == 0) { + rc = 0; + } + else if (step == 1) { + rc = inner_write(self, vbuf.buf, slicelen, start); + } + else { + PyObject *read_blob = inner_read(self, stop - start, start); + if (read_blob == NULL) { + rc = -1; + } + else { + char *blob_buf = PyBytes_AS_STRING(read_blob); + for (Py_ssize_t i = 0, j = 0; i < slicelen; i++, j += step) { + blob_buf[j] = ((char *)vbuf.buf)[i]; + } + rc = inner_write(self, blob_buf, stop - start, start); + Py_DECREF(read_blob); + } + } + PyBuffer_Release(&vbuf); + return rc; +} + +static int +blob_ass_subscript(pysqlite_Blob *self, PyObject *item, PyObject *value) +{ + if (!check_blob(self)) { + return -1; + } + + if (PyIndex_Check(item)) { + return ass_subscript_index(self, item, value); + } + if (PySlice_Check(item)) { + return ass_subscript_slice(self, item, value); + } + + PyErr_SetString(PyExc_TypeError, "Blob indices must be integers"); + return -1; +} + + +static PyMethodDef blob_methods[] = { + BLOB_CLOSE_METHODDEF + BLOB_ENTER_METHODDEF + BLOB_EXIT_METHODDEF + BLOB_READ_METHODDEF + BLOB_SEEK_METHODDEF + BLOB_TELL_METHODDEF + BLOB_WRITE_METHODDEF + {NULL, NULL} +}; + +static struct PyMemberDef blob_members[] = { + {"__weaklistoffset__", T_PYSSIZET, offsetof(pysqlite_Blob, in_weakreflist), READONLY}, + {NULL}, +}; + +static PyType_Slot blob_slots[] = { + {Py_tp_dealloc, blob_dealloc}, + {Py_tp_traverse, blob_traverse}, + {Py_tp_clear, blob_clear}, + {Py_tp_methods, blob_methods}, + {Py_tp_members, blob_members}, + + // Mapping protocol + {Py_mp_length, blob_length}, + {Py_mp_subscript, blob_subscript}, + {Py_mp_ass_subscript, blob_ass_subscript}, + {0, NULL}, +}; + +static PyType_Spec blob_spec = { + .name = MODULE_NAME ".Blob", + .basicsize = sizeof(pysqlite_Blob), + .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | + Py_TPFLAGS_IMMUTABLETYPE), + .slots = blob_slots, +}; + +int +pysqlite_blob_setup_types(PyObject *module) +{ + PyObject *type = PyType_FromModuleAndSpec(module, &blob_spec, NULL); + if (type == NULL) { + return -1; + } + pysqlite_state *state = pysqlite_get_state(module); + state->BlobType = (PyTypeObject *)type; + return 0; +} diff --git a/Modules/_sqlite/blob.h b/Modules/_sqlite/blob.h new file mode 100644 index 00000000000000..b4ac4ae0e6c7dc --- /dev/null +++ b/Modules/_sqlite/blob.h @@ -0,0 +1,24 @@ +#ifndef PYSQLITE_BLOB_H +#define PYSQLITE_BLOB_H + +#include "Python.h" +#include "sqlite3.h" +#include "connection.h" + +#define BLOB_SEEK_START 0 +#define BLOB_SEEK_CUR 1 +#define BLOB_SEEK_END 2 + +typedef struct { + PyObject_HEAD + pysqlite_Connection *connection; + sqlite3_blob *blob; + int offset; + + PyObject *in_weakreflist; +} pysqlite_Blob; + +int pysqlite_blob_setup_types(PyObject *module); +void pysqlite_close_all_blobs(pysqlite_Connection *self); + +#endif diff --git a/Modules/_sqlite/clinic/blob.c.h b/Modules/_sqlite/clinic/blob.c.h new file mode 100644 index 00000000000000..8276f8e140c2e8 --- /dev/null +++ b/Modules/_sqlite/clinic/blob.c.h @@ -0,0 +1,202 @@ +/*[clinic input] +preserve +[clinic start generated code]*/ + +PyDoc_STRVAR(blob_close__doc__, +"close($self, /)\n" +"--\n" +"\n" +"Close blob."); + +#define BLOB_CLOSE_METHODDEF \ + {"close", (PyCFunction)blob_close, METH_NOARGS, blob_close__doc__}, + +static PyObject * +blob_close_impl(pysqlite_Blob *self); + +static PyObject * +blob_close(pysqlite_Blob *self, PyObject *Py_UNUSED(ignored)) +{ + return blob_close_impl(self); +} + +PyDoc_STRVAR(blob_read__doc__, +"read($self, length=-1, /)\n" +"--\n" +"\n" +"Read data from blob."); + +#define BLOB_READ_METHODDEF \ + {"read", (PyCFunction)(void(*)(void))blob_read, METH_FASTCALL, blob_read__doc__}, + +static PyObject * +blob_read_impl(pysqlite_Blob *self, int length); + +static PyObject * +blob_read(pysqlite_Blob *self, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + int length = -1; + + if (!_PyArg_CheckPositional("read", nargs, 0, 1)) { + goto exit; + } + if (nargs < 1) { + goto skip_optional; + } + length = _PyLong_AsInt(args[0]); + if (length == -1 && PyErr_Occurred()) { + goto exit; + } +skip_optional: + return_value = blob_read_impl(self, length); + +exit: + return return_value; +} + +PyDoc_STRVAR(blob_write__doc__, +"write($self, data, /)\n" +"--\n" +"\n" +"Write data to blob."); + +#define BLOB_WRITE_METHODDEF \ + {"write", (PyCFunction)blob_write, METH_O, blob_write__doc__}, + +static PyObject * +blob_write_impl(pysqlite_Blob *self, Py_buffer *data); + +static PyObject * +blob_write(pysqlite_Blob *self, PyObject *arg) +{ + PyObject *return_value = NULL; + Py_buffer data = {NULL, NULL}; + + if (PyObject_GetBuffer(arg, &data, PyBUF_SIMPLE) != 0) { + goto exit; + } + if (!PyBuffer_IsContiguous(&data, 'C')) { + _PyArg_BadArgument("write", "argument", "contiguous buffer", arg); + goto exit; + } + return_value = blob_write_impl(self, &data); + +exit: + /* Cleanup for data */ + if (data.obj) { + PyBuffer_Release(&data); + } + + return return_value; +} + +PyDoc_STRVAR(blob_seek__doc__, +"seek($self, offset, origin=0, /)\n" +"--\n" +"\n" +"Change the access position for a blob."); + +#define BLOB_SEEK_METHODDEF \ + {"seek", (PyCFunction)(void(*)(void))blob_seek, METH_FASTCALL, blob_seek__doc__}, + +static PyObject * +blob_seek_impl(pysqlite_Blob *self, int offset, int origin); + +static PyObject * +blob_seek(pysqlite_Blob *self, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + int offset; + int origin = 0; + + if (!_PyArg_CheckPositional("seek", nargs, 1, 2)) { + goto exit; + } + offset = _PyLong_AsInt(args[0]); + if (offset == -1 && PyErr_Occurred()) { + goto exit; + } + if (nargs < 2) { + goto skip_optional; + } + origin = _PyLong_AsInt(args[1]); + if (origin == -1 && PyErr_Occurred()) { + goto exit; + } +skip_optional: + return_value = blob_seek_impl(self, offset, origin); + +exit: + return return_value; +} + +PyDoc_STRVAR(blob_tell__doc__, +"tell($self, /)\n" +"--\n" +"\n" +"Return current access position for a blob."); + +#define BLOB_TELL_METHODDEF \ + {"tell", (PyCFunction)blob_tell, METH_NOARGS, blob_tell__doc__}, + +static PyObject * +blob_tell_impl(pysqlite_Blob *self); + +static PyObject * +blob_tell(pysqlite_Blob *self, PyObject *Py_UNUSED(ignored)) +{ + return blob_tell_impl(self); +} + +PyDoc_STRVAR(blob_enter__doc__, +"__enter__($self, /)\n" +"--\n" +"\n" +"Blob context manager enter."); + +#define BLOB_ENTER_METHODDEF \ + {"__enter__", (PyCFunction)blob_enter, METH_NOARGS, blob_enter__doc__}, + +static PyObject * +blob_enter_impl(pysqlite_Blob *self); + +static PyObject * +blob_enter(pysqlite_Blob *self, PyObject *Py_UNUSED(ignored)) +{ + return blob_enter_impl(self); +} + +PyDoc_STRVAR(blob_exit__doc__, +"__exit__($self, type, val, tb, /)\n" +"--\n" +"\n" +"Blob context manager exit."); + +#define BLOB_EXIT_METHODDEF \ + {"__exit__", (PyCFunction)(void(*)(void))blob_exit, METH_FASTCALL, blob_exit__doc__}, + +static PyObject * +blob_exit_impl(pysqlite_Blob *self, PyObject *type, PyObject *val, + PyObject *tb); + +static PyObject * +blob_exit(pysqlite_Blob *self, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *type; + PyObject *val; + PyObject *tb; + + if (!_PyArg_CheckPositional("__exit__", nargs, 3, 3)) { + goto exit; + } + type = args[0]; + val = args[1]; + tb = args[2]; + return_value = blob_exit_impl(self, type, val, tb); + +exit: + return return_value; +} +/*[clinic end generated code: output=235d02d1bfa39b2a input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h index 16ad2ee254eca3..c2a45594ca907b 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -145,6 +145,99 @@ pysqlite_connection_cursor(pysqlite_Connection *self, PyObject *const *args, Py_ return return_value; } +PyDoc_STRVAR(blobopen__doc__, +"blobopen($self, table, column, row, /, *, readonly=False, name=\'main\')\n" +"--\n" +"\n" +"Return a blob object. Non-standard."); + +#define BLOBOPEN_METHODDEF \ + {"blobopen", (PyCFunction)(void(*)(void))blobopen, METH_FASTCALL|METH_KEYWORDS, blobopen__doc__}, + +static PyObject * +blobopen_impl(pysqlite_Connection *self, const char *table, const char *col, + int row, int readonly, const char *name); + +static PyObject * +blobopen(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + static const char * const _keywords[] = {"", "", "", "readonly", "name", NULL}; + static _PyArg_Parser _parser = {NULL, _keywords, "blobopen", 0}; + PyObject *argsbuf[5]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 3; + const char *table; + const char *col; + int row; + int readonly = 0; + const char *name = "main"; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 3, 3, 0, argsbuf); + if (!args) { + goto exit; + } + if (!PyUnicode_Check(args[0])) { + _PyArg_BadArgument("blobopen", "argument 1", "str", args[0]); + goto exit; + } + Py_ssize_t table_length; + table = PyUnicode_AsUTF8AndSize(args[0], &table_length); + if (table == NULL) { + goto exit; + } + if (strlen(table) != (size_t)table_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + if (!PyUnicode_Check(args[1])) { + _PyArg_BadArgument("blobopen", "argument 2", "str", args[1]); + goto exit; + } + Py_ssize_t col_length; + col = PyUnicode_AsUTF8AndSize(args[1], &col_length); + if (col == NULL) { + goto exit; + } + if (strlen(col) != (size_t)col_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + row = _PyLong_AsInt(args[2]); + if (row == -1 && PyErr_Occurred()) { + goto exit; + } + if (!noptargs) { + goto skip_optional_kwonly; + } + if (args[3]) { + readonly = _PyLong_AsInt(args[3]); + if (readonly == -1 && PyErr_Occurred()) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (!PyUnicode_Check(args[4])) { + _PyArg_BadArgument("blobopen", "argument 'name'", "str", args[4]); + goto exit; + } + Py_ssize_t name_length; + name = PyUnicode_AsUTF8AndSize(args[4], &name_length); + if (name == NULL) { + goto exit; + } + if (strlen(name) != (size_t)name_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } +skip_optional_kwonly: + return_value = blobopen_impl(self, table, col, row, readonly, name); + +exit: + return return_value; +} + PyDoc_STRVAR(pysqlite_connection_close__doc__, "close($self, /)\n" "--\n" @@ -836,4 +929,4 @@ getlimit(pysqlite_Connection *self, PyObject *arg) #ifndef PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF #define PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF #endif /* !defined(PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF) */ -/*[clinic end generated code: output=c2faf6563397091b input=a9049054013a1b77]*/ +/*[clinic end generated code: output=959de9d109b85d3f input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 02f4ac46b7c356..bf6b5c78d0b1b8 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -26,6 +26,7 @@ #include "connection.h" #include "statement.h" #include "cursor.h" +#include "blob.h" #include "prepare_protocol.h" #include "util.h" @@ -231,10 +232,17 @@ pysqlite_connection_init_impl(pysqlite_Connection *self, return -1; } - // Create list of weak references to cursors. + /* Create lists of weak references to cursors and blobs */ PyObject *cursors = PyList_New(0); if (cursors == NULL) { - Py_DECREF(statement_cache); + Py_XDECREF(statement_cache); + return -1; + } + + PyObject *blobs = PyList_New(0); + if (blobs == NULL) { + Py_XDECREF(statement_cache); + Py_XDECREF(cursors); return -1; } @@ -247,6 +255,7 @@ pysqlite_connection_init_impl(pysqlite_Connection *self, self->thread_ident = PyThread_get_thread_ident(); self->statement_cache = statement_cache; self->cursors = cursors; + self->blobs = blobs; self->created_cursors = 0; self->row_factory = Py_NewRef(Py_None); self->text_factory = Py_NewRef(&PyUnicode_Type); @@ -288,6 +297,7 @@ connection_traverse(pysqlite_Connection *self, visitproc visit, void *arg) Py_VISIT(Py_TYPE(self)); Py_VISIT(self->statement_cache); Py_VISIT(self->cursors); + Py_VISIT(self->blobs); Py_VISIT(self->row_factory); Py_VISIT(self->text_factory); VISIT_CALLBACK_CONTEXT(self->trace_ctx); @@ -311,6 +321,7 @@ connection_clear(pysqlite_Connection *self) { Py_CLEAR(self->statement_cache); Py_CLEAR(self->cursors); + Py_CLEAR(self->blobs); Py_CLEAR(self->row_factory); Py_CLEAR(self->text_factory); clear_callback_context(self->trace_ctx); @@ -426,6 +437,71 @@ pysqlite_connection_cursor_impl(pysqlite_Connection *self, PyObject *factory) return cursor; } +/*[clinic input] +_sqlite3.Connection.blobopen as blobopen + + table: str + column as col: str + row: int + / + * + readonly: bool(accept={int}) = False + name: str = "main" + +Return a blob object. Non-standard. +[clinic start generated code]*/ + +static PyObject * +blobopen_impl(pysqlite_Connection *self, const char *table, const char *col, + int row, int readonly, const char *name) +/*[clinic end generated code: output=0c8e2e58516d0b5c input=1eec15d2b87bf09e]*/ +{ + if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) { + return NULL; + } + + int rc; + sqlite3_blob *blob; + + Py_BEGIN_ALLOW_THREADS + rc = sqlite3_blob_open(self->db, name, table, col, row, !readonly, &blob); + Py_END_ALLOW_THREADS + + if (rc != SQLITE_OK) { + _pysqlite_seterror(self->state, self->db); + return NULL; + } + + pysqlite_Blob *obj = PyObject_GC_New(pysqlite_Blob, self->state->BlobType); + if (obj == NULL) { + goto error; + } + + obj->connection = (pysqlite_Connection *)Py_NewRef(self); + obj->blob = blob; + obj->offset = 0; + obj->in_weakreflist = NULL; + + PyObject_GC_Track(obj); + + // Add our blob to connection blobs list + PyObject *weakref = PyWeakref_NewRef((PyObject *)obj, NULL); + if (weakref == NULL) { + goto error; + } + rc = PyList_Append(self->blobs, weakref); + Py_DECREF(weakref); + if (rc < 0) { + goto error; + } + + return (PyObject *)obj; + +error: + Py_XDECREF(obj); + return NULL; +} + /*[clinic input] _sqlite3.Connection.close as pysqlite_connection_close @@ -448,6 +524,7 @@ pysqlite_connection_close_impl(pysqlite_Connection *self) return NULL; } + pysqlite_close_all_blobs(self); Py_CLEAR(self->statement_cache); connection_close(self); @@ -1969,6 +2046,7 @@ static PyMethodDef connection_methods[] = { PYSQLITE_CONNECTION_SET_TRACE_CALLBACK_METHODDEF SETLIMIT_METHODDEF GETLIMIT_METHODDEF + BLOBOPEN_METHODDEF {NULL, NULL} }; diff --git a/Modules/_sqlite/connection.h b/Modules/_sqlite/connection.h index 84f1f095cb3867..2b946ff3c7369b 100644 --- a/Modules/_sqlite/connection.h +++ b/Modules/_sqlite/connection.h @@ -63,8 +63,9 @@ typedef struct PyObject *statement_cache; - /* Lists of weak references to statements and cursors used within this connection */ - PyObject* cursors; + /* Lists of weak references to cursors and blobs used within this connection */ + PyObject *cursors; + PyObject *blobs; /* Counters for how many cursors were created in the connection. May be * reset to 0 at certain intervals */ diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 3b9f79799b5c59..0eb64bfb12d64a 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -27,6 +27,7 @@ #include "prepare_protocol.h" #include "microprotocols.h" #include "row.h" +#include "blob.h" #if SQLITE_VERSION_NUMBER < 3007015 #error "SQLite 3.7.15 or higher required" @@ -580,6 +581,7 @@ module_traverse(PyObject *module, visitproc visit, void *arg) Py_VISIT(state->Warning); // Types + Py_VISIT(state->BlobType); Py_VISIT(state->ConnectionType); Py_VISIT(state->CursorType); Py_VISIT(state->PrepareProtocolType); @@ -612,6 +614,7 @@ module_clear(PyObject *module) Py_CLEAR(state->Warning); // Types + Py_CLEAR(state->BlobType); Py_CLEAR(state->ConnectionType); Py_CLEAR(state->CursorType); Py_CLEAR(state->PrepareProtocolType); @@ -666,7 +669,8 @@ module_exec(PyObject *module) (pysqlite_cursor_setup_types(module) < 0) || (pysqlite_connection_setup_types(module) < 0) || (pysqlite_statement_setup_types(module) < 0) || - (pysqlite_prepare_protocol_setup_types(module) < 0) + (pysqlite_prepare_protocol_setup_types(module) < 0) || + (pysqlite_blob_setup_types(module) < 0) ) { goto error; } diff --git a/Modules/_sqlite/module.h b/Modules/_sqlite/module.h index 1d319f1ed5541e..3362cb2e2c16d6 100644 --- a/Modules/_sqlite/module.h +++ b/Modules/_sqlite/module.h @@ -53,6 +53,7 @@ typedef struct { int BaseTypeAdapted; int enable_callback_tracebacks; + PyTypeObject *BlobType; PyTypeObject *ConnectionType; PyTypeObject *CursorType; PyTypeObject *PrepareProtocolType; diff --git a/PCbuild/_sqlite3.vcxproj b/PCbuild/_sqlite3.vcxproj index e268c473f4c985..d4b11c3440b4cb 100644 --- a/PCbuild/_sqlite3.vcxproj +++ b/PCbuild/_sqlite3.vcxproj @@ -105,6 +105,7 @@ + @@ -115,6 +116,7 @@ + diff --git a/PCbuild/_sqlite3.vcxproj.filters b/PCbuild/_sqlite3.vcxproj.filters index 79fc17b53fb508..f4a265eba7dd80 100644 --- a/PCbuild/_sqlite3.vcxproj.filters +++ b/PCbuild/_sqlite3.vcxproj.filters @@ -36,6 +36,9 @@ Header Files + + Header Files + @@ -62,6 +65,9 @@ Source Files + + Source Files + diff --git a/setup.py b/setup.py index e30674f31cdb85..30157a68d5ea88 100644 --- a/setup.py +++ b/setup.py @@ -1324,6 +1324,7 @@ def detect_dbm_gdbm(self): def detect_sqlite(self): sources = [ + "_sqlite/blob.c", "_sqlite/connection.c", "_sqlite/cursor.c", "_sqlite/microprotocols.c",