On Sun, Jun 27, 2021 at 10:14:49AM -0500, Justin Pryzby wrote:
> Maybe inserttable should be rewritten in python, to use all the adaptation
> logic and flexibility that already exists there.  I started this, and seems to
> work fine.  Obviously this break compatibility for anyone using
> pg.connect().inserttable().
> 
> commit 94c8e81260b40ac87af6fda7a5368b220dbcb3ff
> Author: Justin Pryzby <pryz...@telsasoft.com>
> Date:   Sun Jun 27 09:14:35 2021 -0500
> 
>     WIP: reimplement inserttable() as a DB() method

Sorry, the work was accidentlly split across two commits.
Find attached my works in progress.

I can polish these and add docs and tests, but I'd like some feedback about the
future of inserttable and adaptation.
>From 06955b958260dee7b5b83ae64ff7c02c187239e4 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryz...@telsasoft.com>
Date: Sat, 26 Jun 2021 23:00:12 -0500
Subject: [PATCH 1/4] fix error checks in inserttable()

---
 pgconn.c | 24 +++++++++++++++++-------
 1 file changed, 17 insertions(+), 7 deletions(-)

diff --git a/pgconn.c b/pgconn.c
index 302dd29..1575df7 100644
--- a/pgconn.c
+++ b/pgconn.c
@@ -770,24 +770,32 @@ conn_inserttable(connObject *self, PyObject *args)
         for (j = 0; j < n; ++j) {
             PyObject *obj = getcolumn(columns, j);
             ssize_t slen;
-            char *col;
+            char *col, *newcol;
 
             if (PyBytes_Check(obj)) {
                 PyBytes_AsStringAndSize(obj, &col, &slen);
-            }
-            else if (PyUnicode_Check(obj)) {
+            } else if (PyUnicode_Check(obj)) {
+                PyObject *oldobj = obj;
                 obj = get_encoded_string(obj, encoding);
-                if (!obj) return NULL; /* pass the UnicodeEncodeError */
+                Py_DECREF(oldobj);
+                if (!obj) {
+                    PyMem_Free(buffer);
+                    return NULL; /* pass the UnicodeEncodeError */
+                }
                 PyBytes_AsStringAndSize(obj, &col, &slen);
                 Py_DECREF(obj);
             } else {
                 PyErr_SetString(
                     PyExc_TypeError,
                     "The third argument must contain only strings");
+                PyMem_Free(buffer);
+                return NULL;
             }
-            col = PQescapeIdentifier(self->cnx, col, (size_t) slen);
+
+            newcol = PQescapeIdentifier(self->cnx, col, (size_t) slen);
             bufpt += sprintf(bufpt, "%s%s", col, j == n - 1 ? ")" : ",");
-            PQfreemem(col);
+            Py_DECREF(col);
+            PQfreemem(newcol);
         }
     }
     sprintf(bufpt, " from stdin");
@@ -796,7 +804,7 @@ conn_inserttable(connObject *self, PyObject *args)
     result = PQexec(self->cnx, buffer);
     Py_END_ALLOW_THREADS
 
-    if (!result) {
+    if (!result || PQresultStatus(result) != PGRES_COPY_IN) {
         PyMem_Free(buffer);
         PyErr_SetString(PyExc_ValueError, PQerrorMessage(self->cnx));
         return NULL;
@@ -819,6 +827,7 @@ conn_inserttable(connObject *self, PyObject *args)
             PyErr_SetString(
                 PyExc_TypeError,
                 "The second argument must contain a tuple or a list");
+            PQendcopy(self->cnx);
             return NULL;
         }
         if (i) {
@@ -827,6 +836,7 @@ conn_inserttable(connObject *self, PyObject *args)
                 PyErr_SetString(
                     PyExc_TypeError,
                     "Arrays contained in second arg must have same size");
+                PQendcopy(self->cnx);
                 return NULL;
             }
         }
-- 
2.17.0

>From 35dfd0fc5c0be107ba3abf1009e50189a28cc6b7 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryz...@telsasoft.com>
Date: Sun, 27 Jun 2021 11:40:10 -0500
Subject: [PATCH 2/4] Allow connect().inserttable() to accept a
 schema-qualified table

---
 pgconn.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 60 insertions(+), 13 deletions(-)

diff --git a/pgconn.c b/pgconn.c
index 1575df7..4be0706 100644
--- a/pgconn.c
+++ b/pgconn.c
@@ -693,6 +693,7 @@ conn_inserttable(connObject *self, PyObject *args)
     char *table, *buffer, *bufpt;
     int encoding;
     size_t bufsiz;
+    PyObject *tableobj;
     PyObject *list, *sublist, *item, *columns = NULL;
     PyObject *(*getitem) (PyObject *, Py_ssize_t);
     PyObject *(*getsubitem) (PyObject *, Py_ssize_t);
@@ -705,13 +706,70 @@ conn_inserttable(connObject *self, PyObject *args)
     }
 
     /* gets arguments */
-    if (!PyArg_ParseTuple(args, "sO|O", &table, &list, &columns)) {
+    if (!PyArg_ParseTuple(args, "OO|O", &tableobj, &list, &columns)) {
         PyErr_SetString(
             PyExc_TypeError,
-            "Method inserttable() expects a string and a list as arguments");
+            "Method inserttable() expects a string/list/tuple and a list as arguments");
         return NULL;
     }
 
+    encoding = PQclientEncoding(self->cnx);
+
+    /* allocate buffer */
+    if (!(bufpt = buffer = PyMem_Malloc(MAX_BUFFER_SIZE)))
+        return PyErr_NoMemory();
+
+    if (PyUnicode_Check(tableobj)) {
+        ssize_t len;
+        tableobj = get_encoded_string(tableobj, encoding);
+        if (!tableobj) return NULL; /* pass the UnicodeEncodeError */
+        // Py_DECREF(obj); //XXX
+        PyBytes_AsStringAndSize(tableobj, &table, &len);
+        table = PQescapeIdentifier(self->cnx, table, (size_t)len);
+        bufpt += sprintf(bufpt, "copy %s", table);
+        PQfreemem(table);
+    } else {
+        ssize_t length = 0;
+        if (PyList_Check(tableobj)) {
+            length = PyList_Size(tableobj);
+            getitem = PyList_GetItem;
+        } else if (PyTuple_Check(tableobj)) {
+            length = PyTuple_Size(tableobj);
+            getitem = PyTuple_GetItem;
+        }
+
+        if (length<0 || length>3) {
+            PyErr_SetString(
+                PyExc_TypeError,
+                "Method inserttable() expects a string or list/tuple of length 1, 2 or 3 as the first argument");
+            return NULL;
+        }
+
+        bufpt += sprintf(bufpt, "copy ");
+        for (i=0; i<length; ++i) {
+            PyObject *obj = getitem(tableobj, i);
+            char *tblpart;
+            ssize_t len;
+
+            if (PyBytes_Check(obj)) {
+                PyBytes_AsStringAndSize(obj, &tblpart, &len);
+            } else if (PyUnicode_Check(obj)) {
+                obj = get_encoded_string(obj, encoding);
+                if (!obj) return NULL; /* pass the UnicodeEncodeError */
+                PyBytes_AsStringAndSize(obj, &tblpart, &len);
+                Py_DECREF(obj);
+            } else {
+                PyErr_SetString(
+                    PyExc_TypeError,
+                    "The first argument must contain only strings");
+                return NULL;
+            }
+            tblpart = PQescapeIdentifier(self->cnx, tblpart, (size_t)len);
+            bufpt += sprintf(bufpt, "%s%s", tblpart, i==length-1 ? "" : ".");
+            PQfreemem(tblpart);
+        }
+    }
+
     /* checks list type */
     if (PyList_Check(list)) {
         m = PyList_Size(list);
@@ -753,17 +811,6 @@ conn_inserttable(connObject *self, PyObject *args)
         }
     }
 
-    /* allocate buffer */
-    if (!(buffer = PyMem_Malloc(MAX_BUFFER_SIZE)))
-        return PyErr_NoMemory();
-
-    encoding = PQclientEncoding(self->cnx);
-
-    /* starts query */
-    bufpt = buffer;
-    table = PQescapeIdentifier(self->cnx, table, strlen(table));
-    bufpt += sprintf(bufpt, "copy %s", table);
-    PQfreemem(table);
     if (columns) {
         /* adds a string like f" ({','.join(columns)})" */
         bufpt += sprintf(bufpt, " (");
-- 
2.17.0

>From 5cdf2c632be60dd1decdb4522a9c4fcb731d490e Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryz...@telsasoft.com>
Date: Sun, 27 Jun 2021 01:11:03 -0500
Subject: [PATCH 3/4] WIP: rewrite inserttable in pg.py

WIP: reimplement inserttable() as a DB() method
---
 pg.py                            | 43 ++++++++++++++++++++++++++++++++
 pgconn.c                         |  4 +--
 tests/test_classic_connection.py |  9 ++++---
 3 files changed, 51 insertions(+), 5 deletions(-)

diff --git a/pg.py b/pg.py
index 6c1bd35..cf6896f 100644
--- a/pg.py
+++ b/pg.py
@@ -1759,6 +1759,49 @@ class DB:
         """Encode a JSON string for use within SQL."""
         return jsonencode(d)
 
+    def inserttable(self, table, rows, columns=None):
+        # PQescapeIdentifier
+        if not isinstance(rows, (list,tuple)):
+            raise TypeError('expects a list or a tuple as second argument')
+
+        sql = 'copy %s' % self.escape_identifier(table)
+        if columns == []:
+            return
+        if columns is not None:
+            sql += '( %s )' % ', '.join([self.escape_identifier(i) for i in columns])
+        sql += ' from stdin'
+        self.query(sql)
+        try:
+            self._inserttable_guts(rows)
+        except Exception as e:
+            raise
+        finally:
+            self.endcopy()
+
+    def _inserttable_guts(self, rows):
+        import re
+        bytesreg = re.compile(b'([\\\t\n])')
+        strreg = re.compile('([\\\t\n])')
+        for row in rows:
+            if not isinstance(row, (list,tuple)):
+                raise TypeError('second argument must contain a tuple or a list')
+
+            toput = []
+            for col in row:
+                if col is None:
+                    toput.append('\\N')
+                elif isinstance(col, bytes):
+                    toput.append(re.sub(bytesreg, br'\\\g<1>', col).decode())
+                elif isinstance(col, (str,unicode)):
+                    toput.append(re.sub(strreg, r'\\\g<1>', col))
+                #elif isinstance(col, (int,long)):
+                    #toput.append(str(col))
+                else:
+                    toput.append(str(col))
+                #pg_str = getattr(value, '__pg_str__', None)
+            toput = '\t'.join(toput) + '\n'
+            self.putline(toput)
+
     def close(self):
         """Close the database connection."""
         # Wraps shared library function so we can track state.
diff --git a/pgconn.c b/pgconn.c
index 4be0706..36ea1b3 100644
--- a/pgconn.c
+++ b/pgconn.c
@@ -1677,8 +1677,8 @@ static struct PyMethodDef conn_methods[] = {
         METH_O, conn_set_notice_receiver__doc__},
     {"getnotify", (PyCFunction) conn_get_notify,
         METH_NOARGS, conn_get_notify__doc__},
-    {"inserttable", (PyCFunction) conn_inserttable,
-        METH_VARARGS, conn_inserttable__doc__},
+    // {"inserttable", (PyCFunction) conn_inserttable,
+        // METH_VARARGS, conn_inserttable__doc__},
     {"transaction", (PyCFunction) conn_transaction,
         METH_NOARGS, conn_transaction__doc__},
     {"parameter", (PyCFunction) conn_parameter,
diff --git a/tests/test_classic_connection.py b/tests/test_classic_connection.py
index 4bb7336..10326df 100755
--- a/tests/test_classic_connection.py
+++ b/tests/test_classic_connection.py
@@ -1800,7 +1800,7 @@ class TestInserttable(unittest.TestCase):
 
     def setUp(self):
         self.assertTrue(self.cls_set_up)
-        self.c = connect()
+        self.c = pg.DB(dbname, dbhost, dbport)
         self.c.query("set client_encoding=utf8")
         self.c.query("set datestyle='ISO,YMD'")
         self.c.query("set lc_monetary='C'")
@@ -1831,6 +1831,8 @@ class TestInserttable(unittest.TestCase):
     def get_back(self, encoding='utf-8'):
         """Convert boolean and decimal values back."""
         data = []
+        pg.set_typecast('date', None)
+        pg.set_typecast('time', None)
         for row in self.c.query("select * from test order by 1").getresult():
             self.assertIsInstance(row, tuple)
             row = list(row)
@@ -2083,7 +2085,7 @@ class TestDirectSocketAccess(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        c = connect()
+        c = pg.DB(dbname, dbhost, dbport)
         c.query("drop table if exists test cascade")
         c.query("create table test (i int, v varchar(16))")
         c.close()
@@ -2097,7 +2099,8 @@ class TestDirectSocketAccess(unittest.TestCase):
 
     def setUp(self):
         self.assertTrue(self.cls_set_up)
-        self.c = connect()
+        #self.c = connect()
+        self.c = pg.DB(dbname, dbhost, dbport)
         self.c.query("set client_encoding=utf8")
 
     def tearDown(self):
-- 
2.17.0

>From 8d5436aeaf25b61c425916b6b0ea2781ba6f6d27 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryz...@telsasoft.com>
Date: Sun, 27 Jun 2021 10:27:29 -0500
Subject: [PATCH 4/4] WIP: inserttable: support schemas

---
 pg.py | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/pg.py b/pg.py
index cf6896f..25362e3 100644
--- a/pg.py
+++ b/pg.py
@@ -1764,7 +1764,18 @@ class DB:
         if not isinstance(rows, (list,tuple)):
             raise TypeError('expects a list or a tuple as second argument')
 
-        sql = 'copy %s' % self.escape_identifier(table)
+        if isinstance(table, (str,unicode)):
+            table = self.escape_identifier(table)
+        elif isinstance(table, (list,tuple)) and\
+            len(table) <= 3 and len(table) > 0:
+            # Can be database.schema.table or schema.table or table
+            # The components must be separately escaped.
+            table = '.'.join(self.escape_identifier(i) for i in table)
+        else:
+            raise TypeError("table must be a string or a tuple of length 1, 2, or 3")
+
+        sql = 'copy %s' % table
+
         if columns == []:
             return
         if columns is not None:
-- 
2.17.0

_______________________________________________
PyGreSQL mailing list
PyGreSQL@Vex.Net
https://mail.vex.net/mailman/listinfo/pygresql

Reply via email to