[feat] add support for the blob interface develop
authorRuben Pollan <meskio@sindominio.net>
Thu, 2 Feb 2017 19:25:20 +0000 (20:25 +0100)
committerRuben Pollan <meskio@sindominio.net>
Thu, 2 Feb 2017 22:58:37 +0000 (23:58 +0100)
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

doc/examples/blob.py [new file with mode: 0644]
doc/examples/blob_with.py [new file with mode: 0644]
doc/sphinx/sqlcipher.rst
lib/test/dbapi.py
setup.py
src/blob.c [new file with mode: 0644]
src/blob.h [new file with mode: 0644]
src/connection.c
src/connection.h
src/module.c

diff --git a/doc/examples/blob.py b/doc/examples/blob.py
new file mode 100644 (file)
index 0000000..2d63b9c
--- /dev/null
@@ -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 (file)
index 0000000..fff9037
--- /dev/null
@@ -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" 
+
index 7785aeb..2332528 100644 (file)
@@ -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
index 05dfd4b..71e5e72 100644 (file)
@@ -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()
index 86c43b4..1b3993e 100644 (file)
--- 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 (file)
index 0000000..b5ef5b2
--- /dev/null
@@ -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 (file)
index 0000000..faaef64
--- /dev/null
@@ -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
index 7e4e96c..115d15a 100644 (file)
@@ -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,
index 3fc5a70..a110ea2 100644 (file)
@@ -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 */
index dbc015c..7a26498 100644 (file)
@@ -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;