Skip to content

Commit 6de4008

Browse files
committed
Support negative indexing json arrays on mysql/maria
1 parent 4230d2f commit 6de4008

5 files changed

Lines changed: 38 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ https://github.com/coleifer/peewee/releases
99

1010
* Lateral joins honor a user-supplied `on=` predicate instead of silently
1111
replacing it with `true`.
12+
* `JSONField` negative path indexes render as `$[last]` / `$[last-n]` on
13+
MySQL/MariaDB. Previously the sqlite-only `$[#-n]` form was emitted, which
14+
MariaDB evaluates to NULL (overwriting the column when used with `set()`)
15+
and MySQL rejects as an invalid path.
16+
* Reflection/pwiz map MySQL JSON columns to the core `JSONField` instead of
17+
emitting `from playhouse.mysql_ext import *` for a re-exported field.
1218

1319
[View commits](https://github.com/coleifer/peewee/compare/4.2.6...master)
1420

peewee.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3142,11 +3142,14 @@ def _contains_value(self, field, value):
31423142
return Value(field._dumps(value), converter=False)
31433143

31443144
@staticmethod
3145-
def _path(keys, suffix=''):
3145+
def _path_index(idx):
3146+
return '[#%d]' % idx if idx < 0 else '[%d]' % idx
3147+
3148+
def _path(self, keys, suffix=''):
31463149
parts = ['$']
31473150
for k in keys:
31483151
if isinstance(k, int):
3149-
parts.append('[#%d]' % k if k < 0 else '[%d]' % k)
3152+
parts.append(self._path_index(k))
31503153
else:
31513154
k = str(k).replace('\\', '\\\\').replace('"', '\\"')
31523155
parts.append('."%s"' % k)
@@ -3415,6 +3418,12 @@ def wrapper(value):
34153418
compact=False)
34163419
return wrapper
34173420

3421+
@staticmethod
3422+
def _path_index(idx):
3423+
if idx < 0:
3424+
return '[last]' if idx == -1 else '[last%d]' % (idx + 1)
3425+
return '[%d]' % idx
3426+
34183427
def extract(self, field, keys):
34193428
if not keys:
34203429
return field
@@ -3476,7 +3485,7 @@ def update(self, field, value):
34763485
return fn.JSON_MERGE_PATCH(field, self._json_value(field, value))
34773486

34783487
def contains(self, field, keys, value):
3479-
# JSON_CONTAINS returns 0/1; wrap in `= 1` so it composes cleanly in
3488+
# JSON_CONTAINS returns 0/1, wrap in `= 1` so it composes cleanly in
34803489
# boolean contexts (NOT, AND, etc.).
34813490
path_args = (self._path(keys),) if keys else ()
34823491
call = fn.JSON_CONTAINS(field, self._contains_value(field, value),

playhouse/reflection.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@
1212
from peewee import _query_val_transform
1313
from peewee import CommaNodeList
1414
from peewee import make_snake_case
15-
try:
16-
from playhouse import mysql_ext
17-
except ImportError:
18-
mysql_ext = None
1915
try:
2016
from playhouse import postgres_ext
2117
except ImportError:
@@ -429,7 +425,6 @@ class MySQLMetadata(Metadata):
429425
'varchar': CharField,
430426
'year': IntegerField,
431427
}
432-
extension_import = 'from playhouse.mysql_ext import *'
433428

434429
def get_column_types(self, table, schema=None):
435430
column_types = {}
@@ -442,9 +437,8 @@ def get_column_types(self, table, schema=None):
442437
for name, data_type, column_type in cursor.fetchall():
443438
if column_type == 'tinyint(1)':
444439
column_types[name] = BooleanField
445-
elif data_type.lower() == 'json' and mysql_ext is not None:
446-
column_types[name] = mysql_ext.JSONField
447-
self.requires_extension = True
440+
elif data_type.lower() == 'json':
441+
column_types[name] = JSONField
448442
else:
449443
column_types[name] = self.column_map.get(data_type.lower(),
450444
UnknownField)

tests/json_field.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,12 @@ def test_bracket(self):
178178
self.assertEqual(JM.select(JM.data['tags'][2]).scalar(), 'fluffy')
179179
self.assertEqual(JM.select(JM.data['matrix'][1][0]).scalar(), 3)
180180

181-
@skip_if(IS_MYSQL)
182181
def test_negative_index(self):
183182
self.assertEqual(JM.select(JM.data['tags'][-1]).scalar(), 'fluffy')
183+
self.assertEqual(JM.select(JM.data['tags'][-2]).scalar(), 'white')
184184
self.assertEqual(JM.select(JM.data['matrix'][-1][-1]).scalar(), 6)
185+
JM.update(data=JM.data['tags'][-1].set('spotted')).execute()
186+
self.assertEqual(JM.select(JM.data['tags'][-1]).scalar(), 'spotted')
185187

186188
def test_special_char_key(self):
187189
self.assertEqual(self._val('a.b'), 'dotted key')

tests/reflection.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .base import IS_CRDB
1010
from .base import IS_CYSQLITE
1111
from .base import IS_MYSQL
12+
from .base import IS_ORACLE_MYSQL
1213
from .base import IS_POSTGRESQL
1314
from .base import IS_SQLITE
1415
from .base import IS_SQLITE_OLD
@@ -186,6 +187,20 @@ def test_sqlite_fk_re(self):
186187
'col_types_id', 'coltypes', 'f11',
187188
))
188189

190+
@skip_if(not IS_ORACLE_MYSQL, 'requires mysql (mariadb reports longtext)')
191+
def test_mysql_json_maps_to_core_field(self):
192+
self.database.execute_sql('drop table if exists rj')
193+
self.database.execute_sql(
194+
'create table rj (id integer not null primary key '
195+
'auto_increment, data json)')
196+
try:
197+
models = self.introspector.generate_models(table_names=['rj'])
198+
field = models['rj']._meta.fields['data']
199+
self.assertTrue(isinstance(field, JSONField))
200+
self.assertFalse(self.introspector.metadata.requires_extension)
201+
finally:
202+
self.database.execute_sql('drop table rj')
203+
189204
def test_make_column_name(self):
190205
# Tests for is_foreign_key=False.
191206
tests = (

0 commit comments

Comments
 (0)