From 6faf28ae983f32728e951f91e115f068ceb253f6 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 3 Jun 2019 11:12:30 -0500 Subject: [PATCH 01/41] Use sqlcipher3 if possible. --- playhouse/sqlcipher_ext.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/playhouse/sqlcipher_ext.py b/playhouse/sqlcipher_ext.py index 96b0107f1..9bad1eca6 100644 --- a/playhouse/sqlcipher_ext.py +++ b/playhouse/sqlcipher_ext.py @@ -53,7 +53,10 @@ if sys.version_info[0] != 3: from pysqlcipher import dbapi2 as sqlcipher else: - from pysqlcipher3 import dbapi2 as sqlcipher + try: + from sqlcipher3 import dbapi2 as sqlcipher + except ImportError: + from pysqlcipher3 import dbapi2 as sqlcipher sqlcipher.register_adapter(decimal.Decimal, str) sqlcipher.register_adapter(datetime.date, str) From c051c91c9386ef88d2044100a1e64bdaa88ca998 Mon Sep 17 00:00:00 2001 From: Weidong Feng Date: Mon, 3 Jun 2019 15:16:17 +0800 Subject: [PATCH 02/41] replace instance delete method with model delete method --- peewee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peewee.py b/peewee.py index 87f9dc309..708c1d0a3 100644 --- a/peewee.py +++ b/peewee.py @@ -6245,7 +6245,7 @@ def delete_instance(self, recursive=False, delete_nullable=False): model.update(**{fk.name: None}).where(query).execute() else: model.delete().where(query).execute() - return self.delete().where(self._pk_expr()).execute() + return type(self).delete().where(self._pk_expr()).execute() def __hash__(self): return hash((self.__class__, self._pk)) From 323983c2ecf2ec70a14ed78ddd00cf5cd17d56e2 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 4 Jun 2019 06:51:19 -0500 Subject: [PATCH 03/41] Add new item to todos re: classmethods -> metaclass. [skip ci] --- TODO.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.rst b/TODO.rst index e8c73de2d..5371597f2 100644 --- a/TODO.rst +++ b/TODO.rst @@ -1,5 +1,6 @@ todo ==== +* move Model classmethods (select/insert/update/delete) to meta-class? * better schema-manager support for sequences (and views?) * additional examples in example dir From 125e27a97d38e37466c6f87d6dc43123fbef8ce4 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 10 Jun 2019 10:36:24 -0500 Subject: [PATCH 04/41] Workaround for windoze lusers without compilers. --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 98d3dac87..00c242536 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,9 @@ if 'sdist' in sys.argv and not cython_installed: raise Exception('Building sdist requires that Cython be installed.') +if sys.version_info[0] < 3: + FileNotFoundError = EnvironmentError + if cython_installed: src_ext = '.pyx' else: @@ -79,6 +82,8 @@ def _have_sqlite_extension_support(): print('unable to compile sqlite3 C extensions - no c compiler?') except DistutilsPlatformError: print('unable to compile sqlite3 C extensions - platform error') + except FileNotFoundError: + print('unable to compile sqlite3 C extensions - no compiler!') else: success = True shutil.rmtree(tmp_dir) From f76ce0bcadd2d7b7024e2e44614a91c102524d95 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 18 Jun 2019 15:03:04 -0500 Subject: [PATCH 05/41] Add Match helper to playhouse.mysql_ext based on #1955. --- playhouse/mysql_ext.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/playhouse/mysql_ext.py b/playhouse/mysql_ext.py index 8eb2a43fa..127764514 100644 --- a/playhouse/mysql_ext.py +++ b/playhouse/mysql_ext.py @@ -7,7 +7,10 @@ from peewee import ImproperlyConfigured from peewee import MySQLDatabase +from peewee import NodeList +from peewee import SQL from peewee import TextField +from peewee import fn class MySQLConnectorDatabase(MySQLDatabase): @@ -32,3 +35,12 @@ def db_value(self, value): def python_value(self, value): if value is not None: return json.loads(value) + + +def Match(columns, expr, modifier=None): + if isinstance(columns, (list, tuple)): + match = fn.MATCH(*columns) # Tuple of one or more columns / fields. + else: + match = fn.MATCH(columns) # Single column / field. + args = expr if modifier is None else NodeList((expr, SQL(modifier))) + return NodeList((match, fn.AGAINST(args))) From a8c02ec270958834b2b17ec398d76c896ea43427 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 18 Jun 2019 15:07:41 -0500 Subject: [PATCH 06/41] Docs and note in changelog. --- CHANGELOG.md | 3 +++ docs/peewee/playhouse.rst | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9980fabaf..29adadbba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ https://github.com/coleifer/peewee/releases ## master +* Add a helper to `playhouse.mysql_ext` for creating `Match` full-text search + expressions. + [View commits](https://github.com/coleifer/peewee/compare/3.9.6...master) ## 3.9.6 diff --git a/docs/peewee/playhouse.rst b/docs/peewee/playhouse.rst index 4ae80c4a4..d504e0ce7 100644 --- a/docs/peewee/playhouse.rst +++ b/docs/peewee/playhouse.rst @@ -1600,6 +1600,25 @@ Example usage: # MySQL database implementation that utilizes mysql-connector driver. db = MySQLConnectorDatabase('my_database', host='1.2.3.4', user='mysql') +Additional MySQL-specific helpers: + +.. py:class:: JSONField() + + Extends :py:class:`TextField` and implements transparent JSON encoding and + decoding in Python. + +.. py:function:: Match(columns, expr[, modifier=None]) + + :param columns: a single :py:class:`Field` or a tuple of multiple fields. + :param str expr: the full-text search expression. + :param str modifier: optional modifiers for the search, e.g. *'in boolean mode'*. + + Helper class for constructing MySQL full-text search queries of the form: + + .. code-block:: sql + + MATCH (columns, ...) AGAINST (expr[ modifier]) + .. _dataset: DataSet From 92b74983d2f0a6e1c214e3cc34bcaf227254523f Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 18 Jun 2019 15:14:53 -0500 Subject: [PATCH 07/41] Add test for Match() helper. --- tests/mysql_ext.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/mysql_ext.py b/tests/mysql_ext.py index ac6ba78c3..781b6b279 100644 --- a/tests/mysql_ext.py +++ b/tests/mysql_ext.py @@ -2,8 +2,10 @@ from peewee import * from playhouse.mysql_ext import JSONField +from playhouse.mysql_ext import Match from .base import IS_MYSQL_JSON +from .base import ModelDatabaseTestCase from .base import ModelTestCase from .base import TestModel from .base import db_loader @@ -90,3 +92,27 @@ def test_mysql_json_field(self): with self.assertRaises(IntegrityError): KJ.create(key='kx', data=None) + + +@requires_mysql +class TestMatchExpression(ModelDatabaseTestCase): + requires = [Person] + + def test_match_expression(self): + query = (Person + .select() + .where(Match(Person.first, 'charlie'))) + self.assertSQL(query, ( + 'SELECT "t1"."id", "t1"."first", "t1"."last", "t1"."dob" ' + 'FROM "person" AS "t1" ' + 'WHERE MATCH("t1"."first") AGAINST(?)'), ['charlie']) + + query = (Person + .select() + .where(Match((Person.first, Person.last), 'huey AND zaizee', + 'IN BOOLEAN MODE'))) + self.assertSQL(query, ( + 'SELECT "t1"."id", "t1"."first", "t1"."last", "t1"."dob" ' + 'FROM "person" AS "t1" ' + 'WHERE MATCH("t1"."first", "t1"."last") ' + 'AGAINST(? IN BOOLEAN MODE)'), ['huey AND zaizee']) From a5416c8b91bc9505afb4b3e6b1841771c259cd1d Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Wed, 19 Jun 2019 09:36:10 -0500 Subject: [PATCH 08/41] Add test for behavior of inherited IDs with dict_to_model. Refs #1956 --- tests/shortcuts.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/shortcuts.py b/tests/shortcuts.py index 4cc1d4c11..39cf0b24f 100644 --- a/tests/shortcuts.py +++ b/tests/shortcuts.py @@ -534,6 +534,28 @@ def test_unknown_attributes(self): inst = dict_to_model(User, data, ignore_unknown=True) self.assertEqual(inst.xx, 'does not exist') + def test_ignore_id_attribute(self): + class Register(Model): + key = CharField(primary_key=True) + + data = {'id': 100, 'key': 'k1'} + self.assertRaises(AttributeError, dict_to_model, Register, data) + + inst = dict_to_model(Register, data, ignore_unknown=True) + self.assertEqual(inst.__data__, {'key': 'k1'}) + + class Base(Model): + class Meta: + primary_key = False + + class Register2(Model): + key = CharField(primary_key=True) + + self.assertRaises(AttributeError, dict_to_model, Register2, data) + + inst = dict_to_model(Register2, data, ignore_unknown=True) + self.assertEqual(inst.__data__, {'key': 'k1'}) + class ReconnectMySQLDatabase(ReconnectMixin, MySQLDatabase): def cursor(self, commit): From 60b41b3c2f29d651d76dbe37716c941059293661 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Wed, 19 Jun 2019 15:39:23 -0500 Subject: [PATCH 09/41] Remove unneeded declarations. --- playhouse/_sqlite_ext.pyx | 2 -- 1 file changed, 2 deletions(-) diff --git a/playhouse/_sqlite_ext.pyx b/playhouse/_sqlite_ext.pyx index 4a04d8572..7fa3949e0 100644 --- a/playhouse/_sqlite_ext.pyx +++ b/playhouse/_sqlite_ext.pyx @@ -289,8 +289,6 @@ cdef extern from "_pysqlite/connection.h": sqlite3* db double timeout int initialized - PyObject* isolation_level - char* begin_statement cdef sqlite_to_python(int argc, sqlite3_value **params): From 840df71365482643020b140620e5a17b93953384 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Thu, 20 Jun 2019 07:49:39 -0500 Subject: [PATCH 10/41] Add regression test for creating model with no primary key. --- tests/regressions.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/regressions.py b/tests/regressions.py index 1cbfde600..c127bbb48 100644 --- a/tests/regressions.py +++ b/tests/regressions.py @@ -869,3 +869,24 @@ def test_multi_fk_join_regression(self): ('r11', 'u1', 'u1'), ('r12', 'u1', 'u2'), ('r21', 'u2', 'u1')]) + + +class NoPK(TestModel): + val1 = IntegerField(index=True) + val2 = IntegerField() + + class Meta: + primary_key = False + + +class TestNoPKSaveRegression(ModelTestCase): + requires = [NoPK] + + def test_no_pk_save_regression(self): + obj = NoPK.create(val1=1, val2=2) + self.assertEqual(obj.val1, 1) + self.assertEqual(obj.val2, 2) + + obj_db = NoPK.get(NoPK.val1 == 1) + self.assertEqual(obj_db.val1, 1) + self.assertEqual(obj_db.val2, 2) From 005f27b4faa1ba517aee1838cfbf7fb4e5b1471c Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Thu, 20 Jun 2019 07:58:04 -0500 Subject: [PATCH 11/41] Allow save() hooks when model has no primary key. Fixes #1959 --- playhouse/signals.py | 2 +- tests/regressions.py | 21 --------------------- tests/signals.py | 26 ++++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/playhouse/signals.py b/playhouse/signals.py index f070bdfdb..4e92872e5 100644 --- a/playhouse/signals.py +++ b/playhouse/signals.py @@ -65,7 +65,7 @@ def __init__(self, *args, **kwargs): pre_init.send(self) def save(self, *args, **kwargs): - pk_value = self._pk + pk_value = self._pk if self._meta.primary_key else True created = kwargs.get('force_insert', False) or not bool(pk_value) pre_save.send(self, created=created) ret = super(Model, self).save(*args, **kwargs) diff --git a/tests/regressions.py b/tests/regressions.py index c127bbb48..1cbfde600 100644 --- a/tests/regressions.py +++ b/tests/regressions.py @@ -869,24 +869,3 @@ def test_multi_fk_join_regression(self): ('r11', 'u1', 'u1'), ('r12', 'u1', 'u2'), ('r21', 'u2', 'u1')]) - - -class NoPK(TestModel): - val1 = IntegerField(index=True) - val2 = IntegerField() - - class Meta: - primary_key = False - - -class TestNoPKSaveRegression(ModelTestCase): - requires = [NoPK] - - def test_no_pk_save_regression(self): - obj = NoPK.create(val1=1, val2=2) - self.assertEqual(obj.val1, 1) - self.assertEqual(obj.val2, 2) - - obj_db = NoPK.get(NoPK.val1 == 1) - self.assertEqual(obj_db.val1, 1) - self.assertEqual(obj_db.val2, 2) diff --git a/tests/signals.py b/tests/signals.py index b2cdae54e..16d86424c 100644 --- a/tests/signals.py +++ b/tests/signals.py @@ -156,3 +156,29 @@ def post_save(sender, instance, created): b = SubB.create() assert b in state + + +class NoPK(BaseSignalModel): + val = IntegerField(index=True) + class Meta: + primary_key = False + + +class TestSaveNoPrimaryKey(ModelTestCase): + database = get_in_memory_db() + requires = [NoPK] + + def test_save_no_pk(self): + accum = [0] + + @signals.pre_save(sender=NoPK) + @signals.post_save(sender=NoPK) + def save_hook(sender, instance, created): + accum[0] += 1 + + obj = NoPK.create(val=1) + self.assertEqual(obj.val, 1) + + obj_db = NoPK.get(NoPK.val == 1) + self.assertEqual(obj_db.val, 1) + self.assertEqual(accum[0], 2) From ff630ee0c1c2f9af78c2d8a5fb75a108e3ca049e Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Fri, 21 Jun 2019 12:43:24 -0500 Subject: [PATCH 12/41] Allow add (concat) operation for string-typed fields on left and right. Replaces #1960 --- peewee.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/peewee.py b/peewee.py index 3204edb34..aa500c946 100644 --- a/peewee.py +++ b/peewee.py @@ -4423,8 +4423,8 @@ def adapt(self, value): return value.decode('utf-8') return text_type(value) - def __add__(self, other): return self.concat(other) - def __radd__(self, other): return other.concat(self) + def __add__(self, other): return StringExpression(self, OP.CONCAT, other) + def __radd__(self, other): return StringExpression(other, OP.CONCAT, self) class CharField(_StringField): From b36a4cf96d7d23c58ede0573fe55c4699abcc557 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Fri, 21 Jun 2019 15:41:02 -0500 Subject: [PATCH 13/41] Add `to_timestamp()` helper to datetime/date-fields. --- peewee.py | 18 ++++++++++++++++++ tests/fields.py | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/peewee.py b/peewee.py index aa500c946..999c9f16b 100644 --- a/peewee.py +++ b/peewee.py @@ -3141,6 +3141,9 @@ def extract_date(self, date_part, date_field): def truncate_date(self, date_part, date_field): raise NotImplementedError + def to_timestamp(self, date_field): + raise NotImplementedError + def bind(self, models, bind_refs=True, bind_backrefs=True): for model in models: model.bind(self, bind_refs=bind_refs, bind_backrefs=bind_backrefs) @@ -3529,6 +3532,9 @@ def extract_date(self, date_part, date_field): def truncate_date(self, date_part, date_field): return fn.date_trunc(date_part, date_field) + def to_timestamp(self, date_field): + return fn.strftime('%s', date_field).cast('integer') + class PostgresqlDatabase(Database): field_types = { @@ -3695,6 +3701,9 @@ def extract_date(self, date_part, date_field): def truncate_date(self, date_part, date_field): return fn.DATE_TRUNC(date_part, date_field) + def to_timestamp(self, date_field): + return self.extract_date('EPOCH', date_field) + def get_noop_select(self, ctx): return ctx.sql(Select().columns(SQL('0')).where(SQL('false'))) @@ -3878,6 +3887,9 @@ def extract_date(self, date_part, date_field): def truncate_date(self, date_part, date_field): return fn.DATE_FORMAT(date_field, __mysql_date_trunc__[date_part]) + def to_timestamp(self, date_field): + return fn.UNIX_TIMESTAMP(date_field) + def get_noop_select(self, ctx): return ctx.literal('DO 0') @@ -4675,6 +4687,9 @@ def adapt(self, value): return format_date_time(value, self.formats) return value + def to_timestamp(self): + return self.model._meta.database.to_timestamp(self) + year = property(_date_part('year')) month = property(_date_part('month')) day = property(_date_part('day')) @@ -4699,6 +4714,9 @@ def adapt(self, value): return value.date() return value + def to_timestamp(self): + return self.model._meta.database.to_timestamp(self) + year = property(_date_part('year')) month = property(_date_part('month')) day = property(_date_part('day')) diff --git a/tests/fields.py b/tests/fields.py index c75c619a5..343de1a24 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -250,6 +250,23 @@ def test_extract_parts(self): 2011., 1., 2., 11., 12., 13.054321, 2012., 2., 3., 3., 13., 37.)) + def test_to_timestamp(self): + dt = datetime.datetime(2019, 1, 2, 3, 4, 5) + ts = calendar.timegm(dt.utctimetuple()) + + dt2 = datetime.datetime(2019, 1, 3) + ts2 = calendar.timegm(dt2.utctimetuple()) + + DateModel.create(date_time=dt, date=dt2.date()) + + query = DateModel.select( + DateModel.date_time.to_timestamp().alias('dt_ts'), + DateModel.date.to_timestamp().alias('dt2_ts')) + obj = query.get() + + self.assertEqual(obj.dt_ts, ts) + self.assertEqual(obj.dt2_ts, ts2) + def test_distinct_date_part(self): years = (1980, 1990, 2000, 2010) for i, year in enumerate(years): @@ -1025,7 +1042,7 @@ def test_date_time_math_pg(self): def test_date_time_math_sqlite(self): # Convert to a timestamp, add the scheduled seconds, then convert back # to a datetime string for comparison with the last occurrence. - next_ts = fn.strftime('%s', Task.last_run) + Schedule.interval + next_ts = Task.last_run.to_timestamp() + Schedule.interval next_occurrence = fn.datetime(next_ts, 'unixepoch') self._do_test_date_time_math(next_occurrence) From f4834dba4a3d60f40852c7d690564f197a292c2e Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Fri, 21 Jun 2019 15:50:20 -0500 Subject: [PATCH 14/41] Add docs and additional test case for to_timestamp() helper. --- docs/peewee/api.rst | 21 +++++++++++++++++++++ tests/fields.py | 10 ++++++++++ 2 files changed, 31 insertions(+) diff --git a/docs/peewee/api.rst b/docs/peewee/api.rst index 4703eb156..3c74810f8 100644 --- a/docs/peewee/api.rst +++ b/docs/peewee/api.rst @@ -3019,6 +3019,23 @@ Fields Reference the second of the value stored in the column in a query. + .. py:method:: to_timestamp() + + Method that returns a database-specific function call that will allow + you to work with the given date-time value as a numeric timestamp. This + can sometimes simplify tasks like date math in a compatible way. + + Example: + + .. code-block:: python + + # Find all events that are exactly 1 hour long. + query = (Event + .select() + .where((Event.start.to_timestamp() + 3600) == + Event.stop.to_timestamp()) + .order_by(Event.start)) + .. py:class:: DateField([formats=None[, **kwargs]]) :param list formats: A list of format strings to use when coercing a string @@ -3055,6 +3072,10 @@ Fields Reference the day of the value stored in the column in a query. + .. py:method:: to_timestamp() + + See :py:meth:`DateTimeField.to_timestamp`. + .. py:class:: TimeField([formats=None[, **kwargs]]) :param list formats: A list of format strings to use when coercing a string diff --git a/tests/fields.py b/tests/fields.py index 343de1a24..3cf551161 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -260,6 +260,7 @@ def test_to_timestamp(self): DateModel.create(date_time=dt, date=dt2.date()) query = DateModel.select( + DateModel.id, DateModel.date_time.to_timestamp().alias('dt_ts'), DateModel.date.to_timestamp().alias('dt2_ts')) obj = query.get() @@ -267,6 +268,15 @@ def test_to_timestamp(self): self.assertEqual(obj.dt_ts, ts) self.assertEqual(obj.dt2_ts, ts2) + ts3 = ts + 86400 + query = (DateModel.select() + .where((DateModel.date_time.to_timestamp() + 86400) < ts3)) + self.assertRaises(DateModel.DoesNotExist, query.get) + + query = (DateModel.select() + .where((DateModel.date.to_timestamp() + 86400) > ts3)) + self.assertEqual(query.get().id, obj.id) + def test_distinct_date_part(self): years = (1980, 1990, 2000, 2010) for i, year in enumerate(years): From 553b9e25cf03e008e15429b875ad7a2063aeac8e Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Fri, 21 Jun 2019 19:51:06 -0500 Subject: [PATCH 15/41] Add date part properties and helpers to TimestampField. Adds helper properties and methods to TimestampField which should simplify converting to-and-from native datetime types in the database, as well as extracting portions from the datetime being represented. --- peewee.py | 52 +++++++++++++++++++++++++++++------ tests/fields.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 113 insertions(+), 12 deletions(-) diff --git a/peewee.py b/peewee.py index 999c9f16b..fe77907c8 100644 --- a/peewee.py +++ b/peewee.py @@ -3144,6 +3144,9 @@ def truncate_date(self, date_part, date_field): def to_timestamp(self, date_field): raise NotImplementedError + def from_timestamp(self, date_field): + raise NotImplementedError + def bind(self, models, bind_refs=True, bind_backrefs=True): for model in models: model.bind(self, bind_refs=bind_refs, bind_backrefs=bind_backrefs) @@ -3535,6 +3538,9 @@ def truncate_date(self, date_part, date_field): def to_timestamp(self, date_field): return fn.strftime('%s', date_field).cast('integer') + def from_timestamp(self, date_field): + return fn.datetime(date_field, 'unixepoch') + class PostgresqlDatabase(Database): field_types = { @@ -3704,6 +3710,10 @@ def truncate_date(self, date_part, date_field): def to_timestamp(self, date_field): return self.extract_date('EPOCH', date_field) + def from_timestamp(self, date_field): + # Ironically, here, Postgres means "to the Postgresql timestamp type". + return fn.to_timestamp(date_field) + def get_noop_select(self, ctx): return ctx.sql(Select().columns(SQL('0')).where(SQL('false'))) @@ -3890,6 +3900,9 @@ def truncate_date(self, date_part, date_field): def to_timestamp(self, date_field): return fn.UNIX_TIMESTAMP(date_field) + def from_timestamp(self, date_field): + return fn.FROM_UNIXTIME(date_field) + def get_noop_select(self, ctx): return ctx.literal('DO 0') @@ -4748,6 +4761,15 @@ def adapt(self, value): second = property(_date_part('second')) +def _timestamp_date_part(date_part): + def dec(self): + db = self.model._meta.database + expr = ((self / Value(self.resolution, converter=False)) + if self.resolution > 1 else self) + return db.extract_date(date_part, db.from_timestamp(expr)) + return dec + + class TimestampField(BigIntegerField): # Support second -> microsecond resolution. valid_resolutions = [10**i for i in range(7)] @@ -4761,6 +4783,7 @@ def __init__(self, *args, **kwargs): elif self.resolution not in self.valid_resolutions: raise ValueError('TimestampField resolution must be one of: %s' % ', '.join(str(i) for i in self.valid_resolutions)) + self.ticks_to_microsecond = 1000000 // self.resolution self.utc = kwargs.pop('utc', False) or False dflt = datetime.datetime.utcnow if self.utc else datetime.datetime.now @@ -4782,6 +4805,13 @@ def utc_to_local(self, dt): ts = calendar.timegm(dt.utctimetuple()) return datetime.datetime.fromtimestamp(ts) + def get_timestamp(self, value): + if self.utc: + # If utc-mode is on, then we assume all naive datetimes are in UTC. + return calendar.timegm(value.utctimetuple()) + else: + return time.mktime(value.timetuple()) + def db_value(self, value): if value is None: return @@ -4793,12 +4823,7 @@ def db_value(self, value): else: return int(round(value * self.resolution)) - if self.utc: - # If utc-mode is on, then we assume all naive datetimes are in UTC. - timestamp = calendar.timegm(value.utctimetuple()) - else: - timestamp = time.mktime(value.timetuple()) - + timestamp = self.get_timestamp(value) if self.resolution > 1: timestamp += (value.microsecond * .000001) timestamp *= self.resolution @@ -4807,9 +4832,8 @@ def db_value(self, value): def python_value(self, value): if value is not None and isinstance(value, (int, float, long)): if self.resolution > 1: - ticks_to_microsecond = 1000000 // self.resolution value, ticks = divmod(value, self.resolution) - microseconds = int(ticks * ticks_to_microsecond) + microseconds = int(ticks * self.ticks_to_microsecond) else: microseconds = 0 @@ -4823,6 +4847,18 @@ def python_value(self, value): return value + def from_timestamp(self): + expr = ((self / Value(self.resolution, converter=False)) + if self.resolution > 1 else self) + return self.model._meta.database.from_timestamp(expr) + + year = property(_timestamp_date_part('year')) + month = property(_timestamp_date_part('month')) + day = property(_timestamp_date_part('day')) + hour = property(_timestamp_date_part('hour')) + minute = property(_timestamp_date_part('minute')) + second = property(_timestamp_date_part('second')) + class IPField(BigIntegerField): def db_value(self, val): diff --git a/tests/fields.py b/tests/fields.py index 3cf551161..c0d6d4ca6 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -869,6 +869,7 @@ class TSModel(TestModel): ts_s = TimestampField() ts_us = TimestampField(resolution=10 ** 6) ts_ms = TimestampField(resolution=3) # Milliseconds. + ts_u = TimestampField(null=True, utc=True) class TestTimestampField(ModelTestCase): @@ -877,20 +878,23 @@ class TestTimestampField(ModelTestCase): def test_timestamp_field(self): dt = datetime.datetime(2018, 3, 1, 3, 3, 7) dt = dt.replace(microsecond=31337) # us=031_337, ms=031. - ts = TSModel.create(ts_s=dt, ts_us=dt, ts_ms=dt) + ts = TSModel.create(ts_s=dt, ts_us=dt, ts_ms=dt, ts_u=dt) ts_db = TSModel.get(TSModel.id == ts.id) self.assertEqual(ts_db.ts_s, dt.replace(microsecond=0)) self.assertEqual(ts_db.ts_ms, dt.replace(microsecond=31000)) self.assertEqual(ts_db.ts_us, dt) + self.assertEqual(ts_db.ts_u, dt.replace(microsecond=0)) self.assertEqual(TSModel.get(TSModel.ts_s == dt).id, ts.id) self.assertEqual(TSModel.get(TSModel.ts_ms == dt).id, ts.id) self.assertEqual(TSModel.get(TSModel.ts_us == dt).id, ts.id) + self.assertEqual(TSModel.get(TSModel.ts_u == dt).id, ts.id) def test_timestamp_field_value_as_ts(self): dt = datetime.datetime(2018, 3, 1, 3, 3, 7, 31337) unix_ts = time.mktime(dt.timetuple()) + 0.031337 - ts = TSModel.create(ts_s=unix_ts, ts_us=unix_ts, ts_ms=unix_ts) + ts = TSModel.create(ts_s=unix_ts, ts_us=unix_ts, ts_ms=unix_ts, + ts_u=unix_ts) # Fetch from the DB and validate the values were stored correctly. ts_db = TSModel[ts.id] @@ -898,18 +902,22 @@ def test_timestamp_field_value_as_ts(self): self.assertEqual(ts_db.ts_ms, dt.replace(microsecond=31000)) self.assertEqual(ts_db.ts_us, dt) + utc_dt = TimestampField().local_to_utc(dt) + self.assertEqual(ts_db.ts_u, utc_dt) + # Verify we can query using a timestamp. self.assertEqual(TSModel.get(TSModel.ts_s == unix_ts).id, ts.id) self.assertEqual(TSModel.get(TSModel.ts_ms == unix_ts).id, ts.id) self.assertEqual(TSModel.get(TSModel.ts_us == unix_ts).id, ts.id) + self.assertEqual(TSModel.get(TSModel.ts_u == unix_ts).id, ts.id) def test_timestamp_utc_vs_localtime(self): local_field = TimestampField() utc_field = TimestampField(utc=True) dt = datetime.datetime(2019, 1, 1, 12) - unix_ts = int(time.mktime(dt.timetuple())) - utc_ts = int(calendar.timegm(dt.utctimetuple())) + unix_ts = int(local_field.get_timestamp(dt)) + utc_ts = int(utc_field.get_timestamp(dt)) # Local timestamp is unmodified. Verify that when utc=True, the # timestamp is converted from local time to UTC. @@ -926,6 +934,63 @@ def test_timestamp_utc_vs_localtime(self): dbv, pyv = utc_field.db_value, utc_field.python_value self.assertEqual(pyv(dbv(pyv(dbv(dt)))), dt) + def test_timestamp_field_parts(self): + dt = datetime.datetime(2019, 1, 2, 3, 4, 5) + dt_utc = TimestampField().local_to_utc(dt) + ts = TSModel.create(ts_s=dt, ts_us=dt, ts_ms=dt, ts_u=dt_utc) + + fields = (TSModel.ts_s, TSModel.ts_us, TSModel.ts_ms, TSModel.ts_u) + attrs = ('year', 'month', 'day', 'hour', 'minute', 'second') + selection = [] + for field in fields: + for attr in attrs: + selection.append(getattr(field, attr)) + + row = TSModel.select(*selection).tuples()[0] + + # First ensure that all 3 fields are returning the same data. + ts_s, ts_us, ts_ms, ts_u = row[:6], row[6:12], row[12:18], row[18:] + self.assertEqual(ts_s, ts_us) + self.assertEqual(ts_s, ts_ms) + self.assertEqual(ts_s, ts_u) + + # Now validate that the data is correct. We will receive the data back + # as a UTC unix timestamp, however! + y, m, d, H, M, S = ts_s + self.assertEqual(y, 2019) + self.assertEqual(m, 1) + self.assertEqual(d, 2) + self.assertEqual(H, dt_utc.hour) + self.assertEqual(M, 4) + self.assertEqual(S, 5) + + def test_timestamp_field_from_ts(self): + dt = datetime.datetime(2019, 1, 2, 3, 4, 5) + dt_utc = TimestampField().local_to_utc(dt) + + ts = TSModel.create(ts_s=dt, ts_us=dt, ts_ms=dt, ts_u=dt_utc) + query = TSModel.select( + TSModel.ts_s.from_timestamp().alias('dt_s'), + TSModel.ts_us.from_timestamp().alias('dt_us'), + TSModel.ts_ms.from_timestamp().alias('dt_ms'), + TSModel.ts_u.from_timestamp().alias('dt_u')) + + # Get row and unpack into variables corresponding to the fields. + row = query.tuples()[0] + dt_s, dt_us, dt_ms, dt_u = row + + # Ensure the timestamp values for all 4 fields are the same. + self.assertEqual(dt_s, dt_us) + self.assertEqual(dt_s, dt_ms) + self.assertEqual(dt_s, dt_u) + if IS_SQLITE: + expected = dt_utc.strftime('%Y-%m-%d %H:%M:%S') + self.assertEqual(dt_s, expected) + elif IS_POSTGRESQL: + # Postgres returns an aware UTC datetime. Strip this to compare + # against our naive UTC datetime. + self.assertEqual(dt_s.replace(tzinfo=None), dt_utc) + def test_invalid_resolution(self): self.assertRaises(ValueError, TimestampField, resolution=7) self.assertRaises(ValueError, TimestampField, resolution=20) From cdf9256fb75930c6c5fcc952170f25020b162b9c Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Sun, 23 Jun 2019 15:44:44 -0500 Subject: [PATCH 16/41] Add `autoconnect` parameter to Database class. Prior to this patch, Peewee would always open a connection on-demand if a query was executed. This patch adds a new parameter to the Database initializer which can disable this "on-demand" behavior. --- docs/peewee/api.rst | 4 +++- docs/peewee/database.rst | 24 +++++++++++++++++------- peewee.py | 9 +++++++-- tests/database.py | 7 +++++++ 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/docs/peewee/api.rst b/docs/peewee/api.rst index 3c74810f8..6a866e98a 100644 --- a/docs/peewee/api.rst +++ b/docs/peewee/api.rst @@ -8,7 +8,7 @@ This document specifies Peewee's APIs. Database -------- -.. py:class:: Database(database[, thread_safe=True[, autorollback=False[, field_types=None[, operations=None[, **kwargs]]]]]) +.. py:class:: Database(database[, thread_safe=True[, autorollback=False[, field_types=None[, operations=None[, autoconnect=True[, **kwargs]]]]]]) :param str database: Database name or filename for SQLite (or ``None`` to :ref:`defer initialization `, in which case @@ -19,6 +19,8 @@ Database **not** in an explicit transaction. :param dict field_types: A mapping of additional field types to support. :param dict operations: A mapping of additional operations to support. + :param bool autoconnect: Automatically connect to database if attempting to + execute a query on a closed database. :param kwargs: Arbitrary keyword arguments that will be passed to the database driver when a connection is created, for example ``password``, ``host``, etc. diff --git a/docs/peewee/database.rst b/docs/peewee/database.rst index 1fdd2c780..23a96d318 100644 --- a/docs/peewee/database.rst +++ b/docs/peewee/database.rst @@ -760,16 +760,26 @@ You can test whether the database is closed using the >>> db.is_closed() True -A note of caution +Using autoconnect ^^^^^^^^^^^^^^^^^ -Although it is not necessary to explicitly connect to the database before using -it, managing connections explicitly is considered a **best practice**. For -example, if the connection fails, the exception will be caught when the +It is not necessary to explicitly connect to the database before using +it if the database is initialized with ``autoconnect=True`` (the default). +Managing connections explicitly is considered a **best practice**, therefore +you may consider disabling the ``autoconnect`` behavior. + +It is very helpful to be explicit about your connection lifetimes. If the +connection fails, for instance, the exception will be caught when the connection is being opened, rather than some arbitrary time later when a query -is executed. Furthermore, if you are using a :ref:`connection pool `, it -is necessary to call :py:meth:`~Database.connect` and -:py:meth:`~Database.close` to ensure connections are recycled properly. +is executed. Furthermore, if using a :ref:`connection pool `, it is +necessary to call :py:meth:`~Database.connect` and :py:meth:`~Database.close` +to ensure connections are recycled properly. + +For the best guarantee of correctness, disable ``autoconnect``: + +.. code-block:: python + + db = PostgresqlDatabase('my_app', user='postgres', autoconnect=False) Thread Safety ^^^^^^^^^^^^^ diff --git a/peewee.py b/peewee.py index fe77907c8..6fded9b32 100644 --- a/peewee.py +++ b/peewee.py @@ -2817,7 +2817,8 @@ class Database(_callable_context_manager): truncate_table = True def __init__(self, database, thread_safe=True, autorollback=False, - field_types=None, operations=None, autocommit=None, **kwargs): + field_types=None, operations=None, autocommit=None, + autoconnect=True, **kwargs): self._field_types = merge_dict(FIELD, self.field_types) self._operations = merge_dict(OP, self.operations) if field_types: @@ -2825,6 +2826,7 @@ def __init__(self, database, thread_safe=True, autorollback=False, if operations: self._operations.update(operations) + self.autoconnect = autoconnect self.autorollback = autorollback self.thread_safe = thread_safe if thread_safe: @@ -2930,7 +2932,10 @@ def connection(self): def cursor(self, commit=None): if self.is_closed(): - self.connect() + if self.autoconnect: + self.connect() + else: + raise InterfaceError('Error, database connection not opened.') return self._state.conn.cursor() def execute_sql(self, sql, params=None, commit=SENTINEL): diff --git a/tests/database.py b/tests/database.py index 8372ea8f0..52d2d2796 100644 --- a/tests/database.py +++ b/tests/database.py @@ -293,6 +293,13 @@ def _set_server_version(self, conn): self.assertEqual(db.server_version, (1, 2, 3)) db.close() + def test_explicit_connect(self): + db = get_in_memory_db(autoconnect=False) + self.assertRaises(InterfaceError, db.execute_sql, 'pragma cache_size') + with db: + db.execute_sql('pragma cache_size') + self.assertRaises(InterfaceError, db.cursor) + class TestThreadSafety(ModelTestCase): nthreads = 4 From 127f94e27bb9aabcb75816ab72af3f10ed4c58f3 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 24 Jun 2019 10:38:01 -0500 Subject: [PATCH 17/41] Unwrap expression LHS node in attempt to populate converter. --- peewee.py | 12 ++++++++++-- tests/fields.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/peewee.py b/peewee.py index 6fded9b32..6f9668164 100644 --- a/peewee.py +++ b/peewee.py @@ -1376,8 +1376,16 @@ def __init__(self, lhs, op, rhs, flat=False): def __sql__(self, ctx): overrides = {'parentheses': not self.flat, 'in_expr': True} - if isinstance(self.lhs, Field): - overrides['converter'] = self.lhs.db_value + + # First attempt to unwrap the node on the left-hand-side, so that we + # can get at the underlying Field if one is present. + node = self.lhs + if isinstance(node, WrappedNode): + node = node.unwrap() + + # Set up the appropriate converter if we have a field on the left side. + if isinstance(node, Field): + overrides['converter'] = node.db_value else: overrides['converter'] = None diff --git a/tests/fields.py b/tests/fields.py index c0d6d4ca6..2917861b8 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -890,6 +890,24 @@ def test_timestamp_field(self): self.assertEqual(TSModel.get(TSModel.ts_us == dt).id, ts.id) self.assertEqual(TSModel.get(TSModel.ts_u == dt).id, ts.id) + def test_timestamp_field_math(self): + dt = datetime.datetime(2019, 1, 2, 3, 4, 5, 31337) + ts = TSModel.create(ts_s=dt, ts_us=dt, ts_ms=dt) + + # Although these fields use different scales for storing the + # timestamps, adding "1" has the effect of adding a single second - + # the value will be multiplied by the correct scale via the converter. + TSModel.update( + ts_s=TSModel.ts_s + 1, + ts_us=TSModel.ts_us + 1, + ts_ms=TSModel.ts_ms + 1).execute() + + ts_db = TSModel.get(TSModel.id == ts.id) + dt2 = dt + datetime.timedelta(seconds=1) + self.assertEqual(ts_db.ts_s, dt2.replace(microsecond=0)) + self.assertEqual(ts_db.ts_us, dt2) + self.assertEqual(ts_db.ts_ms, dt2.replace(microsecond=31000)) + def test_timestamp_field_value_as_ts(self): dt = datetime.datetime(2018, 3, 1, 3, 3, 7, 31337) unix_ts = time.mktime(dt.timetuple()) + 0.031337 From 754d87cff8b5511ab4a8ba1d95c3cd9df9259588 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 24 Jun 2019 14:21:50 -0500 Subject: [PATCH 18/41] Add database-agnostic interface for random numbers. --- docs/peewee/api.rst | 30 ++++++++++++++++++++++++++++++ peewee.py | 6 ++++++ 2 files changed, 36 insertions(+) diff --git a/docs/peewee/api.rst b/docs/peewee/api.rst index 6a866e98a..73a9c2bba 100644 --- a/docs/peewee/api.rst +++ b/docs/peewee/api.rst @@ -534,6 +534,36 @@ Database # ... models are bound to test database ... pass + .. py:method:: extract_date(date_part, date_field) + + :param str date_part: date part to extract, e.g. 'year'. + :param Node date_field: a SQL node containing a date/time, for example + a :py:class:`DateTimeField`. + :returns: a SQL node representing a function call that will return the + provided date part. + + Provides a compatible interface for extracting a portion of a datetime. + + .. py:method:: truncate_date(date_part, date_field) + + :param str date_part: date part to truncate to, e.g. 'day'. + :param Node date_field: a SQL node containing a date/time, for example + a :py:class:`DateTimeField`. + :returns: a SQL node representing a function call that will return the + truncated date part. + + Provides a compatible interface for truncating a datetime to the given + resolution. + + .. py:method:: random() + + :returns: a SQL node representing a function call that returns a random + value. + + A compatible interface for calling the appropriate random number + generation function provided by the database. For Postgres and Sqlite, + this is equivalent to ``fn.random()``, for MySQL ``fn.rand()``. + .. py:class:: SqliteDatabase(database[, pragmas=None[, timeout=5[, **kwargs]]]) diff --git a/peewee.py b/peewee.py index 6f9668164..1b7e6f86a 100644 --- a/peewee.py +++ b/peewee.py @@ -3160,6 +3160,9 @@ def to_timestamp(self, date_field): def from_timestamp(self, date_field): raise NotImplementedError + def random(self): + return fn.random() + def bind(self, models, bind_refs=True, bind_backrefs=True): for model in models: model.bind(self, bind_refs=bind_refs, bind_backrefs=bind_backrefs) @@ -3916,6 +3919,9 @@ def to_timestamp(self, date_field): def from_timestamp(self, date_field): return fn.FROM_UNIXTIME(date_field) + def random(self): + return fn.rand() + def get_noop_select(self, ctx): return ctx.literal('DO 0') From 32376036231712e359eca2f0915846fb5f461cd7 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 2 Jul 2019 08:44:36 -0500 Subject: [PATCH 19/41] Support .exists() with compound select queries. Fixes #1961 --- peewee.py | 5 +++++ tests/regressions.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/peewee.py b/peewee.py index 1b7e6f86a..d021e9be9 100644 --- a/peewee.py +++ b/peewee.py @@ -2098,6 +2098,11 @@ def __init__(self, lhs, op, rhs): def _returning(self): return self.lhs._returning + @database_required + def exists(self, database): + query = Select((self.limit(1),), (SQL('1'),)).bind(database) + return bool(query.scalar()) + def _get_query_key(self): return (self.lhs.get_query_key(), self.rhs.get_query_key()) diff --git a/tests/regressions.py b/tests/regressions.py index 1cbfde600..6a67af05e 100644 --- a/tests/regressions.py +++ b/tests/regressions.py @@ -869,3 +869,18 @@ def test_multi_fk_join_regression(self): ('r11', 'u1', 'u1'), ('r12', 'u1', 'u2'), ('r21', 'u2', 'u1')]) + + +class TestCompoundExistsRegression(ModelTestCase): + requires = [User] + + def test_compound_regressions_1961(self): + UA = User.alias() + cq = (User.select(User.id) | UA.select(UA.id)) + # Calling .exists() fails with AttributeError, no attribute "columns". + self.assertFalse(cq.exists()) + self.assertEqual(cq.count(), 0) + + User.create(username='u1') + self.assertTrue(cq.exists()) + self.assertEqual(cq.count(), 1) From d6087e1effe46d3959be81e869cfab625ba763ac Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Thu, 4 Jul 2019 11:33:49 -0500 Subject: [PATCH 20/41] Add test of FK to different schema. Refs #1021 --- tests/schema.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/schema.py b/tests/schema.py index cbaa8663c..0df2b6074 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -94,6 +94,29 @@ def assertIndexes(self, model_class, expected): self.assertEqual(indexes, expected) + def test_model_fk_schema(self): + class Base(TestModel): + class Meta: + database = self.database + class User(Base): + username = TextField() + class Meta: + schema = 'foo' + class Tweet(Base): + user = ForeignKeyField(User) + content = TextField() + class Meta: + schema = 'bar' + + self.assertCreateTable(User, [ + ('CREATE TABLE "foo"."user" ("id" INTEGER NOT NULL PRIMARY KEY, ' + '"username" TEXT NOT NULL)')]) + self.assertCreateTable(Tweet, [ + ('CREATE TABLE "bar"."tweet" ("id" INTEGER NOT NULL PRIMARY KEY, ' + '"user_id" INTEGER NOT NULL, "content" TEXT NOT NULL, ' + 'FOREIGN KEY ("user_id") REFERENCES "foo"."user" ("id"))'), + ('CREATE INDEX "bar"."tweet_user_id" ON "tweet" ("user_id")')]) + def test_model_indexes_with_schema(self): # Attach cache database so we can reference "cache." as the schema. self.database.execute_sql("attach database ':memory:' as cache;") From efc0b5115ec7c3bab813e03ca2f85b177612d3d9 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Thu, 4 Jul 2019 11:46:19 -0500 Subject: [PATCH 21/41] When multiple FKs present, default to FK whose name matches rel model. Fixes #1963 and replaces #1964. Thanks to @nad2000 for the implementation and test-case. --- peewee.py | 7 +++++++ tests/regressions.py | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/peewee.py b/peewee.py index d021e9be9..99126e41c 100644 --- a/peewee.py +++ b/peewee.py @@ -6753,6 +6753,13 @@ def _generate_on_clause(self, src, dest, to_field=None, on=None): return fk_fields[0], is_backref if on is None: + # If multiple foreign-keys exist, try using the FK whose name + # matches that of the related model. If not, raise an error as this + # is ambiguous. + for fk in fk_fields: + if fk.name == dest._meta.name: + return fk, is_backref + raise ValueError('More than one foreign key between %s and %s.' ' Please specify which you are joining on.' % (src, dest)) diff --git a/tests/regressions.py b/tests/regressions.py index 6a67af05e..e26ac179e 100644 --- a/tests/regressions.py +++ b/tests/regressions.py @@ -649,7 +649,14 @@ def test_model_graph_multi_fk(self): .join_from(Task, P2, LO, on=Task.alt) .order_by(Task.name)) - for query in (q1, q2): + # Query specifying with missing target field. + q3 = (Task + .select(Task, P1, P2) + .join_from(Task, P1, LO) + .join_from(Task, P2, LO, on=Task.alt) + .order_by(Task.name)) + + for query in (q1, q2, q3): with self.assertQueryCount(1): t1, t2 = list(query) self.assertEqual(t1.project.name, 'a') From 86e414ba4a0d791bc7ff7aac881301edba784cd0 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 8 Jul 2019 10:58:26 -0500 Subject: [PATCH 22/41] Improve Sqlite/MySQL date truncation to convert to datetimes. Also adds a truncate() helper method to DateTimeField and DateField. --- peewee.py | 29 ++++++++++++++++++++--------- tests/fields.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/peewee.py b/peewee.py index 99126e41c..91c627c22 100644 --- a/peewee.py +++ b/peewee.py @@ -206,15 +206,15 @@ def reraise(tp, value, tb=None): '%H:%M') __sqlite_date_trunc__ = { - 'year': '%Y', - 'month': '%Y-%m', - 'day': '%Y-%m-%d', - 'hour': '%Y-%m-%d %H', - 'minute': '%Y-%m-%d %H:%M', + 'year': '%Y-01-01 00:00:00', + 'month': '%Y-%m-01 00:00:00', + 'day': '%Y-%m-%d 00:00:00', + 'hour': '%Y-%m-%d %H:00:00', + 'minute': '%Y-%m-%d %H:%M:00', 'second': '%Y-%m-%d %H:%M:%S'} __mysql_date_trunc__ = __sqlite_date_trunc__.copy() -__mysql_date_trunc__['minute'] = '%Y-%m-%d %H:%i' +__mysql_date_trunc__['minute'] = '%Y-%m-%d %H:%i:00' __mysql_date_trunc__['second'] = '%Y-%m-%d %H:%i:%S' def _sqlite_date_part(lookup_type, datetime_string): @@ -3551,10 +3551,11 @@ def conflict_update(self, oc, query): return self._build_on_conflict_update(oc, query) def extract_date(self, date_part, date_field): - return fn.date_part(date_part, date_field) + return fn.date_part(date_part, date_field, python_value=int) def truncate_date(self, date_part, date_field): - return fn.date_trunc(date_part, date_field) + return fn.date_trunc(date_part, date_field, + python_value=simple_date_time) def to_timestamp(self, date_field): return fn.strftime('%s', date_field).cast('integer') @@ -3916,7 +3917,8 @@ def extract_date(self, date_part, date_field): return fn.EXTRACT(NodeList((SQL(date_part), SQL('FROM'), date_field))) def truncate_date(self, date_part, date_field): - return fn.DATE_FORMAT(date_field, __mysql_date_trunc__[date_part]) + return fn.DATE_FORMAT(date_field, __mysql_date_trunc__[date_part], + python_value=simple_date_time) def to_timestamp(self, date_field): return fn.UNIX_TIMESTAMP(date_field) @@ -4701,6 +4703,9 @@ def format_date_time(value, formats, post_process=None): pass return value +def simple_date_time(value): + return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S') + class _BaseFormattedField(Field): formats = None @@ -4727,6 +4732,9 @@ def adapt(self, value): def to_timestamp(self): return self.model._meta.database.to_timestamp(self) + def truncate(self, part): + return self.model._meta.database.truncate_date(part, self) + year = property(_date_part('year')) month = property(_date_part('month')) day = property(_date_part('day')) @@ -4754,6 +4762,9 @@ def adapt(self, value): def to_timestamp(self): return self.model._meta.database.to_timestamp(self) + def truncate(self, part): + return self.model._meta.database.truncate_date(part, self) + year = property(_date_part('year')) month = property(_date_part('month')) day = property(_date_part('day')) diff --git a/tests/fields.py b/tests/fields.py index 2917861b8..04f0013c8 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -250,6 +250,35 @@ def test_extract_parts(self): 2011., 1., 2., 11., 12., 13.054321, 2012., 2., 3., 3., 13., 37.)) + def test_truncate_date(self): + dm = DateModel.create( + date_time=datetime.datetime(2001, 2, 3, 4, 5, 6, 7), + date=datetime.date(2002, 3, 4)) + + accum = [] + for p in ('year', 'month', 'day', 'hour', 'minute', 'second'): + accum.append(DateModel.date_time.truncate(p)) + for p in ('year', 'month', 'day'): + accum.append(DateModel.date.truncate(p)) + + query = DateModel.select(*accum).tuples() + data = list(query[0]) + + # Postgres includes timezone info, so strip that for comparison. + if IS_POSTGRESQL: + data = [dt.replace(tzinfo=None) for dt in data] + + self.assertEqual(data, [ + datetime.datetime(2001, 1, 1, 0, 0, 0), + datetime.datetime(2001, 2, 1, 0, 0, 0), + datetime.datetime(2001, 2, 3, 0, 0, 0), + datetime.datetime(2001, 2, 3, 4, 0, 0), + datetime.datetime(2001, 2, 3, 4, 5, 0), + datetime.datetime(2001, 2, 3, 4, 5, 6), + datetime.datetime(2002, 1, 1, 0, 0, 0), + datetime.datetime(2002, 3, 1, 0, 0, 0), + datetime.datetime(2002, 3, 4, 0, 0, 0)]) + def test_to_timestamp(self): dt = datetime.datetime(2019, 1, 2, 3, 4, 5) ts = calendar.timegm(dt.utctimetuple()) From 3b9fcb2476d6693bce5db13745260b3583b31c78 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 8 Jul 2019 11:03:47 -0500 Subject: [PATCH 23/41] Docs for change, and make truncate converter more robust for Sqlite. --- docs/peewee/api.rst | 15 +++++++++++++++ peewee.py | 5 ++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/peewee/api.rst b/docs/peewee/api.rst index 73a9c2bba..0a4e114d9 100644 --- a/docs/peewee/api.rst +++ b/docs/peewee/api.rst @@ -3068,6 +3068,15 @@ Fields Event.stop.to_timestamp()) .order_by(Event.start)) + .. py:method:: truncate(date_part) + + :param str date_part: year, month, day, hour, minute or second. + :returns: expression node to truncate date/time to given resolution. + + Truncates the value in the column to the given part. This method is + useful for finding all rows within a given month, for instance. + + .. py:class:: DateField([formats=None[, **kwargs]]) :param list formats: A list of format strings to use when coercing a string @@ -3108,6 +3117,12 @@ Fields See :py:meth:`DateTimeField.to_timestamp`. + .. py:method:: truncate(date_part) + + See :py:meth:`DateTimeField.truncate`. Note that only *year*, *month*, + and *day* are meaningful for :py:class:`DateField`. + + .. py:class:: TimeField([formats=None[, **kwargs]]) :param list formats: A list of format strings to use when coercing a string diff --git a/peewee.py b/peewee.py index 91c627c22..91777b7db 100644 --- a/peewee.py +++ b/peewee.py @@ -4704,7 +4704,10 @@ def format_date_time(value, formats, post_process=None): return value def simple_date_time(value): - return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S') + try: + return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S') + except (TypeError, ValueError): + return value class _BaseFormattedField(Field): From 66ab734109ba896bce843928fb558e4f63d27daa Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Thu, 11 Jul 2019 09:42:30 -0500 Subject: [PATCH 24/41] Tests for string fields to handle bytes or unicode. --- tests/fields.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/fields.py b/tests/fields.py index 04f0013c8..4fb7f9f6f 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -1247,3 +1247,39 @@ def test_fk_lazy_select_related(self): self.assertEqual(bi.nq_lazy.name, 'b') self.assertTrue(bi.nq_null is None) self.assertTrue(bi.nq_lazy_null is None) + + +class SM(TestModel): + text_field = TextField() + char_field = CharField() + + +class TestStringFields(ModelTestCase): + requires = [SM] + + def test_string_fields(self): + bdata = b'b1' + udata = b'u1'.decode('utf8') + + sb = SM.create(text_field=bdata, char_field=bdata) + su = SM.create(text_field=udata, char_field=udata) + + sb_db = SM.get(SM.id == sb.id) + self.assertEqual(sb_db.text_field, 'b1') + self.assertEqual(sb_db.char_field, 'b1') + + su_db = SM.get(SM.id == su.id) + self.assertEqual(su_db.text_field, 'u1') + self.assertEqual(su_db.char_field, 'u1') + + bvals = (b'b1', u'b1') + uvals = (b'u1', u'u1') + + for field in (SM.text_field, SM.char_field): + for bval in bvals: + sb_db = SM.get(field == bval) + self.assertEqual(sb.id, sb_db.id) + + for uval in uvals: + sb_db = SM.get(field == uval) + self.assertEqual(su.id, su_db.id) From c49d830e8674a9e5af0242e64076cf3ebb8ca9b7 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Fri, 12 Jul 2019 10:31:11 -0500 Subject: [PATCH 25/41] Drop unneeded header files. --- playhouse/_pysqlite/backup.h | 43 - playhouse/_pysqlite/cursor.h | 68 -- playhouse/_pysqlite/microprotocols.h | 55 -- playhouse/_pysqlite/prepare_protocol.h | 41 - playhouse/_pysqlite/row.h | 39 - playhouse/_pysqlite/sqlite_constants.h | 1002 ------------------------ playhouse/_pysqlite/statement.h | 59 -- playhouse/_pysqlite/util.h | 42 - 8 files changed, 1349 deletions(-) delete mode 100644 playhouse/_pysqlite/backup.h delete mode 100644 playhouse/_pysqlite/cursor.h delete mode 100644 playhouse/_pysqlite/microprotocols.h delete mode 100644 playhouse/_pysqlite/prepare_protocol.h delete mode 100644 playhouse/_pysqlite/row.h delete mode 100644 playhouse/_pysqlite/sqlite_constants.h delete mode 100644 playhouse/_pysqlite/statement.h delete mode 100644 playhouse/_pysqlite/util.h diff --git a/playhouse/_pysqlite/backup.h b/playhouse/_pysqlite/backup.h deleted file mode 100644 index 9be4e21bf..000000000 --- a/playhouse/_pysqlite/backup.h +++ /dev/null @@ -1,43 +0,0 @@ -/* backup.h - definitions for the backup type - * - * Copyright (C) 2010-2015 Gerhard Häring - * - * This file is part of pysqlite. - * - * 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_BACKUP_H -#define PYSQLITE_BACKUP_H -#include "Python.h" - -#include "sqlite3.h" -#include "connection.h" - -typedef struct -{ - PyObject_HEAD - sqlite3_backup* backup; - pysqlite_Connection* source_con; - pysqlite_Connection* dest_con; -} pysqlite_Backup; - -extern PyTypeObject pysqlite_BackupType; - -int pysqlite_backup_setup_types(void); - -#endif diff --git a/playhouse/_pysqlite/cursor.h b/playhouse/_pysqlite/cursor.h deleted file mode 100644 index 65ff78f0c..000000000 --- a/playhouse/_pysqlite/cursor.h +++ /dev/null @@ -1,68 +0,0 @@ -/* cursor.h - definitions for the cursor type - * - * Copyright (C) 2004-2015 Gerhard Häring - * - * This file is part of pysqlite. - * - * 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_CURSOR_H -#define PYSQLITE_CURSOR_H -#include "Python.h" - -#include "statement.h" -#include "connection.h" -#include "module.h" - -typedef struct -{ - PyObject_HEAD - pysqlite_Connection* connection; - PyObject* description; - PyObject* row_cast_map; - int arraysize; - PyObject* lastrowid; - long rowcount; - pysqlite_Statement* statement; - int closed; - int reset; - int locked; - int initialized; - - /* the next row to be returned, NULL if no next row available */ - PyObject* next_row; - - PyObject* in_weakreflist; /* List of weak references */ -} pysqlite_Cursor; - -extern PyTypeObject pysqlite_CursorType; - -PyObject* pysqlite_cursor_execute(pysqlite_Cursor* self, PyObject* args); -PyObject* pysqlite_cursor_executemany(pysqlite_Cursor* self, PyObject* args); -PyObject* pysqlite_cursor_getiter(pysqlite_Cursor *self); -PyObject* pysqlite_cursor_iternext(pysqlite_Cursor *self); -PyObject* pysqlite_cursor_fetchone(pysqlite_Cursor* self, PyObject* args); -PyObject* pysqlite_cursor_fetchmany(pysqlite_Cursor* self, PyObject* args, PyObject* kwargs); -PyObject* pysqlite_cursor_fetchall(pysqlite_Cursor* self, PyObject* args); -PyObject* pysqlite_noop(pysqlite_Connection* self, PyObject* args); -PyObject* pysqlite_cursor_close(pysqlite_Cursor* self, PyObject* args); - -int pysqlite_cursor_setup_types(void); - -#define UNKNOWN (-1) -#endif diff --git a/playhouse/_pysqlite/microprotocols.h b/playhouse/_pysqlite/microprotocols.h deleted file mode 100644 index 3a9944fc7..000000000 --- a/playhouse/_pysqlite/microprotocols.h +++ /dev/null @@ -1,55 +0,0 @@ -/* microprotocols.c - definitions for minimalist and non-validating protocols - * - * Copyright (C) 2003-2004 Federico Di Gregorio - * - * This file is part of psycopg and was adapted for pysqlite. Federico Di - * Gregorio gave the permission to use it within pysqlite under the following - * license: - * - * 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 PSYCOPG_MICROPROTOCOLS_H -#define PSYCOPG_MICROPROTOCOLS_H 1 - -#include - -/** adapters registry **/ - -extern PyObject *psyco_adapters; - -/** the names of the three mandatory methods **/ - -#define MICROPROTOCOLS_GETQUOTED_NAME "getquoted" -#define MICROPROTOCOLS_GETSTRING_NAME "getstring" -#define MICROPROTOCOLS_GETBINARY_NAME "getbinary" - -/** exported functions **/ - -/* used by module.c to init the microprotocols system */ -extern int pysqlite_microprotocols_init(PyObject *dict); -extern int pysqlite_microprotocols_add( - PyTypeObject *type, PyObject *proto, PyObject *cast); -extern PyObject *pysqlite_microprotocols_adapt( - PyObject *obj, PyObject *proto, PyObject *alt); - -extern PyObject * - pysqlite_adapt(pysqlite_Cursor* self, PyObject *args); -#define pysqlite_adapt_doc \ - "adapt(obj, protocol, alternate) -> adapt obj to given protocol. Non-standard." - -#endif /* !defined(PSYCOPG_MICROPROTOCOLS_H) */ diff --git a/playhouse/_pysqlite/prepare_protocol.h b/playhouse/_pysqlite/prepare_protocol.h deleted file mode 100644 index 15ab4bc8d..000000000 --- a/playhouse/_pysqlite/prepare_protocol.h +++ /dev/null @@ -1,41 +0,0 @@ -/* prepare_protocol.h - the protocol for preparing values for SQLite - * - * Copyright (C) 2005-2015 Gerhard Häring - * - * This file is part of pysqlite. - * - * 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_PREPARE_PROTOCOL_H -#define PYSQLITE_PREPARE_PROTOCOL_H -#include "Python.h" - -typedef struct -{ - PyObject_HEAD -} pysqlite_PrepareProtocol; - -extern PyTypeObject pysqlite_PrepareProtocolType; - -int pysqlite_prepare_protocol_init(pysqlite_PrepareProtocol* self, PyObject* args, PyObject* kwargs); -void pysqlite_prepare_protocol_dealloc(pysqlite_PrepareProtocol* self); - -int pysqlite_prepare_protocol_setup_types(void); - -#define UNKNOWN (-1) -#endif diff --git a/playhouse/_pysqlite/row.h b/playhouse/_pysqlite/row.h deleted file mode 100644 index 8ad5f83b4..000000000 --- a/playhouse/_pysqlite/row.h +++ /dev/null @@ -1,39 +0,0 @@ -/* row.h - an enhanced tuple for database rows - * - * Copyright (C) 2005-2015 Gerhard Häring - * - * This file is part of pysqlite. - * - * 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_ROW_H -#define PYSQLITE_ROW_H -#include "Python.h" - -typedef struct _Row -{ - PyObject_HEAD - PyObject* data; - PyObject* description; -} pysqlite_Row; - -extern PyTypeObject pysqlite_RowType; - -int pysqlite_row_setup_types(void); - -#endif diff --git a/playhouse/_pysqlite/sqlite_constants.h b/playhouse/_pysqlite/sqlite_constants.h deleted file mode 100644 index 79e98e791..000000000 --- a/playhouse/_pysqlite/sqlite_constants.h +++ /dev/null @@ -1,1002 +0,0 @@ -#ifdef SQLITE_VERSION_NUMBER -{"SQLITE_VERSION_NUMBER", SQLITE_VERSION_NUMBER}, -#endif -#ifdef SQLITE_ABORT -{"SQLITE_ABORT", SQLITE_ABORT}, -#endif -#ifdef SQLITE_AUTH -{"SQLITE_AUTH", SQLITE_AUTH}, -#endif -#ifdef SQLITE_BUSY -{"SQLITE_BUSY", SQLITE_BUSY}, -#endif -#ifdef SQLITE_CANTOPEN -{"SQLITE_CANTOPEN", SQLITE_CANTOPEN}, -#endif -#ifdef SQLITE_CONSTRAINT -{"SQLITE_CONSTRAINT", SQLITE_CONSTRAINT}, -#endif -#ifdef SQLITE_CORRUPT -{"SQLITE_CORRUPT", SQLITE_CORRUPT}, -#endif -#ifdef SQLITE_DONE -{"SQLITE_DONE", SQLITE_DONE}, -#endif -#ifdef SQLITE_EMPTY -{"SQLITE_EMPTY", SQLITE_EMPTY}, -#endif -#ifdef SQLITE_ERROR -{"SQLITE_ERROR", SQLITE_ERROR}, -#endif -#ifdef SQLITE_FORMAT -{"SQLITE_FORMAT", SQLITE_FORMAT}, -#endif -#ifdef SQLITE_FULL -{"SQLITE_FULL", SQLITE_FULL}, -#endif -#ifdef SQLITE_INTERNAL -{"SQLITE_INTERNAL", SQLITE_INTERNAL}, -#endif -#ifdef SQLITE_INTERRUPT -{"SQLITE_INTERRUPT", SQLITE_INTERRUPT}, -#endif -#ifdef SQLITE_IOERR -{"SQLITE_IOERR", SQLITE_IOERR}, -#endif -#ifdef SQLITE_LOCKED -{"SQLITE_LOCKED", SQLITE_LOCKED}, -#endif -#ifdef SQLITE_MISMATCH -{"SQLITE_MISMATCH", SQLITE_MISMATCH}, -#endif -#ifdef SQLITE_MISUSE -{"SQLITE_MISUSE", SQLITE_MISUSE}, -#endif -#ifdef SQLITE_NOLFS -{"SQLITE_NOLFS", SQLITE_NOLFS}, -#endif -#ifdef SQLITE_NOMEM -{"SQLITE_NOMEM", SQLITE_NOMEM}, -#endif -#ifdef SQLITE_NOTADB -{"SQLITE_NOTADB", SQLITE_NOTADB}, -#endif -#ifdef SQLITE_NOTFOUND -{"SQLITE_NOTFOUND", SQLITE_NOTFOUND}, -#endif -#ifdef SQLITE_NOTICE -{"SQLITE_NOTICE", SQLITE_NOTICE}, -#endif -#ifdef SQLITE_OK -{"SQLITE_OK", SQLITE_OK}, -#endif -#ifdef SQLITE_PERM -{"SQLITE_PERM", SQLITE_PERM}, -#endif -#ifdef SQLITE_PROTOCOL -{"SQLITE_PROTOCOL", SQLITE_PROTOCOL}, -#endif -#ifdef SQLITE_RANGE -{"SQLITE_RANGE", SQLITE_RANGE}, -#endif -#ifdef SQLITE_READONLY -{"SQLITE_READONLY", SQLITE_READONLY}, -#endif -#ifdef SQLITE_ROW -{"SQLITE_ROW", SQLITE_ROW}, -#endif -#ifdef SQLITE_SCHEMA -{"SQLITE_SCHEMA", SQLITE_SCHEMA}, -#endif -#ifdef SQLITE_TOOBIG -{"SQLITE_TOOBIG", SQLITE_TOOBIG}, -#endif -#ifdef SQLITE_WARNING -{"SQLITE_WARNING", SQLITE_WARNING}, -#endif -#ifdef SQLITE_ABORT_ROLLBACK -{"SQLITE_ABORT_ROLLBACK", SQLITE_ABORT_ROLLBACK}, -#endif -#ifdef SQLITE_AUTH_USER -{"SQLITE_AUTH_USER", SQLITE_AUTH_USER}, -#endif -#ifdef SQLITE_BUSY_RECOVERY -{"SQLITE_BUSY_RECOVERY", SQLITE_BUSY_RECOVERY}, -#endif -#ifdef SQLITE_BUSY_SNAPSHOT -{"SQLITE_BUSY_SNAPSHOT", SQLITE_BUSY_SNAPSHOT}, -#endif -#ifdef SQLITE_CANTOPEN_CONVPATH -{"SQLITE_CANTOPEN_CONVPATH", SQLITE_CANTOPEN_CONVPATH}, -#endif -#ifdef SQLITE_CANTOPEN_FULLPATH -{"SQLITE_CANTOPEN_FULLPATH", SQLITE_CANTOPEN_FULLPATH}, -#endif -#ifdef SQLITE_CANTOPEN_ISDIR -{"SQLITE_CANTOPEN_ISDIR", SQLITE_CANTOPEN_ISDIR}, -#endif -#ifdef SQLITE_CANTOPEN_NOTEMPDIR -{"SQLITE_CANTOPEN_NOTEMPDIR", SQLITE_CANTOPEN_NOTEMPDIR}, -#endif -#ifdef SQLITE_CONSTRAINT_CHECK -{"SQLITE_CONSTRAINT_CHECK", SQLITE_CONSTRAINT_CHECK}, -#endif -#ifdef SQLITE_CONSTRAINT_COMMITHOOK -{"SQLITE_CONSTRAINT_COMMITHOOK", SQLITE_CONSTRAINT_COMMITHOOK}, -#endif -#ifdef SQLITE_CONSTRAINT_FOREIGNKEY -{"SQLITE_CONSTRAINT_FOREIGNKEY", SQLITE_CONSTRAINT_FOREIGNKEY}, -#endif -#ifdef SQLITE_CONSTRAINT_FUNCTION -{"SQLITE_CONSTRAINT_FUNCTION", SQLITE_CONSTRAINT_FUNCTION}, -#endif -#ifdef SQLITE_CONSTRAINT_NOTNULL -{"SQLITE_CONSTRAINT_NOTNULL", SQLITE_CONSTRAINT_NOTNULL}, -#endif -#ifdef SQLITE_CONSTRAINT_PRIMARYKEY -{"SQLITE_CONSTRAINT_PRIMARYKEY", SQLITE_CONSTRAINT_PRIMARYKEY}, -#endif -#ifdef SQLITE_CONSTRAINT_ROWID -{"SQLITE_CONSTRAINT_ROWID", SQLITE_CONSTRAINT_ROWID}, -#endif -#ifdef SQLITE_CONSTRAINT_TRIGGER -{"SQLITE_CONSTRAINT_TRIGGER", SQLITE_CONSTRAINT_TRIGGER}, -#endif -#ifdef SQLITE_CONSTRAINT_UNIQUE -{"SQLITE_CONSTRAINT_UNIQUE", SQLITE_CONSTRAINT_UNIQUE}, -#endif -#ifdef SQLITE_CONSTRAINT_VTAB -{"SQLITE_CONSTRAINT_VTAB", SQLITE_CONSTRAINT_VTAB}, -#endif -#ifdef SQLITE_CORRUPT_VTAB -{"SQLITE_CORRUPT_VTAB", SQLITE_CORRUPT_VTAB}, -#endif -#ifdef SQLITE_IOERR_ACCESS -{"SQLITE_IOERR_ACCESS", SQLITE_IOERR_ACCESS}, -#endif -#ifdef SQLITE_IOERR_BLOCKED -{"SQLITE_IOERR_BLOCKED", SQLITE_IOERR_BLOCKED}, -#endif -#ifdef SQLITE_IOERR_CHECKRESERVEDLOCK -{"SQLITE_IOERR_CHECKRESERVEDLOCK", SQLITE_IOERR_CHECKRESERVEDLOCK}, -#endif -#ifdef SQLITE_IOERR_CLOSE -{"SQLITE_IOERR_CLOSE", SQLITE_IOERR_CLOSE}, -#endif -#ifdef SQLITE_IOERR_CONVPATH -{"SQLITE_IOERR_CONVPATH", SQLITE_IOERR_CONVPATH}, -#endif -#ifdef SQLITE_IOERR_DELETE -{"SQLITE_IOERR_DELETE", SQLITE_IOERR_DELETE}, -#endif -#ifdef SQLITE_IOERR_DELETE_NOENT -{"SQLITE_IOERR_DELETE_NOENT", SQLITE_IOERR_DELETE_NOENT}, -#endif -#ifdef SQLITE_IOERR_DIR_CLOSE -{"SQLITE_IOERR_DIR_CLOSE", SQLITE_IOERR_DIR_CLOSE}, -#endif -#ifdef SQLITE_IOERR_DIR_FSYNC -{"SQLITE_IOERR_DIR_FSYNC", SQLITE_IOERR_DIR_FSYNC}, -#endif -#ifdef SQLITE_IOERR_FSTAT -{"SQLITE_IOERR_FSTAT", SQLITE_IOERR_FSTAT}, -#endif -#ifdef SQLITE_IOERR_FSYNC -{"SQLITE_IOERR_FSYNC", SQLITE_IOERR_FSYNC}, -#endif -#ifdef SQLITE_IOERR_GETTEMPPATH -{"SQLITE_IOERR_GETTEMPPATH", SQLITE_IOERR_GETTEMPPATH}, -#endif -#ifdef SQLITE_IOERR_LOCK -{"SQLITE_IOERR_LOCK", SQLITE_IOERR_LOCK}, -#endif -#ifdef SQLITE_IOERR_MMAP -{"SQLITE_IOERR_MMAP", SQLITE_IOERR_MMAP}, -#endif -#ifdef SQLITE_IOERR_NOMEM -{"SQLITE_IOERR_NOMEM", SQLITE_IOERR_NOMEM}, -#endif -#ifdef SQLITE_IOERR_RDLOCK -{"SQLITE_IOERR_RDLOCK", SQLITE_IOERR_RDLOCK}, -#endif -#ifdef SQLITE_IOERR_READ -{"SQLITE_IOERR_READ", SQLITE_IOERR_READ}, -#endif -#ifdef SQLITE_IOERR_SEEK -{"SQLITE_IOERR_SEEK", SQLITE_IOERR_SEEK}, -#endif -#ifdef SQLITE_IOERR_SHMLOCK -{"SQLITE_IOERR_SHMLOCK", SQLITE_IOERR_SHMLOCK}, -#endif -#ifdef SQLITE_IOERR_SHMMAP -{"SQLITE_IOERR_SHMMAP", SQLITE_IOERR_SHMMAP}, -#endif -#ifdef SQLITE_IOERR_SHMOPEN -{"SQLITE_IOERR_SHMOPEN", SQLITE_IOERR_SHMOPEN}, -#endif -#ifdef SQLITE_IOERR_SHMSIZE -{"SQLITE_IOERR_SHMSIZE", SQLITE_IOERR_SHMSIZE}, -#endif -#ifdef SQLITE_IOERR_SHORT_READ -{"SQLITE_IOERR_SHORT_READ", SQLITE_IOERR_SHORT_READ}, -#endif -#ifdef SQLITE_IOERR_TRUNCATE -{"SQLITE_IOERR_TRUNCATE", SQLITE_IOERR_TRUNCATE}, -#endif -#ifdef SQLITE_IOERR_UNLOCK -{"SQLITE_IOERR_UNLOCK", SQLITE_IOERR_UNLOCK}, -#endif -#ifdef SQLITE_IOERR_WRITE -{"SQLITE_IOERR_WRITE", SQLITE_IOERR_WRITE}, -#endif -#ifdef SQLITE_LOCKED_SHAREDCACHE -{"SQLITE_LOCKED_SHAREDCACHE", SQLITE_LOCKED_SHAREDCACHE}, -#endif -#ifdef SQLITE_NOTICE_RECOVER_ROLLBACK -{"SQLITE_NOTICE_RECOVER_ROLLBACK", SQLITE_NOTICE_RECOVER_ROLLBACK}, -#endif -#ifdef SQLITE_NOTICE_RECOVER_WAL -{"SQLITE_NOTICE_RECOVER_WAL", SQLITE_NOTICE_RECOVER_WAL}, -#endif -#ifdef SQLITE_READONLY_CANTLOCK -{"SQLITE_READONLY_CANTLOCK", SQLITE_READONLY_CANTLOCK}, -#endif -#ifdef SQLITE_READONLY_DBMOVED -{"SQLITE_READONLY_DBMOVED", SQLITE_READONLY_DBMOVED}, -#endif -#ifdef SQLITE_READONLY_RECOVERY -{"SQLITE_READONLY_RECOVERY", SQLITE_READONLY_RECOVERY}, -#endif -#ifdef SQLITE_READONLY_ROLLBACK -{"SQLITE_READONLY_ROLLBACK", SQLITE_READONLY_ROLLBACK}, -#endif -#ifdef SQLITE_WARNING_AUTOINDEX -{"SQLITE_WARNING_AUTOINDEX", SQLITE_WARNING_AUTOINDEX}, -#endif -#ifdef SQLITE_OPEN_AUTOPROXY -{"SQLITE_OPEN_AUTOPROXY", SQLITE_OPEN_AUTOPROXY}, -#endif -#ifdef SQLITE_OPEN_CREATE -{"SQLITE_OPEN_CREATE", SQLITE_OPEN_CREATE}, -#endif -#ifdef SQLITE_OPEN_DELETEONCLOSE -{"SQLITE_OPEN_DELETEONCLOSE", SQLITE_OPEN_DELETEONCLOSE}, -#endif -#ifdef SQLITE_OPEN_EXCLUSIVE -{"SQLITE_OPEN_EXCLUSIVE", SQLITE_OPEN_EXCLUSIVE}, -#endif -#ifdef SQLITE_OPEN_FULLMUTEX -{"SQLITE_OPEN_FULLMUTEX", SQLITE_OPEN_FULLMUTEX}, -#endif -#ifdef SQLITE_OPEN_MAIN_DB -{"SQLITE_OPEN_MAIN_DB", SQLITE_OPEN_MAIN_DB}, -#endif -#ifdef SQLITE_OPEN_MAIN_JOURNAL -{"SQLITE_OPEN_MAIN_JOURNAL", SQLITE_OPEN_MAIN_JOURNAL}, -#endif -#ifdef SQLITE_OPEN_MASTER_JOURNAL -{"SQLITE_OPEN_MASTER_JOURNAL", SQLITE_OPEN_MASTER_JOURNAL}, -#endif -#ifdef SQLITE_OPEN_MEMORY -{"SQLITE_OPEN_MEMORY", SQLITE_OPEN_MEMORY}, -#endif -#ifdef SQLITE_OPEN_NOMUTEX -{"SQLITE_OPEN_NOMUTEX", SQLITE_OPEN_NOMUTEX}, -#endif -#ifdef SQLITE_OPEN_PRIVATECACHE -{"SQLITE_OPEN_PRIVATECACHE", SQLITE_OPEN_PRIVATECACHE}, -#endif -#ifdef SQLITE_OPEN_READONLY -{"SQLITE_OPEN_READONLY", SQLITE_OPEN_READONLY}, -#endif -#ifdef SQLITE_OPEN_READWRITE -{"SQLITE_OPEN_READWRITE", SQLITE_OPEN_READWRITE}, -#endif -#ifdef SQLITE_OPEN_SHAREDCACHE -{"SQLITE_OPEN_SHAREDCACHE", SQLITE_OPEN_SHAREDCACHE}, -#endif -#ifdef SQLITE_OPEN_SUBJOURNAL -{"SQLITE_OPEN_SUBJOURNAL", SQLITE_OPEN_SUBJOURNAL}, -#endif -#ifdef SQLITE_OPEN_TEMP_DB -{"SQLITE_OPEN_TEMP_DB", SQLITE_OPEN_TEMP_DB}, -#endif -#ifdef SQLITE_OPEN_TEMP_JOURNAL -{"SQLITE_OPEN_TEMP_JOURNAL", SQLITE_OPEN_TEMP_JOURNAL}, -#endif -#ifdef SQLITE_OPEN_TRANSIENT_DB -{"SQLITE_OPEN_TRANSIENT_DB", SQLITE_OPEN_TRANSIENT_DB}, -#endif -#ifdef SQLITE_OPEN_URI -{"SQLITE_OPEN_URI", SQLITE_OPEN_URI}, -#endif -#ifdef SQLITE_OPEN_WAL -{"SQLITE_OPEN_WAL", SQLITE_OPEN_WAL}, -#endif -#ifdef SQLITE_IOCAP_ATOMIC -{"SQLITE_IOCAP_ATOMIC", SQLITE_IOCAP_ATOMIC}, -#endif -#ifdef SQLITE_IOCAP_ATOMIC16K -{"SQLITE_IOCAP_ATOMIC16K", SQLITE_IOCAP_ATOMIC16K}, -#endif -#ifdef SQLITE_IOCAP_ATOMIC1K -{"SQLITE_IOCAP_ATOMIC1K", SQLITE_IOCAP_ATOMIC1K}, -#endif -#ifdef SQLITE_IOCAP_ATOMIC2K -{"SQLITE_IOCAP_ATOMIC2K", SQLITE_IOCAP_ATOMIC2K}, -#endif -#ifdef SQLITE_IOCAP_ATOMIC32K -{"SQLITE_IOCAP_ATOMIC32K", SQLITE_IOCAP_ATOMIC32K}, -#endif -#ifdef SQLITE_IOCAP_ATOMIC4K -{"SQLITE_IOCAP_ATOMIC4K", SQLITE_IOCAP_ATOMIC4K}, -#endif -#ifdef SQLITE_IOCAP_ATOMIC512 -{"SQLITE_IOCAP_ATOMIC512", SQLITE_IOCAP_ATOMIC512}, -#endif -#ifdef SQLITE_IOCAP_ATOMIC64K -{"SQLITE_IOCAP_ATOMIC64K", SQLITE_IOCAP_ATOMIC64K}, -#endif -#ifdef SQLITE_IOCAP_ATOMIC8K -{"SQLITE_IOCAP_ATOMIC8K", SQLITE_IOCAP_ATOMIC8K}, -#endif -#ifdef SQLITE_IOCAP_IMMUTABLE -{"SQLITE_IOCAP_IMMUTABLE", SQLITE_IOCAP_IMMUTABLE}, -#endif -#ifdef SQLITE_IOCAP_POWERSAFE_OVERWRITE -{"SQLITE_IOCAP_POWERSAFE_OVERWRITE", SQLITE_IOCAP_POWERSAFE_OVERWRITE}, -#endif -#ifdef SQLITE_IOCAP_SAFE_APPEND -{"SQLITE_IOCAP_SAFE_APPEND", SQLITE_IOCAP_SAFE_APPEND}, -#endif -#ifdef SQLITE_IOCAP_SEQUENTIAL -{"SQLITE_IOCAP_SEQUENTIAL", SQLITE_IOCAP_SEQUENTIAL}, -#endif -#ifdef SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN -{"SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN", SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN}, -#endif -#ifdef SQLITE_LOCK_EXCLUSIVE -{"SQLITE_LOCK_EXCLUSIVE", SQLITE_LOCK_EXCLUSIVE}, -#endif -#ifdef SQLITE_LOCK_NONE -{"SQLITE_LOCK_NONE", SQLITE_LOCK_NONE}, -#endif -#ifdef SQLITE_LOCK_PENDING -{"SQLITE_LOCK_PENDING", SQLITE_LOCK_PENDING}, -#endif -#ifdef SQLITE_LOCK_RESERVED -{"SQLITE_LOCK_RESERVED", SQLITE_LOCK_RESERVED}, -#endif -#ifdef SQLITE_LOCK_SHARED -{"SQLITE_LOCK_SHARED", SQLITE_LOCK_SHARED}, -#endif -#ifdef SQLITE_SYNC_DATAONLY -{"SQLITE_SYNC_DATAONLY", SQLITE_SYNC_DATAONLY}, -#endif -#ifdef SQLITE_SYNC_FULL -{"SQLITE_SYNC_FULL", SQLITE_SYNC_FULL}, -#endif -#ifdef SQLITE_SYNC_NORMAL -{"SQLITE_SYNC_NORMAL", SQLITE_SYNC_NORMAL}, -#endif -#ifdef SQLITE_FCNTL_BUSYHANDLER -{"SQLITE_FCNTL_BUSYHANDLER", SQLITE_FCNTL_BUSYHANDLER}, -#endif -#ifdef SQLITE_FCNTL_CHUNK_SIZE -{"SQLITE_FCNTL_CHUNK_SIZE", SQLITE_FCNTL_CHUNK_SIZE}, -#endif -#ifdef SQLITE_FCNTL_COMMIT_PHASETWO -{"SQLITE_FCNTL_COMMIT_PHASETWO", SQLITE_FCNTL_COMMIT_PHASETWO}, -#endif -#ifdef SQLITE_FCNTL_FILE_POINTER -{"SQLITE_FCNTL_FILE_POINTER", SQLITE_FCNTL_FILE_POINTER}, -#endif -#ifdef SQLITE_FCNTL_GET_LOCKPROXYFILE -{"SQLITE_FCNTL_GET_LOCKPROXYFILE", SQLITE_FCNTL_GET_LOCKPROXYFILE}, -#endif -#ifdef SQLITE_FCNTL_HAS_MOVED -{"SQLITE_FCNTL_HAS_MOVED", SQLITE_FCNTL_HAS_MOVED}, -#endif -#ifdef SQLITE_FCNTL_LAST_ERRNO -{"SQLITE_FCNTL_LAST_ERRNO", SQLITE_FCNTL_LAST_ERRNO}, -#endif -#ifdef SQLITE_FCNTL_LOCKSTATE -{"SQLITE_FCNTL_LOCKSTATE", SQLITE_FCNTL_LOCKSTATE}, -#endif -#ifdef SQLITE_FCNTL_MMAP_SIZE -{"SQLITE_FCNTL_MMAP_SIZE", SQLITE_FCNTL_MMAP_SIZE}, -#endif -#ifdef SQLITE_FCNTL_OVERWRITE -{"SQLITE_FCNTL_OVERWRITE", SQLITE_FCNTL_OVERWRITE}, -#endif -#ifdef SQLITE_FCNTL_PERSIST_WAL -{"SQLITE_FCNTL_PERSIST_WAL", SQLITE_FCNTL_PERSIST_WAL}, -#endif -#ifdef SQLITE_FCNTL_POWERSAFE_OVERWRITE -{"SQLITE_FCNTL_POWERSAFE_OVERWRITE", SQLITE_FCNTL_POWERSAFE_OVERWRITE}, -#endif -#ifdef SQLITE_FCNTL_PRAGMA -{"SQLITE_FCNTL_PRAGMA", SQLITE_FCNTL_PRAGMA}, -#endif -#ifdef SQLITE_FCNTL_RBU -{"SQLITE_FCNTL_RBU", SQLITE_FCNTL_RBU}, -#endif -#ifdef SQLITE_FCNTL_SET_LOCKPROXYFILE -{"SQLITE_FCNTL_SET_LOCKPROXYFILE", SQLITE_FCNTL_SET_LOCKPROXYFILE}, -#endif -#ifdef SQLITE_FCNTL_SIZE_HINT -{"SQLITE_FCNTL_SIZE_HINT", SQLITE_FCNTL_SIZE_HINT}, -#endif -#ifdef SQLITE_FCNTL_SYNC -{"SQLITE_FCNTL_SYNC", SQLITE_FCNTL_SYNC}, -#endif -#ifdef SQLITE_FCNTL_SYNC_OMITTED -{"SQLITE_FCNTL_SYNC_OMITTED", SQLITE_FCNTL_SYNC_OMITTED}, -#endif -#ifdef SQLITE_FCNTL_TEMPFILENAME -{"SQLITE_FCNTL_TEMPFILENAME", SQLITE_FCNTL_TEMPFILENAME}, -#endif -#ifdef SQLITE_FCNTL_TRACE -{"SQLITE_FCNTL_TRACE", SQLITE_FCNTL_TRACE}, -#endif -#ifdef SQLITE_FCNTL_VFSNAME -{"SQLITE_FCNTL_VFSNAME", SQLITE_FCNTL_VFSNAME}, -#endif -#ifdef SQLITE_FCNTL_WAL_BLOCK -{"SQLITE_FCNTL_WAL_BLOCK", SQLITE_FCNTL_WAL_BLOCK}, -#endif -#ifdef SQLITE_FCNTL_WIN32_AV_RETRY -{"SQLITE_FCNTL_WIN32_AV_RETRY", SQLITE_FCNTL_WIN32_AV_RETRY}, -#endif -#ifdef SQLITE_FCNTL_WIN32_SET_HANDLE -{"SQLITE_FCNTL_WIN32_SET_HANDLE", SQLITE_FCNTL_WIN32_SET_HANDLE}, -#endif -#ifdef SQLITE_FCNTL_ZIPVFS -{"SQLITE_FCNTL_ZIPVFS", SQLITE_FCNTL_ZIPVFS}, -#endif -#ifdef SQLITE_ACCESS_EXISTS -{"SQLITE_ACCESS_EXISTS", SQLITE_ACCESS_EXISTS}, -#endif -#ifdef SQLITE_ACCESS_READ -{"SQLITE_ACCESS_READ", SQLITE_ACCESS_READ}, -#endif -#ifdef SQLITE_ACCESS_READWRITE -{"SQLITE_ACCESS_READWRITE", SQLITE_ACCESS_READWRITE}, -#endif -#ifdef SQLITE_SHM_EXCLUSIVE -{"SQLITE_SHM_EXCLUSIVE", SQLITE_SHM_EXCLUSIVE}, -#endif -#ifdef SQLITE_SHM_LOCK -{"SQLITE_SHM_LOCK", SQLITE_SHM_LOCK}, -#endif -#ifdef SQLITE_SHM_SHARED -{"SQLITE_SHM_SHARED", SQLITE_SHM_SHARED}, -#endif -#ifdef SQLITE_SHM_UNLOCK -{"SQLITE_SHM_UNLOCK", SQLITE_SHM_UNLOCK}, -#endif -#ifdef SQLITE_SHM_NLOCK -{"SQLITE_SHM_NLOCK", SQLITE_SHM_NLOCK}, -#endif -#ifdef SQLITE_CONFIG_COVERING_INDEX_SCAN -{"SQLITE_CONFIG_COVERING_INDEX_SCAN", SQLITE_CONFIG_COVERING_INDEX_SCAN}, -#endif -#ifdef SQLITE_CONFIG_GETMALLOC -{"SQLITE_CONFIG_GETMALLOC", SQLITE_CONFIG_GETMALLOC}, -#endif -#ifdef SQLITE_CONFIG_GETMUTEX -{"SQLITE_CONFIG_GETMUTEX", SQLITE_CONFIG_GETMUTEX}, -#endif -#ifdef SQLITE_CONFIG_GETPCACHE -{"SQLITE_CONFIG_GETPCACHE", SQLITE_CONFIG_GETPCACHE}, -#endif -#ifdef SQLITE_CONFIG_GETPCACHE2 -{"SQLITE_CONFIG_GETPCACHE2", SQLITE_CONFIG_GETPCACHE2}, -#endif -#ifdef SQLITE_CONFIG_HEAP -{"SQLITE_CONFIG_HEAP", SQLITE_CONFIG_HEAP}, -#endif -#ifdef SQLITE_CONFIG_LOG -{"SQLITE_CONFIG_LOG", SQLITE_CONFIG_LOG}, -#endif -#ifdef SQLITE_CONFIG_LOOKASIDE -{"SQLITE_CONFIG_LOOKASIDE", SQLITE_CONFIG_LOOKASIDE}, -#endif -#ifdef SQLITE_CONFIG_MALLOC -{"SQLITE_CONFIG_MALLOC", SQLITE_CONFIG_MALLOC}, -#endif -#ifdef SQLITE_CONFIG_MEMSTATUS -{"SQLITE_CONFIG_MEMSTATUS", SQLITE_CONFIG_MEMSTATUS}, -#endif -#ifdef SQLITE_CONFIG_MMAP_SIZE -{"SQLITE_CONFIG_MMAP_SIZE", SQLITE_CONFIG_MMAP_SIZE}, -#endif -#ifdef SQLITE_CONFIG_MULTITHREAD -{"SQLITE_CONFIG_MULTITHREAD", SQLITE_CONFIG_MULTITHREAD}, -#endif -#ifdef SQLITE_CONFIG_MUTEX -{"SQLITE_CONFIG_MUTEX", SQLITE_CONFIG_MUTEX}, -#endif -#ifdef SQLITE_CONFIG_PAGECACHE -{"SQLITE_CONFIG_PAGECACHE", SQLITE_CONFIG_PAGECACHE}, -#endif -#ifdef SQLITE_CONFIG_PCACHE -{"SQLITE_CONFIG_PCACHE", SQLITE_CONFIG_PCACHE}, -#endif -#ifdef SQLITE_CONFIG_PCACHE2 -{"SQLITE_CONFIG_PCACHE2", SQLITE_CONFIG_PCACHE2}, -#endif -#ifdef SQLITE_CONFIG_PCACHE_HDRSZ -{"SQLITE_CONFIG_PCACHE_HDRSZ", SQLITE_CONFIG_PCACHE_HDRSZ}, -#endif -#ifdef SQLITE_CONFIG_PMASZ -{"SQLITE_CONFIG_PMASZ", SQLITE_CONFIG_PMASZ}, -#endif -#ifdef SQLITE_CONFIG_SCRATCH -{"SQLITE_CONFIG_SCRATCH", SQLITE_CONFIG_SCRATCH}, -#endif -#ifdef SQLITE_CONFIG_SERIALIZED -{"SQLITE_CONFIG_SERIALIZED", SQLITE_CONFIG_SERIALIZED}, -#endif -#ifdef SQLITE_CONFIG_SINGLETHREAD -{"SQLITE_CONFIG_SINGLETHREAD", SQLITE_CONFIG_SINGLETHREAD}, -#endif -#ifdef SQLITE_CONFIG_SQLLOG -{"SQLITE_CONFIG_SQLLOG", SQLITE_CONFIG_SQLLOG}, -#endif -#ifdef SQLITE_CONFIG_URI -{"SQLITE_CONFIG_URI", SQLITE_CONFIG_URI}, -#endif -#ifdef SQLITE_CONFIG_WIN32_HEAPSIZE -{"SQLITE_CONFIG_WIN32_HEAPSIZE", SQLITE_CONFIG_WIN32_HEAPSIZE}, -#endif -#ifdef SQLITE_DBCONFIG_ENABLE_FKEY -{"SQLITE_DBCONFIG_ENABLE_FKEY", SQLITE_DBCONFIG_ENABLE_FKEY}, -#endif -#ifdef SQLITE_DBCONFIG_ENABLE_TRIGGER -{"SQLITE_DBCONFIG_ENABLE_TRIGGER", SQLITE_DBCONFIG_ENABLE_TRIGGER}, -#endif -#ifdef SQLITE_DBCONFIG_LOOKASIDE -{"SQLITE_DBCONFIG_LOOKASIDE", SQLITE_DBCONFIG_LOOKASIDE}, -#endif -#ifdef SQLITE_DENY -{"SQLITE_DENY", SQLITE_DENY}, -#endif -#ifdef SQLITE_IGNORE -{"SQLITE_IGNORE", SQLITE_IGNORE}, -#endif -#ifdef SQLITE_ALTER_TABLE -{"SQLITE_ALTER_TABLE", SQLITE_ALTER_TABLE}, -#endif -#ifdef SQLITE_ANALYZE -{"SQLITE_ANALYZE", SQLITE_ANALYZE}, -#endif -#ifdef SQLITE_ATTACH -{"SQLITE_ATTACH", SQLITE_ATTACH}, -#endif -#ifdef SQLITE_COPY -{"SQLITE_COPY", SQLITE_COPY}, -#endif -#ifdef SQLITE_CREATE_INDEX -{"SQLITE_CREATE_INDEX", SQLITE_CREATE_INDEX}, -#endif -#ifdef SQLITE_CREATE_TABLE -{"SQLITE_CREATE_TABLE", SQLITE_CREATE_TABLE}, -#endif -#ifdef SQLITE_CREATE_TEMP_INDEX -{"SQLITE_CREATE_TEMP_INDEX", SQLITE_CREATE_TEMP_INDEX}, -#endif -#ifdef SQLITE_CREATE_TEMP_TABLE -{"SQLITE_CREATE_TEMP_TABLE", SQLITE_CREATE_TEMP_TABLE}, -#endif -#ifdef SQLITE_CREATE_TEMP_TRIGGER -{"SQLITE_CREATE_TEMP_TRIGGER", SQLITE_CREATE_TEMP_TRIGGER}, -#endif -#ifdef SQLITE_CREATE_TEMP_VIEW -{"SQLITE_CREATE_TEMP_VIEW", SQLITE_CREATE_TEMP_VIEW}, -#endif -#ifdef SQLITE_CREATE_TRIGGER -{"SQLITE_CREATE_TRIGGER", SQLITE_CREATE_TRIGGER}, -#endif -#ifdef SQLITE_CREATE_VIEW -{"SQLITE_CREATE_VIEW", SQLITE_CREATE_VIEW}, -#endif -#ifdef SQLITE_CREATE_VTABLE -{"SQLITE_CREATE_VTABLE", SQLITE_CREATE_VTABLE}, -#endif -#ifdef SQLITE_DELETE -{"SQLITE_DELETE", SQLITE_DELETE}, -#endif -#ifdef SQLITE_DETACH -{"SQLITE_DETACH", SQLITE_DETACH}, -#endif -#ifdef SQLITE_DROP_INDEX -{"SQLITE_DROP_INDEX", SQLITE_DROP_INDEX}, -#endif -#ifdef SQLITE_DROP_TABLE -{"SQLITE_DROP_TABLE", SQLITE_DROP_TABLE}, -#endif -#ifdef SQLITE_DROP_TEMP_INDEX -{"SQLITE_DROP_TEMP_INDEX", SQLITE_DROP_TEMP_INDEX}, -#endif -#ifdef SQLITE_DROP_TEMP_TABLE -{"SQLITE_DROP_TEMP_TABLE", SQLITE_DROP_TEMP_TABLE}, -#endif -#ifdef SQLITE_DROP_TEMP_TRIGGER -{"SQLITE_DROP_TEMP_TRIGGER", SQLITE_DROP_TEMP_TRIGGER}, -#endif -#ifdef SQLITE_DROP_TEMP_VIEW -{"SQLITE_DROP_TEMP_VIEW", SQLITE_DROP_TEMP_VIEW}, -#endif -#ifdef SQLITE_DROP_TRIGGER -{"SQLITE_DROP_TRIGGER", SQLITE_DROP_TRIGGER}, -#endif -#ifdef SQLITE_DROP_VIEW -{"SQLITE_DROP_VIEW", SQLITE_DROP_VIEW}, -#endif -#ifdef SQLITE_DROP_VTABLE -{"SQLITE_DROP_VTABLE", SQLITE_DROP_VTABLE}, -#endif -#ifdef SQLITE_FUNCTION -{"SQLITE_FUNCTION", SQLITE_FUNCTION}, -#endif -#ifdef SQLITE_INSERT -{"SQLITE_INSERT", SQLITE_INSERT}, -#endif -#ifdef SQLITE_PRAGMA -{"SQLITE_PRAGMA", SQLITE_PRAGMA}, -#endif -#ifdef SQLITE_READ -{"SQLITE_READ", SQLITE_READ}, -#endif -#ifdef SQLITE_RECURSIVE -{"SQLITE_RECURSIVE", SQLITE_RECURSIVE}, -#endif -#ifdef SQLITE_REINDEX -{"SQLITE_REINDEX", SQLITE_REINDEX}, -#endif -#ifdef SQLITE_SAVEPOINT -{"SQLITE_SAVEPOINT", SQLITE_SAVEPOINT}, -#endif -#ifdef SQLITE_SELECT -{"SQLITE_SELECT", SQLITE_SELECT}, -#endif -#ifdef SQLITE_TRANSACTION -{"SQLITE_TRANSACTION", SQLITE_TRANSACTION}, -#endif -#ifdef SQLITE_UPDATE -{"SQLITE_UPDATE", SQLITE_UPDATE}, -#endif -#ifdef SQLITE_LIMIT_ATTACHED -{"SQLITE_LIMIT_ATTACHED", SQLITE_LIMIT_ATTACHED}, -#endif -#ifdef SQLITE_LIMIT_COLUMN -{"SQLITE_LIMIT_COLUMN", SQLITE_LIMIT_COLUMN}, -#endif -#ifdef SQLITE_LIMIT_COMPOUND_SELECT -{"SQLITE_LIMIT_COMPOUND_SELECT", SQLITE_LIMIT_COMPOUND_SELECT}, -#endif -#ifdef SQLITE_LIMIT_EXPR_DEPTH -{"SQLITE_LIMIT_EXPR_DEPTH", SQLITE_LIMIT_EXPR_DEPTH}, -#endif -#ifdef SQLITE_LIMIT_FUNCTION_ARG -{"SQLITE_LIMIT_FUNCTION_ARG", SQLITE_LIMIT_FUNCTION_ARG}, -#endif -#ifdef SQLITE_LIMIT_LENGTH -{"SQLITE_LIMIT_LENGTH", SQLITE_LIMIT_LENGTH}, -#endif -#ifdef SQLITE_LIMIT_LIKE_PATTERN_LENGTH -{"SQLITE_LIMIT_LIKE_PATTERN_LENGTH", SQLITE_LIMIT_LIKE_PATTERN_LENGTH}, -#endif -#ifdef SQLITE_LIMIT_SQL_LENGTH -{"SQLITE_LIMIT_SQL_LENGTH", SQLITE_LIMIT_SQL_LENGTH}, -#endif -#ifdef SQLITE_LIMIT_TRIGGER_DEPTH -{"SQLITE_LIMIT_TRIGGER_DEPTH", SQLITE_LIMIT_TRIGGER_DEPTH}, -#endif -#ifdef SQLITE_LIMIT_VARIABLE_NUMBER -{"SQLITE_LIMIT_VARIABLE_NUMBER", SQLITE_LIMIT_VARIABLE_NUMBER}, -#endif -#ifdef SQLITE_LIMIT_VDBE_OP -{"SQLITE_LIMIT_VDBE_OP", SQLITE_LIMIT_VDBE_OP}, -#endif -#ifdef SQLITE_LIMIT_WORKER_THREADS -{"SQLITE_LIMIT_WORKER_THREADS", SQLITE_LIMIT_WORKER_THREADS}, -#endif -#ifdef SQLITE_BLOB -{"SQLITE_BLOB", SQLITE_BLOB}, -#endif -#ifdef SQLITE_FLOAT -{"SQLITE_FLOAT", SQLITE_FLOAT}, -#endif -#ifdef SQLITE_INTEGER -{"SQLITE_INTEGER", SQLITE_INTEGER}, -#endif -#ifdef SQLITE_NULL -{"SQLITE_NULL", SQLITE_NULL}, -#endif -#ifdef SQLITE_TEXT -{"SQLITE_TEXT", SQLITE_TEXT}, -#endif -#ifdef SQLITE_ANY -{"SQLITE_ANY", SQLITE_ANY}, -#endif -#ifdef SQLITE_UTF16 -{"SQLITE_UTF16", SQLITE_UTF16}, -#endif -#ifdef SQLITE_UTF16BE -{"SQLITE_UTF16BE", SQLITE_UTF16BE}, -#endif -#ifdef SQLITE_UTF16LE -{"SQLITE_UTF16LE", SQLITE_UTF16LE}, -#endif -#ifdef SQLITE_UTF16_ALIGNED -{"SQLITE_UTF16_ALIGNED", SQLITE_UTF16_ALIGNED}, -#endif -#ifdef SQLITE_UTF8 -{"SQLITE_UTF8", SQLITE_UTF8}, -#endif -#ifdef SQLITE_DETERMINISTIC -{"SQLITE_DETERMINISTIC", SQLITE_DETERMINISTIC}, -#endif -#ifdef SQLITE_STATIC -{"SQLITE_STATIC", SQLITE_STATIC}, -#endif -#ifdef SQLITE_TRANSIENT -{"SQLITE_TRANSIENT", SQLITE_TRANSIENT}, -#endif -#ifdef SQLITE_INDEX_CONSTRAINT_EQ -{"SQLITE_INDEX_CONSTRAINT_EQ", SQLITE_INDEX_CONSTRAINT_EQ}, -#endif -#ifdef SQLITE_INDEX_CONSTRAINT_GE -{"SQLITE_INDEX_CONSTRAINT_GE", SQLITE_INDEX_CONSTRAINT_GE}, -#endif -#ifdef SQLITE_INDEX_CONSTRAINT_GT -{"SQLITE_INDEX_CONSTRAINT_GT", SQLITE_INDEX_CONSTRAINT_GT}, -#endif -#ifdef SQLITE_INDEX_CONSTRAINT_LE -{"SQLITE_INDEX_CONSTRAINT_LE", SQLITE_INDEX_CONSTRAINT_LE}, -#endif -#ifdef SQLITE_INDEX_CONSTRAINT_LT -{"SQLITE_INDEX_CONSTRAINT_LT", SQLITE_INDEX_CONSTRAINT_LT}, -#endif -#ifdef SQLITE_INDEX_CONSTRAINT_MATCH -{"SQLITE_INDEX_CONSTRAINT_MATCH", SQLITE_INDEX_CONSTRAINT_MATCH}, -#endif -#ifdef SQLITE_MUTEX_FAST -{"SQLITE_MUTEX_FAST", SQLITE_MUTEX_FAST}, -#endif -#ifdef SQLITE_MUTEX_RECURSIVE -{"SQLITE_MUTEX_RECURSIVE", SQLITE_MUTEX_RECURSIVE}, -#endif -#ifdef SQLITE_MUTEX_STATIC_APP1 -{"SQLITE_MUTEX_STATIC_APP1", SQLITE_MUTEX_STATIC_APP1}, -#endif -#ifdef SQLITE_MUTEX_STATIC_APP2 -{"SQLITE_MUTEX_STATIC_APP2", SQLITE_MUTEX_STATIC_APP2}, -#endif -#ifdef SQLITE_MUTEX_STATIC_APP3 -{"SQLITE_MUTEX_STATIC_APP3", SQLITE_MUTEX_STATIC_APP3}, -#endif -#ifdef SQLITE_MUTEX_STATIC_LRU -{"SQLITE_MUTEX_STATIC_LRU", SQLITE_MUTEX_STATIC_LRU}, -#endif -#ifdef SQLITE_MUTEX_STATIC_LRU2 -{"SQLITE_MUTEX_STATIC_LRU2", SQLITE_MUTEX_STATIC_LRU2}, -#endif -#ifdef SQLITE_MUTEX_STATIC_MASTER -{"SQLITE_MUTEX_STATIC_MASTER", SQLITE_MUTEX_STATIC_MASTER}, -#endif -#ifdef SQLITE_MUTEX_STATIC_MEM -{"SQLITE_MUTEX_STATIC_MEM", SQLITE_MUTEX_STATIC_MEM}, -#endif -#ifdef SQLITE_MUTEX_STATIC_MEM2 -{"SQLITE_MUTEX_STATIC_MEM2", SQLITE_MUTEX_STATIC_MEM2}, -#endif -#ifdef SQLITE_MUTEX_STATIC_OPEN -{"SQLITE_MUTEX_STATIC_OPEN", SQLITE_MUTEX_STATIC_OPEN}, -#endif -#ifdef SQLITE_MUTEX_STATIC_PMEM -{"SQLITE_MUTEX_STATIC_PMEM", SQLITE_MUTEX_STATIC_PMEM}, -#endif -#ifdef SQLITE_MUTEX_STATIC_PRNG -{"SQLITE_MUTEX_STATIC_PRNG", SQLITE_MUTEX_STATIC_PRNG}, -#endif -#ifdef SQLITE_MUTEX_STATIC_VFS1 -{"SQLITE_MUTEX_STATIC_VFS1", SQLITE_MUTEX_STATIC_VFS1}, -#endif -#ifdef SQLITE_MUTEX_STATIC_VFS2 -{"SQLITE_MUTEX_STATIC_VFS2", SQLITE_MUTEX_STATIC_VFS2}, -#endif -#ifdef SQLITE_MUTEX_STATIC_VFS3 -{"SQLITE_MUTEX_STATIC_VFS3", SQLITE_MUTEX_STATIC_VFS3}, -#endif -#ifdef SQLITE_TESTCTRL_ALWAYS -{"SQLITE_TESTCTRL_ALWAYS", SQLITE_TESTCTRL_ALWAYS}, -#endif -#ifdef SQLITE_TESTCTRL_ASSERT -{"SQLITE_TESTCTRL_ASSERT", SQLITE_TESTCTRL_ASSERT}, -#endif -#ifdef SQLITE_TESTCTRL_BENIGN_MALLOC_HOOKS -{"SQLITE_TESTCTRL_BENIGN_MALLOC_HOOKS", SQLITE_TESTCTRL_BENIGN_MALLOC_HOOKS}, -#endif -#ifdef SQLITE_TESTCTRL_BITVEC_TEST -{"SQLITE_TESTCTRL_BITVEC_TEST", SQLITE_TESTCTRL_BITVEC_TEST}, -#endif -#ifdef SQLITE_TESTCTRL_BYTEORDER -{"SQLITE_TESTCTRL_BYTEORDER", SQLITE_TESTCTRL_BYTEORDER}, -#endif -#ifdef SQLITE_TESTCTRL_EXPLAIN_STMT -{"SQLITE_TESTCTRL_EXPLAIN_STMT", SQLITE_TESTCTRL_EXPLAIN_STMT}, -#endif -#ifdef SQLITE_TESTCTRL_FAULT_INSTALL -{"SQLITE_TESTCTRL_FAULT_INSTALL", SQLITE_TESTCTRL_FAULT_INSTALL}, -#endif -#ifdef SQLITE_TESTCTRL_FIRST -{"SQLITE_TESTCTRL_FIRST", SQLITE_TESTCTRL_FIRST}, -#endif -#ifdef SQLITE_TESTCTRL_IMPOSTER -{"SQLITE_TESTCTRL_IMPOSTER", SQLITE_TESTCTRL_IMPOSTER}, -#endif -#ifdef SQLITE_TESTCTRL_ISINIT -{"SQLITE_TESTCTRL_ISINIT", SQLITE_TESTCTRL_ISINIT}, -#endif -#ifdef SQLITE_TESTCTRL_ISKEYWORD -{"SQLITE_TESTCTRL_ISKEYWORD", SQLITE_TESTCTRL_ISKEYWORD}, -#endif -#ifdef SQLITE_TESTCTRL_LAST -{"SQLITE_TESTCTRL_LAST", SQLITE_TESTCTRL_LAST}, -#endif -#ifdef SQLITE_TESTCTRL_LOCALTIME_FAULT -{"SQLITE_TESTCTRL_LOCALTIME_FAULT", SQLITE_TESTCTRL_LOCALTIME_FAULT}, -#endif -#ifdef SQLITE_TESTCTRL_NEVER_CORRUPT -{"SQLITE_TESTCTRL_NEVER_CORRUPT", SQLITE_TESTCTRL_NEVER_CORRUPT}, -#endif -#ifdef SQLITE_TESTCTRL_OPTIMIZATIONS -{"SQLITE_TESTCTRL_OPTIMIZATIONS", SQLITE_TESTCTRL_OPTIMIZATIONS}, -#endif -#ifdef SQLITE_TESTCTRL_PENDING_BYTE -{"SQLITE_TESTCTRL_PENDING_BYTE", SQLITE_TESTCTRL_PENDING_BYTE}, -#endif -#ifdef SQLITE_TESTCTRL_PRNG_RESET -{"SQLITE_TESTCTRL_PRNG_RESET", SQLITE_TESTCTRL_PRNG_RESET}, -#endif -#ifdef SQLITE_TESTCTRL_PRNG_RESTORE -{"SQLITE_TESTCTRL_PRNG_RESTORE", SQLITE_TESTCTRL_PRNG_RESTORE}, -#endif -#ifdef SQLITE_TESTCTRL_PRNG_SAVE -{"SQLITE_TESTCTRL_PRNG_SAVE", SQLITE_TESTCTRL_PRNG_SAVE}, -#endif -#ifdef SQLITE_TESTCTRL_RESERVE -{"SQLITE_TESTCTRL_RESERVE", SQLITE_TESTCTRL_RESERVE}, -#endif -#ifdef SQLITE_TESTCTRL_SCRATCHMALLOC -{"SQLITE_TESTCTRL_SCRATCHMALLOC", SQLITE_TESTCTRL_SCRATCHMALLOC}, -#endif -#ifdef SQLITE_TESTCTRL_SORTER_MMAP -{"SQLITE_TESTCTRL_SORTER_MMAP", SQLITE_TESTCTRL_SORTER_MMAP}, -#endif -#ifdef SQLITE_TESTCTRL_VDBE_COVERAGE -{"SQLITE_TESTCTRL_VDBE_COVERAGE", SQLITE_TESTCTRL_VDBE_COVERAGE}, -#endif -#ifdef SQLITE_STATUS_MALLOC_COUNT -{"SQLITE_STATUS_MALLOC_COUNT", SQLITE_STATUS_MALLOC_COUNT}, -#endif -#ifdef SQLITE_STATUS_MALLOC_SIZE -{"SQLITE_STATUS_MALLOC_SIZE", SQLITE_STATUS_MALLOC_SIZE}, -#endif -#ifdef SQLITE_STATUS_MEMORY_USED -{"SQLITE_STATUS_MEMORY_USED", SQLITE_STATUS_MEMORY_USED}, -#endif -#ifdef SQLITE_STATUS_PAGECACHE_OVERFLOW -{"SQLITE_STATUS_PAGECACHE_OVERFLOW", SQLITE_STATUS_PAGECACHE_OVERFLOW}, -#endif -#ifdef SQLITE_STATUS_PAGECACHE_SIZE -{"SQLITE_STATUS_PAGECACHE_SIZE", SQLITE_STATUS_PAGECACHE_SIZE}, -#endif -#ifdef SQLITE_STATUS_PAGECACHE_USED -{"SQLITE_STATUS_PAGECACHE_USED", SQLITE_STATUS_PAGECACHE_USED}, -#endif -#ifdef SQLITE_STATUS_PARSER_STACK -{"SQLITE_STATUS_PARSER_STACK", SQLITE_STATUS_PARSER_STACK}, -#endif -#ifdef SQLITE_STATUS_SCRATCH_OVERFLOW -{"SQLITE_STATUS_SCRATCH_OVERFLOW", SQLITE_STATUS_SCRATCH_OVERFLOW}, -#endif -#ifdef SQLITE_STATUS_SCRATCH_SIZE -{"SQLITE_STATUS_SCRATCH_SIZE", SQLITE_STATUS_SCRATCH_SIZE}, -#endif -#ifdef SQLITE_STATUS_SCRATCH_USED -{"SQLITE_STATUS_SCRATCH_USED", SQLITE_STATUS_SCRATCH_USED}, -#endif -#ifdef SQLITE_DBSTATUS_CACHE_HIT -{"SQLITE_DBSTATUS_CACHE_HIT", SQLITE_DBSTATUS_CACHE_HIT}, -#endif -#ifdef SQLITE_DBSTATUS_CACHE_MISS -{"SQLITE_DBSTATUS_CACHE_MISS", SQLITE_DBSTATUS_CACHE_MISS}, -#endif -#ifdef SQLITE_DBSTATUS_CACHE_USED -{"SQLITE_DBSTATUS_CACHE_USED", SQLITE_DBSTATUS_CACHE_USED}, -#endif -#ifdef SQLITE_DBSTATUS_CACHE_WRITE -{"SQLITE_DBSTATUS_CACHE_WRITE", SQLITE_DBSTATUS_CACHE_WRITE}, -#endif -#ifdef SQLITE_DBSTATUS_DEFERRED_FKS -{"SQLITE_DBSTATUS_DEFERRED_FKS", SQLITE_DBSTATUS_DEFERRED_FKS}, -#endif -#ifdef SQLITE_DBSTATUS_LOOKASIDE_HIT -{"SQLITE_DBSTATUS_LOOKASIDE_HIT", SQLITE_DBSTATUS_LOOKASIDE_HIT}, -#endif -#ifdef SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL -{"SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL", SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL}, -#endif -#ifdef SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE -{"SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE", SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE}, -#endif -#ifdef SQLITE_DBSTATUS_LOOKASIDE_USED -{"SQLITE_DBSTATUS_LOOKASIDE_USED", SQLITE_DBSTATUS_LOOKASIDE_USED}, -#endif -#ifdef SQLITE_DBSTATUS_MAX -{"SQLITE_DBSTATUS_MAX", SQLITE_DBSTATUS_MAX}, -#endif -#ifdef SQLITE_DBSTATUS_SCHEMA_USED -{"SQLITE_DBSTATUS_SCHEMA_USED", SQLITE_DBSTATUS_SCHEMA_USED}, -#endif -#ifdef SQLITE_DBSTATUS_STMT_USED -{"SQLITE_DBSTATUS_STMT_USED", SQLITE_DBSTATUS_STMT_USED}, -#endif -#ifdef SQLITE_STMTSTATUS_AUTOINDEX -{"SQLITE_STMTSTATUS_AUTOINDEX", SQLITE_STMTSTATUS_AUTOINDEX}, -#endif -#ifdef SQLITE_STMTSTATUS_FULLSCAN_STEP -{"SQLITE_STMTSTATUS_FULLSCAN_STEP", SQLITE_STMTSTATUS_FULLSCAN_STEP}, -#endif -#ifdef SQLITE_STMTSTATUS_SORT -{"SQLITE_STMTSTATUS_SORT", SQLITE_STMTSTATUS_SORT}, -#endif -#ifdef SQLITE_STMTSTATUS_VM_STEP -{"SQLITE_STMTSTATUS_VM_STEP", SQLITE_STMTSTATUS_VM_STEP}, -#endif -#ifdef SQLITE_CHECKPOINT_FULL -{"SQLITE_CHECKPOINT_FULL", SQLITE_CHECKPOINT_FULL}, -#endif -#ifdef SQLITE_CHECKPOINT_PASSIVE -{"SQLITE_CHECKPOINT_PASSIVE", SQLITE_CHECKPOINT_PASSIVE}, -#endif -#ifdef SQLITE_CHECKPOINT_RESTART -{"SQLITE_CHECKPOINT_RESTART", SQLITE_CHECKPOINT_RESTART}, -#endif -#ifdef SQLITE_CHECKPOINT_TRUNCATE -{"SQLITE_CHECKPOINT_TRUNCATE", SQLITE_CHECKPOINT_TRUNCATE}, -#endif -#ifdef SQLITE_VTAB_CONSTRAINT_SUPPORT -{"SQLITE_VTAB_CONSTRAINT_SUPPORT", SQLITE_VTAB_CONSTRAINT_SUPPORT}, -#endif -#ifdef SQLITE_FAIL -{"SQLITE_FAIL", SQLITE_FAIL}, -#endif -#ifdef SQLITE_REPLACE -{"SQLITE_REPLACE", SQLITE_REPLACE}, -#endif -#ifdef SQLITE_ROLLBACK -{"SQLITE_ROLLBACK", SQLITE_ROLLBACK}, -#endif -#ifdef SQLITE_SCANSTAT_EST -{"SQLITE_SCANSTAT_EST", SQLITE_SCANSTAT_EST}, -#endif -#ifdef SQLITE_SCANSTAT_EXPLAIN -{"SQLITE_SCANSTAT_EXPLAIN", SQLITE_SCANSTAT_EXPLAIN}, -#endif -#ifdef SQLITE_SCANSTAT_NAME -{"SQLITE_SCANSTAT_NAME", SQLITE_SCANSTAT_NAME}, -#endif -#ifdef SQLITE_SCANSTAT_NLOOP -{"SQLITE_SCANSTAT_NLOOP", SQLITE_SCANSTAT_NLOOP}, -#endif -#ifdef SQLITE_SCANSTAT_NVISIT -{"SQLITE_SCANSTAT_NVISIT", SQLITE_SCANSTAT_NVISIT}, -#endif -#ifdef SQLITE_SCANSTAT_SELECTID -{"SQLITE_SCANSTAT_SELECTID", SQLITE_SCANSTAT_SELECTID}, -#endif diff --git a/playhouse/_pysqlite/statement.h b/playhouse/_pysqlite/statement.h deleted file mode 100644 index ecf39f275..000000000 --- a/playhouse/_pysqlite/statement.h +++ /dev/null @@ -1,59 +0,0 @@ -/* statement.h - definitions for the statement type - * - * Copyright (C) 2005-2015 Gerhard Häring - * - * This file is part of pysqlite. - * - * 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_STATEMENT_H -#define PYSQLITE_STATEMENT_H -#include "Python.h" - -#include "connection.h" -#include "sqlite3.h" - -#define PYSQLITE_TOO_MUCH_SQL (-100) -#define PYSQLITE_SQL_WRONG_TYPE (-101) - -typedef struct -{ - PyObject_HEAD - sqlite3* db; - sqlite3_stmt* st; - PyObject* sql; - int in_use; - int is_ddl; - PyObject* in_weakreflist; /* List of weak references */ -} pysqlite_Statement; - -extern PyTypeObject pysqlite_StatementType; - -int pysqlite_statement_create(pysqlite_Statement* self, pysqlite_Connection* connection, PyObject* sql); -void pysqlite_statement_dealloc(pysqlite_Statement* self); - -int pysqlite_statement_bind_parameter(pysqlite_Statement* self, int pos, PyObject* parameter, int allow_8bit_chars); -void pysqlite_statement_bind_parameters(pysqlite_Statement* self, PyObject* parameters, int allow_8bit_chars); - -int pysqlite_statement_finalize(pysqlite_Statement* self); -int pysqlite_statement_reset(pysqlite_Statement* self); -void pysqlite_statement_mark_dirty(pysqlite_Statement* self); - -int pysqlite_statement_setup_types(void); - -#endif diff --git a/playhouse/_pysqlite/util.h b/playhouse/_pysqlite/util.h deleted file mode 100644 index a7b46d5a9..000000000 --- a/playhouse/_pysqlite/util.h +++ /dev/null @@ -1,42 +0,0 @@ -/* util.h - various utility functions - * - * Copyright (C) 2005-2015 Gerhard Häring - * - * This file is part of pysqlite. - * - * 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_UTIL_H -#define PYSQLITE_UTIL_H -#include "Python.h" -#include "pythread.h" -#include "sqlite3.h" -#include "connection.h" - -int pysqlite_step(sqlite3_stmt* statement, pysqlite_Connection* connection); - -/** - * Checks the SQLite error code and sets the appropriate DB-API exception. - * Returns the error code (0 means no error occurred). - */ -int _pysqlite_seterror(sqlite3* db, sqlite3_stmt* st); - -PyObject * _pysqlite_long_from_int64(sqlite_int64 value); -sqlite_int64 _pysqlite_long_as_int64(PyObject * value); - -#endif From 637cc89ae5d1d6a5f48d99e9a03cc86c961bf85b Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Wed, 17 Jul 2019 09:13:51 -0500 Subject: [PATCH 26/41] Add test for filter with aggregates on newest Sqlite. --- tests/base.py | 1 + tests/models.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/tests/base.py b/tests/base.py index 4b2995fe9..7c76ed688 100644 --- a/tests/base.py +++ b/tests/base.py @@ -85,6 +85,7 @@ def new_connection(): IS_SQLITE_15 = IS_SQLITE and sqlite3.sqlite_version_info >= (3, 15) IS_SQLITE_24 = IS_SQLITE and sqlite3.sqlite_version_info >= (3, 24) IS_SQLITE_25 = IS_SQLITE and sqlite3.sqlite_version_info >= (3, 25) +IS_SQLITE_30 = IS_SQLITE and sqlite3.sqlite_version_info >= (3, 30) IS_SQLITE_9 = IS_SQLITE and sqlite3.sqlite_version_info >= (3, 9) IS_MYSQL_ADVANCED_FEATURES = False IS_MYSQL_JSON = False diff --git a/tests/models.py b/tests/models.py index 490f501b5..747f9e20c 100644 --- a/tests/models.py +++ b/tests/models.py @@ -27,6 +27,7 @@ from .base import IS_SQLITE_15 # Row-values. from .base import IS_SQLITE_24 # Upsert. from .base import IS_SQLITE_25 # Window functions. +from .base import IS_SQLITE_30 # FILTER clause functions. from .base import IS_SQLITE_9 from .base import ModelTestCase from .base import TestModel @@ -2034,6 +2035,19 @@ def test_filter_clause(self): (3, 100., 103.), ]) + @skip_if(IS_MYSQL or (IS_SQLITE and not IS_SQLITE_30), + 'requires FILTER with aggregates') + def test_filter_with_aggregate(self): + condsum = fn.SUM(Sample.value).filter(Sample.counter > 1) + query = (Sample + .select(Sample.counter, condsum.alias('cs')) + .group_by(Sample.counter) + .order_by(Sample.counter)) + self.assertEqual(list(query.tuples()), [ + (1, None), + (2, 4.), + (3, 100.)]) + @skip_if(IS_SQLITE or (IS_MYSQL and not IS_MYSQL_ADVANCED_FEATURES)) class TestForUpdateIntegration(ModelTestCase): From 9fb1c89353fbaae0f33a5420f4426f823eae1326 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 22 Jul 2019 04:34:16 -0500 Subject: [PATCH 27/41] APIs for managing isolation level in postgres. Refs #1972 --- docs/peewee/api.rst | 4 +++- docs/peewee/database.rst | 37 +++++++++++++++++++++++++++++++++++++ peewee.py | 6 +++++- tests/postgres.py | 16 ++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/docs/peewee/api.rst b/docs/peewee/api.rst index 0a4e114d9..f2178d135 100644 --- a/docs/peewee/api.rst +++ b/docs/peewee/api.rst @@ -938,7 +938,7 @@ Database strategy (defaults to DEFERRED). -.. py:class:: PostgresqlDatabase(database[, register_unicode=True[, encoding=None]]) +.. py:class:: PostgresqlDatabase(database[, register_unicode=True[, encoding=None[, isolation_level=None]]]) Postgresql database implementation. @@ -946,6 +946,8 @@ Database :param bool register_unicode: Register unicode types. :param str encoding: Database encoding. + :param int isolation_level: Isolation level constant, defined in the + ``psycopg2.extensions`` module. .. py:method:: set_time_zone(timezone) diff --git a/docs/peewee/database.rst b/docs/peewee/database.rst index 23a96d318..09a2e2082 100644 --- a/docs/peewee/database.rst +++ b/docs/peewee/database.rst @@ -132,6 +132,43 @@ If you would like to use these awesome features, use the psql_db = PostgresqlExtDatabase('my_database', user='postgres') + +Isolation level +^^^^^^^^^^^^^^^ + +As of Peewee 3.9.7, the isolation level can be specified as an initialization +parameter, using the symbolic constants in ``psycopg2.extensions``: + +.. code-block:: python + + from psycopg2.extensions import ISOLATION_LEVEL_SERIALIZABLE + + db = PostgresqlDatabase('my_app', user='postgres', host='db-host', + isolation_level=ISOLATION_LEVEL_SERIALIZABLE) + +.. note:: + + In older versions, you can manually set the isolation level on the + underlying psycopg2 connection. This can be done in a one-off fashion: + + .. code-block:: python + + db = PostgresqlDatabase(...) + conn = db.connection() # returns current connection. + + from psycopg2.extensions import ISOLATION_LEVEL_SERIALIZABLE + conn.set_isolation_level(ISOLATION_LEVEL_SERIALIZABLE) + + To run this every time a connection is created, subclass and implement + the ``_initialize_database()`` hook, which is designed for this purpose: + + .. code-block:: python + + class SerializedPostgresqlDatabase(PostgresqlDatabase): + def _initialize_connection(self, conn): + conn.set_isolation_level(ISOLATION_LEVEL_SERIALIZABLE) + + .. _using_sqlite: Using SQLite diff --git a/peewee.py b/peewee.py index 91777b7db..f4e25220d 100644 --- a/peewee.py +++ b/peewee.py @@ -3586,9 +3586,11 @@ class PostgresqlDatabase(Database): safe_create_index = False sequences = True - def init(self, database, register_unicode=True, encoding=None, **kwargs): + def init(self, database, register_unicode=True, encoding=None, + isolation_level=None, **kwargs): self._register_unicode = register_unicode self._encoding = encoding + self._isolation_level = isolation_level super(PostgresqlDatabase, self).init(database, **kwargs) def _connect(self): @@ -3600,6 +3602,8 @@ def _connect(self): pg_extensions.register_type(pg_extensions.UNICODEARRAY, conn) if self._encoding: conn.set_client_encoding(self._encoding) + if self._isolation_level: + conn.set_isolation_level(self._isolation_level) return conn def _set_server_version(self, conn): diff --git a/tests/postgres.py b/tests/postgres.py index 9144fc345..b7c14377a 100644 --- a/tests/postgres.py +++ b/tests/postgres.py @@ -8,6 +8,7 @@ from playhouse.postgres_ext import * from .base import BaseTestCase +from .base import DatabaseTestCase from .base import ModelTestCase from .base import TestModel from .base import db_loader @@ -1076,3 +1077,18 @@ def test_atomic_block_exception(self): KX.create(key='k1', value=10) self.assertEqual(KX.select().count(), 1) + + +class TestPostgresIsolationLevel(DatabaseTestCase): + database = db_loader('postgres', isolation_level=3) # SERIALIZABLE. + + def test_isolation_level(self): + conn = self.database.connection() + self.assertEqual(conn.isolation_level, 3) + + conn.set_isolation_level(2) + self.assertEqual(conn.isolation_level, 2) + + self.database.close() + conn = self.database.connection() + self.assertEqual(conn.isolation_level, 3) From 9e4f1fa3ad891f21eb30554d057fbf5ace8b754d Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 23 Jul 2019 06:41:17 -0500 Subject: [PATCH 28/41] Quick workaround for #1969. --- peewee.py | 15 +++++++++++++++ playhouse/hybrid.py | 7 +++++-- tests/hybrid.py | 9 +++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/peewee.py b/peewee.py index f4e25220d..ff5d69397 100644 --- a/peewee.py +++ b/peewee.py @@ -460,6 +460,9 @@ def savepoint(self): return _savepoint(self) +class ModelDescriptor(object): pass + + # SQL Generation. @@ -6423,6 +6426,18 @@ def __init__(self, model, alias=None): self.__dict__['alias'] = alias def __getattr__(self, attr): + # Hack to work-around the fact that properties or other objects + # implementing the descriptor protocol (on the model being aliased), + # will not work correctly when we use getattr(). So we explicitly pass + # the model alias to the descriptor's getter. + try: + obj = self.model.__dict__[attr] + except KeyError: + pass + else: + if isinstance(obj, ModelDescriptor): + return obj.__get__(None, self) + model_attr = getattr(self.model, attr) if isinstance(model_attr, Field): self.__dict__[attr] = FieldAlias.create(self, model_attr) diff --git a/playhouse/hybrid.py b/playhouse/hybrid.py index 53f226288..50531cc35 100644 --- a/playhouse/hybrid.py +++ b/playhouse/hybrid.py @@ -1,6 +1,9 @@ +from peewee import ModelDescriptor + + # Hybrid methods/attributes, based on similar functionality in SQLAlchemy: # http://docs.sqlalchemy.org/en/improve_toc/orm/extensions/hybrid.html -class hybrid_method(object): +class hybrid_method(ModelDescriptor): def __init__(self, func, expr=None): self.func = func self.expr = expr or func @@ -15,7 +18,7 @@ def expression(self, expr): return self -class hybrid_property(object): +class hybrid_property(ModelDescriptor): def __init__(self, fget, fset=None, fdel=None, expr=None): self.fget = fget self.fset = fset diff --git a/tests/hybrid.py b/tests/hybrid.py index d0f38e513..97e460a47 100644 --- a/tests/hybrid.py +++ b/tests/hybrid.py @@ -99,3 +99,12 @@ def test_string_fields(self): query = Person.select().where(Person.full_name.startswith('huey c')) huey_db = query.get() self.assertEqual(huey_db.id, huey.id) + + def test_hybrid_model_alias(self): + Person.create(first='huey', last='cat') + PA = Person.alias() + query = PA.select(PA.full_name).where(PA.last == 'cat') + self.assertSQL(query, ( + 'SELECT (("t1"."first" || ?) || "t1"."last") ' + 'FROM "person" AS "t1" WHERE ("t1"."last" = ?)'), [' ', 'cat']) + self.assertEqual(query.tuples()[0], ('huey cat',)) From 15967ee5d78c992d677b23dc3fe598b02670dc53 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 23 Jul 2019 07:31:19 -0500 Subject: [PATCH 29/41] Fix for empty vs null values in model_to_dict() Fixes #1973 --- playhouse/shortcuts.py | 2 +- tests/shortcuts.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/playhouse/shortcuts.py b/playhouse/shortcuts.py index e1851b181..22b6b8d19 100644 --- a/playhouse/shortcuts.py +++ b/playhouse/shortcuts.py @@ -73,7 +73,7 @@ def model_to_dict(model, recurse=True, backrefs=False, only=None, field_data = model.__data__.get(field.name) if isinstance(field, ForeignKeyField) and recurse: - if field_data: + if field_data is not None: seen.add(field) rel_obj = getattr(model, field.name) field_data = model_to_dict( diff --git a/tests/shortcuts.py b/tests/shortcuts.py index 39cf0b24f..a7f795ee7 100644 --- a/tests/shortcuts.py +++ b/tests/shortcuts.py @@ -74,6 +74,13 @@ class Device(TestModel): host = ForeignKeyField(Host, backref='+') name = TextField() +class Basket(TestModel): + id = IntegerField(primary_key=True) + +class Item(TestModel): + id = IntegerField(primary_key=True) + basket = ForeignKeyField(Basket) + class TestModelToDict(ModelTestCase): database = get_in_memory_db() @@ -448,6 +455,17 @@ def test_model_to_dict_disabled_backref(self): {'id': 1, 'name': 'ssh'}, {'id': 2, 'name': 'vpn'}]) + @requires_models(Basket, Item) + def test_empty_vs_null_fk(self): + b = Basket.create(id=0) + i = Item.create(id=0, basket=b) + + data = model_to_dict(i) + self.assertEqual(data, {'id': 0, 'basket': {'id': 0}}) + + data = model_to_dict(i, recurse=False) + self.assertEqual(data, {'id': 0, 'basket': 0}) + class TestDictToModel(ModelTestCase): database = get_in_memory_db() From e3ee666b99e55eb7cdd8d72263fbe5f47dc42f82 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Wed, 24 Jul 2019 14:32:48 +0000 Subject: [PATCH 30/41] If a selected node is an un-aliased field, assign to field name. This is more aggressive about renaming selected columns and slotting them into the appropriate model field. The tests do not reveal any regressions, but there may be some use-case or scenario I've missed. Fixes #1975 --- peewee.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/peewee.py b/peewee.py index ff5d69397..12b7af677 100644 --- a/peewee.py +++ b/peewee.py @@ -7117,8 +7117,7 @@ def _initialize_columns(self): if raw_node._coerce: converters[idx] = node.python_value fields[idx] = node - if (column == node.name or column == node.column_name) and \ - not raw_node.is_alias(): + if not raw_node.is_alias(): self.columns[idx] = node.name elif isinstance(node, Function) and node._coerce: if node._python_value is not None: From eb98113d1eecbb13b35b7bbe0c6094a99999383e Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Wed, 24 Jul 2019 14:37:01 +0000 Subject: [PATCH 31/41] Subtract a small random value from timestamp in pool checkout code. This patch replaces #1976 and fixes #1883 --- playhouse/pool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playhouse/pool.py b/playhouse/pool.py index 9ade1da94..2ee3b486f 100644 --- a/playhouse/pool.py +++ b/playhouse/pool.py @@ -33,6 +33,7 @@ class Meta: """ import heapq import logging +import random import time from collections import namedtuple from itertools import chain @@ -153,7 +154,7 @@ def _connect(self): len(self._in_use) >= self._max_connections): raise MaxConnectionsExceeded('Exceeded maximum connections.') conn = super(PooledDatabase, self)._connect() - ts = time.time() + ts = time.time() - random.random() / 1000 key = self.conn_key(conn) logger.debug('Created new connection %s.', key) From 0ee463752c4b946c906d17495d894e9e2d3faee9 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Wed, 24 Jul 2019 10:17:20 -0500 Subject: [PATCH 32/41] Add regression test for field->view mapping, refs #1975 --- tests/regressions.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/regressions.py b/tests/regressions.py index e26ac179e..95bce22aa 100644 --- a/tests/regressions.py +++ b/tests/regressions.py @@ -891,3 +891,26 @@ def test_compound_regressions_1961(self): User.create(username='u1') self.assertTrue(cq.exists()) self.assertEqual(cq.count(), 1) + + +class TestViewFieldMapping(ModelTestCase): + requires = [User] + + def tearDown(self): + try: + self.execute('drop view user_testview_fm') + except Exception as exc: + pass + super(TestViewFieldMapping, self).tearDown() + + def test_view_field_mapping(self): + user = User.create(username='huey') + self.execute('create view user_testview_fm as ' + 'select id, username from users') + + class View(User): + class Meta: + table_name = 'user_testview_fm' + + self.assertEqual([(v.id, v.username) for v in View.select()], + [(user.id, 'huey')]) From c5a7e4d17f05ed83d9099387cfdaf7cd29bfdb39 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 29 Jul 2019 11:53:27 -0500 Subject: [PATCH 33/41] Apply PK converter to model instance values. Fixes #1979. --- peewee.py | 3 ++- tests/fields.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/peewee.py b/peewee.py index 12b7af677..c878cf613 100644 --- a/peewee.py +++ b/peewee.py @@ -6359,7 +6359,8 @@ def __ne__(self, other): return not self == other def __sql__(self, ctx): - return ctx.sql(getattr(self, self._meta.primary_key.name)) + return ctx.sql(Value(getattr(self, self._meta.primary_key.name), + converter=self._meta.primary_key.db_value)) @classmethod def bind(cls, database, bind_refs=True, bind_backrefs=True): diff --git a/tests/fields.py b/tests/fields.py index 4fb7f9f6f..841650f1f 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -894,6 +894,41 @@ def test_binary_uuid_field(self): self.assertEqual(u_db.bdata, uu) +class UU1(TestModel): + id = UUIDField(default=uuid.uuid4, primary_key=True) + name = TextField() + +class UU2(TestModel): + id = UUIDField(default=uuid.uuid4, primary_key=True) + u1 = ForeignKeyField(UU1) + name = TextField() + + +class TestForeignKeyUUIDField(ModelTestCase): + requires = [UU1, UU2] + + def test_bulk_insert(self): + # Create three UU1 instances. + UU1.insert_many([{UU1.name: name} for name in 'abc'], + fields=[UU1.id, UU1.name]).execute() + ua, ub, uc = UU1.select().order_by(UU1.name) + + # Create several UU2 instances. + data = ( + ('a1', ua), + ('b1', ub), + ('b2', ub), + ('c1', uc)) + iq = UU2.insert_many([{UU2.name: name, UU2.u1: u} for name, u in data], + fields=[UU2.id, UU2.name, UU2.u1]) + iq.execute() + + query = UU2.select().order_by(UU2.name) + for (name, u1), u2 in zip(data, query): + self.assertEqual(u2.name, name) + self.assertEqual(u2.u1.id, u1.id) + + class TSModel(TestModel): ts_s = TimestampField() ts_us = TimestampField(resolution=10 ** 6) From bb45c6601f75b3b035f706cddd7a60bfee550144 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 29 Jul 2019 22:25:06 -0500 Subject: [PATCH 34/41] More permissive bulk-insert. --- bench.py | 2 +- peewee.py | 39 +++++++++------ tests/model_sql.py | 122 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 15 deletions(-) diff --git a/bench.py b/bench.py index 8eba6659e..554aa5bdd 100644 --- a/bench.py +++ b/bench.py @@ -31,7 +31,7 @@ def inner(*args, **kwargs): start = time.time() fn(i, *args, **kwargs) times.append(time.time() - start) - print('%0.2f ... %s' % (round(sum(times) / N, 2), fn.__name__)) + print('%0.3f ... %s' % (round(sum(times) / N, 3), fn.__name__)) return inner def populate_register(s, n): diff --git a/peewee.py b/peewee.py index c878cf613..1a44210a6 100644 --- a/peewee.py +++ b/peewee.py @@ -2449,7 +2449,6 @@ def _generate_insert(self, insert, ctx): # Load and organize column defaults (if provided). defaults = self.get_default_data() - value_lookups = {} # First figure out what columns are being inserted (if they weren't # specified explicitly). Resulting columns are normalized and ordered. @@ -2466,40 +2465,41 @@ def _generate_insert(self, insert, ctx): else: # Infer column names from the dict of data being inserted. accum = [] - uses_strings = False # Are the dict keys strings or columns? - for key in row: - if isinstance(key, basestring): - column = getattr(self.table, key) - uses_strings = True - else: - column = key + for column in row: + if isinstance(column, basestring): + column = getattr(self.table, column) accum.append(column) - value_lookups[column] = key # Add any columns present in the default data that are not # accounted for by the dictionary of row data. column_set = set(accum) for col in (set(defaults) - column_set): accum.append(col) - value_lookups[col] = col.name if uses_strings else col columns = sorted(accum, key=lambda obj: obj.get_sort_key(ctx)) rows_iter = itertools.chain(iter((row,)), rows_iter) else: clean_columns = [] + seen = set() for column in columns: if isinstance(column, basestring): column_obj = getattr(self.table, column) else: column_obj = column - value_lookups[column_obj] = column clean_columns.append(column_obj) + seen.add(column_obj) columns = clean_columns for col in sorted(defaults, key=lambda obj: obj.get_sort_key(ctx)): - if col not in value_lookups: + if col not in seen: columns.append(col) - value_lookups[col] = col + + value_lookups = {} + for column in columns: + lookups = [column, column.name] + if isinstance(column, Field) and column.name != column.column_name: + lookups.append(column.column_name) + value_lookups[column] = lookups ctx.sql(EnclosedNodeList(columns)).literal(' VALUES ') columns_converters = [ @@ -2513,7 +2513,18 @@ def _generate_insert(self, insert, ctx): for i, (column, converter) in enumerate(columns_converters): try: if is_dict: - val = row[value_lookups[column]] + # The logic is a bit convoluted, but in order to be + # flexible in what we accept (dict keyed by + # column/field, field name, or underlying column name), + # we try accessing the row data dict using each + # possible key. If no match is found, throw an error. + for lookup in value_lookups[column]: + try: + val = row[lookup] + except KeyError: pass + else: break + else: + raise KeyError else: val = row[i] except (KeyError, IndexError): diff --git a/tests/model_sql.py b/tests/model_sql.py index 208b82a80..daa50ef4d 100644 --- a/tests/model_sql.py +++ b/tests/model_sql.py @@ -488,6 +488,128 @@ class Meta: self.assertSQL(query, ( 'INSERT INTO "person" ("name") VALUES (?)'), ['huey']) + def test_insert_get_field_values(self): + class User(TestModel): + username = TextField(primary_key=True) + class Meta: + database = self.database + + class Tweet(TestModel): + user = ForeignKeyField(User) + content = TextField() + class Meta: + database = self.database + + queries = ( + User.insert(username='a'), + User.insert({'username': 'a'}), + User.insert({User.username: 'a'})) + for query in queries: + self.assertSQL(query, ('INSERT INTO "user" ("username") ' + 'VALUES (?)'), ['a']) + + # Verify that we can provide all kinds of combinations to the + # constructor to INSERT and it will map the parameters correctly + # without losing values. + a = User(username='a') + queries = ( + Tweet.insert(user=a, content='ca'), + Tweet.insert({'user': a, 'content': 'ca'}), + Tweet.insert({Tweet.user: a, 'content': 'ca'}), + Tweet.insert({'user': a, Tweet.content: 'ca'}), + Tweet.insert({Tweet.user: a, Tweet.content: 'ca'}), + Tweet.insert({Tweet.user: a}, content='ca'), + Tweet.insert({Tweet.content: 'ca'}, user=a), + Tweet.insert({'user': a}, content='ca'), + Tweet.insert({'content': 'ca'}, user=a), + + # Also test using the foreign-key descriptor and column name. + Tweet.insert({Tweet.user_id: a, Tweet.content: 'ca'}), + Tweet.insert(user_id=a, content='ca'), + Tweet.insert({'user_id': a, 'content': 'ca'})) + + for query in queries: + self.assertSQL(query, ('INSERT INTO "tweet" ("user_id", "content")' + ' VALUES (?, ?)'), ['a', 'ca']) + + def test_insert_many_get_field_values(self): + class User(TestModel): + username = TextField(primary_key=True) + class Meta: + database = self.database + + class Tweet(TestModel): + user = ForeignKeyField(User) + content = TextField() + class Meta: + database = self.database + + # Ensure we can handle any combination of insert-data key and field + # list value. + pairs = ((User.username, 'username'), + ('username', User.username), + ('username', 'username'), + (User.username, User.username)) + + for dict_key, fields_key in pairs: + iq = User.insert_many([{dict_key: u} for u in 'abc'], + fields=[fields_key]) + self.assertSQL(iq, ( + 'INSERT INTO "user" ("username") VALUES (?), (?), (?)'), + ['a', 'b', 'c']) + + a, b = User(username='a'), User(username='b') + user_content = ( + (a, 'ca1'), + (a, 'ca2'), + (b, 'cb1'), + ('a', 'ca3')) # Specify user id directly. + + # Ensure we can mix-and-match key type within insert-data. + pairs = (('user', 'content'), + (Tweet.user, Tweet.content), + (Tweet.user, 'content'), + ('user', Tweet.content), + ('user_id', 'content'), + (Tweet.user_id, Tweet.content)) + + for ukey, ckey in pairs: + iq = Tweet.insert_many([{ukey: u, ckey: c} + for u, c in user_content]) + self.assertSQL(iq, ( + 'INSERT INTO "tweet" ("user_id", "content") VALUES ' + '(?, ?), (?, ?), (?, ?), (?, ?)'), + ['a', 'ca1', 'a', 'ca2', 'b', 'cb1', 'a', 'ca3']) + + def test_insert_many_dict_and_list(self): + class R(TestModel): + k = TextField(column_name='key') + v = IntegerField(column_name='value', default=0) + class Meta: + database = self.database + + data = ( + {'k': 'k1', 'v': 1}, + {R.k: 'k2', R.v: 2}, + {'key': 'k3', 'value': 3}, + ('k4', 4), + ('k5', '5'), # Will be converted properly. + {R.k: 'k6', R.v: '6'}, + {'key': 'k7', 'value': '7'}, + {'k': 'kx'}, + ('ky',)) + + param_str = ', '.join('(?, ?)' for _ in range(len(data))) + queries = ( + R.insert_many(data), + R.insert_many(data, fields=[R.k, R.v]), + R.insert_many(data, fields=['k', 'v'])) + for query in queries: + self.assertSQL(query, ( + 'INSERT INTO "r" ("key", "value") VALUES %s' % param_str), + ['k1', 1, 'k2', 2, 'k3', 3, 'k4', 4, 'k5', 5, 'k6', 6, + 'k7', 7, 'kx', 0, 'ky', 0]) + def test_update(self): class Stat(TestModel): url = TextField() From a6956e4562354bdbef38152d59521345bcb5942c Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 29 Jul 2019 22:46:15 -0500 Subject: [PATCH 35/41] Use Mapping for type checks elsewhere in INSERT code. --- peewee.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/peewee.py b/peewee.py index 1a44210a6..1e49f9681 100644 --- a/peewee.py +++ b/peewee.py @@ -2458,7 +2458,7 @@ def _generate_insert(self, insert, ctx): except StopIteration: raise self.DefaultValuesException('Error: no rows to insert.') - if not isinstance(row, dict): + if not isinstance(row, Mapping): columns = self.get_default_columns() if columns is None: raise ValueError('Bulk insert must specify columns.') @@ -2571,7 +2571,7 @@ def __sql__(self, ctx): .sql(self.table) .literal(' ')) - if isinstance(self._insert, dict) and not self._columns: + if isinstance(self._insert, Mapping) and not self._columns: try: self._simple_insert(ctx) except self.DefaultValuesException: From cb998a534dae148b39d4eb760018679607e86c5e Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 30 Jul 2019 14:33:59 -0500 Subject: [PATCH 36/41] Add tests for paginate() method. --- tests/model_sql.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/model_sql.py b/tests/model_sql.py index daa50ef4d..9c108d003 100644 --- a/tests/model_sql.py +++ b/tests/model_sql.py @@ -116,6 +116,19 @@ def test_order_by_extend(self): 'SELECT "t1"."id", "t1"."username" FROM "users" AS "t1" ' 'ORDER BY "t1"."username" DESC, "t1"."id"'), []) + def test_paginate(self): + # Get the first page, default is limit of 20. + query = User.select().paginate(1) + self.assertSQL(query, ( + 'SELECT "t1"."id", "t1"."username" FROM "users" AS "t1" ' + 'LIMIT ? OFFSET ?'), [20, 0]) + + # Page 3 contains rows 31-45. + query = User.select().paginate(3, 15) + self.assertSQL(query, ( + 'SELECT "t1"."id", "t1"."username" FROM "users" AS "t1" ' + 'LIMIT ? OFFSET ?'), [15, 30]) + def test_subquery_correction(self): users = User.select().where(User.username.in_(['foo', 'bar'])) query = Tweet.select().where(Tweet.user.in_(users)) From fd2580678d84b0f57bf4fbaf4962ece3a383e6ad Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 30 Jul 2019 14:51:42 -0500 Subject: [PATCH 37/41] Test insert w/modelalias. --- tests/model_sql.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/model_sql.py b/tests/model_sql.py index 9c108d003..feb506a05 100644 --- a/tests/model_sql.py +++ b/tests/model_sql.py @@ -623,6 +623,13 @@ class Meta: ['k1', 1, 'k2', 2, 'k3', 3, 'k4', 4, 'k5', 5, 'k6', 6, 'k7', 7, 'kx', 0, 'ky', 0]) + def test_insert_modelalias(self): + UA = User.alias('ua') + self.assertSQL(UA.insert({UA.username: 'huey'}), ( + 'INSERT INTO "users" ("username") VALUES (?)'), ['huey']) + self.assertSQL(UA.insert(username='huey'), ( + 'INSERT INTO "users" ("username") VALUES (?)'), ['huey']) + def test_update(self): class Stat(TestModel): url = TextField() From ba57ccc7abb065804a768843c6d14af4e67a1bec Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 30 Jul 2019 15:12:43 -0500 Subject: [PATCH 38/41] Add test for update+subqueries. --- tests/model_sql.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/model_sql.py b/tests/model_sql.py index feb506a05..5abd17b3b 100644 --- a/tests/model_sql.py +++ b/tests/model_sql.py @@ -654,6 +654,31 @@ class Stat(TestModel): 'WHERE ("stat"."url" = ?)'), [1, '/peewee']) + def test_update_subquery(self): + class U(TestModel): + username = TextField() + flood_count = IntegerField() + + class T(TestModel): + user = ForeignKeyField(U) + + ctq = T.select(fn.COUNT(T.id) / 100).where(T.user == U.id) + subq = (T + .select(T.user) + .group_by(T.user) + .having(fn.COUNT(T.id) > 100)) + query = (U + .update({U.flood_count: ctq}) + .where(U.id.in_(subq))) + self.assertSQL(query, ( + 'UPDATE "u" SET "flood_count" = (' + 'SELECT (COUNT("t1"."id") / ?) FROM "t" AS "t1" ' + 'WHERE ("t1"."user_id" = "u"."id")) ' + 'WHERE ("u"."id" IN (' + 'SELECT "t1"."user_id" FROM "t" AS "t1" ' + 'GROUP BY "t1"."user_id" ' + 'HAVING (COUNT("t1"."id") > ?)))'), [100, 100]) + def test_update_from(self): class SalesPerson(TestModel): first = TextField() From ef27f0209a7942e28ba976264ceaf85891277bb1 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Thu, 1 Aug 2019 09:58:28 -0500 Subject: [PATCH 39/41] Improve the data-type conversion for JSON values. --- playhouse/postgres_ext.py | 7 +++++-- tests/postgres.py | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/playhouse/postgres_ext.py b/playhouse/postgres_ext.py index 6a2893eb5..9504a1b6c 100644 --- a/playhouse/postgres_ext.py +++ b/playhouse/postgres_ext.py @@ -3,6 +3,7 @@ * Support for hstore, a key/value type storage """ +import json import logging import uuid @@ -277,18 +278,19 @@ def contains_any(self, *keys): class JSONField(Field): field_type = 'JSON' + _json_datatype = 'json' def __init__(self, dumps=None, *args, **kwargs): if Json is None: raise Exception('Your version of psycopg2 does not support JSON.') - self.dumps = dumps + self.dumps = dumps or json.dumps super(JSONField, self).__init__(*args, **kwargs) def db_value(self, value): if value is None: return value if not isinstance(value, Json): - return Json(value, dumps=self.dumps) + return Cast(self.dumps(value), self._json_datatype) return value def __getitem__(self, value): @@ -307,6 +309,7 @@ def cast_jsonb(node): class BinaryJSONField(IndexedFieldMixin, JSONField): field_type = 'JSONB' + _json_datatype = 'jsonb' __hash__ = Field.__hash__ def contains(self, other): diff --git a/tests/postgres.py b/tests/postgres.py index b7c14377a..acd1b49c6 100644 --- a/tests/postgres.py +++ b/tests/postgres.py @@ -617,7 +617,8 @@ def test_json_field_sql(self): table = self.M._meta.table_name self.assertSQL(j, ( 'SELECT "t1"."id", "t1"."data" ' - 'FROM "%s" AS "t1" WHERE ("t1"."data" = ?)') % table) + 'FROM "%s" AS "t1" WHERE ("t1"."data" = CAST(? AS %s))') + % (table, self.M.data._json_datatype)) j = (self.M .select() @@ -938,6 +939,24 @@ def test_conflict_update(self): self.assertEqual(BJson.select().count(), 1) +@skip_unless(JSON_SUPPORT, 'json support unavailable') +class TestBinaryJsonFieldBulkUpdate(ModelTestCase): + database = db + requires = [BJson] + + def test_binary_json_field_bulk_update(self): + b1 = BJson.create(data={'k1': 'v1'}) + b2 = BJson.create(data={'k2': 'v2'}) + b1.data['k1'] = 'v1-x' + b2.data['k2'] = 'v2-y' + BJson.bulk_update([b1, b2], fields=[BJson.data]) + + b1_db = BJson.get(BJson.id == b1.id) + b2_db = BJson.get(BJson.id == b2.id) + self.assertEqual(b1_db.data, {'k1': 'v1-x'}) + self.assertEqual(b2_db.data, {'k2': 'v2-y'}) + + class TestIntervalField(ModelTestCase): database = db requires = [Event] From cbc90ee2885a962a467c0605a781bde420163124 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Thu, 1 Aug 2019 10:50:40 -0500 Subject: [PATCH 40/41] Update changelog, bringing it current. --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29adadbba..c42a6d771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,23 @@ https://github.com/coleifer/peewee/releases * Add a helper to `playhouse.mysql_ext` for creating `Match` full-text search expressions. +* Added date-part properties to `TimestampField` for accessing the year, month, + day, etc., within a SQL expression. +* Added `to_timestamp()` helper for `DateField` and `DateTimeField` that + produces an expression returning a unix timestamp. +* Add `autoconnect` parameter to `Database` classes. +* Added database-agnostic interface for obtaining a random value. +* Improved date truncation logic for Sqlite and MySQL to make more compatible + with Postgres' `date_trunc()` behavior. +* Support aggregates with FILTER predicates on the latest Sqlite. +* Fix for differentiating empty values from NULL values in `model_to_dict`. +* More aggressively slot row values into the appropriate field when building + objects from the database cursor (rather than using whatever + `cursor.description` tells us, which is buggy in older Sqlite). +* Be more permissive in what we accept in the `insert_many()` and `insert()` + methods. +* Save hooks can now be called for models without a primary key. +* Fixed bug in the conversion of Python values to JSON when using Postgres. [View commits](https://github.com/coleifer/peewee/compare/3.9.6...master) From b17859784ee0e2f17535c2d33fbd3d7feb20e985 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Sat, 3 Aug 2019 04:46:52 -0500 Subject: [PATCH 41/41] 3.10.0 --- CHANGELOG.md | 41 ++++++++++++++++++++++++++++++++++++----- peewee.py | 2 +- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c42a6d771..be3671967 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,27 +7,58 @@ https://github.com/coleifer/peewee/releases ## master +[View commits](https://github.com/coleifer/peewee/compare/3.10.0...master) + +## 3.10.0 + * Add a helper to `playhouse.mysql_ext` for creating `Match` full-text search expressions. * Added date-part properties to `TimestampField` for accessing the year, month, day, etc., within a SQL expression. * Added `to_timestamp()` helper for `DateField` and `DateTimeField` that produces an expression returning a unix timestamp. -* Add `autoconnect` parameter to `Database` classes. +* Add `autoconnect` parameter to `Database` classes. This parameter defaults to + `True` and is compatible with previous versions of Peewee, in which executing + a query on a closed database would open a connection automatically. To make + it easier to catch inconsistent use of the database connection, this behavior + can now be disabled by specifying `autoconnect=False`, making an explicit + call to `Database.connect()` needed before executing a query. * Added database-agnostic interface for obtaining a random value. -* Improved date truncation logic for Sqlite and MySQL to make more compatible - with Postgres' `date_trunc()` behavior. +* Allow `isolation_level` to be specified when initializing a Postgres db. +* Allow hybrid properties to be used on model aliases. Refs #1969. * Support aggregates with FILTER predicates on the latest Sqlite. -* Fix for differentiating empty values from NULL values in `model_to_dict`. + +#### Changes + * More aggressively slot row values into the appropriate field when building objects from the database cursor (rather than using whatever `cursor.description` tells us, which is buggy in older Sqlite). * Be more permissive in what we accept in the `insert_many()` and `insert()` methods. +* When implicitly joining a model with multiple foreign-keys, choose the + foreign-key whose name matches that of the related model. Previously, this + would have raised a `ValueError` stating that multiple FKs existed. +* Improved date truncation logic for Sqlite and MySQL to make more compatible + with Postgres' `date_trunc()` behavior. Previously, truncating a datetime to + month resolution would return `'2019-08'` for example. As of 3.10.0, the + Sqlite and MySQL `date_trunc` implementation returns a full datetime, e.g. + `'2019-08-01 00:00:00'`. +* Apply slightly different logic for casting JSON values with Postgres. + Previously, Peewee just wrapped the value in the psycopg2 `Json()` helper. + In this version, Peewee now dumps the json to a string and applies an + explicit cast to the underlying JSON data-type (e.g. json or jsonb). + +#### Bug fixes + * Save hooks can now be called for models without a primary key. * Fixed bug in the conversion of Python values to JSON when using Postgres. +* Fix for differentiating empty values from NULL values in `model_to_dict`. +* Fixed a bug referencing primary-key values that required some kind of + conversion (e.g., a UUID). See #1979 for details. +* Add small jitter to the pool connection timestamp to avoid issues when + multiple connections are checked-out at the same exact time. -[View commits](https://github.com/coleifer/peewee/compare/3.9.6...master) +[View commits](https://github.com/coleifer/peewee/compare/3.9.6...3.10.0) ## 3.9.6 diff --git a/peewee.py b/peewee.py index 1e49f9681..a95081fd0 100644 --- a/peewee.py +++ b/peewee.py @@ -65,7 +65,7 @@ mysql = None -__version__ = '3.9.6' +__version__ = '3.10.0' __all__ = [ 'AsIs', 'AutoField',