Allow SQL expression as value of fields

issue6460
review279431002
This commit is contained in:
Cédric Krier
2019-06-26 21:56:16 +02:00
parent 604046d271
commit 51c99aaa12
23 changed files with 177 additions and 80 deletions
+1
View File
@@ -1,3 +1,4 @@
* Allow SQL expression as value of fields
* Allow customizing Dict keys order with a sequence field
* Add invalid domain in DomainValidationError
* Add domain inversion tools
+1
View File
@@ -10,6 +10,7 @@ class Boolean(Field):
'''
_type = 'boolean'
_sql_type = 'BOOL'
_py_type = bool
def __init__(self, string='', help='', readonly=False, domain=None,
states=None, select=False, on_change=None, on_change_with=None,
+1 -6
View File
@@ -12,6 +12,7 @@ class Char(FieldTranslate):
Define a char field (``unicode``).
'''
_type = 'char'
_py_type = str
def __init__(self, string='', size=None, help='', required=False,
readonly=False, domain=None, states=None, translate=False,
@@ -47,12 +48,6 @@ class Char(FieldTranslate):
size = property(_get_size, _set_size)
def sql_format(self, value):
if value is None:
return None
assert isinstance(value, str)
return value
@property
def _sql_type(self):
return 'VARCHAR(%s)' % self.size if self.size else 'VARCHAR'
+36 -36
View File
@@ -1,6 +1,7 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
from sql.functions import Function
from trytond.pyson import PYSONEncoder
@@ -29,23 +30,16 @@ class Date(Field):
'''
_type = 'date'
_sql_type = 'DATE'
_py_type = datetime.date
def sql_format(self, value):
if value is None:
return None
if isinstance(value, str):
year, month, day = list(map(int, value.split("-", 2)))
return datetime.date(year, month, day)
if (not isinstance(value, datetime.date)
# Allow datetime with min time for XML-RPC
# datetime must be tested separately because datetime is a
# subclass of date
or (isinstance(value, datetime.datetime)
and value.time() != datetime.time())):
raise ValueError("invalid type '%s' for %s"
% (type(value), type(self)))
return value
value = datetime.date(year, month, day)
elif isinstance(value, datetime.datetime):
if value.time() != datetime.time():
raise ValueError("Date field can not have time")
return super().sql_format(value)
def sql_cast(self, expression):
if backend.name() == 'sqlite':
@@ -68,11 +62,10 @@ class Timestamp(FormatMixin, Field):
'''
_type = 'timestamp'
_sql_type = 'TIMESTAMP'
_py_type = datetime.datetime
format = '%H:%M:%S.%f'
def sql_format(self, value):
if value is None:
return None
if isinstance(value, str):
datepart, timepart = value.split(" ")
year, month, day = map(int, datepart.split("-", 2))
@@ -82,12 +75,9 @@ class Timestamp(FormatMixin, Field):
microseconds = int(timepart_full[1])
else:
microseconds = 0
return datetime.datetime(year, month, day, hours, minutes, seconds,
microseconds)
if not isinstance(value, datetime.datetime):
raise ValueError("invalid type '%s' for %s"
% (type(value), type(self)))
return value
value = datetime.datetime(
year, month, day, hours, minutes, seconds, microseconds)
return super().sql_format(value)
def sql_cast(self, expression):
if backend.name() == 'sqlite':
@@ -119,28 +109,40 @@ class DateTime(Timestamp):
def sql_format(self, value):
value = super().sql_format(value)
if value:
if isinstance(value, datetime.datetime):
value = value.replace(microsecond=0)
return value
class Time(DateTime):
class Time(FormatMixin, Field):
'''
Define a time field (``time``).
'''
_type = 'time'
_sql_type = 'TIME'
_py_type = datetime.time
def __init__(self, string='', format='%H:%M:%S', help='', required=False,
readonly=False, domain=None, states=None, select=False,
on_change=None, on_change_with=None, depends=None,
context=None, loading='eager'):
'''
:param format: The validation format as used by strftime.
'''
super().__init__(string=string, help=help,
required=required, readonly=readonly, domain=domain, states=states,
select=select, on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
self.format = format
def sql_format(self, value):
if value is None:
return None
if isinstance(value, str):
hours, minutes, seconds = map(int, value.split(":"))
return datetime.time(hours, minutes, seconds)
if not isinstance(value, datetime.time):
raise ValueError("invalid type '%s' for %s"
% (type(value), type(self)))
return value.replace(microsecond=0)
value = datetime.time(hours, minutes, seconds)
value = super().sql_format(value)
if isinstance(value, datetime.time):
value = value.replace(microsecond=0)
return value
def sql_cast(self, expression):
if backend.name() == 'sqlite':
@@ -154,6 +156,7 @@ class TimeDelta(Field):
'''
_type = 'timedelta'
_sql_type = 'INTERVAL'
_py_type = datetime.timedelta
def __init__(self, string='', converter=None, help='', required=False,
readonly=False, domain=None, states=None, select=False,
@@ -170,12 +173,9 @@ class TimeDelta(Field):
self.converter = converter
def sql_format(self, value):
if value is None:
return None
if not isinstance(value, datetime.timedelta):
raise ValueError("invalid type '%s' for %s"
% (type(value), type(self)))
return super(TimeDelta, self).sql_format(value)
if isinstance(value, (int, float)):
raise TypeError("TimeDelta requires a timedelta")
return super().sql_format(value)
@classmethod
def get(cls, ids, model, name, values=None):
+5 -5
View File
@@ -21,6 +21,7 @@ class Dict(Field):
'Define dict field.'
_type = 'dict'
_sql_type = 'TEXT'
_py_type = dict
def __init__(self, schema_model, string='', help='', required=False,
readonly=False, domain=None, states=None, select=False,
@@ -44,11 +45,10 @@ class Dict(Field):
return dicts
def sql_format(self, value):
if value is None:
return None
assert isinstance(value, dict)
value = {k: v for k, v in value.items() if v is not None}
return dumps(value)
value = super().sql_format(value)
if isinstance(value, dict):
value = dumps({k: v for k, v in value.items() if v is not None})
return value
def translated(self, name=None, type_='values'):
"Return a descriptor for the translated value of the field"
+4
View File
@@ -210,6 +210,7 @@ SQL_OPERATORS = {
class Field(object):
_type = None
_sql_type = None
_py_type = None
def __init__(self, string='', help='', required=False, readonly=False,
domain=None, states=None, select=False, on_change=None,
@@ -343,6 +344,9 @@ class Field(object):
assert self._sql_type is not None
database = Transaction().database
if (self._py_type and value is not None
and not isinstance(value, self._py_type)):
value = self._py_type(value)
return database.sql_format(self._sql_type, value)
def sql_type(self):
+1 -5
View File
@@ -22,6 +22,7 @@ class Float(Field):
'''
_type = 'float'
_sql_type = 'FLOAT'
_py_type = float
def __init__(self, string='', digits=None, help='', required=False,
readonly=False, domain=None, states=None, select=False,
@@ -49,11 +50,6 @@ class Float(Field):
digits = property(_get_digits, _set_digits)
def sql_format(self, value):
if value is None:
return None
return float(value)
def definition(self, model, language):
encoder = PYSONEncoder()
definition = super().definition(model, language)
+1 -5
View File
@@ -9,11 +9,7 @@ class Integer(Field):
'''
_type = 'integer'
_sql_type = 'INTEGER'
def sql_format(self, value):
if value is None:
return None
return int(str(value))
_py_type = int
class BigInteger(Integer):
+2 -3
View File
@@ -19,6 +19,7 @@ class Many2One(Field):
'''
_type = 'many2one'
_sql_type = 'INTEGER'
_py_type = int
def __init__(self, model_name, string='', left=None, right=None,
ondelete='SET NULL', datetime_field=None, target_search='join',
@@ -111,10 +112,8 @@ class Many2One(Field):
super(Many2One, self).__set__(inst, value)
def sql_format(self, value):
if value is None:
return None
assert value is not False
return int(value)
return super().sql_format(value)
def convert_domain_mptt(self, domain, tables):
cursor = Transaction().connection.cursor()
+1 -8
View File
@@ -20,14 +20,7 @@ class Numeric(Float):
'''
_type = 'numeric'
_sql_type = 'NUMERIC'
def sql_format(self, value):
if value is None:
return None
if isinstance(value, int):
value = Decimal(str(value))
value = Decimal(value)
return value
_py_type = Decimal
def sql_column(self, table):
column = super(Numeric, self).sql_column(table)
+1
View File
@@ -20,6 +20,7 @@ class Reference(SelectionMixin, Field):
'''
_type = 'reference'
_sql_type = 'VARCHAR'
_py_type = str
def __init__(self, string='', selection=None, sort=True,
selection_change_with=None, translate=True, search_order=None,
+1
View File
@@ -59,6 +59,7 @@ class Selection(SelectionMixin, Field):
'''
_type = 'selection'
_sql_type = 'VARCHAR'
_py_type = str
def __init__(self, selection, string='', sort=True,
selection_change_with=None, translate=True, help='',
+7
View File
@@ -35,10 +35,17 @@ class TimeFormat(ModelSQL):
time = fields.Time(string='Time', format='%H:%M')
class TimePrecision(ModelSQL):
"Time precision"
__name__ = 'test.time_precision'
time = fields.Time(string="Time", format="%H:%M:%S.%f")
def register(module):
Pool.register(
Time,
TimeDefault,
TimeRequired,
TimeFormat,
TimePrecision,
module=module, type_='model')
+13
View File
@@ -4,6 +4,8 @@ import shutil
import tempfile
import unittest
from sql import Literal
from trytond.config import config
from trytond.model.exceptions import RequiredValidationError
from trytond.model import fields
@@ -110,6 +112,17 @@ class FieldBinaryTestCase(unittest.TestCase):
self.assertEqual(binary.binary, None)
self.assertEqual(binary.binary_id, None)
@with_transaction()
def test_create_with_sql_value(self):
"Test create binary with SQL value"
Binary = Pool().get('test.binary')
binary, = Binary.create([{
'binary': Literal('foo'),
}])
self.assertEqual(binary.binary, cast(b'foo'))
@with_transaction()
def test_read_size(self):
"Test read binary size"
+11
View File
@@ -2,6 +2,8 @@
# this repository contains the full copyright notices and license terms.
import unittest
from sql import Literal
from trytond.pool import Pool
from trytond.tests.test_tryton import activate_module, with_transaction
@@ -48,6 +50,15 @@ class FieldBooleanTestCase(unittest.TestCase):
self.assertEqual(boolean.boolean, True)
@with_transaction()
def test_create_with_sql_value(self):
"Test create boolean with SQL value"
Boolean = Pool().get('test.boolean')
boolean, = Boolean.create([{'boolean': Literal(True)}])
self.assertEqual(boolean.boolean, True)
@with_transaction()
def test_search_equals_true(self):
"Test search boolean equals True"
+11
View File
@@ -3,6 +3,8 @@
# this repository contains the full copyright notices and license terms.
import unittest
from sql import Literal
from trytond import backend
from trytond.model.exceptions import RequiredValidationError
from trytond.pool import Pool
@@ -37,6 +39,15 @@ class CommonTestCaseMixin:
self.assertEqual(char.char, "é")
@with_transaction()
def test_create_with_sql_value(self):
"Test create with SQL value"
Char = self.Char()
char, = Char.create([{'char': Literal('Foo')}])
self.assertEqual(char.char, "Foo")
@with_transaction()
def test_search_equals(self):
"Test search char equals"
+13 -2
View File
@@ -3,6 +3,8 @@
import datetime
import unittest
from sql.functions import CurrentDate
from trytond.model.exceptions import RequiredValidationError
from trytond.pool import Pool
from trytond.tests.test_tryton import activate_module, with_transaction
@@ -73,6 +75,15 @@ class FieldDateTestCase(unittest.TestCase):
self.assertEqual(date.date, default_date)
@with_transaction()
def test_create_with_sql_value(self):
"Test create date with SQL value"
Date = Pool().get('test.date')
date, = Date.create([{'date': CurrentDate()}])
self.assert_(date.date)
@with_transaction()
def test_create_non_date(self):
"Test create date with non date"
@@ -88,7 +99,7 @@ class FieldDateTestCase(unittest.TestCase):
"Test create date with integer"
Date = Pool().get('test.date')
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
Date.create([{
'date': 42,
}])
@@ -413,7 +424,7 @@ class FieldDateTestCase(unittest.TestCase):
'date': today,
}])
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
Date.write([date], {
'date': 42,
})
+17 -4
View File
@@ -3,6 +3,8 @@
import datetime
import unittest
from sql.functions import CurrentTimestamp
from trytond.model.exceptions import (
RequiredValidationError, TimeFormatValidationError)
from trytond.pool import Pool
@@ -74,6 +76,17 @@ class FieldDateTimeTestCase(unittest.TestCase):
self.assertEqual(datetime.datetime, default_datetime)
@with_transaction()
def test_create_with_sql_value(self):
"Test create datetime with SQL value"
DateTime = Pool().get('test.datetime')
datetime, = DateTime.create([{
'datetime': DateTime.datetime.sql_cast(CurrentTimestamp()),
}])
self.assert_(datetime.datetime)
@with_transaction()
def test_create_non_datetime(self):
"Test create datetime with non datetime"
@@ -89,7 +102,7 @@ class FieldDateTimeTestCase(unittest.TestCase):
"Test create datetime with integer"
DateTime = Pool().get('test.datetime')
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
DateTime.create([{
'datetime': 42,
}])
@@ -99,7 +112,7 @@ class FieldDateTimeTestCase(unittest.TestCase):
"Test create datetime with date"
DateTime = Pool().get('test.datetime')
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
DateTime.create([{
'datetime': datetime.date(2009, 1, 1),
}])
@@ -447,7 +460,7 @@ class FieldDateTimeTestCase(unittest.TestCase):
'datetime': today,
}])
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
DateTime.write([datetime], {
'datetime': 42,
})
@@ -460,7 +473,7 @@ class FieldDateTimeTestCase(unittest.TestCase):
'datetime': today,
}])
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
DateTime.write([datetime_], {
'datetime': datetime.date(2009, 1, 1),
})
+11
View File
@@ -2,6 +2,8 @@
# this repository contains the full copyright notices and license terms.
import unittest
from sql import Literal
from trytond.model.exceptions import (
RequiredValidationError, DigitsValidationError)
from trytond.pool import Pool
@@ -58,6 +60,15 @@ class FieldFloatTestCase(unittest.TestCase):
self.assertEqual(float_.float, 5.5)
@with_transaction()
def test_create_with_sql_value(self):
"Test create float with SQL value"
Float = Pool().get('test.float')
float_, = Float.create([{'float': Literal(5.) / Literal(2.)}])
self.assertEqual(float_.float, 2.5)
@with_transaction()
def test_create_non_float(self):
"Test create float with non float"
+11
View File
@@ -2,6 +2,8 @@
# this repository contains the full copyright notices and license terms.
import unittest
from sql import Literal
from trytond.model.exceptions import (
RequiredValidationError, DomainValidationError)
from trytond.pool import Pool
@@ -47,6 +49,15 @@ class FieldIntegerTestCase(unittest.TestCase):
self.assertEqual(integer.integer, 5)
@with_transaction()
def test_create_with_sql_value(self):
"Test create integer with SQL value"
Integer = Pool().get('test.integer_default')
integer, = Integer.create([{'integer': Literal(5)}])
self.assertEqual(integer.integer, 5)
@with_transaction()
def test_create_non_integer(self):
"Test create integer with non integer"
+11
View File
@@ -3,6 +3,8 @@
import unittest
from decimal import Decimal, InvalidOperation
from sql import Literal
from trytond.model.exceptions import (
RequiredValidationError, DigitsValidationError)
from trytond.pool import Pool
@@ -59,6 +61,15 @@ class FieldNumericTestCase(unittest.TestCase):
self.assertEqual(numeric.numeric, Decimal('5.5'))
@with_transaction()
def test_create_with_sql_value(self):
"Test create numeric with SQL value"
Numeric = Pool().get('test.numeric')
numeric, = Numeric.create([{'numeric': Literal(5.) / Literal(2.)}])
self.assertEqual(numeric.numeric, Decimal('2.5'))
@with_transaction()
def test_create_non_numeric(self):
"Test create numeric with non numeric"
+13 -2
View File
@@ -3,6 +3,8 @@
import datetime
import unittest
from sql.functions import CurrentTimestamp
from trytond.model.exceptions import (
RequiredValidationError, TimeFormatValidationError)
from trytond.pool import Pool
@@ -74,6 +76,15 @@ class FieldTimeTestCase(unittest.TestCase):
self.assertEqual(time.time, default_time)
@with_transaction()
def test_create_with_sql_value(self):
"Test create time with SQL value"
Time = Pool().get('test.time_precision')
time, = Time.create([{'time': Time.time.sql_cast(CurrentTimestamp())}])
self.assert_(time.time)
@with_transaction()
def test_create_non_time(self):
"Test create time with non time"
@@ -99,7 +110,7 @@ class FieldTimeTestCase(unittest.TestCase):
"Test create time with date"
Time = Pool().get('test.time')
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
Time.create([{
'time': datetime.date(2009, 1, 1),
}])
@@ -460,7 +471,7 @@ class FieldTimeTestCase(unittest.TestCase):
'time': evening,
}])
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
Time.write([time], {
'time': datetime.date(2009, 1, 1),
})
+4 -4
View File
@@ -57,7 +57,7 @@ class FieldTimeDeltaTestCase(unittest.TestCase):
"Test create timedelta with non timedelta"
TimeDelta = Pool().get('test.timedelta')
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
TimeDelta.create([{
'timedelta': 'non timedelta',
}])
@@ -67,7 +67,7 @@ class FieldTimeDeltaTestCase(unittest.TestCase):
"Test create timedelta with integer"
TimeDelta = Pool().get('test.timedelta')
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
TimeDelta.create([{
'timedelta': 42,
}])
@@ -369,7 +369,7 @@ class FieldTimeDeltaTestCase(unittest.TestCase):
'timedelta': hour,
}])
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
TimeDelta.write([timedelta], {
'timedelta': 'non timedelta',
})
@@ -382,7 +382,7 @@ class FieldTimeDeltaTestCase(unittest.TestCase):
'timedelta': hour,
}])
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
TimeDelta.write([timedelta], {
'timedelta': 42,
})