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", pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy