diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index ccb82278bdaa13..004d98429a73f4 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -494,14 +494,21 @@ Connection Objects .. literalinclude:: ../includes/sqlite3/row_factory.py If returning a tuple doesn't suffice and you want name-based access to - columns, you should consider setting :attr:`row_factory` to the - highly-optimized :class:`sqlite3.Row` type. :class:`Row` provides both - index-based and case-insensitive name-based access to columns with almost no - memory overhead. It will probably be better than your own custom - dictionary-based approach or even a db_row based solution. + columns, you should consider setting :attr:`row_factory` one of the + highly optimized row_factory types :class:`sqlite3.Row` or + :class:`sqlite3.NamedRow`. + + :class:`Row` provides both index-based and case-insensitive name-based + access to columns with almost no memory overhead. It will probably be + better than your own custom dictionary-based approach or even a db_row + based solution. .. XXX what's a db_row-based solution? + :class:`NamedRow` provides both attribute and index by column name or number + to columns with the same low memory requirements of :class:`sqlite3.Row`. + As a compiled class, it will out perform a custom python row_factory solution. + .. attribute:: text_factory @@ -808,6 +815,72 @@ Now we plug :class:`Row` in:: 100.0 35.14 +.. class:: NamedRow + + A :class:`NamedRow` instance is an optimized :attr:`~Connection.row_factory` + for :class:`Connection` objects. + It tries to mimic a namedtuple in most of its features. + + It supports mapping access by attribute, column name, and index. + It also supports iteration, slicing, equality testing, :func:`contains`, + and :func:`len`. + +Here is a simple example, with a Japanese column name:: + + >>> db_connection = sqlite3.connect(":memory:") + >>> cursor = db_connection.cursor() + >>> cursor.row_factory = sqlite3.NamedRow + >>> row = cursor.execute("SELECT 'seven' AS english, '七' as 日本").fetchone() + >>> type(row) + + >>> len(row) + 2 + >>> '日本' in row + True + >>> row.english, row.日本 + ('seven', '七') + >>> row[0], row['english'] + ('seven', 'seven') + >>> tuple(row) + (('english', 'seven'), ('日本', '七')) + >>> dict(row) + {'english': 'seven', '日本': '七'} + >>> [x[0] for x in row] # Get column names + ['english', '日本'] + >>> [x[1] for x in row] # Get column values + ['seven', '七'] + +For columns names that are invalid attribute names, such as "count(*)" +or python reserved words, create a valid attribute name using the SQL +"AS" keyword. +For example, assume we have an address table and want the number of rows +where the city is Kahlotus.:: + + >>> cursor = db_connection.cursor() + >>> cursor.row_factory = sqlite3.NamedRow + >>> row = cursor.execute("SELECT city, count(*) FROM address WHERE city=?", + ... ('Kahlotus')).fetchone() + >>> row.count + Traceback (most recent call last): + File "", line 1, in + AttributeError: 'sqlite3.NamedRow' object has no attribute 'count' + >>> tuple(row) + (('city', 'Kahlotus'), ('count(*)', 2)) + >>> row = cursor.execute("SELECT city, count(*) AS cnt FROM address WHERE city=?", + ... ('Kahlotus')).fetchone() + (('city', 'Kahlotus'), ('count', 2)) + >>> row.count + 2 + +If you are unable to change the query, index by name or number.:: + + >>> tuple(row) + (('city', 'Kahlotus'), ('count(*)', 2)) + >>> row['count(*)'] + 2 + >>> row[1] + 2 + .. _sqlite3-exceptions: diff --git a/Lib/sqlite3/dbapi2.py b/Lib/sqlite3/dbapi2.py index 991682ce9ef3b7..5b5ed8fa7da738 100644 --- a/Lib/sqlite3/dbapi2.py +++ b/Lib/sqlite3/dbapi2.py @@ -52,6 +52,8 @@ def TimestampFromTicks(ticks): Binary = memoryview collections.abc.Sequence.register(Row) +collections.abc.Sequence.register(NamedRow) + def register_adapters_and_converters(): def adapt_date(val): diff --git a/Lib/sqlite3/test/test_namedrow.py b/Lib/sqlite3/test/test_namedrow.py new file mode 100644 index 00000000000000..e22b9c530ae023 --- /dev/null +++ b/Lib/sqlite3/test/test_namedrow.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# Tests the NamedRow row_factory with uft-8 for non ascii attribute names +# +# Copyright (C) 2019 Clinton James "JIDN" +# +# This software is provided 'as-is', without any express or implied +# warranty. In no even will the authors be held liable for any damages +# arising from the use of this software. +# +# Permission is granted to anyone to use this software for any purpose, +# including commercial applications, and to alter it and redistribute it +# freely, subject to the following restrictions: +# +# 1. Ths origin of this software must to be misrepresented; you must not +# claim that you wrote the original software. If you use this software +# in a product, an acknowledgment in the product documentation would be +# appreciated but is not required. +# 2. Altered source version must be plainly marked as such, and must not be +# misrepresented as being the original software. +# 3. This notice may not be removed or altered from any source distribution. + +import unittest +import sqlite3 + + +class NamedRowFactoryTests(unittest.TestCase): + def setUp(self): + self.conn = sqlite3.connect(":memory:") + self.conn.row_factory = sqlite3.NamedRow + + def tearDown(self): + self.conn.close() + + def CheckInstance(self): + """row_factory creates NamedRow instances""" + row = self.conn.execute("SELECT 1, 2").fetchone() + self.assertIsInstance(row, sqlite3.NamedRow) + self.assertIn("NamedRow", row.__class__.__name__) + self.assertIn("NamedRow", repr(row)) + self.assertIn( + "optimized row_factory for column name access", sqlite3.NamedRow.__doc__ + ) + + def CheckByNumberIndex(self): + """Get values by numbered index [0]""" + row = self.conn.execute("SELECT 1, 2").fetchone() + # len(row) + self.assertEqual(2, len(row)) + + self.assertEqual(row[0], 1) + self.assertEqual(row[1], 2) + self.assertEqual(row[-1], 2) + self.assertEqual(row[-2], 1) + + # Check past boundary + with self.assertRaises(IndexError): + row[len(row) + 1] + with self.assertRaises(IndexError): + row[-1 * (len(row) + 1)] + + def CheckByStringIndex(self): + """Get values by string index ['field']""" + row = self.conn.execute('SELECT 1 AS a, 2 AS abcdefg, 4 AS "四"').fetchone() + self.assertEqual(1, row["a"]) + self.assertEqual(2, row["abcdefg"]) + self.assertEqual(4, row["四"]) + + def CheckByAttribute(self): + """Get values by attribute row.field""" + row = self.conn.execute('SELECT 1 AS a, 2 AS abcdefg, 4 AS "四"').fetchone() + self.assertEqual(1, row.a) + self.assertEqual(2, row.abcdefg) + self.assertEqual(4, row.四) + + def CheckContainsField(self): + row = self.conn.execute("SELECT 1 AS a, 2 AS b").fetchone() + self.assertIn("a", row) + self.assertNotIn("A", row) + + def CheckSlice(self): + """Does NamedRow slice like a list.""" + row = self.conn.execute("SELECT 1, 2, 3, 4").fetchone() + self.assertEqual(row[0:0], ()) + self.assertEqual(row[0:1], (1,)) + self.assertEqual(row[1:3], (2, 3)) + self.assertEqual(row[3:1], ()) + # Explicit bounds are optional. + self.assertEqual(row[1:], (2, 3, 4)) + self.assertEqual(row[:3], (1, 2, 3)) + # Slices can use negative indices. + self.assertEqual(row[-2:-1], (3,)) + self.assertEqual(row[-2:], (3, 4)) + # Slicing supports steps. + self.assertEqual(row[0:4:2], (1, 3)) + self.assertEqual(row[3:0:-2], (4, 2)) + + def CheckHashComparison(self): + row1 = self.conn.execute("SELECT 1 AS a, 2 AS b").fetchone() + row2 = self.conn.execute("SELECT 1 AS a, 2 AS b").fetchone() + row3 = self.conn.execute("SELECT 1 AS a, 3 AS b").fetchone() + + self.assertEqual(row1, row1) + self.assertEqual(row1, row2) + self.assertTrue(row2 != row3) + + self.assertFalse(row1 != row1) + self.assertFalse(row1 != row2) + self.assertFalse(row2 == row3) + + self.assertEqual(row1, row2) + self.assertEqual(hash(row1), hash(row2)) + self.assertNotEqual(row1, row3) + self.assertNotEqual(hash(row1), hash(row3)) + + with self.assertRaises(TypeError): + row1 > row2 + with self.assertRaises(TypeError): + row1 < row2 + with self.assertRaises(TypeError): + row1 >= row2 + with self.assertRaises(TypeError): + row1 <= row1 + + def CheckFakeCursorClass(self): + # Issue #24257: Incorrect use of PyObject_IsInstance() caused + # segmentation fault. + # Issue #27861: Also applies for cursor factory. + class FakeCursor(str): + __class__ = sqlite3.Cursor + + self.assertRaises(TypeError, self.conn.cursor, FakeCursor) + self.assertRaises(TypeError, sqlite3.NamedRow, FakeCursor(), ()) + + def CheckIterable(self): + """Is NamedRow iterable.""" + row = self.conn.execute("SELECT 1 AS a, 2 AS abcdefg").fetchone() + self.assertEqual(2, len(row)) + for col in row: + # Its is a key/value pair + self.assertEqual(2, len(col)) + + def CheckIterablePairs(self): + expected = (("a", 1), ("b", 2)) + row = self.conn.execute("SELECT 1 AS a, 2 AS b").fetchone() + self.assertSequenceEqual(expected, [x for x in row]) + self.assertSequenceEqual(expected, tuple(row)) + self.assertSequenceEqual(expected, list(row)) + self.assertSequenceEqual(dict(expected), dict(row)) + + def CheckExpectedExceptions(self): + row = self.conn.execute("SELECT 1 AS a").fetchone() + with self.assertRaises(IndexError): + row["no_such_field"] + with self.assertRaises(AttributeError): + row.no_such_field + with self.assertRaises(TypeError): + row[0] = 100 + with self.assertRaises(TypeError): + row["a"] = 100 + with self.assertRaises(TypeError) as err: + row.a = 100 + self.assertIn("does not support item assignment", str(err.exception)) + + with self.assertRaises(TypeError): + setattr(row, "a", 100) + + +def suite(): + return unittest.makeSuite(NamedRowFactoryTests, "Check") + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_sqlite.py b/Lib/test/test_sqlite.py index 73002f228fa70b..a3c644d7bc922d 100644 --- a/Lib/test/test_sqlite.py +++ b/Lib/test/test_sqlite.py @@ -1,26 +1,46 @@ import test.support from test.support import import_helper -# Skip test if _sqlite3 module not installed +# Skip test if _sqlite3 module not ins import_helper.import_module('_sqlite3') import unittest import sqlite3 -from sqlite3.test import (dbapi, types, userfunctions, - factory, transactions, hooks, regression, - dump, backup) +from sqlite3.test import ( + dbapi, + types, + userfunctions, + factory, + transactions, + hooks, + regression, + dump, + backup, + test_namedrow, +) + def load_tests(*args): if test.support.verbose: - print("test_sqlite: testing with version", - "{!r}, sqlite_version {!r}".format(sqlite3.version, - sqlite3.sqlite_version)) - return unittest.TestSuite([dbapi.suite(), types.suite(), - userfunctions.suite(), - factory.suite(), transactions.suite(), - hooks.suite(), regression.suite(), - dump.suite(), - backup.suite()]) + print( + "test_sqlite: testing with version", + "{!r}, sqlite_version {!r}".format(sqlite3.version, sqlite3.sqlite_version), + ) + return unittest.TestSuite( + [ + dbapi.suite(), + types.suite(), + userfunctions.suite(), + factory.suite(), + transactions.suite(), + hooks.suite(), + regression.suite(), + dump.suite(), + backup.suite(), + test_namedrow.suite(), + ] + ) + if __name__ == "__main__": unittest.main() diff --git a/Misc/ACKS b/Misc/ACKS index b585769608f4e9..764af48eb66c8c 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -792,6 +792,7 @@ Manuel Jacob David Jacobs Kevin Jacobs Kjetil Jacobsen +Clinton James Shantanu Jain Bertrand Janin Geert Jansen diff --git a/Misc/NEWS.d/next/Library/2019-12-31-14-45-27.bpo-39170.bh3u0Q.rst b/Misc/NEWS.d/next/Library/2019-12-31-14-45-27.bpo-39170.bh3u0Q.rst new file mode 100644 index 00000000000000..7f9b37ff0da159 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-12-31-14-45-27.bpo-39170.bh3u0Q.rst @@ -0,0 +1 @@ ++ Add sqlite3 row_factory for attribute access with the same memory requirements and speed of sqlite3.Row \ No newline at end of file diff --git a/Modules/Makefile b/Modules/Makefile new file mode 100644 index 00000000000000..c38daf40a1b063 --- /dev/null +++ b/Modules/Makefile @@ -0,0 +1,54 @@ + +# Rules appended by makesetup + +./posixmodule.o: $(srcdir)/./posixmodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -I$(srcdir)/Include/internal -c $(srcdir)/./posixmodule.c -o ./posixmodule.o +./posix$(EXT_SUFFIX): ./posixmodule.o; $(BLDSHARED) ./posixmodule.o -o ./posix$(EXT_SUFFIX) +./errnomodule.o: $(srcdir)/./errnomodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./errnomodule.c -o ./errnomodule.o +./errno$(EXT_SUFFIX): ./errnomodule.o; $(BLDSHARED) ./errnomodule.o -o ./errno$(EXT_SUFFIX) +./pwdmodule.o: $(srcdir)/./pwdmodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./pwdmodule.c -o ./pwdmodule.o +./pwd$(EXT_SUFFIX): ./pwdmodule.o; $(BLDSHARED) ./pwdmodule.o -o ./pwd$(EXT_SUFFIX) +./_sre.o: $(srcdir)/./_sre.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./_sre.c -o ./_sre.o +./_sre$(EXT_SUFFIX): ./_sre.o; $(BLDSHARED) ./_sre.o -o ./_sre$(EXT_SUFFIX) +./_codecsmodule.o: $(srcdir)/./_codecsmodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./_codecsmodule.c -o ./_codecsmodule.o +./_codecs$(EXT_SUFFIX): ./_codecsmodule.o; $(BLDSHARED) ./_codecsmodule.o -o ./_codecs$(EXT_SUFFIX) +./_weakref.o: $(srcdir)/./_weakref.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./_weakref.c -o ./_weakref.o +./_weakref$(EXT_SUFFIX): ./_weakref.o; $(BLDSHARED) ./_weakref.o -o ./_weakref$(EXT_SUFFIX) +./_functoolsmodule.o: $(srcdir)/./_functoolsmodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -I$(srcdir)/Include/internal -c $(srcdir)/./_functoolsmodule.c -o ./_functoolsmodule.o +./_functools$(EXT_SUFFIX): ./_functoolsmodule.o; $(BLDSHARED) ./_functoolsmodule.o -o ./_functools$(EXT_SUFFIX) +./_operator.o: $(srcdir)/./_operator.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./_operator.c -o ./_operator.o +./_operator$(EXT_SUFFIX): ./_operator.o; $(BLDSHARED) ./_operator.o -o ./_operator$(EXT_SUFFIX) +./_collectionsmodule.o: $(srcdir)/./_collectionsmodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./_collectionsmodule.c -o ./_collectionsmodule.o +./_collections$(EXT_SUFFIX): ./_collectionsmodule.o; $(BLDSHARED) ./_collectionsmodule.o -o ./_collections$(EXT_SUFFIX) +./_abc.o: $(srcdir)/./_abc.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./_abc.c -o ./_abc.o +./_abc$(EXT_SUFFIX): ./_abc.o; $(BLDSHARED) ./_abc.o -o ./_abc$(EXT_SUFFIX) +./itertoolsmodule.o: $(srcdir)/./itertoolsmodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./itertoolsmodule.c -o ./itertoolsmodule.o +./itertools$(EXT_SUFFIX): ./itertoolsmodule.o; $(BLDSHARED) ./itertoolsmodule.o -o ./itertools$(EXT_SUFFIX) +./atexitmodule.o: $(srcdir)/./atexitmodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./atexitmodule.c -o ./atexitmodule.o +./atexit$(EXT_SUFFIX): ./atexitmodule.o; $(BLDSHARED) ./atexitmodule.o -o ./atexit$(EXT_SUFFIX) +./signalmodule.o: $(srcdir)/./signalmodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -I$(srcdir)/Include/internal -c $(srcdir)/./signalmodule.c -o ./signalmodule.o +./_signal$(EXT_SUFFIX): ./signalmodule.o; $(BLDSHARED) ./signalmodule.o -o ./_signal$(EXT_SUFFIX) +./_stat.o: $(srcdir)/./_stat.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./_stat.c -o ./_stat.o +./_stat$(EXT_SUFFIX): ./_stat.o; $(BLDSHARED) ./_stat.o -o ./_stat$(EXT_SUFFIX) +./timemodule.o: $(srcdir)/./timemodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -I$(srcdir)/Include/internal -c $(srcdir)/./timemodule.c -o ./timemodule.o +./time$(EXT_SUFFIX): ./timemodule.o; $(BLDSHARED) ./timemodule.o -o ./time$(EXT_SUFFIX) +./_threadmodule.o: $(srcdir)/./_threadmodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -I$(srcdir)/Include/internal -c $(srcdir)/./_threadmodule.c -o ./_threadmodule.o +./_thread$(EXT_SUFFIX): ./_threadmodule.o; $(BLDSHARED) ./_threadmodule.o -o ./_thread$(EXT_SUFFIX) +./_localemodule.o: $(srcdir)/./_localemodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -c $(srcdir)/./_localemodule.c -o ./_localemodule.o +./_locale$(EXT_SUFFIX): ./_localemodule.o; $(BLDSHARED) ./_localemodule.o -o ./_locale$(EXT_SUFFIX) +./_iomodule.o: $(srcdir)/./_io/_iomodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -I$(srcdir)/Include/internal -I$(srcdir)/Modules/_io -c $(srcdir)/./_io/_iomodule.c -o ./_iomodule.o +./iobase.o: $(srcdir)/./_io/iobase.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -I$(srcdir)/Include/internal -I$(srcdir)/Modules/_io -c $(srcdir)/./_io/iobase.c -o ./iobase.o +./fileio.o: $(srcdir)/./_io/fileio.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -I$(srcdir)/Include/internal -I$(srcdir)/Modules/_io -c $(srcdir)/./_io/fileio.c -o ./fileio.o +./bytesio.o: $(srcdir)/./_io/bytesio.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -I$(srcdir)/Include/internal -I$(srcdir)/Modules/_io -c $(srcdir)/./_io/bytesio.c -o ./bytesio.o +./bufferedio.o: $(srcdir)/./_io/bufferedio.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -I$(srcdir)/Include/internal -I$(srcdir)/Modules/_io -c $(srcdir)/./_io/bufferedio.c -o ./bufferedio.o +./textio.o: $(srcdir)/./_io/textio.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -I$(srcdir)/Include/internal -I$(srcdir)/Modules/_io -c $(srcdir)/./_io/textio.c -o ./textio.o +./stringio.o: $(srcdir)/./_io/stringio.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -DPy_BUILD_CORE_BUILTIN -I$(srcdir)/Include/internal -I$(srcdir)/Modules/_io -c $(srcdir)/./_io/stringio.c -o ./stringio.o +./_io$(EXT_SUFFIX): ./_iomodule.o ./iobase.o ./fileio.o ./bytesio.o ./bufferedio.o ./textio.o ./stringio.o; $(BLDSHARED) ./_iomodule.o ./iobase.o ./fileio.o ./bytesio.o ./bufferedio.o ./textio.o ./stringio.o -o ./_io$(EXT_SUFFIX) +./faulthandler.o: $(srcdir)/./faulthandler.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./faulthandler.c -o ./faulthandler.o +./faulthandler$(EXT_SUFFIX): ./faulthandler.o; $(BLDSHARED) ./faulthandler.o -o ./faulthandler$(EXT_SUFFIX) +./_tracemalloc.o: $(srcdir)/./_tracemalloc.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./_tracemalloc.c -o ./_tracemalloc.o +./hashtable.o: $(srcdir)/./hashtable.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./hashtable.c -o ./hashtable.o +./_tracemalloc$(EXT_SUFFIX): ./_tracemalloc.o ./hashtable.o; $(BLDSHARED) ./_tracemalloc.o ./hashtable.o -o ./_tracemalloc$(EXT_SUFFIX) +./symtablemodule.o: $(srcdir)/./symtablemodule.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./symtablemodule.c -o ./symtablemodule.o +./_symtable$(EXT_SUFFIX): ./symtablemodule.o; $(BLDSHARED) ./symtablemodule.o -o ./_symtable$(EXT_SUFFIX) +./xxsubtype.o: $(srcdir)/./xxsubtype.c; $(CC) $(PY_BUILTIN_MODULE_CFLAGS) -c $(srcdir)/./xxsubtype.c -o ./xxsubtype.o +./xxsubtype$(EXT_SUFFIX): ./xxsubtype.o; $(BLDSHARED) ./xxsubtype.o -o ./xxsubtype$(EXT_SUFFIX) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 71d951ee887e47..4ff0b8ee94c370 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -28,6 +28,7 @@ #include "prepare_protocol.h" #include "microprotocols.h" #include "row.h" +#include "named_row.h" #if SQLITE_VERSION_NUMBER >= 3003003 #define HAVE_SHARED_CACHE @@ -364,6 +365,7 @@ PyMODINIT_FUNC PyInit__sqlite3(void) if (!module || (pysqlite_row_setup_types() < 0) || + (pysqlite_named_row_setup_types() < 0) || (pysqlite_cursor_setup_types() < 0) || (pysqlite_connection_setup_types() < 0) || (pysqlite_cache_setup_types() < 0) || @@ -378,6 +380,7 @@ PyMODINIT_FUNC PyInit__sqlite3(void) ADD_TYPE(module, pysqlite_CursorType); ADD_TYPE(module, pysqlite_PrepareProtocolType); ADD_TYPE(module, pysqlite_RowType); + ADD_TYPE(module, pysqlite_NamedRowType); if (!(dict = PyModule_GetDict(module))) { goto error; diff --git a/Modules/_sqlite/named_row.c b/Modules/_sqlite/named_row.c new file mode 100644 index 00000000000000..a3bc96812d7558 --- /dev/null +++ b/Modules/_sqlite/named_row.c @@ -0,0 +1,323 @@ +/* named_row.c - an row_factory with attribute access for database rows + * + * Copyright (C) 2019 Clinton James + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + */ + +#include "named_row.h" +#include "cursor.h" + +PyDoc_STRVAR(named_row__doc__, + "NamedRow is an optimized row_factory for column name access.\n" + "\n" + "NamedRow supports mapping by attribute, column name, and index.\n" + "Iteration gives field/value pairs similar to dict.items().\n" + "For attribute names that would be illegal, either alter the SQL\n" + "using the 'AS' statement (SELECT count(*) AS count FROM ...) \n" + "or index by name as in `row['count(*)']`.\n" + ); + +static void +named_row_dealloc(pysqlite_NamedRow* self) +{ + Py_XDECREF(self->data); + Py_XDECREF(self->fields); + Py_TYPE(self)->tp_free((PyObject*)self); +} + +static PyObject* +named_row_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + PyObject* data; + pysqlite_Cursor* cursor; + + assert(type != NULL && type->tp_alloc != NULL); + + if (!_PyArg_NoKeywords("NamedRow", kwargs)) { + return NULL; + } + if (!PyArg_ParseTuple(args, "OO", &cursor, &data)) { + return NULL; + } + + if (!PyObject_TypeCheck((PyObject*)cursor, &pysqlite_CursorType)) { + PyErr_SetString(PyExc_TypeError, + "instance of cursor required for first argument"); + return NULL; + } + + if (!PyTuple_Check(data)) { + PyErr_SetString(PyExc_TypeError, "tuple required for second argument"); + return NULL; + } + + pysqlite_NamedRow *self = (pysqlite_NamedRow *) type->tp_alloc(type, 0); + if (self == NULL) { + return NULL; + } + + Py_INCREF(data); + self->data = data; + + Py_INCREF(cursor->description); + self->fields = cursor->description; + + return (PyObject *) self; +} + +// Get data value by index +static PyObject* +named_row_item(pysqlite_NamedRow* self, Py_ssize_t idx) +{ + PyObject* item = PyTuple_GetItem(self->data, idx); + Py_XINCREF(item); + return item; +} + +// Find index from field name +static Py_ssize_t +named_row_find_index(pysqlite_NamedRow* self, PyObject* name) +{ + Py_ssize_t name_len = PyUnicode_GET_LENGTH(name); + Py_ssize_t nitems = PyTuple_Size(self->fields); + + for (Py_ssize_t i = 0; i < nitems; i++) { + PyObject* obj = PyTuple_GET_ITEM(PyTuple_GET_ITEM(self->fields, i), 0); + + if (name_len != PyUnicode_GET_LENGTH(obj)) { + continue; + } + + Py_ssize_t len = name_len; + const Py_UCS1 *p1 = PyUnicode_1BYTE_DATA(name); + const Py_UCS1 *p2 = PyUnicode_1BYTE_DATA(obj); + for (; len; len--, p1++, p2++) { + if (*p1 == *p2) { + return i; + } + } + } + return -1; +} + +static PyObject* +named_row_getattro(pysqlite_NamedRow* self, PyObject* attr_obj) +{ + // Calling function checked `name` is PyUnicode, no need to check here. + Py_INCREF(attr_obj); + Py_ssize_t idx = named_row_find_index(self, attr_obj); + Py_DECREF(attr_obj); + + if (idx < 0) { + return PyObject_GenericGetAttr((PyObject *)self, attr_obj); + } + + PyObject* value = PyTuple_GET_ITEM(self->data, idx); + Py_INCREF(value); + return value; +} + +static int +named_row_setattro(PyObject* self, PyObject* name, PyObject* value) +{ + PyErr_Format(PyExc_TypeError, + "'%.50s' object does not support item assignment", + Py_TYPE(self)->tp_name); + return -1; +} + +// Find the data value by either number or string +static PyObject* +named_row_index(pysqlite_NamedRow* self, PyObject* index) +{ + PyObject* item; + + if (PyLong_Check(index)) { + // Process integer index + Py_ssize_t idx = PyNumber_AsSsize_t(index, PyExc_IndexError); + if (idx == -1 && PyErr_Occurred()) { + return NULL; + } + if (idx < 0) { + idx += PyTuple_GET_SIZE(self->data); + } + item = PyTuple_GetItem(self->data, idx); + Py_XINCREF(item); + return item; + } + else if (PyUnicode_Check(index)) { + // Process string index, dict like + Py_ssize_t idx = named_row_find_index(self, index); + + if (idx < 0) { + PyErr_SetString(PyExc_IndexError, "No item with that key"); + return NULL; + } + item = PyTuple_GetItem(self->data, idx); + Py_INCREF(item); + return item; + } + else if (PySlice_Check(index)) { + return PyObject_GetItem(self->data, index); + } + else { + PyErr_SetString(PyExc_IndexError, "Index must be int or str"); + return NULL; + } +} + +static Py_ssize_t +named_row_length(pysqlite_NamedRow* self) +{ + return PyTuple_GET_SIZE(self->data); +} + +// Check for a field name in NamedRow +static int +named_row_contains(pysqlite_NamedRow* self, PyObject* name) +{ + if (PyUnicode_Check(name)) { + return named_row_find_index(self, name) < 0 ? 0 : 1; + } + return -1; +} + +static Py_hash_t +named_row_hash(pysqlite_NamedRow* self) +{ + return PyObject_Hash(self->fields) ^ PyObject_Hash(self->data); +} + +static PyObject* +named_row_richcompare(pysqlite_NamedRow *self, PyObject *_other, int opid) +{ + if (opid != Py_EQ && opid != Py_NE) { + Py_RETURN_NOTIMPLEMENTED; + } + + if (PyObject_TypeCheck(_other, &pysqlite_NamedRowType)) { + pysqlite_NamedRow *other = (pysqlite_NamedRow *)_other; + int eq = PyObject_RichCompareBool(self->fields, other->fields, Py_EQ); + if (eq < 0) { + return NULL; + } + if (eq) { + return PyObject_RichCompare(self->data, other->data, opid); + } + return PyBool_FromLong(opid != Py_EQ); + } + Py_RETURN_NOTIMPLEMENTED; +} + +PyMappingMethods named_row_as_mapping = { + .mp_length = (lenfunc)named_row_length, + .mp_subscript = (binaryfunc)named_row_index, +}; + +static PySequenceMethods named_row_as_sequence = { + .sq_length = (lenfunc)named_row_length, + .sq_item = (ssizeargfunc)named_row_item, + .was_sq_slice = (ssizeargfunc)named_row_item, + .sq_contains = (objobjproc)named_row_contains, +}; + +static PyObject* named_row_iterator(pysqlite_NamedRow* self); + +PyTypeObject pysqlite_NamedRowType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = MODULE_NAME ".NamedRow", + .tp_doc = named_row__doc__, + .tp_flags = Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, + .tp_basicsize = sizeof(pysqlite_NamedRow), + .tp_dealloc = (destructor)named_row_dealloc, + .tp_as_sequence = &named_row_as_sequence, + .tp_hash = (hashfunc)named_row_hash, + .tp_getattro = (getattrofunc)named_row_getattro, + .tp_setattro = (setattrofunc)named_row_setattro, + .tp_richcompare = (richcmpfunc)named_row_richcompare, + .tp_iter = (getiterfunc)named_row_iterator, +}; + +extern int pysqlite_named_row_setup_types(void) +{ + pysqlite_NamedRowType.tp_new = named_row_new; + pysqlite_NamedRowType.tp_as_mapping = &named_row_as_mapping; + pysqlite_NamedRowType.tp_as_sequence = &named_row_as_sequence; + return PyType_Ready(&pysqlite_NamedRowType); +} + + +/***************** Iterator *****************/ + +typedef struct _NamedRowIter{ + PyObject_HEAD + Py_ssize_t idx; + Py_ssize_t len; + pysqlite_NamedRow *row; +} NamedRowIter; + +static void +named_row_iter_dealloc(NamedRowIter *it) +{ + Py_XDECREF(it->row); +} + +static PyObject* +named_row_iter_iter(PyObject *self) +{ + Py_INCREF(self); + return self; +} + +static PyObject* +named_row_iter_next(PyObject *self) +{ + NamedRowIter *p = (NamedRowIter *)self; + if (p->idx >= p->len) + { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + PyObject *item = PyTuple_New(2); + + // Get the field name + PyObject *cursor_descript_field = PyTuple_GET_ITEM(p->row->fields, p->idx); + PyObject *key = PyTuple_GET_ITEM(cursor_descript_field, 0); + Py_INCREF(key); + PyTuple_SET_ITEM(item, 0, key); + + // Get the value + PyObject *value = PyTuple_GET_ITEM(p->row->data, p->idx); + Py_INCREF(value); + PyTuple_SET_ITEM(item, 1, value); + (p->idx)++; + return item; +} + +PyTypeObject NamedRowIterType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = MODULE_NAME ".NamedRowIter", + .tp_doc = "Internal NamedRow iterator", + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_basicsize = sizeof(NamedRowIter), + .tp_dealloc = (destructor)named_row_iter_dealloc, + .tp_iter = named_row_iter_iter, + .tp_iternext = named_row_iter_next, +}; + +static PyObject* +named_row_iterator(pysqlite_NamedRow* self) +{ + NamedRowIter *iter = PyObject_New(NamedRowIter, &NamedRowIterType); + if (!iter) { + return NULL; + } + Py_INCREF(self); + iter->idx = 0; + iter->len = PyTuple_GET_SIZE(self->data); + iter->row = self; + return (PyObject *)iter; +} diff --git a/Modules/_sqlite/named_row.h b/Modules/_sqlite/named_row.h new file mode 100644 index 00000000000000..aaa44f322cc8d2 --- /dev/null +++ b/Modules/_sqlite/named_row.h @@ -0,0 +1,38 @@ +/* named_row.h - an enhanced namedtuple for database rows + * + * Copyright (C) 2019 Clinton James + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#ifndef PYSQLITE_NAMED_ROW_H +#define PYSQLITE_NAMED_ROW_H +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +typedef struct _NamedRow +{ + PyObject_HEAD + PyObject* data; + PyObject* fields; +} pysqlite_NamedRow; + +extern PyTypeObject pysqlite_NamedRowType; + +int pysqlite_named_row_setup_types(void); + +#endif diff --git a/PCbuild/_sqlite3.vcxproj b/PCbuild/_sqlite3.vcxproj index 7e0062692b8f83..d53f174fc62792 100644 --- a/PCbuild/_sqlite3.vcxproj +++ b/PCbuild/_sqlite3.vcxproj @@ -105,6 +105,7 @@ + @@ -116,6 +117,7 @@ + @@ -135,4 +137,4 @@ - \ No newline at end of file + diff --git a/PCbuild/_sqlite3.vcxproj.filters b/PCbuild/_sqlite3.vcxproj.filters index 51830f6a4451a4..358c61142f7e3f 100644 --- a/PCbuild/_sqlite3.vcxproj.filters +++ b/PCbuild/_sqlite3.vcxproj.filters @@ -33,6 +33,9 @@ Header Files + + Header Files + Header Files @@ -62,6 +65,9 @@ Source Files + + Source Files + Source Files @@ -74,4 +80,4 @@ Resource Files - \ No newline at end of file + diff --git a/setup.py b/setup.py index 21a5a58981fc15..f5bd4928afe1d3 100644 --- a/setup.py +++ b/setup.py @@ -1522,6 +1522,7 @@ def detect_sqlite(self): '_sqlite/module.c', '_sqlite/prepare_protocol.c', '_sqlite/row.c', + '_sqlite/named_row.c', '_sqlite/statement.c', '_sqlite/util.c', ]