summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuben Pollan <meskio@sindominio.net>2017-02-02 20:25:20 +0100
committerRuben Pollan <meskio@sindominio.net>2017-02-02 23:58:37 +0100
commitbb9cc1216873604459724860d606283c398ea06b (patch)
tree877eb31a32e52619092e90ab83241986bdd475ef
parenta55df58db59d38d7d320f51fa760f20cf1f69312 (diff)
[feat] add support for the blob interfacedevelop
Pysqlcipher support for the sqlite blob interface: https://sqlite.org/c3ref/blob_open.html Copying the code from the PR in pysqlite: https://github.com/ghaering/pysqlite/pull/93
-rw-r--r--doc/examples/blob.py13
-rw-r--r--doc/examples/blob_with.py12
-rw-r--r--doc/sphinx/sqlcipher.rst61
-rw-r--r--lib/test/dbapi.py264
-rw-r--r--setup.py2
-rw-r--r--src/blob.c346
-rw-r--r--src/blob.h24
-rw-r--r--src/connection.c90
-rw-r--r--src/connection.h3
-rw-r--r--src/module.c2
10 files changed, 812 insertions, 5 deletions
diff --git a/doc/examples/blob.py b/doc/examples/blob.py
new file mode 100644
index 0000000..2d63b9c
--- /dev/null
+++ b/doc/examples/blob.py
@@ -0,0 +1,13 @@
+from pysqlcipher import dbapi2 as sqlite3
+con = sqlite3.connect(":memory:")
+# creating the table
+con.execute("create table test(id integer primary key, blob_col blob)")
+con.execute("insert into test(blob_col) values (zeroblob(10))")
+# opening blob handle
+blob = con.blob("test", "blob_col", 1, 1)
+blob.write("a" * 5)
+blob.write("b" * 5)
+blob.seek(0)
+print blob.read() # will print "aaaaabbbbb"
+blob.close()
+
diff --git a/doc/examples/blob_with.py b/doc/examples/blob_with.py
new file mode 100644
index 0000000..fff9037
--- /dev/null
+++ b/doc/examples/blob_with.py
@@ -0,0 +1,12 @@
+from pysqlcipher import dbapi2 as sqlite3
+con = sqlite3.connect(":memory:")
+# creating the table
+con.execute("create table test(id integer primary key, blob_col blob)")
+con.execute("insert into test(blob_col) values (zeroblob(10))")
+# opening blob handle
+with con.blob("test", "blob_col", 1, 1) as blob:
+ blob.write("a" * 5)
+ blob.write("b" * 5)
+ blob.seek(0)
+ print blob.read() # will print "aaaaabbbbb"
+
diff --git a/doc/sphinx/sqlcipher.rst b/doc/sphinx/sqlcipher.rst
index 7785aeb..2332528 100644
--- a/doc/sphinx/sqlcipher.rst
+++ b/doc/sphinx/sqlcipher.rst
@@ -236,6 +236,13 @@ Connection Objects
:class:`sqlite3.Cursor`.
+.. method:: Connection.blob(table, column, row, flags=0, dbname="main")
+
+ On success a :class:`Blob` handle to the blob located in row 'row',
+ column 'column', table 'table' in database 'dbname' will be returned.
+ The flags represent the blob mode. 0 for read-only otherwise read-write.
+
+
.. method:: Connection.commit()
This method commits the current transaction. If you don't call this method,
@@ -631,6 +638,60 @@ Now we plug :class:`Row` in::
35.14
+.. _sqlite3-blob-objects:
+
+Blob Objects
+--------------
+
+.. class:: Blob
+
+A :class:`Blob` instance has the following attributes and methods:
+
+ A SQLite database blob has the following attributes and methods:
+
+.. method:: Blob.close()
+
+ Close the blob now (rather than whenever __del__ is called).
+
+ The blob will be unusable from this point forward; an Error (or subclass)
+ exception will be raised if any operation is attempted with the blob.
+
+.. method:: Blob.length()
+
+ Return the blob size.
+
+.. method:: Blob.read([length])
+
+ Read lnegth 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
+ length is not specified or negative we will read up to end of blob.
+
+.. method:: Blob.write(data)
+
+ Write data to the blob at the current offset. This function cannot changed blob
+ length. If data write will result in writing to more then blob current size an
+ error will be raised.
+
+.. method:: Blob.tell()
+
+ Return the current offset of the blob.
+
+.. method:: Blob.seek(offset, [whence])
+
+ Set the blob offset. The whence argument is optional and defaults to os.SEEK_SET
+ or 0 (absolute blob positioning); other values are os.SEEK_CUR or 1 (seek
+ relative to the current position) and os.SEEK_END or 2 (seek relative to the blob’s end).
+
+
+:class:`Blob` example:
+
+ .. literalinclude:: ../includes/sqlite3/blob.py
+
+A :class:`Blob` can also be used with context manager:
+
+ .. literalinclude:: ../includes/sqlite3/blob_with.py
+
+
.. _sqlite3-types:
SQLite and Python types
diff --git a/lib/test/dbapi.py b/lib/test/dbapi.py
index 05dfd4b..71e5e72 100644
--- a/lib/test/dbapi.py
+++ b/lib/test/dbapi.py
@@ -465,6 +465,155 @@ class CursorTests(unittest.TestCase):
except TypeError:
pass
+
+class BlobTests(unittest.TestCase):
+ def setUp(self):
+ self.cx = sqlite.connect(":memory:")
+ self.cx.execute("create table test(id integer primary key, blob_col blob)")
+ self.blob_data = "a" * 100
+ self.cx.execute("insert into test(blob_col) values (?)", (self.blob_data, ))
+ self.blob = self.cx.blob("test", "blob_col", 1, 1)
+ self.second_data = "b" * 100
+
+ def tearDown(self):
+ self.blob.close()
+ self.cx.close()
+
+ def CheckLength(self):
+ self.assertEqual(self.blob.length(), 100)
+
+ def CheckTell(self):
+ self.assertEqual(self.blob.tell(), 0)
+
+ def CheckSeekFromBlobStart(self):
+ self.blob.seek(10)
+ self.assertEqual(self.blob.tell(), 10)
+ self.blob.seek(10, 0)
+ self.assertEqual(self.blob.tell(), 10)
+
+ def CheckSeekFromCurrentPosition(self):
+ self.blob.seek(10,1)
+ self.blob.seek(10,1)
+ self.assertEqual(self.blob.tell(), 20)
+
+ def CheckSeekFromBlobEnd(self):
+ self.blob.seek(-10,2)
+ self.assertEqual(self.blob.tell(), 90)
+
+ def CheckBlobSeekOverBlobSize(self):
+ try:
+ self.blob.seek(1000)
+ self.fail("should have raised a ValueError")
+ except ValueError:
+ pass
+ except Exception:
+ self.fail("should have raised a ValueError")
+
+ def CheckBlobSeekUnderBlobSize(self):
+ try:
+ self.blob.seek(-10)
+ self.fail("should have raised a ValueError")
+ except ValueError:
+ pass
+ except Exception:
+ self.fail("should have raised a ValueError")
+
+ def CheckBlobRead(self):
+ self.assertEqual(self.blob.read(), self.blob_data)
+
+ def CheckBlobReadSize(self):
+ self.assertEqual(len(self.blob.read(10)), 10)
+
+ def CheckBlobReadAdvanceOffset(self):
+ self.blob.read(10)
+ self.assertEqual(self.blob.tell(), 10)
+
+ def CheckBlobReadStartAtOffset(self):
+ self.blob.seek(10)
+ self.blob.write(self.second_data[:10])
+ self.blob.seek(10)
+ self.assertEqual(self.blob.read(10), self.second_data[:10])
+
+ def CheckBlobWrite(self):
+ self.blob.write(self.second_data)
+ self.assertEqual(str(self.cx.execute("select blob_col from test").fetchone()[0]), self.second_data)
+
+ def CheckBlobWriteAtOffset(self):
+ self.blob.seek(50)
+ self.blob.write(self.second_data[:50])
+ self.assertEqual(str(self.cx.execute("select blob_col from test").fetchone()[0]),
+ self.blob_data[:50] + self.second_data[:50])
+
+ def CheckBlobWriteAdvanceOffset(self):
+ self.blob.write(self.second_data[:50])
+ self.assertEqual(self.blob.tell(), 50)
+
+ def CheckBlobWriteMoreThenBlobSize(self):
+ try:
+ self.blob.write("a" * 1000)
+ self.fail("should have raised a sqlite.OperationalError")
+ except sqlite.OperationalError:
+ pass
+ except Exception:
+ self.fail("should have raised a sqlite.OperationalError")
+
+ def CheckBlobReadAfterRowChange(self):
+ self.cx.execute("UPDATE test SET blob_col='aaaa' where id=1")
+ try:
+ self.blob.read()
+ self.fail("should have raised a sqlite.OperationalError")
+ except sqlite.OperationalError:
+ pass
+ except Exception:
+ self.fail("should have raised a sqlite.OperationalError")
+
+ def CheckBlobWriteAfterRowChange(self):
+ self.cx.execute("UPDATE test SET blob_col='aaaa' where id=1")
+ try:
+ self.blob.write("aaa")
+ self.fail("should have raised a sqlite.OperationalError")
+ except sqlite.OperationalError:
+ pass
+ except Exception:
+ self.fail("should have raised a sqlite.OperationalError")
+
+ def CheckBlobOpenWithBadDb(self):
+ try:
+ self.cx.blob("test", "blob_col", 1, 1, dbname="notexisintg")
+ self.fail("should have raised a sqlite.OperationalError")
+ except sqlite.OperationalError:
+ pass
+ except Exception:
+ self.fail("should have raised a sqlite.OperationalError")
+
+ def CheckBlobOpenWithBadTable(self):
+ try:
+ self.cx.blob("notexisintg", "blob_col", 1, 1)
+ self.fail("should have raised a sqlite.OperationalError")
+ except sqlite.OperationalError:
+ pass
+ except Exception:
+ self.fail("should have raised a sqlite.OperationalError")
+
+ def CheckBlobOpenWithBadColumn(self):
+ try:
+ self.cx.blob("test", "notexisting", 1, 1)
+ self.fail("should have raised a sqlite.OperationalError")
+ except sqlite.OperationalError:
+ pass
+ except Exception:
+ self.fail("should have raised a sqlite.OperationalError")
+
+ def CheckBlobOpenWithBadRow(self):
+ try:
+ self.cx.blob("test", "blob_col", 2, 1)
+ self.fail("should have raised a sqlite.OperationalError")
+ except sqlite.OperationalError:
+ pass
+ except Exception:
+ self.fail("should have raised a sqlite.OperationalError")
+
+
class ThreadTests(unittest.TestCase):
def setUp(self):
self.con = sqlite.connect(":memory:")
@@ -763,6 +912,20 @@ class ClosedConTests(unittest.TestCase):
except:
self.fail("Should have raised a ProgrammingError")
+ def CheckClosedBlobRead(self):
+ con = sqlite.connect(":memory:")
+ con.execute("create table test(id integer primary key, blob_col blob)")
+ con.execute("insert into test(blob_col) values (zeroblob(100))")
+ blob = con.blob("test", "blob_col", 1)
+ con.close()
+ try:
+ blob.read()
+ self.fail("Should have raised a ProgrammingError")
+ except sqlite.ProgrammingError:
+ pass
+ except:
+ self.fail("Should have raised a ProgrammingError")
+
def CheckClosedCreateFunction(self):
con = sqlite.connect(":memory:")
con.close()
@@ -859,16 +1022,115 @@ class ClosedCurTests(unittest.TestCase):
except:
self.fail("Should have raised a ProgrammingError: " + method_name)
+
+class ClosedBlobTests(unittest.TestCase):
+ def setUp(self):
+ self.cx = sqlite.connect(":memory:")
+ self.cx.execute("create table test(id integer primary key, blob_col blob)")
+ self.cx.execute("insert into test(blob_col) values (zeroblob(100))")
+
+ def tearDown(self):
+ self.cx.close()
+
+ def CheckClosedRead(self):
+ self.blob = self.cx.blob("test", "blob_col", 1)
+ self.blob.close()
+ try:
+ self.blob.read()
+ self.fail("Should have raised a ProgrammingError")
+ except sqlite.ProgrammingError:
+ pass
+ except Exception:
+ self.fail("Should have raised a ProgrammingError")
+
+ def CheckClosedWrite(self):
+ self.blob = self.cx.blob("test", "blob_col", 1)
+ self.blob.close()
+ try:
+ self.blob.write("aaaaaaaaa")
+ self.fail("Should have raised a ProgrammingError")
+ except sqlite.ProgrammingError:
+ pass
+ except Exception:
+ self.fail("Should have raised a ProgrammingError")
+
+ def CheckClosedSeek(self):
+ self.blob = self.cx.blob("test", "blob_col", 1)
+ self.blob.close()
+ try:
+ self.blob.seek(10)
+ self.fail("Should have raised a ProgrammingError")
+ except sqlite.ProgrammingError:
+ pass
+ except Exception:
+ self.fail("Should have raised a ProgrammingError")
+
+ def CheckClosedTell(self):
+ self.blob = self.cx.blob("test", "blob_col", 1)
+ self.blob.close()
+ try:
+ self.blob.tell()
+ self.fail("Should have raised a ProgrammingError")
+ except sqlite.ProgrammingError:
+ pass
+ except Exception:
+ self.fail("Should have raised a ProgrammingError")
+
+ def CheckClosedClose(self):
+ self.blob = self.cx.blob("test", "blob_col", 1)
+ self.blob.close()
+ try:
+ self.blob.close()
+ self.fail("Should have raised a ProgrammingError")
+ except sqlite.ProgrammingError:
+ pass
+ except Exception:
+ self.fail("Should have raised a ProgrammingError")
+
+
+class BlobContextManagerTests(unittest.TestCase):
+ def setUp(self):
+ self.cx = sqlite.connect(":memory:")
+ self.cx.execute("create table test(id integer primary key, blob_col blob)")
+ self.cx.execute("insert into test(blob_col) values (zeroblob(100))")
+
+ def tearDown(self):
+ self.cx.close()
+
+ def CheckContextExecute(self):
+ data = "a" * 100
+ with self.cx.blob("test", "blob_col", 1, 1) as blob:
+ blob.write("a" * 100)
+ self.assertEqual(str(self.cx.execute("select blob_col from test").fetchone()[0]), data)
+
+ def CheckContextCloseBlob(self):
+ with self.cx.blob("test", "blob_col", 1) as blob:
+ blob.seek(10)
+ try:
+ blob.close()
+ self.fail("Should have raised a ProgrammingError")
+ except sqlite.ProgrammingError:
+ pass
+ except Exception:
+ self.fail("Should have raised a ProgrammingError")
+
+
+
def suite():
module_suite = unittest.makeSuite(ModuleTests, "Check")
connection_suite = unittest.makeSuite(ConnectionTests, "Check")
cursor_suite = unittest.makeSuite(CursorTests, "Check")
+ blob_suite = unittest.makeSuite(BlobTests, "Check")
thread_suite = unittest.makeSuite(ThreadTests, "Check")
constructor_suite = unittest.makeSuite(ConstructorTests, "Check")
ext_suite = unittest.makeSuite(ExtensionTests, "Check")
closed_con_suite = unittest.makeSuite(ClosedConTests, "Check")
closed_cur_suite = unittest.makeSuite(ClosedCurTests, "Check")
- return unittest.TestSuite((module_suite, connection_suite, cursor_suite, thread_suite, constructor_suite, ext_suite, closed_con_suite, closed_cur_suite))
+ closed_blob_suite = unittest.makeSuite(ClosedBlobTests, "Check")
+ blob_context_manager_suite = unittest.makeSuite(BlobContextManagerTests, "Check")
+ return unittest.TestSuite((module_suite, connection_suite, cursor_suite, blob_suite, thread_suite,
+ constructor_suite, ext_suite, closed_con_suite, closed_cur_suite, closed_blob_suite,
+ blob_context_manager_suite, context_suite))
def test():
runner = unittest.TextTestRunner()
diff --git a/setup.py b/setup.py
index 86c43b4..1b3993e 100644
--- a/setup.py
+++ b/setup.py
@@ -52,7 +52,7 @@ PATCH_VERSION = None
sources = ["src/module.c", "src/connection.c", "src/cursor.c", "src/cache.c",
"src/microprotocols.c", "src/prepare_protocol.c", "src/statement.c",
- "src/util.c", "src/row.c"]
+ "src/util.c", "src/row.c", "src/blob.c"]
if PYSQLITE_EXPERIMENTAL:
sources.append("src/backup.c")
diff --git a/src/blob.c b/src/blob.c
new file mode 100644
index 0000000..b5ef5b2
--- /dev/null
+++ b/src/blob.c
@@ -0,0 +1,346 @@
+#include "blob.h"
+#include "util.h"
+
+
+int pysqlite_blob_init(pysqlite_Blob *self, pysqlite_Connection* connection, sqlite3_blob *blob)
+{
+ Py_INCREF(connection);
+ self->connection = connection;
+ self->offset=0;
+ self->blob = blob;
+ self->in_weakreflist = NULL;
+
+ if (!pysqlite_check_thread(self->connection)){
+ return -1;
+ }
+ return 0;
+}
+
+static void remove_blob_from_connection_blob_list(pysqlite_Blob* self)
+{
+ Py_ssize_t i;
+
+ for(i=0;i<PyList_GET_SIZE(self->connection->blobs);i++)
+ {
+ if(PyWeakref_GetObject(PyList_GET_ITEM(self->connection->blobs, i))==(PyObject *)self)
+ {
+ PyList_SetSlice(self->connection->blobs, i, i+1, NULL);
+ break;
+ }
+ }
+}
+
+
+
+static void pysqlite_blob_dealloc(pysqlite_Blob* self)
+{
+ /* close the blob */
+ if (self->blob) {
+ Py_BEGIN_ALLOW_THREADS
+ sqlite3_blob_close(self->blob);
+ Py_END_ALLOW_THREADS
+ }
+
+ // remove from connection weaklist
+ remove_blob_from_connection_blob_list(self);
+ if (self->in_weakreflist != NULL) {
+ PyObject_ClearWeakRefs((PyObject*)self);
+ }
+
+ Py_XDECREF(self->connection);
+
+ self->blob = NULL;
+
+ self->ob_type->tp_free((PyObject*)self);
+}
+
+
+/*
+ * Checks if a blob object is usable (i. e. not closed).
+ *
+ * 0 => error; 1 => ok
+ */
+int pysqlite_check_blob(pysqlite_Blob* blob)
+{
+
+ if (!blob->blob) {
+ PyErr_SetString(pysqlite_ProgrammingError, "Cannot operate on a closed blob.");
+ return 0;
+ } else if (!pysqlite_check_connection(blob->connection) || !pysqlite_check_thread(blob->connection)) {
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
+
+PyObject* pysqlite_blob_close(pysqlite_Blob *self){
+ if (!pysqlite_check_blob(self)){
+ return NULL;
+ }
+ /* close the blob */
+ if (self->blob) {
+ Py_BEGIN_ALLOW_THREADS
+ sqlite3_blob_close(self->blob);
+ Py_END_ALLOW_THREADS
+ }
+
+ self->blob = NULL;
+
+ // remove from connection weaklist
+ remove_blob_from_connection_blob_list(self);
+ if (self->in_weakreflist != NULL) {
+ PyObject_ClearWeakRefs((PyObject*)self);
+ }
+
+ Py_RETURN_NONE;
+};
+
+
+PyObject* pysqlite_blob_length(pysqlite_Blob *self){
+ int blob_length;
+ if (!pysqlite_check_blob(self)){
+ return NULL;
+ }
+ Py_BEGIN_ALLOW_THREADS
+ blob_length = sqlite3_blob_bytes(self->blob);
+ Py_END_ALLOW_THREADS
+
+ return PyInt_FromLong(blob_length);
+};
+
+
+PyObject* pysqlite_blob_read(pysqlite_Blob *self, PyObject *args){
+ int read_length = -1;
+ int blob_length = 0;
+ PyObject* buffer;
+ char* raw_buffer;
+ int rc;
+
+ if (!PyArg_ParseTuple(args, "|i", &read_length)) {
+ return NULL;
+ }
+
+ if (!pysqlite_check_blob(self)){
+ return NULL;
+ }
+
+
+ //TODO: make this multithreaded and safe!
+ Py_BEGIN_ALLOW_THREADS
+ blob_length = sqlite3_blob_bytes(self->blob);
+ Py_END_ALLOW_THREADS
+
+ if (read_length < 0) {
+ // same as file read.
+ read_length = blob_length;
+ }
+
+ // making sure we don't read more then blob size
+ if (self->offset + read_length > blob_length){
+ read_length = blob_length - self->offset;
+ }
+
+ buffer = PyBytes_FromStringAndSize(NULL, read_length);
+ if (!buffer) {
+ return NULL;
+ }
+ raw_buffer = PyBytes_AS_STRING(buffer);
+
+ Py_BEGIN_ALLOW_THREADS
+ rc = sqlite3_blob_read(self->blob, raw_buffer, read_length, self->offset);
+ Py_END_ALLOW_THREADS
+
+ if (rc != SQLITE_OK){
+ Py_DECREF(buffer);
+ // For some reasone after modifieng blob the error is not set on the connection db.
+ if (rc == SQLITE_ABORT){
+ PyErr_SetString(pysqlite_OperationalError, "Cannot operate on modified blob");
+ } else {
+ _pysqlite_seterror(self->connection->db, NULL);
+ }
+ return NULL;
+ }
+
+ // update offset.
+ self->offset += read_length;
+
+ return buffer;
+};
+
+
+PyObject* pysqlite_blob_write(pysqlite_Blob *self, PyObject *data){
+ Py_ssize_t data_size;
+ char *data_buffer;
+ int rc;
+
+ if (PyBytes_AsStringAndSize(data, &data_buffer, &data_size)){
+ return NULL;
+ }
+
+ if (!pysqlite_check_blob(self)){
+ return NULL;
+ }
+
+ //TODO: throw better error on data bigger then blob.
+
+ Py_BEGIN_ALLOW_THREADS
+ rc = sqlite3_blob_write(self->blob, data_buffer, data_size, self->offset);
+ Py_END_ALLOW_THREADS
+ if (rc != SQLITE_OK) {
+ // For some reasone after modifieng blob the error is not set on the connection db.
+ if (rc == SQLITE_ABORT){
+ PyErr_SetString(pysqlite_OperationalError, "Cannot operate on modified blob");
+ } else {
+ _pysqlite_seterror(self->connection->db, NULL);
+ }
+ return NULL;
+ }
+
+ self->offset += (int)data_size;
+ Py_RETURN_NONE;
+}
+
+
+PyObject* pysqlite_blob_seek(pysqlite_Blob *self, PyObject *args){
+ int blob_length, offset, from_what=0;
+
+ if(!PyArg_ParseTuple(args, "i|i", &offset, &from_what)){
+ return NULL;
+ }
+
+
+ if (!pysqlite_check_blob(self)){
+ return NULL;
+ }
+
+ Py_BEGIN_ALLOW_THREADS
+ blob_length = sqlite3_blob_bytes(self->blob);
+ Py_END_ALLOW_THREADS
+
+ switch(from_what){
+ case 0: //realtive to blob begin
+ break;
+ case 1: //realtive to current position
+ offset = self->offset + offset;
+ break;
+ case 2: //realtive to blob end
+ offset = blob_length + offset;
+ break;
+ default:
+ return PyErr_Format(PyExc_ValueError, "from_what should be 0, 1 or 2");
+ }
+
+ if (offset < 0 || offset > blob_length){
+ return PyErr_Format(PyExc_ValueError, "offset out of blob range");
+ }
+
+ self->offset = offset;
+ Py_RETURN_NONE;
+};
+
+
+PyObject* pysqlite_blob_tell(pysqlite_Blob *self){
+ if (!pysqlite_check_blob(self)){
+ return NULL;
+ }
+
+ return PyInt_FromLong(self->offset);
+}
+
+
+PyObject* pysqlite_blob_enter(pysqlite_Blob *self){
+ if (!pysqlite_check_blob(self)){
+ return NULL;
+ }
+
+ Py_INCREF(self);
+ return (PyObject *)self;
+}
+
+
+PyObject* pysqlite_blob_exit(pysqlite_Blob *self, PyObject *args){
+ PyObject *res;
+ if (!pysqlite_check_blob(self)){
+ return NULL;
+ }
+
+ res = pysqlite_blob_close(self);
+ Py_XDECREF(res);
+ if (!res) {
+ return NULL;
+ }
+
+ Py_RETURN_FALSE;
+}
+
+
+static PyMethodDef blob_methods[] = {
+ {"length", (PyCFunction)pysqlite_blob_length, METH_NOARGS,
+ PyDoc_STR("return blob length")},
+ {"read", (PyCFunction)pysqlite_blob_read, METH_VARARGS,
+ PyDoc_STR("read data from blob")},
+ {"write", (PyCFunction)pysqlite_blob_write, METH_O,
+ PyDoc_STR("write data to blob")},
+ {"close", (PyCFunction)pysqlite_blob_close, METH_NOARGS,
+ PyDoc_STR("close blob")},
+ {"seek", (PyCFunction)pysqlite_blob_seek, METH_VARARGS,
+ PyDoc_STR("change blob current offset")},
+ {"tell", (PyCFunction)pysqlite_blob_tell, METH_NOARGS,
+ PyDoc_STR("return blob current offset")},
+ {"__enter__", (PyCFunction)pysqlite_blob_enter, METH_NOARGS,
+ PyDoc_STR("blob context manager enter")},
+ {"__exit__", (PyCFunction)pysqlite_blob_exit, METH_VARARGS,
+ PyDoc_STR("blob context manager exit")},
+ {NULL, NULL}
+};
+
+
+PyTypeObject pysqlite_BlobType = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ MODULE_NAME ".Blob", /* tp_name */
+ sizeof(pysqlite_Blob), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ (destructor)pysqlite_blob_dealloc, /* tp_dealloc */
+ 0, /* tp_print */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ 0, /* tp_compare */
+ 0, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ 0, /* tp_as_mapping */
+ 0, /* tp_hash */
+ 0, /* tp_call */
+ 0, /* tp_str */
+ 0, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT|Py_TPFLAGS_HAVE_WEAKREFS, /* tp_flags */
+ 0, /* tp_doc */
+ 0, /* tp_traverse */
+ 0, /* tp_clear */
+ 0, /* tp_richcompare */
+ offsetof(pysqlite_Blob, in_weakreflist), /* tp_weaklistoffset */
+ 0, /* tp_iter */
+ 0, /* tp_iternext */
+ blob_methods, /* tp_methods */
+ 0, /* tp_members */
+ 0, /* tp_getset */
+ 0, /* tp_base */
+ 0, /* tp_dict */
+ 0, /* tp_descr_get */
+ 0, /* tp_descr_set */
+ 0, /* tp_dictoffset */
+ (initproc)pysqlite_blob_init, /* tp_init */
+ 0, /* tp_alloc */
+ 0, /* tp_new */
+ 0 /* tp_free */
+};
+
+extern int pysqlite_blob_setup_types(void)
+{
+ pysqlite_BlobType.tp_new = PyType_GenericNew;
+ return PyType_Ready(&pysqlite_BlobType);
+}
+
diff --git a/src/blob.h b/src/blob.h
new file mode 100644
index 0000000..faaef64
--- /dev/null
+++ b/src/blob.h
@@ -0,0 +1,24 @@
+#ifndef PYSQLITE_BLOB_H
+#define PYSQLITE_BLOB_H
+#include "Python.h"
+#include "sqlite3.h"
+#include "connection.h"
+
+typedef struct
+{
+ PyObject_HEAD
+ pysqlite_Connection* connection;
+ sqlite3_blob *blob;
+ unsigned int offset;
+
+ PyObject* in_weakreflist; /* List of weak references */
+} pysqlite_Blob;
+
+extern PyTypeObject pysqlite_BlobType;
+
+int pysqlite_blob_init(pysqlite_Blob* self, pysqlite_Connection* connection, sqlite3_blob *blob);
+PyObject* pysqlite_blob_close(pysqlite_Blob *self);
+
+int pysqlite_blob_setup_types(void);
+
+#endif
diff --git a/src/connection.c b/src/connection.c
index 7e4e96c..115d15a 100644
--- a/src/connection.c
+++ b/src/connection.c
@@ -30,6 +30,8 @@
#include "util.h"
#include "sqlitecompat.h"
+#include "blob.h"
+
#ifdef PYSQLITE_EXPERIMENTAL
#include "backup.h"
#endif
@@ -91,6 +93,7 @@ int pysqlite_connection_init(pysqlite_Connection* self, PyObject* args, PyObject
self->statement_cache = NULL;
self->statements = NULL;
self->cursors = NULL;
+ self->blobs = NULL;
Py_INCREF(Py_None);
self->row_factory = Py_None;
@@ -167,10 +170,12 @@ int pysqlite_connection_init(pysqlite_Connection* self, PyObject* args, PyObject
self->created_statements = 0;
self->created_cursors = 0;
- /* Create lists of weak references to statements/cursors */
+ /* Create lists of weak references to statements/cursors/blobs */
self->statements = PyList_New(0);
self->cursors = PyList_New(0);
- if (!self->statements || !self->cursors) {
+ self->blobs = PyList_New(0);
+
+ if (!self->statements || !self->cursors || !self->blobs) {
return -1;
}
@@ -293,6 +298,7 @@ void pysqlite_connection_dealloc(pysqlite_Connection* self)
Py_XDECREF(self->collations);
Py_XDECREF(self->statements);
Py_XDECREF(self->cursors);
+ Py_XDECREF(self->blobs);
self->ob_type->tp_free((PyObject*)self);
}
@@ -390,6 +396,82 @@ PyObject* pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args,
}
#endif
+PyObject* pysqlite_connection_blob(pysqlite_Connection* self, PyObject* args, PyObject* kwargs)
+{
+ static char *kwlist[] = {"table", "column", "row", "flags", "dbname", NULL, NULL};
+ int rc;
+ const char *dbname = "main", *table, *column;
+ sqlite3_int64 row;
+ int flags = 0;
+ sqlite3_blob* blob;
+ pysqlite_Blob *pyblob=0;
+ PyObject *weakref;
+
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ssL|is", kwlist,
+ &table, &column, &row, &flags, &dbname)) {
+ return NULL;
+ }
+
+ Py_BEGIN_ALLOW_THREADS
+ rc = sqlite3_blob_open(self->db, dbname, table, column, row, flags, &blob);
+ Py_END_ALLOW_THREADS
+
+ if (rc != SQLITE_OK) {
+ _pysqlite_seterror(self->db, NULL);
+ return NULL;
+ }
+
+ pyblob = PyObject_New(pysqlite_Blob, &pysqlite_BlobType);
+ if (!pyblob){
+ goto error;
+ }
+
+ rc = pysqlite_blob_init(pyblob, self, blob);
+ if (rc) {
+ Py_CLEAR(pyblob);
+ goto error;
+ }
+
+ // Add our blob to connection blobs list
+ weakref=PyWeakref_NewRef((PyObject*)pyblob, NULL);
+ if (!weakref){
+ Py_CLEAR(pyblob);
+ goto error;
+ }
+ if (PyList_Append(self->blobs, weakref) != 0) {
+ Py_CLEAR(pyblob);
+ Py_CLEAR(weakref);
+ goto error;
+ }
+ Py_DECREF(weakref);
+
+ return (PyObject*)pyblob;
+
+error:
+ Py_BEGIN_ALLOW_THREADS
+ sqlite3_blob_close(blob);
+ Py_END_ALLOW_THREADS
+ return NULL;
+}
+
+
+void pysqlite_close_all_blobs(pysqlite_Connection* self)
+{
+ int i;
+ PyObject* weakref;
+ PyObject* blob;
+
+ for (i = 0; i < PyList_Size(self->blobs); i++) {
+ weakref = PyList_GetItem(self->blobs, i);
+ blob = PyWeakref_GetObject(weakref);
+ if (blob != Py_None) {
+ pysqlite_blob_close((pysqlite_Blob*)blob);
+ }
+ }
+}
+
+
PyObject* pysqlite_connection_close(pysqlite_Connection* self, PyObject* args)
{
PyObject* ret;
@@ -401,6 +483,8 @@ PyObject* pysqlite_connection_close(pysqlite_Connection* self, PyObject* args)
pysqlite_do_all_statements(self, ACTION_FINALIZE, 1);
+ pysqlite_close_all_blobs(self);
+
if (self->db) {
if (self->apsw_connection) {
ret = PyObject_CallMethod(self->apsw_connection, "close", "");
@@ -1597,6 +1681,8 @@ static PyMethodDef connection_methods[] = {
{"backup", (PyCFunction)pysqlite_connection_backup, METH_VARARGS|METH_KEYWORDS,
PyDoc_STR("Backup database.")},
#endif
+ {"blob", (PyCFunction)pysqlite_connection_blob, METH_VARARGS|METH_KEYWORDS,
+ PyDoc_STR("return a blob object")},
{"cursor", (PyCFunction)pysqlite_connection_cursor, METH_VARARGS|METH_KEYWORDS,
PyDoc_STR("Return a cursor for the connection.")},
{"close", (PyCFunction)pysqlite_connection_close, METH_NOARGS,
diff --git a/src/connection.h b/src/connection.h
index 3fc5a70..a110ea2 100644
--- a/src/connection.h
+++ b/src/connection.h
@@ -70,9 +70,10 @@ typedef struct
pysqlite_Cache* statement_cache;
- /* Lists of weak references to statements and cursors used within this connection */
+ /* Lists of weak references to statements, blobs and cursors used within this connection */
PyObject* statements;
PyObject* cursors;
+ PyObject* blobs;
/* Counters for how many statements/cursors were created in the connection. May be
* reset to 0 at certain intervals */
diff --git a/src/module.c b/src/module.c
index dbc015c..7a26498 100644
--- a/src/module.c
+++ b/src/module.c
@@ -28,6 +28,7 @@
#include "prepare_protocol.h"
#include "microprotocols.h"
#include "row.h"
+#include "blob.h"
#ifdef PYSQLITE_EXPERIMENTAL
#include "backup.h"
@@ -321,6 +322,7 @@ PyMODINIT_FUNC init_sqlite(void)
#ifdef PYSQLITE_EXPERIMENTAL
(pysqlite_backup_setup_types() < 0) ||
#endif
+ (pysqlite_blob_setup_types() < 0) ||
(pysqlite_prepare_protocol_setup_types() < 0)
) {
return;